diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 22546e11c..a031d45d6 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2095,19 +2095,28 @@ List? urlLinkToCmdArgs(Uri uri) { return null; } -connectMainDesktop( - String id, { - required bool isFileTransfer, - required bool isTcpTunneling, - required bool isRDP, - bool? forceRelay, -}) async { +connectMainDesktop(String id, + {required bool isFileTransfer, + required bool isTcpTunneling, + required bool isRDP, + bool? forceRelay, + String? password, + bool? isSharedPassword}) async { if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay); + await rustDeskWinManager.newFileTransfer(id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay); + await rustDeskWinManager.newPortForward(id, isRDP, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay); } else { - await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay); + await rustDeskWinManager.newRemoteDesktop(id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay); } } @@ -2115,14 +2124,13 @@ connectMainDesktop( /// If [isFileTransfer], starts a session only for file transfer. /// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isRDP], starts a session only for rdp. -connect( - BuildContext context, - String id, { - bool isFileTransfer = false, - bool isTcpTunneling = false, - bool isRDP = false, - bool forceRelay = false, -}) async { +connect(BuildContext context, String id, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false, + bool forceRelay = false, + String? password, + bool? isSharedPassword}) async { if (id == '') return; if (!isDesktop || desktopType == DesktopType.main) { try { @@ -2150,6 +2158,8 @@ connect( isFileTransfer: isFileTransfer, isTcpTunneling: isTcpTunneling, isRDP: isRDP, + password: password, + isSharedPassword: isSharedPassword, forceRelay: forceRelay2, ); } else { @@ -2158,6 +2168,8 @@ connect( 'isFileTransfer': isFileTransfer, 'isTcpTunneling': isTcpTunneling, 'isRDP': isRDP, + 'password': password, + 'isSharedPassword': isSharedPassword, 'forceRelay': forceRelay, }); } @@ -2171,14 +2183,16 @@ connect( Navigator.push( context, MaterialPageRoute( - builder: (BuildContext context) => FileManagerPage(id: id), + builder: (BuildContext context) => FileManagerPage( + id: id, password: password, isSharedPassword: isSharedPassword), ), ); } else { Navigator.push( context, MaterialPageRoute( - builder: (BuildContext context) => RemotePage(id: id), + builder: (BuildContext context) => RemotePage( + id: id, password: password, isSharedPassword: isSharedPassword), ), ); } diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index b500afd5e..664da7c9d 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/peer_model.dart'; @@ -188,3 +189,107 @@ class RequestException implements Exception { return "RequestException, statusCode: $statusCode, error: $cause"; } } + +enum ShareRule { + read(1), + readWrite(2), + fullControl(3); + + const ShareRule(this.value); + final int value; + + static String desc(int v) { + if (v == ShareRule.read.value) { + return translate('Read-only'); + } + if (v == ShareRule.readWrite.value) { + return translate('Read/Write'); + } + if (v == ShareRule.fullControl.value) { + return translate('Full Control'); + } + return v.toString(); + } + + static String shortDesc(int v) { + if (v == ShareRule.read.value) { + return 'R'; + } + if (v == ShareRule.readWrite.value) { + return 'RW'; + } + if (v == ShareRule.fullControl.value) { + return 'F'; + } + return v.toString(); + } + + static ShareRule? fromValue(int v) { + if (v == ShareRule.read.value) { + return ShareRule.read; + } + if (v == ShareRule.readWrite.value) { + return ShareRule.readWrite; + } + if (v == ShareRule.fullControl.value) { + return ShareRule.fullControl; + } + return null; + } +} + +enum ShareLevel { + user(1), + group(2), + team(3); + + const ShareLevel(this.value); + final int value; + + static String teamName = translate('Everyone'); +} + +class AbProfile { + String guid; + String name; + String owner; + String? note; + int rule; + + AbProfile(this.guid, this.name, this.owner, this.note, this.rule); + + AbProfile.fromJson(Map json) + : guid = json['guid'] ?? '', + name = json['name'] ?? '', + owner = json['owner'] ?? '', + note = json['note'] ?? '', + rule = json['rule'] ?? 0; +} + +class AbTag { + String name; + int color; + + AbTag(this.name, this.color); + + AbTag.fromJson(Map json) + : name = json['name'] ?? '', + color = json['color'] ?? ''; +} + +class AbRulePayload { + String guid; + int level; + String name; + int rule; + String? group; + + AbRulePayload(this.guid, this.level, this.name, this.rule, {this.group}); + + AbRulePayload.fromJson(Map json) + : guid = json['guid'] ?? '', + level = json['level'] ?? 0, + name = json['name'] ?? '', + rule = json['rule'] ?? 0, + group = json['group']; +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 983b2219e..b2be3502e 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -1,13 +1,17 @@ import 'dart:math'; +import 'package:bot_toast/bot_toast.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:dynamic_layouts/dynamic_layouts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_simple_treeview/flutter_simple_treeview.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; import 'package:flex_color_picker/flex_color_picker.dart'; @@ -43,25 +47,24 @@ class _AddressBookState extends State { child: ElevatedButton( onPressed: loginDialog, child: Text(translate("Login")))); } else { - if (gFFI.abModel.abLoading.value && gFFI.abModel.emtpy) { + if (gFFI.abModel.currentAbLoading.value && + gFFI.abModel.currentAbEmtpy) { return const Center( child: CircularProgressIndicator(), ); } return Column( children: [ - // NOT use Offstage to wrap LinearProgressIndicator - if (gFFI.abModel.retrying.value) LinearProgressIndicator(), buildErrorBanner(context, - loading: gFFI.abModel.abLoading, - err: gFFI.abModel.pullError, + loading: gFFI.abModel.currentAbLoading, + err: gFFI.abModel.currentAbPullError, retry: null, - close: () => gFFI.abModel.pullError.value = ''), + close: () => gFFI.abModel.currentAbPullError.value = ''), buildErrorBanner(context, - loading: gFFI.abModel.abLoading, - err: gFFI.abModel.pushError, - retry: () => gFFI.abModel.pushAb(isRetry: true), - close: () => gFFI.abModel.pushError.value = ''), + loading: gFFI.abModel.currentAbLoading, + err: gFFI.abModel.currentAbPushError, + retry: null, // remove retry + close: () => gFFI.abModel.currentAbPushError.value = ''), Expanded( child: isDesktop ? _buildAddressBookDesktop() @@ -82,11 +85,12 @@ class _AddressBookState extends State { border: Border.all( color: Theme.of(context).colorScheme.background)), child: Container( - width: 150, + width: 180, height: double.infinity, padding: const EdgeInsets.all(8.0), child: Column( children: [ + _buildAbDropdown(), _buildTagHeader().marginOnly(left: 8.0, right: 0), Expanded( child: Container( @@ -94,7 +98,8 @@ class _AddressBookState extends State { height: double.infinity, child: _buildTags(), ), - ) + ), + _buildAbPermission(), ], ), ), @@ -119,11 +124,13 @@ class _AddressBookState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ + _buildAbDropdown(), _buildTagHeader().marginOnly(left: 8.0, right: 0), Container( width: double.infinity, child: _buildTags(), ), + _buildAbPermission(), ], ), ), @@ -133,6 +140,120 @@ class _AddressBookState extends State { ); } + Widget _buildAbPermission() { + icon(IconData data, String tooltip) { + return Tooltip( + message: translate(tooltip), + waitDuration: Duration.zero, + child: Icon(data, size: 12.0).marginSymmetric(horizontal: 2.0)); + } + + return Obx(() { + if (gFFI.abModel.legacyMode.value) return Offstage(); + if (gFFI.abModel.current.isPersonal()) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + icon(Icons.cloud_off, "Personal"), + ], + ); + } else { + List children = []; + final rule = gFFI.abModel.current.sharedProfile()?.rule; + if (rule == ShareRule.read.value) { + children.add( + icon(Icons.visibility, ShareRule.desc(ShareRule.read.value))); + } else if (rule == ShareRule.readWrite.value) { + children + .add(icon(Icons.edit, ShareRule.desc(ShareRule.readWrite.value))); + } else if (rule == ShareRule.fullControl.value) { + children.add(icon( + Icons.security, ShareRule.desc(ShareRule.fullControl.value))); + } + final owner = gFFI.abModel.current.sharedProfile()?.owner; + if (owner != null) { + children.add(icon(Icons.person, "${translate("Owner")}: $owner")); + } + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: children, + ); + } + }); + } + + Widget _buildAbDropdown() { + if (gFFI.abModel.legacyMode.value) { + return Offstage(); + } + final names = gFFI.abModel.addressBookNames(); + if (!names.contains(gFFI.abModel.currentName.value)) { + return Offstage(); + } + final TextEditingController textEditingController = TextEditingController(); + + return DropdownButton2( + value: gFFI.abModel.currentName.value, + onChanged: (value) { + if (value != null) { + gFFI.abModel.setCurrentName(value); + bind.setLocalFlutterOption(k: 'current-ab-name', v: value); + } + }, + items: names + .map((e) => DropdownMenuItem( + value: e, + child: Row( + children: [ + Expanded( + child: Tooltip( + message: e, + child: Text(gFFI.abModel.translatedName(e), + style: TextStyle(fontSize: 14))), + ), + ], + ))) + .toList(), + isExpanded: true, + dropdownSearchData: DropdownSearchData( + searchController: textEditingController, + searchInnerWidgetHeight: 50, + searchInnerWidget: Container( + height: 50, + padding: const EdgeInsets.only( + top: 8, + bottom: 4, + right: 8, + left: 8, + ), + child: TextFormField( + expands: true, + maxLines: null, + controller: textEditingController, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + hintText: translate('Search'), + hintStyle: const TextStyle(fontSize: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + searchMatchFn: (item, searchValue) { + return item.value + .toString() + .toLowerCase() + .contains(searchValue.toLowerCase()); + }, + ), + ); + } + Widget _buildTagHeader() { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -154,11 +275,12 @@ class _AddressBookState extends State { return Obx(() { final List tags; if (gFFI.abModel.sortTags.value) { - tags = gFFI.abModel.tags.toList(); + tags = gFFI.abModel.currentAbTags.toList(); tags.sort(); } else { - tags = gFFI.abModel.tags; + tags = gFFI.abModel.currentAbTags; } + final editPermission = gFFI.abModel.current.canWrite(); tagBuilder(String e) { return AddressBookTag( name: e, @@ -169,7 +291,8 @@ class _AddressBookState extends State { } else { gFFI.abModel.selectedTags.add(e); } - }); + }, + showActionMenu: editPermission); } final gridView = DynamicGridView.builder( @@ -193,7 +316,7 @@ class _AddressBookState extends State { alignment: Alignment.topLeft, child: AddressBookPeersView( menuPadding: widget.menuPadding, - initPeers: gFFI.abModel.peers, + getInitPeers: () => gFFI.abModel.currentAbPeers, )), ); } @@ -207,7 +330,7 @@ class _AddressBookState extends State { return shouldSyncAb(); }, setter: (bool v) async { - bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : ''); + gFFI.abModel.setShouldAsync(v); }, dismissOnClicked: true, ); @@ -246,9 +369,27 @@ class _AddressBookState extends State { } void _showMenu(RelativeRect pos) { + final currentProfile = gFFI.abModel.current.sharedProfile(); + final shardFullControl = !gFFI.abModel.current.isPersonal() && + gFFI.abModel.current.fullControl(); + final shared = [ + getEntry(translate('Add shared address book'), + () => createOrUpdateSharedAb(null)), + if (gFFI.abModel.current.fullControl() && + !gFFI.abModel.current.isPersonal()) + getEntry(translate('Update this address book'), + () => createOrUpdateSharedAb(currentProfile)), + if (shardFullControl) + getEntry(translate('Delete this address book'), deleteSharedAb), + if (shardFullControl) + getEntry(translate('Share this address book'), shareAb), + MenuEntryDivider(), + ]; + final canWrite = gFFI.abModel.current.canWrite(); final items = [ - getEntry(translate("Add ID"), abAddId), - getEntry(translate("Add Tag"), abAddTag), + if (!gFFI.abModel.legacyMode.value) ...shared, + if (canWrite) getEntry(translate("Add ID"), addIdToCurrentAb), + if (canWrite) getEntry(translate("Add Tag"), abAddTag), getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags), sortMenuItem(), syncMenuItem(), @@ -271,17 +412,19 @@ class _AddressBookState extends State { ); } - void abAddId() async { - if (gFFI.abModel.isFull(true)) { + void addIdToCurrentAb() async { + if (gFFI.abModel.isCurrentAbFull(true)) { return; } var isInProgress = false; IDTextEditingController idController = IDTextEditingController(text: ''); TextEditingController aliasController = TextEditingController(text: ''); - final tags = List.of(gFFI.abModel.tags); + TextEditingController passwordController = TextEditingController(text: ''); + final tags = List.of(gFFI.abModel.currentAbTags); var selectedTag = List.empty(growable: true).obs; final style = TextStyle(fontSize: 14.0); String? errorMsg; + final isCurrentAbShared = !gFFI.abModel.current.isPersonal(); gFFI.dialogManager.show((setState, close, context) { submit() async { @@ -293,16 +436,26 @@ class _AddressBookState extends State { if (id.isEmpty) { // pass } else { - if (gFFI.abModel.idContainBy(id)) { + if (gFFI.abModel.idContainByCurrent(id)) { setState(() { isInProgress = false; errorMsg = translate('ID already exists'); }); return; } - gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag); - gFFI.abModel.pushAb(); - this.setState(() {}); + var password = ''; + if (isCurrentAbShared) { + password = passwordController.text; + } + String? errMsg2 = await gFFI.abModel.addIdToCurrent( + id, aliasController.text.trim(), password, selectedTag); + if (errMsg2 != null) { + setState(() { + isInProgress = false; + errorMsg = errMsg2; + }); + return; + } // final currentPeers } close(); @@ -334,7 +487,8 @@ class _AddressBookState extends State { TextField( controller: idController, inputFormatters: [IDTextInputFormatter()], - decoration: InputDecoration(errorText: errorMsg), + decoration: + InputDecoration(errorText: errorMsg, errorMaxLines: 5), ), Align( alignment: Alignment.centerLeft, @@ -346,6 +500,19 @@ class _AddressBookState extends State { TextField( controller: aliasController, ), + if (isCurrentAbShared) + Align( + alignment: Alignment.centerLeft, + child: Text( + translate('Password'), + style: style, + ), + ).marginOnly(top: 8, bottom: marginBottom), + if (isCurrentAbShared) + TextField( + controller: passwordController, + obscureText: true, + ), Align( alignment: Alignment.centerLeft, child: Text( @@ -376,6 +543,14 @@ class _AddressBookState extends State { const SizedBox( height: 4.0, ), + if (!gFFI.abModel.current.isPersonal()) + Row(children: [ + Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), + Text( + translate('share_warning_tip'), + style: TextStyle(fontSize: 12), + ) + ]).marginSymmetric(vertical: 10), // NOT use Offstage to wrap LinearProgressIndicator if (isInProgress) const LinearProgressIndicator(), ], @@ -407,10 +582,7 @@ class _AddressBookState extends State { } else { final tags = field.trim().split(RegExp(r"[\s,;\n]+")); field = tags.join(','); - for (final tag in tags) { - gFFI.abModel.addTag(tag); - } - gFFI.abModel.pushAb(); + gFFI.abModel.addTags(tags); // final currentPeers } close(); @@ -455,6 +627,201 @@ class _AddressBookState extends State { ); }); } + + void createOrUpdateSharedAb(AbProfile? profile) async { + final isAdd = profile == null; + var msg = ""; + var isInProgress = false; + final style = TextStyle(fontSize: 14.0); + double marginBottom = 4; + TextEditingController nameController = + TextEditingController(text: profile?.name ?? ''); + TextEditingController noteController = + TextEditingController(text: profile?.note ?? ''); + + gFFI.dialogManager.show((setState, close, context) { + submit() async { + final name = nameController.text.trim(); + if (isAdd && name.isEmpty) { + // pass + } else { + final note = noteController.text.trim(); + setState(() { + msg = ""; + isInProgress = true; + }); + final oldName = profile?.name; + final errMsg = (profile == null + ? await gFFI.abModel.addSharedAb(name, note) + : await gFFI.abModel.updateSharedAb(profile.guid, name, note)); + if (errMsg.isNotEmpty) { + setState(() { + msg = errMsg; + isInProgress = false; + }); + return; + } + await gFFI.abModel.pullAb(); + if (gFFI.abModel.addressBookNames().contains(name)) { + gFFI.abModel.setCurrentName(name); + } + // workaround for showing empty peers + if (oldName != null && oldName != name) { + Future.delayed(Duration.zero, () async { + await gFFI.abModel.pullAb(); + }); + } + } + close(); + } + + return CustomAlertDialog( + title: Text(translate(isAdd ? 'Add shared address book' : 'Update')), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + Text( + '*', + style: TextStyle(color: Colors.red, fontSize: 14), + ), + Text( + translate('Name'), + style: style, + ), + ], + ), + ).marginOnly(bottom: marginBottom), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + decoration: InputDecoration( + errorText: msg.isEmpty ? null : translate(msg), + errorMaxLines: 3, + ), + controller: nameController, + autofocus: true, + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate('Note'), + style: style, + ), + ).marginOnly(top: 8, bottom: marginBottom), + TextField( + controller: noteController, + maxLength: 100, + ), + const SizedBox( + height: 4.0, + ), + // NOT use Offstage to wrap LinearProgressIndicator + if (isInProgress) const LinearProgressIndicator(), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + void deleteSharedAb() async { + RxBool isInProgress = false.obs; + + String currentName = gFFI.abModel.currentName.value; + gFFI.dialogManager.show((setState, close, context) { + submit() async { + isInProgress.value = true; + String errMsg = await gFFI.abModel.deleteSharedAb(currentName); + close(); + isInProgress.value = false; + if (errMsg.isEmpty) { + showToast(translate('Successful')); + } else { + BotToast.showText(contentColor: Colors.red, text: translate(errMsg)); + } + gFFI.abModel.pullAb(); + } + + cancel() { + close(); + } + + return CustomAlertDialog( + content: Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(translate( + 'Are you sure you want to delete address book {$currentName}?')), + // NOT use Offstage to wrap LinearProgressIndicator + isInProgress.value + ? const LinearProgressIndicator() + : Offstage() + ], + )), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); + } + + void shareAb() async { + gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + content: _RuleTree(), + actions: [ + Row(children: [ + Icon(Icons.info, color: MyTheme.accent, size: 20) + .marginSymmetric(horizontal: isDesktop ? 10 : 5), + Expanded( + child: Text( + translate('permission_priority_tip'), + style: TextStyle(fontSize: 12), + textAlign: TextAlign.left, + ), + ) + ]), + dialogButton( + "Close", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + ], + onCancel: close, + onSubmit: close, + ); + }); + } } class AddressBookTag extends StatelessWidget { @@ -491,7 +858,7 @@ class AddressBookTag extends StatelessWidget { child: Obx(() => Container( decoration: BoxDecoration( color: tags.contains(name) - ? gFFI.abModel.getTagColor(name) + ? gFFI.abModel.getCurrentAbTagColor(name) : Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(4)), margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0), @@ -506,7 +873,7 @@ class AddressBookTag extends StatelessWidget { shape: BoxShape.circle, color: tags.contains(name) ? Colors.white - : gFFI.abModel.getTagColor(name)), + : gFFI.abModel.getCurrentAbTagColor(name)), ).marginOnly(right: radius / 2), Expanded( child: Text(name, @@ -530,7 +897,8 @@ class AddressBookTag extends StatelessWidget { if (newName == null || newName.isEmpty) { return translate('Can not be empty'); } - if (newName != name && gFFI.abModel.tags.contains(newName)) { + if (newName != name && + gFFI.abModel.currentAbTags.contains(newName)) { return translate('Already exists'); } return null; @@ -538,7 +906,6 @@ class AddressBookTag extends StatelessWidget { onSubmit: (String newName) { if (name != newName) { gFFI.abModel.renameTag(name, newName); - gFFI.abModel.pushAb(); } Future.delayed(Duration.zero, () => Get.back()); }, @@ -548,7 +915,7 @@ class AddressBookTag extends StatelessWidget { }), getEntry(translate(translate('Change Color')), () async { final model = gFFI.abModel; - Color oldColor = model.getTagColor(name); + Color oldColor = model.getCurrentAbTagColor(name); Color newColor = await showColorPickerDialog( context, oldColor, @@ -567,12 +934,10 @@ class AddressBookTag extends StatelessWidget { ); if (oldColor != newColor) { model.setTagColor(name, newColor); - model.pushAb(); } }), getEntry(translate("Delete"), () { gFFI.abModel.deleteTag(name); - gFFI.abModel.pushAb(); Future.delayed(Duration.zero, () => Get.back()); }), ]; @@ -604,3 +969,412 @@ MenuEntryButton getEntry(String title, VoidCallback proc) { dismissOnClicked: true, ); } + +class _RuleTree extends StatefulWidget { + const _RuleTree(); + + @override + State<_RuleTree> createState() => __RuleTreeState(); +} + +class __RuleTreeState extends State<_RuleTree> { + final TreeController _controller = TreeController(allNodesExpanded: true); + bool mapFetched = false; + Map> map = Map.fromEntries([]); + List rules = []; + bool isInProgress = false; + double totalWidth = isDesktop ? 400.0 : 180.0; + double col1Width = isDesktop ? 300.0 : 100.0; + double col2Width = 30.0; + double indent = isDesktop ? 40.0 : 12.0; + double iconSize = isDesktop ? 24.0 : 12.0; + double iconButtonSize = 24.0; + bool onlyShowExisting = false; + String searchText = ''; + TextStyle? textStyle = isDesktop ? null : TextStyle(fontSize: 12); + + @override + void initState() { + super.initState(); + onlyShowExisting = + bind.getLocalFlutterOption(k: 'only-show-existing-rules') == 'Y'; + refresh(); + } + + void refresh() async { + setState(() { + isInProgress = true; + }); + if (!mapFetched) { + map = await gFFI.abModel.getNamesTree(); + mapFetched = true; + } + final allRules = await gFFI.abModel.getAllRules(); + setState(() { + isInProgress = false; + rules = allRules; + }); + } + + bool match(String name) { + return searchText.isEmpty || + name.toLowerCase().contains(searchText.toLowerCase()); + } + + List getNodes() { + int keyIndex = 0; + List buildUserNodes(List users) { + List userNodes = []; + for (var user in users) { + if (!match(user)) { + continue; + } + final userRuleIndex = rules.indexWhere( + (e) => e.level == ShareLevel.user.value && e.name == user); + if (userRuleIndex < 0) { + if (!onlyShowExisting) { + userNodes.add(TreeNode( + content: _buildEmptyNodeContent( + ShareLevel.user, user, totalWidth, indent * 2), + key: ValueKey(keyIndex++), + children: [])); + } + } else { + final userRule = rules[userRuleIndex]; + userNodes.add(TreeNode( + content: _buildRuleNodeContent(userRule, totalWidth, indent * 2), + key: ValueKey(keyIndex++), + children: [])); + } + } + return userNodes; + } + + List groupNodes = []; + map.forEach((group, users) { + final groupRuleIndex = rules.indexWhere( + (e) => e.level == ShareLevel.group.value && e.name == group); + final children = buildUserNodes(users); + if (!match(group) && children.isEmpty) { + return; + } + if (groupRuleIndex < 0) { + if (!onlyShowExisting || children.isNotEmpty) { + groupNodes.add(TreeNode( + content: _buildEmptyNodeContent( + ShareLevel.group, group, totalWidth, indent), + key: ValueKey(keyIndex++), + children: children)); + } + } else { + final groupRule = rules[groupRuleIndex]; + groupNodes.add(TreeNode( + content: _buildRuleNodeContent(groupRule, totalWidth, indent), + key: ValueKey(keyIndex++), + children: buildUserNodes(users))); + } + }); + + List totalNodes = []; + final teamRuleIndex = + rules.indexWhere((e) => e.level == ShareLevel.team.value); + if (!match(ShareLevel.teamName) && groupNodes.isEmpty) { + return []; + } + if (teamRuleIndex < 0) { + if (!onlyShowExisting || groupNodes.isNotEmpty) { + totalNodes.add(TreeNode( + content: _buildEmptyNodeContent( + ShareLevel.team, ShareLevel.teamName, totalWidth, 0), + key: ValueKey(keyIndex++), + children: groupNodes)); + } + } else { + final rule = rules[teamRuleIndex]; + totalNodes.add(TreeNode( + content: _buildRuleNodeContent( + AbRulePayload( + rule.guid, rule.level, ShareLevel.teamName, rule.rule), + totalWidth, + 0), + key: ValueKey(keyIndex++), + children: groupNodes)); + } + return totalNodes; + } + + @override + Widget build(BuildContext context) { + Widget switchWidget = Switch( + value: onlyShowExisting, + onChanged: (v) { + setState(() { + onlyShowExisting = v; + bind.setLocalFlutterOption( + k: 'only-show-existing-rules', v: v ? 'Y' : ''); + }); + }); + Widget switchLabel = + _text(translate('Only show existing')).marginOnly(right: 20); + Widget searchTextField = TextField( + decoration: InputDecoration( + hintText: translate('Search'), + contentPadding: const EdgeInsets.symmetric(horizontal: 6), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10.0), + ), + prefixIcon: Icon(Icons.search), + filled: true, + ), + onChanged: (v) { + setState(() { + searchText = v; + }); + }, + ).marginSymmetric(horizontal: 10); + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (isDesktop) + Row( + children: [ + switchWidget, + Expanded(child: switchLabel), + Expanded(child: searchTextField), + ], + ), + if (!isDesktop) + Row( + children: [ + switchWidget, + Expanded(child: switchLabel), + ], + ), + if (!isDesktop) searchTextField, + // NOT use Offstage to wrap LinearProgressIndicator + isInProgress ? const LinearProgressIndicator() : Offstage(), + SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: TreeView( + treeController: _controller, + indent: indent, + iconSize: iconSize, + nodes: getNodes(), + ), + ), + ), + ], + ); + } + + Widget _buildEmptyNodeContent( + ShareLevel level, String name, double totalWidth, double indent) { + return SizedBox( + width: totalWidth - indent, + child: Row( + children: [ + SizedBox(width: col1Width - indent, child: _text(name)), + SizedBox(width: col2Width), + const Spacer(), + if (!onlyShowExisting) + _iconButton( + icon: const Icon(Icons.add, color: MyTheme.accent), + onPressed: () { + onSubmit(int rule) async { + if (ShareRule.fromValue(rule) == null) { + BotToast.showText( + contentColor: Colors.red, text: "Invalid rule: $rule"); + return; + } + setState(() { + isInProgress = true; + }); + final errMsg = + await gFFI.abModel.addRule(name, level.value, rule); + setState(() { + isInProgress = false; + }); + if (errMsg != null) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + } else { + refresh(); + } + } + + _addOrUpdateRuleDialog(onSubmit, ShareRule.read.value, null); + }, + ) + ], + ), + ); + } + + Widget _buildRuleNodeContent( + AbRulePayload rule, double totalWidth, double indent) { + return SizedBox( + width: totalWidth - indent, + child: Row( + children: [ + SizedBox(width: col1Width - indent, child: _text(rule.name)), + SizedBox( + width: col2Width, child: _text(ShareRule.shortDesc(rule.rule))), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + _iconButton( + icon: const Icon(Icons.edit, color: MyTheme.accent), + onPressed: () { + onSubmit(int v) async { + setState(() { + isInProgress = true; + }); + final errMsg = await gFFI.abModel.updateRule(rule.guid, v); + setState(() { + isInProgress = false; + }); + if (errMsg != null) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + } else { + refresh(); + } + } + + if (ShareRule.fromValue(rule.rule) == null) { + BotToast.showText( + contentColor: Colors.red, + text: "Invalid rule: ${rule.rule}"); + return; + } + _addOrUpdateRuleDialog(onSubmit, rule.rule, rule.name); + }, + ), + _iconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () async { + onSubmit() async { + setState(() { + isInProgress = true; + }); + final errMsg = await gFFI.abModel.deleteRules([rule.guid]); + setState(() { + isInProgress = false; + }); + if (errMsg != null) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + } else { + refresh(); + } + } + + deleteConfirmDialog(onSubmit, translate('Confirm Delete')); + }, + ), + ], + ) + ], + ), + ); + } + + Widget _iconButton({required Widget icon, required VoidCallback? onPressed}) { + return GestureDetector( + child: + SizedBox(width: iconButtonSize, height: iconButtonSize, child: icon), + onTap: onPressed, + ); + } + + Text _text(String text) { + return Text(text, style: textStyle); + } +} + +void _addOrUpdateRuleDialog( + Future Function(int) onSubmit, int initialRule, String? name) async { + bool isAdd = name == null; + var currentRule = initialRule; + gFFI.dialogManager.show( + (setState, close, context) { + submit() async { + if (ShareRule.fromValue(currentRule) != null) { + onSubmit(currentRule); + } + close(); + } + + final keys = [ + ShareRule.read.value, + ShareRule.readWrite.value, + ShareRule.fullControl.value, + ]; + TextEditingController controller = TextEditingController(); + return CustomAlertDialog( + contentBoxConstraints: BoxConstraints(maxWidth: 300), + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Text( + '${translate(isAdd ? "Add" : "Update")}${name != null ? " $name" : ""}', + overflow: TextOverflow.ellipsis) + .paddingOnly( + left: 10, + ), + ), + ], + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + DropdownMenu( + initialSelection: initialRule, + onSelected: (value) { + if (value != null) { + setState(() { + currentRule = value; + }); + } + }, + dropdownMenuEntries: keys + .map((e) => + DropdownMenuEntry(value: e, label: ShareRule.desc(e))) + .toList(), + inputDecorationTheme: InputDecorationTheme( + isDense: true, border: UnderlineInputBorder()), + enableFilter: false, + controller: controller, + ), + if (currentRule == ShareRule.fullControl.value) + Row( + children: [ + Icon(Icons.warning_amber, color: Colors.amber) + .marginOnly(right: 10), + Flexible( + child: Text(translate('full_control_tip'), + style: TextStyle(fontSize: 12))), + ], + ).marginSymmetric(vertical: 10), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }, + ); +} diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index d4b185706..07a11904d 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -9,9 +9,6 @@ import 'package:flutter_hbb/common/widgets/peer_card.dart'; Future> getAllPeers() async { Map recentPeers = jsonDecode(bind.mainLoadRecentPeersSync()); Map lanPeers = jsonDecode(bind.mainLoadLanPeersSync()); - Map abPeers = jsonDecode(bind.mainLoadAbSync()); - Map groupPeers = jsonDecode(bind.mainLoadGroupSync()); - Map combinedPeers = {}; void mergePeers(Map peers) { @@ -42,8 +39,16 @@ Future> getAllPeers() async { mergePeers(recentPeers); mergePeers(lanPeers); - mergePeers(abPeers); - mergePeers(groupPeers); + for (var p in gFFI.abModel.allPeers()) { + if (!combinedPeers.containsKey(p.id)) { + combinedPeers[p.id] = p.toJson(); + } + } + for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) { + if (!combinedPeers.containsKey(p.id)) { + combinedPeers[p.id] = p.toJson(); + } + } List parsedPeers = []; @@ -181,7 +186,7 @@ class AutocompletePeerTileState extends State { ], )))); final colors = _frontN(widget.peer.tags, 25) - .map((e) => gFFI.abModel.getTagColor(e)) + .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) .toList(); return Tooltip( message: isMobile diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index 6cfa31af9..9dbdfd91b 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -2,11 +2,14 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:get/get.dart'; import 'package:qr_flutter/qr_flutter.dart'; @@ -1583,7 +1586,7 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); } -void deletePeerConfirmDialog(Function onSubmit, String title) async { +void deleteConfirmDialog(Function onSubmit, String title) async { gFFI.dialogManager.show( (setState, close, context) { submit() async { @@ -1631,7 +1634,7 @@ void editAbTagDialog( List currentTags, Function(List) onSubmit) { var isInProgress = false; - final tags = List.of(gFFI.abModel.tags); + final tags = List.of(gFFI.abModel.currentAbTags); var selectedTag = currentTags.obs; gFFI.dialogManager.show((setState, close, context) { @@ -1909,3 +1912,178 @@ void showWindowsSessionsDialog( ); }); } + +void addPeersToAbDialog( + List peers, +) async { + Future addTo(String abname) async { + final mapList = peers.map((e) { + var json = e.toJson(); + // remove shared password when add to other address book + json.remove('password'); + if (gFFI.abModel.addressbooks[abname]?.isPersonal() != true) { + json.remove('hash'); + } + return json; + }).toList(); + final errMsg = await gFFI.abModel.addPeersTo(mapList, abname); + if (errMsg == null) { + showToast(translate('Successful')); + return true; + } else { + BotToast.showText(text: errMsg, contentColor: Colors.red); + return false; + } + } + + // if only one address book and it is personal, add to it directly + if (gFFI.abModel.addressbooks.length == 1 && + gFFI.abModel.current.isPersonal()) { + await addTo(gFFI.abModel.currentName.value); + return; + } + + RxBool isInProgress = false.obs; + final names = gFFI.abModel.addressBooksCanWrite(); + RxString currentName = gFFI.abModel.currentName.value.obs; + TextEditingController controller = TextEditingController(); + if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) { + names.remove(currentName.value); + } + if (names.isEmpty) { + debugPrint('no address book to add peers to, should not happen'); + return; + } + if (!names.contains(currentName.value)) { + currentName.value = names[0]; + } + gFFI.dialogManager.show((setState, close, context) { + submit() async { + if (controller.text != gFFI.abModel.translatedName(currentName.value)) { + BotToast.showText( + text: 'illegal address book name: ${controller.text}', + contentColor: Colors.red); + return; + } + isInProgress.value = true; + if (await addTo(currentName.value)) { + close(); + } + isInProgress.value = false; + } + + cancel() { + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(IconFont.addressBook, color: MyTheme.accent), + Text(translate('Add to address book')).paddingOnly(left: 10), + ], + ), + content: Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + DropdownMenu( + initialSelection: currentName.value, + onSelected: (value) { + if (value != null) { + currentName.value = value; + } + }, + dropdownMenuEntries: names + .map((e) => DropdownMenuEntry( + value: e, label: gFFI.abModel.translatedName(e))) + .toList(), + inputDecorationTheme: InputDecorationTheme( + isDense: true, border: UnderlineInputBorder()), + enableFilter: true, + controller: controller, + ), + // NOT use Offstage to wrap LinearProgressIndicator + isInProgress.value ? const LinearProgressIndicator() : Offstage() + ], + )), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); +} + +void setSharedAbPasswordDialog(String abName, Peer peer) { + TextEditingController controller = TextEditingController(text: peer.password); + RxBool isInProgress = false.obs; + gFFI.dialogManager.show((setState, close, context) { + submit() async { + isInProgress.value = true; + bool res = await gFFI.abModel + .changeSharedPassword(abName, peer.id, controller.text); + close(); + isInProgress.value = false; + if (res) { + showToast(translate('Successful')); + } + } + + cancel() { + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.key, color: MyTheme.accent), + Text(translate('Set shared password')).paddingOnly(left: 10), + ], + ), + content: Obx(() => Column(children: [ + TextField( + controller: controller, + obscureText: true, + autofocus: true, + ), + Row(children: [ + Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), + Text( + translate('share_warning_tip'), + style: TextStyle(fontSize: 12), + ) + ]).marginSymmetric(vertical: 10), + // NOT use Offstage to wrap LinearProgressIndicator + isInProgress.value ? const LinearProgressIndicator() : Offstage() + ])), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); +} diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 18b038623..c2e64e931 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -83,7 +83,7 @@ class _MyGroupState extends State { alignment: Alignment.topLeft, child: MyGroupPeerView( menuPadding: widget.menuPadding, - initPeers: gFFI.groupModel.peers)), + getInitPeers: () => gFFI.groupModel.peers)), ) ], ); @@ -115,7 +115,7 @@ class _MyGroupState extends State { alignment: Alignment.topLeft, child: MyGroupPeerView( menuPadding: widget.menuPadding, - initPeers: gFFI.groupModel.peers)), + getInitPeers: () => gFFI.groupModel.peers)), ) ], ); diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 7c186c919..c042244dc 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart'; @@ -70,12 +71,12 @@ class _PeerCardState extends State<_PeerCard> peerTabModel.select(peer); } else { if (!isWebDesktop) { - connectInPeerTab(context, peer.id, widget.tab); + connectInPeerTab(context, peer, widget.tab); } } }, onDoubleTap: isWebDesktop - ? () => connectInPeerTab(context, peer.id, widget.tab) + ? () => connectInPeerTab(context, peer, widget.tab) : null, onLongPress: () { peerTabModel.select(peer); @@ -199,8 +200,9 @@ class _PeerCardState extends State<_PeerCard> ) ], ); - final colors = - _frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList(); + final colors = _frontN(peer.tags, 25) + .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) + .toList(); return Tooltip( message: isMobile ? '' @@ -216,6 +218,12 @@ class _PeerCardState extends State<_PeerCard> child: child, ), ), + if (_shouldBuildPasswordIcon(peer)) + Positioned( + top: 2, + left: isMobile ? 60 : 50, + child: Icon(Icons.key, size: 12), + ), if (colors.isNotEmpty) Positioned( top: 2, @@ -310,14 +318,21 @@ class _PeerCardState extends State<_PeerCard> ), ); - final colors = - _frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList(); + final colors = _frontN(peer.tags, 25) + .map((e) => gFFI.abModel.getCurrentAbTagColor(e)) + .toList(); return Tooltip( message: peer.tags.isNotEmpty ? '${translate('Tags')}: ${peer.tags.join(', ')}' : '', child: Stack(children: [ child, + if (_shouldBuildPasswordIcon(peer)) + Positioned( + top: 4, + left: 12, + child: Icon(Icons.key, size: 12), + ), if (colors.isNotEmpty) Positioned( top: 4, @@ -401,6 +416,12 @@ class _PeerCardState extends State<_PeerCard> onPointerUp: (_) => _showPeerMenu(peer.id), child: build_more(context)); + bool _shouldBuildPasswordIcon(Peer peer) { + if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) return false; + if (gFFI.abModel.current.isPersonal()) return false; + return peer.password.isNotEmpty; + } + /// Show the peer menu and handle user's choice. /// User might remove the peer or send a file to the peer. void _showPeerMenu(String id) async { @@ -431,7 +452,7 @@ abstract class BasePeerCard extends StatelessWidget { peer: peer, tab: tab, connect: (BuildContext context, String id) => - connectInPeerTab(context, id, tab), + connectInPeerTab(context, peer, tab), popupMenuEntryBuilder: _buildPopupMenuEntry, ); } @@ -453,7 +474,6 @@ abstract class BasePeerCard extends StatelessWidget { MenuEntryBase _connectCommonAction( BuildContext context, - String id, String title, { bool isFileTransfer = false, bool isTcpTunneling = false, @@ -467,7 +487,7 @@ abstract class BasePeerCard extends StatelessWidget { proc: () { connectInPeerTab( context, - peer.id, + peer, tab, isFileTransfer: isFileTransfer, isTcpTunneling: isTcpTunneling, @@ -480,10 +500,9 @@ abstract class BasePeerCard extends StatelessWidget { } @protected - MenuEntryBase _connectAction(BuildContext context, Peer peer) { + MenuEntryBase _connectAction(BuildContext context) { return _connectCommonAction( context, - peer.id, (peer.alias.isEmpty ? translate('Connect') : '${translate('Connect')} ${peer.id}'), @@ -491,20 +510,18 @@ abstract class BasePeerCard extends StatelessWidget { } @protected - MenuEntryBase _transferFileAction(BuildContext context, String id) { + MenuEntryBase _transferFileAction(BuildContext context) { return _connectCommonAction( context, - id, translate('Transfer file'), isFileTransfer: true, ); } @protected - MenuEntryBase _tcpTunnelingAction(BuildContext context, String id) { + MenuEntryBase _tcpTunnelingAction(BuildContext context) { return _connectCommonAction( context, - id, translate('TCP tunneling'), isTcpTunneling: true, ); @@ -541,7 +558,7 @@ abstract class BasePeerCard extends StatelessWidget { ], )), proc: () { - connectInPeerTab(context, id, tab, isRDP: true); + connectInPeerTab(context, peer, tab, isRDP: true); }, padding: menuPadding, dismissOnClicked: true, @@ -648,9 +665,8 @@ abstract class BasePeerCard extends StatelessWidget { onSubmit: (String newName) async { if (newName != oldName) { if (tab == PeerTabIndex.ab) { - gFFI.abModel.changeAlias(id: id, alias: newName); + await gFFI.abModel.changeAlias(id: id, alias: newName); await bind.mainSetPeerAlias(id: id, alias: newName); - gFFI.abModel.pushAb(); } else { await bind.mainSetPeerAlias(id: id, alias: newName); showToast(translate('Successful')); @@ -702,11 +718,7 @@ abstract class BasePeerCard extends StatelessWidget { await bind.mainLoadLanPeers(); break; case PeerTabIndex.ab: - gFFI.abModel.deletePeer(id); - final future = gFFI.abModel.pushAb(); - if (await bind.mainPeerExists(id: peer.id)) { - gFFI.abModel.reSyncToast(future); - } + await gFFI.abModel.deletePeers([id]); break; case PeerTabIndex.group: break; @@ -716,7 +728,7 @@ abstract class BasePeerCard extends StatelessWidget { } } - deletePeerConfirmDialog(onSubmit, + deleteConfirmDialog(onSubmit, '${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?'); }, padding: menuPadding, @@ -732,14 +744,14 @@ abstract class BasePeerCard extends StatelessWidget { style: style, ), proc: () async { - bool result = gFFI.abModel.changePassword(id, ''); + bool succ = await gFFI.abModel.changePersonalHashPassword(id, ''); await bind.mainForgetPassword(id: id); - bool toast = false; - if (result) { - toast = tab == PeerTabIndex.ab; - gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast); + if (succ) { + showToast(translate('Successful')); + } else { + BotToast.showText( + contentColor: Colors.red, text: translate("Failed")); } - if (!toast) showToast(translate('Successful')); }, padding: menuPadding, dismissOnClicked: true, @@ -824,13 +836,7 @@ abstract class BasePeerCard extends StatelessWidget { ), proc: () { () async { - if (gFFI.abModel.isFull(true)) { - return; - } - if (!gFFI.abModel.idContainBy(peer.id)) { - gFFI.abModel.addPeer(peer); - gFFI.abModel.pushAb(); - } + addPeersToAbDialog([Peer.copy(peer)]); }(); }, padding: menuPadding, @@ -858,14 +864,14 @@ class RecentPeerCard extends BasePeerCard { Future>> _buildMenuItems( BuildContext context) async { final List> menuItems = [ - _connectAction(context, peer), - _transferFileAction(context, peer.id), + _connectAction(context), + _transferFileAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); if (isDesktop && peer.platform != kPeerPlatformAndroid) { - menuItems.add(_tcpTunnelingAction(context, peer.id)); + menuItems.add(_tcpTunnelingAction(context)); } // menuItems.add(await _openNewConnInOptAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id)); @@ -888,9 +894,7 @@ class RecentPeerCard extends BasePeerCard { } if (gFFI.userModel.userName.isNotEmpty) { - if (!gFFI.abModel.idContainBy(peer.id)) { - menuItems.add(_addToAb(peer)); - } + menuItems.add(_addToAb(peer)); } menuItems.add(MenuEntryDivider()); @@ -915,11 +919,11 @@ class FavoritePeerCard extends BasePeerCard { Future>> _buildMenuItems( BuildContext context) async { final List> menuItems = [ - _connectAction(context, peer), - _transferFileAction(context, peer.id), + _connectAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { - menuItems.add(_tcpTunnelingAction(context, peer.id)); + menuItems.add(_tcpTunnelingAction(context)); } // menuItems.add(await _openNewConnInOptAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id)); @@ -939,9 +943,7 @@ class FavoritePeerCard extends BasePeerCard { })); if (gFFI.userModel.userName.isNotEmpty) { - if (!gFFI.abModel.idContainBy(peer.id)) { - menuItems.add(_addToAb(peer)); - } + menuItems.add(_addToAb(peer)); } menuItems.add(MenuEntryDivider()); @@ -966,14 +968,14 @@ class DiscoveredPeerCard extends BasePeerCard { Future>> _buildMenuItems( BuildContext context) async { final List> menuItems = [ - _connectAction(context, peer), - _transferFileAction(context, peer.id), + _connectAction(context), + _transferFileAction(context), ]; final List favs = (await bind.mainGetFav()).toList(); if (isDesktop && peer.platform != kPeerPlatformAndroid) { - menuItems.add(_tcpTunnelingAction(context, peer.id)); + menuItems.add(_tcpTunnelingAction(context)); } // menuItems.add(await _openNewConnInOptAction(peer.id)); menuItems.add(await _forceAlwaysRelayAction(peer.id)); @@ -992,9 +994,7 @@ class DiscoveredPeerCard extends BasePeerCard { } if (gFFI.userModel.userName.isNotEmpty) { - if (!gFFI.abModel.idContainBy(peer.id)) { - menuItems.add(_addToAb(peer)); - } + menuItems.add(_addToAb(peer)); } menuItems.add(MenuEntryDivider()); @@ -1019,31 +1019,45 @@ class AddressBookPeerCard extends BasePeerCard { Future>> _buildMenuItems( BuildContext context) async { final List> menuItems = [ - _connectAction(context, peer), - _transferFileAction(context, peer.id), + _connectAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { - menuItems.add(_tcpTunnelingAction(context, peer.id)); + menuItems.add(_tcpTunnelingAction(context)); } // menuItems.add(await _openNewConnInOptAction(peer.id)); - menuItems.add(await _forceAlwaysRelayAction(peer.id)); + // menuItems.add(await _forceAlwaysRelayAction(peer.id)); if (Platform.isWindows && peer.platform == kPeerPlatformWindows) { menuItems.add(_rdpAction(context, peer.id)); } if (Platform.isWindows) { menuItems.add(_createShortCutAction(peer.id)); } - menuItems.add(MenuEntryDivider()); - menuItems.add(_renameAction(peer.id)); - if (peer.hash.isNotEmpty) { - menuItems.add(_unrememberPasswordAction(peer.id)); + if (gFFI.abModel.current.canWrite()) { + menuItems.add(MenuEntryDivider()); + menuItems.add(_renameAction(peer.id)); + if (gFFI.abModel.current.isPersonal() && peer.hash.isNotEmpty) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + if (!gFFI.abModel.current.isPersonal()) { + menuItems.add(_changeSharedAbPassword()); + } + if (gFFI.abModel.currentAbTags.isNotEmpty) { + menuItems.add(_editTagAction(peer.id)); + } } - if (gFFI.abModel.tags.isNotEmpty) { - menuItems.add(_editTagAction(peer.id)); + final addressbooks = gFFI.abModel.addressBooksCanWrite(); + if (gFFI.peerTabModel.currentTab == PeerTabIndex.ab.index) { + addressbooks.remove(gFFI.abModel.currentName.value); + } + if (addressbooks.isNotEmpty) { + menuItems.add(_addToAb(peer)); + } + menuItems.add(_existIn()); + if (gFFI.abModel.current.canWrite()) { + menuItems.add(MenuEntryDivider()); + menuItems.add(_removeAction(peer.id)); } - - menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id)); return menuItems; } @@ -1060,8 +1074,7 @@ class AddressBookPeerCard extends BasePeerCard { ), proc: () { editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async { - gFFI.abModel.changeTagForPeer(id, selectedTag); - gFFI.abModel.pushAb(); + await gFFI.abModel.changeTagForPeers([id], selectedTag); }); }, padding: super.menuPadding, @@ -1073,6 +1086,52 @@ class AddressBookPeerCard extends BasePeerCard { @override Future _getAlias(String id) async => gFFI.abModel.find(id)?.alias ?? ''; + + MenuEntryBase _changeSharedAbPassword() { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Set shared password'), + style: style, + ), + proc: () { + setSharedAbPasswordDialog(gFFI.abModel.currentName.value, peer); + }, + padding: super.menuPadding, + dismissOnClicked: true, + ); + } + + MenuEntryBase _existIn() { + final names = gFFI.abModel.idExistIn(peer.id); + final text = names.join(', '); + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Exist in'), + style: style, + ), + proc: () { + gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Text(translate('Exist in')), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(text)]), + actions: [ + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: close, + ), + ], + onSubmit: close, + onCancel: close, + ); + }); + }, + padding: super.menuPadding, + dismissOnClicked: true, + ); + } } class MyGroupPeerCard extends BasePeerCard { @@ -1087,11 +1146,11 @@ class MyGroupPeerCard extends BasePeerCard { Future>> _buildMenuItems( BuildContext context) async { final List> menuItems = [ - _connectAction(context, peer), - _transferFileAction(context, peer.id), + _connectAction(context), + _transferFileAction(context), ]; if (isDesktop && peer.platform != kPeerPlatformAndroid) { - menuItems.add(_tcpTunnelingAction(context, peer.id)); + menuItems.add(_tcpTunnelingAction(context)); } // menuItems.add(await _openNewConnInOptAction(peer.id)); // menuItems.add(await _forceAlwaysRelayAction(peer.id)); @@ -1107,9 +1166,7 @@ class MyGroupPeerCard extends BasePeerCard { // menuItems.add(_unrememberPasswordAction(peer.id)); // } if (gFFI.userModel.userName.isNotEmpty) { - if (!gFFI.abModel.idContainBy(peer.id)) { - menuItems.add(_addToAb(peer)); - } + menuItems.add(_addToAb(peer)); } return menuItems; } @@ -1305,24 +1362,32 @@ class TagPainter extends CustomPainter { } } -void connectInPeerTab(BuildContext context, String id, PeerTabIndex tab, +void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, {bool isFileTransfer = false, bool isTcpTunneling = false, bool isRDP = false}) async { + var password = ''; + bool isSharedPassword = false; if (tab == PeerTabIndex.ab) { // If recent peer's alias is empty, set it to ab's alias // Because the platform is not set, it may not take effect, but it is more important not to display if the connection is not successful - Peer? p = gFFI.abModel.find(id); - if (p != null && - p.alias.isNotEmpty && - (await bind.mainGetPeerOption(id: id, key: "alias")).isEmpty) { + if (peer.alias.isNotEmpty && + (await bind.mainGetPeerOption(id: peer.id, key: "alias")).isEmpty) { await bind.mainSetPeerAlias( - id: id, - alias: p.alias, + id: peer.id, + alias: peer.alias, ); } + if (!gFFI.abModel.current.isPersonal()) { + if (peer.password.isNotEmpty) { + password = peer.password; + isSharedPassword = true; + } + } } - connect(context, id, + connect(context, peer.id, + password: password, + isSharedPassword: isSharedPassword, isFileTransfer: isFileTransfer, isTcpTunneling: isTcpTunneling, isRDP: isRDP); diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index d3f2c01cd..27ec38935 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -13,6 +13,7 @@ import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/ab_model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -392,21 +393,7 @@ class _PeerTabPageState extends State await bind.mainLoadLanPeers(); break; case 3: - { - bool hasSynced = false; - if (shouldSyncAb()) { - for (var p in peers) { - if (await bind.mainPeerExists(id: p.id)) { - hasSynced = true; - } - } - } - gFFI.abModel.deletePeers(peers.map((p) => p.id).toList()); - final future = gFFI.abModel.pushAb(); - if (hasSynced) { - gFFI.abModel.reSyncToast(future); - } - } + await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList()); break; default: break; @@ -415,7 +402,7 @@ class _PeerTabPageState extends State if (model.currentTab != 3) showToast(translate('Successful')); } - deletePeerConfirmDialog(onSubmit, translate('Delete')); + deleteConfirmDialog(onSubmit, translate('Delete')); }, child: Tooltip( message: translate('Delete'), @@ -450,24 +437,18 @@ class _PeerTabPageState extends State Widget addSelectionToAb() { final model = Provider.of(context); + final addressbooks = gFFI.abModel.addressBooksCanWrite(); + if (model.currentTab == PeerTabIndex.ab.index) { + addressbooks.remove(gFFI.abModel.currentName.value); + } return Offstage( - offstage: - !gFFI.userModel.isLogin || model.currentTab == PeerTabIndex.ab.index, + offstage: !gFFI.userModel.isLogin || addressbooks.isEmpty, child: _hoverAction( context: context, onTap: () { - if (gFFI.abModel.isFull(true)) { - return; - } - final peers = model.selectedPeers; - gFFI.abModel.addPeers(peers); - final future = gFFI.abModel.pushAb(); + final peers = model.selectedPeers.map((e) => Peer.copy(e)).toList(); + addPeersToAbDialog(peers); model.setMultiSelectionMode(false); - Future.delayed(Duration.zero, () async { - await future; - await Future.delayed(Duration(seconds: 2)); // toast - gFFI.abModel.isFull(true); - }); }, child: Tooltip( message: translate('Add to address book'), @@ -481,15 +462,14 @@ class _PeerTabPageState extends State return Offstage( offstage: !gFFI.userModel.isLogin || model.currentTab != PeerTabIndex.ab.index || - gFFI.abModel.tags.isEmpty, + gFFI.abModel.currentAbTags.isEmpty, child: _hoverAction( context: context, onTap: () { editAbTagDialog(List.empty(), (selectedTags) async { final peers = model.selectedPeers; - gFFI.abModel.changeTagForPeers( + await gFFI.abModel.changeTagForPeers( peers.map((p) => p.id).toList(), selectedTags); - gFFI.abModel.pushAb(); model.setMultiSelectionMode(false); showToast(translate('Successful')); }); @@ -556,7 +536,8 @@ class _PeerTabPageState extends State final model = Provider.of(context); return [ const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13), - _createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading), + _createRefresh( + index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), _createRefresh( index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading), Offstage( @@ -624,7 +605,8 @@ class _PeerTabPageState extends State List actions = [ const PeerSearchBar(), if (model.currentTab == PeerTabIndex.ab.index) - _createRefresh(index: PeerTabIndex.ab, loading: gFFI.abModel.abLoading), + _createRefresh( + index: PeerTabIndex.ab, loading: gFFI.abModel.currentAbLoading), if (model.currentTab == PeerTabIndex.group.index) _createRefresh( index: PeerTabIndex.group, loading: gFFI.groupModel.groupLoading), diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index d0e6e4c34..2c553d464 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -196,18 +196,25 @@ class _PeersViewState extends State<_PeersView> with WindowListener { // No need to listen the currentTab change event. // Because the currentTab change event will trigger the peers change event, // and the peers change event will trigger _buildPeersView(). - final currentTab = Provider.of(context, listen: false).currentTab; - final hideAbTagsPanel = bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty; + final currentTab = + Provider.of(context, listen: false).currentTab; + final hideAbTagsPanel = + bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty; return isDesktop ? Obx( () => SizedBox( width: peerCardUiType.value != PeerUiType.list ? 220 - : currentTab == PeerTabIndex.group.index || (currentTab == PeerTabIndex.ab.index && !hideAbTagsPanel) - ? windowWidth - 390 : - windowWidth - 227, - height: - peerCardUiType.value == PeerUiType.grid ? 140 : peerCardUiType.value != PeerUiType.list ? 42 : 45, + : currentTab == PeerTabIndex.group.index || + (currentTab == PeerTabIndex.ab.index && + !hideAbTagsPanel) + ? windowWidth - 390 + : windowWidth - 227, + height: peerCardUiType.value == PeerUiType.grid + ? 140 + : peerCardUiType.value != PeerUiType.list + ? 42 + : 45, child: visibilityChild, ), ) @@ -354,7 +361,7 @@ abstract class BasePeersView extends StatelessWidget { final String loadEvent; final PeerFilter? peerFilter; final PeerCardBuilder peerCardBuilder; - final RxList? initPeers; + final GetInitPeers? getInitPeers; const BasePeersView({ Key? key, @@ -362,13 +369,14 @@ abstract class BasePeersView extends StatelessWidget { required this.loadEvent, this.peerFilter, required this.peerCardBuilder, - required this.initPeers, + required this.getInitPeers, }) : super(key: key); @override Widget build(BuildContext context) { return _PeersView( - peers: Peers(name: name, loadEvent: loadEvent, initPeers: initPeers), + peers: + Peers(name: name, loadEvent: loadEvent, getInitPeers: getInitPeers), peerFilter: peerFilter, peerCardBuilder: peerCardBuilder); } @@ -385,7 +393,7 @@ class RecentPeersView extends BasePeersView { peer: peer, menuPadding: menuPadding, ), - initPeers: null, + getInitPeers: null, ); @override @@ -407,7 +415,7 @@ class FavoritePeersView extends BasePeersView { peer: peer, menuPadding: menuPadding, ), - initPeers: null, + getInitPeers: null, ); @override @@ -429,7 +437,7 @@ class DiscoveredPeersView extends BasePeersView { peer: peer, menuPadding: menuPadding, ), - initPeers: null, + getInitPeers: null, ); @override @@ -445,7 +453,7 @@ class AddressBookPeersView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController, - required RxList initPeers}) + required GetInitPeers getInitPeers}) : super( key: key, name: 'address book peer', @@ -456,7 +464,7 @@ class AddressBookPeersView extends BasePeersView { peer: peer, menuPadding: menuPadding, ), - initPeers: initPeers, + getInitPeers: getInitPeers, ); static bool _hitTag(List selectedTags, List idents) { @@ -486,7 +494,7 @@ class MyGroupPeerView extends BasePeersView { {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController, - required RxList initPeers}) + required GetInitPeers getInitPeers}) : super( key: key, name: 'group peer', @@ -496,7 +504,7 @@ class MyGroupPeerView extends BasePeersView { peer: peer, menuPadding: menuPadding, ), - initPeers: initPeers, + getInitPeers: getInitPeers, ); static bool filter(Peer peer) { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 2dc5161f6..4c727374e 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -359,6 +359,7 @@ class _ConnectionPageState extends State platform: '', tags: [], hash: '', + password: '', forceAlwaysRelay: false, rdpPort: '', rdpUsername: '', diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index ac5e60028..ebc3a591b 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -800,6 +800,7 @@ class _DesktopHomePageState extends State isFileTransfer: call.arguments['isFileTransfer'], isTcpTunneling: call.arguments['isTcpTunneling'], isRDP: call.arguments['isRDP'], + password: call.arguments['password'], forceRelay: call.arguments['forceRelay'], ); } else if (call.method == kWindowEventMoveTabToNewWindow) { diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 712ed0c5b..687c9520d 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -53,11 +53,13 @@ class FileManagerPage extends StatefulWidget { {Key? key, required this.id, required this.password, + required this.isSharedPassword, required this.tabController, this.forceRelay}) : super(key: key); final String id; final String? password; + final bool? isSharedPassword; final bool? forceRelay; final DesktopTabController tabController; @@ -84,6 +86,7 @@ class _FileManagerPageState extends State _ffi.start(widget.id, isFileTransfer: true, password: widget.password, + isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 5af937269..51b953560 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -45,6 +45,7 @@ class _FileManagerTabPageState extends State { key: ValueKey(params['id']), id: params['id'], password: params['password'], + isSharedPassword: params['isSharedPassword'], tabController: tabController, forceRelay: params['forceRelay'], ))); @@ -74,6 +75,7 @@ class _FileManagerTabPageState extends State { key: ValueKey(id), id: id, password: args['password'], + isSharedPassword: args['isSharedPassword'], tabController: tabController, forceRelay: args['forceRelay'], ))); diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index 0e74d4a8f..efeeb4cf8 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -25,19 +25,21 @@ class _PortForward { } class PortForwardPage extends StatefulWidget { - const PortForwardPage( - {Key? key, - required this.id, - required this.password, - required this.tabController, - required this.isRDP, - this.forceRelay}) - : super(key: key); + const PortForwardPage({ + Key? key, + required this.id, + required this.password, + required this.tabController, + required this.isRDP, + required this.isSharedPassword, + this.forceRelay, + }) : super(key: key); final String id; final String? password; final DesktopTabController tabController; final bool isRDP; final bool? forceRelay; + final bool? isSharedPassword; @override State createState() => _PortForwardPageState(); @@ -58,6 +60,7 @@ class _PortForwardPageState extends State _ffi.start(widget.id, isPortForward: true, password: widget.password, + isSharedPassword: widget.isSharedPassword, forceRelay: widget.forceRelay, isRdp: widget.isRDP); Get.put(_ffi, tag: 'pf_${widget.id}'); diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index 886b2693d..c03fdb7a7 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -44,6 +44,7 @@ class _PortForwardTabPageState extends State { key: ValueKey(params['id']), id: params['id'], password: params['password'], + isSharedPassword: params['isSharedPassword'], tabController: tabController, isRDP: isRDP, forceRelay: params['forceRelay'], @@ -79,6 +80,7 @@ class _PortForwardTabPageState extends State { key: ValueKey(args['id']), id: id, password: args['password'], + isSharedPassword: args['isSharedPassword'], isRDP: isRDP, tabController: tabController, forceRelay: args['forceRelay'], diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 398cfcf0d..740e6cfd7 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -45,6 +45,7 @@ class RemotePage extends StatefulWidget { required this.tabController, this.switchUuid, this.forceRelay, + this.isSharedPassword, }) : super(key: key); final String id; @@ -56,6 +57,7 @@ class RemotePage extends StatefulWidget { final ToolbarState toolbarState; final String? switchUuid; final bool? forceRelay; + final bool? isSharedPassword; final SimpleWrapper?> _lastState = SimpleWrapper(null); final DesktopTabController tabController; @@ -111,6 +113,7 @@ class _RemotePageState extends State _ffi.start( widget.id, password: widget.password, + isSharedPassword: widget.isSharedPassword, switchUuid: widget.switchUuid, forceRelay: widget.forceRelay, tabWindowId: widget.tabWindowId, diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index dee2d2e29..e4e9431fc 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -95,6 +95,7 @@ class _ConnectionTabPageState extends State { tabController: tabController, switchUuid: params['switch_uuid'], forceRelay: params['forceRelay'], + isSharedPassword: params['isSharedPassword'], ), )); _update_remote_count(); @@ -153,6 +154,7 @@ class _ConnectionTabPageState extends State { tabController: tabController, switchUuid: switchUuid, forceRelay: args['forceRelay'], + isSharedPassword: args['isSharedPassword'], ), )); } else if (call.method == kWindowDisableGrabKeyboard) { diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index f2755a143..09c1e7ea6 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -166,6 +166,7 @@ class _ConnectionPageState extends State { platform: '', tags: [], hash: '', + password: '', forceAlwaysRelay: false, rdpPort: '', rdpUsername: '', diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 1e9a070fe..5a504e172 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -12,8 +12,12 @@ import '../../common.dart'; import '../../common/widgets/dialog.dart'; class FileManagerPage extends StatefulWidget { - FileManagerPage({Key? key, required this.id}) : super(key: key); + FileManagerPage( + {Key? key, required this.id, this.password, this.isSharedPassword}) + : super(key: key); final String id; + final String? password; + final bool? isSharedPassword; @override State createState() => _FileManagerPageState(); @@ -68,7 +72,10 @@ class _FileManagerPageState extends State { @override void initState() { super.initState(); - gFFI.start(widget.id, isFileTransfer: true); + gFFI.start(widget.id, + isFileTransfer: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword); WidgetsBinding.instance.addPostFrameCallback((_) { gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 1e36e49b2..c22feebed 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -25,9 +25,12 @@ import '../widgets/dialog.dart'; final initText = '1' * 1024; class RemotePage extends StatefulWidget { - RemotePage({Key? key, required this.id}) : super(key: key); + RemotePage({Key? key, required this.id, this.password, this.isSharedPassword}) + : super(key: key); final String id; + final String? password; + final bool? isSharedPassword; @override State createState() => _RemotePageState(); @@ -54,7 +57,11 @@ class _RemotePageState extends State { @override void initState() { super.initState(); - gFFI.start(widget.id); + gFFI.start( + widget.id, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); gFFI.dialogManager diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 555a803ca..2c564de50 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/peer_model.dart'; @@ -28,303 +29,607 @@ bool filterAbTagByIntersection() { return bind.mainGetLocalOption(key: filterAbTagOption).isNotEmpty; } +const _personalAddressBookName = "My address book"; +const _legacyAddressBookName = "Legacy address book"; + class AbModel { - final abLoading = false.obs; - final pullError = "".obs; - final pushError = "".obs; - final tags = [].obs; - final RxMap tagColors = Map.fromEntries([]).obs; - final peers = List.empty(growable: true).obs; + final addressbooks = Map.fromEntries([]).obs; + List abProfiles = List.empty(growable: true); + final RxString _currentName = ''.obs; + RxString get currentName => _currentName; + final _dummyAb = DummyAb(); + BaseAb get current => addressbooks[_currentName.value] ?? _dummyAb; + + RxList get currentAbPeers => current.peers; + RxList get currentAbTags => current.tags; + RxList get selectedTags => current.selectedTags; + + RxBool get currentAbLoading => current.abLoading; + RxString get currentAbPullError => current.pullError; + RxString get currentAbPushError => current.pushError; + bool get currentAbEmtpy => currentAbPeers.isEmpty && currentAbTags.isEmpty; + String? _personalAbGuid; + RxBool legacyMode = true.obs; + final sortTags = shouldSortTags().obs; final filterByIntersection = filterAbTagByIntersection().obs; - final retrying = false.obs; - bool get emtpy => peers.isEmpty && tags.isEmpty; - final selectedTags = List.empty(growable: true).obs; - var initialized = false; + // licensedDevices is obtained from personal ab, shared ab restrict it in server var licensedDevices = 0; + var _syncAllFromRecent = true; var _syncFromRecentLock = false; + var _allInitialized = false; var _timerCounter = 0; var _cacheLoadOnceFlag = false; + var _everPulledProfiles = false; + // ignore: unused_field + var _maxPeerOneAb = 0; WeakReference parent; AbModel(this.parent) { + addressbooks.clear(); if (desktopType == DesktopType.main) { Timer.periodic(Duration(milliseconds: 500), (timer) async { if (_timerCounter++ % 6 == 0) { if (!gFFI.userModel.isLogin) return; - if (!initialized) return; - syncFromRecent(); + if (!_allInitialized) return; + _syncFromRecent(); } }); } } + reset() async { + print("reset ab model"); + _allInitialized = false; + abProfiles.clear(); + addressbooks.clear(); + setCurrentName(''); + await bind.mainClearAb(); + licensedDevices = 0; + _everPulledProfiles = false; + } + +// #region ab Future pullAb({force = true, quiet = false}) async { + await _pullAb(force: force, quiet: quiet); + _refreshTab(); + } + + Future _pullAb({force = true, quiet = false}) async { debugPrint("pullAb, force:$force, quiet:$quiet"); if (!gFFI.userModel.isLogin) return; - if (abLoading.value) return; - if (!force && initialized) return; - if (pushError.isNotEmpty) { - try { - // push to retry - await pushAb(toastIfFail: false, toastIfSucc: false); - } catch (_) {} - } - if (!quiet) { - abLoading.value = true; - pullError.value = ""; - } - final api = "${await bind.mainGetApiServer()}/api/ab"; - int? statusCode; + if (!force && _allInitialized) return; + _allInitialized = false; try { - var authHeaders = getHttpHeaders(); - authHeaders['Content-Type'] = "application/json"; - authHeaders['Accept-Encoding'] = "gzip"; - final resp = await http.get(Uri.parse(api), headers: authHeaders); - statusCode = resp.statusCode; - if (resp.body.toLowerCase() == "null") { - // normal reply, emtpy ab return null - tags.clear(); - tagColors.clear(); - peers.clear(); - } else if (resp.body.isNotEmpty) { - Map json = - _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode); - if (json.containsKey('error')) { - throw json['error']; - } else if (json.containsKey('data')) { - try { - gFFI.abModel.licensedDevices = json['licensed_devices']; - // ignore: empty_catches - } catch (e) {} - final data = jsonDecode(json['data']); - if (data != null) { - _deserialize(data); - _saveCache(); // save on success - } + // Get personal address book guid + _personalAbGuid = null; + await _getPersonalAbGuid(); + // Determine legacy mode based on whether _personalAbGuid is null + legacyMode.value = _personalAbGuid == null; + if (_personalAbGuid != null) { + await _getAbSettings(); + List tmpAbProfiles = List.empty(growable: true); + tmpAbProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName, + gFFI.userModel.userName.value, null, ShareRule.read.value)); + // get all address book name + await _getSharedAbProfiles(tmpAbProfiles); + abProfiles = tmpAbProfiles; + addressbooks.clear(); + for (int i = 0; i < abProfiles.length; i++) { + AbProfile p = abProfiles[i]; + addressbooks[p.name] = Ab(p, p.guid == _personalAbGuid); } - } - } catch (err) { - if (!quiet) { - pullError.value = - '${translate('pull_ab_failed_tip')}: ${translate(err.toString())}'; - } - } finally { - abLoading.value = false; - initialized = true; - _syncAllFromRecent = true; - _timerCounter = 0; - if (pullError.isNotEmpty) { - if (statusCode == 401) { - gFFI.userModel.reset(resetOther: true); - } - } - platformFFI.tryHandle({'name': LoadEvent.addressBook}); - } - } - - void addId(String id, String alias, List tags) { - if (idContainBy(id)) { - return; - } - final peer = Peer.fromJson({ - 'id': id, - 'alias': alias, - 'tags': tags, - }); - _mergePeerFromGroup(peer); - peers.add(peer); - } - - bool isFull(bool warn) { - final res = licensedDevices > 0 && peers.length >= licensedDevices; - if (res && warn) { - BotToast.showText( - contentColor: Colors.red, text: translate("exceed_max_devices")); - } - return res; - } - - void addPeer(Peer peer) { - final index = peers.indexWhere((e) => e.id == peer.id); - if (index >= 0) { - merge(peer, peers[index]); - } else { - peers.add(peer); - } - } - - bool addPeers(List ps) { - bool allAdded = true; - for (var p in ps) { - if (!isFull(false)) { - addPeer(p); } else { - allAdded = false; + // only legacy address book + addressbooks.clear(); + addressbooks[_legacyAddressBookName] = LegacyAb(); } - } - return allAdded; - } - - void addTag(String tag) async { - if (tagContainBy(tag)) { - return; - } - tags.add(tag); - } - - void changeTagForPeer(String id, List tags) { - final it = peers.where((element) => element.id == id); - if (it.isEmpty) { - return; - } - it.first.tags = tags; - } - - void changeTagForPeers(List ids, List tags) { - peers.map((e) { - if (ids.contains(e.id)) { - e.tags = tags; + // set current address book name + if (!_everPulledProfiles) { + _everPulledProfiles = true; + final name = bind.getLocalFlutterOption(k: 'current-ab-name'); + if (addressbooks.containsKey(name)) { + _currentName.value = name; + } } - }).toList(); - } - - void changeAlias({required String id, required String alias}) { - final it = peers.where((element) => element.id == id); - if (it.isEmpty) { - return; - } - it.first.alias = alias; - } - - bool changePassword(String id, String hash) { - final it = peers.where((element) => element.id == id); - if (it.isNotEmpty) { - if (it.first.hash != hash) { - it.first.hash = hash; - return true; + if (!addressbooks.containsKey(_currentName.value)) { + setCurrentName(_personalAddressBookName); } + // pull shared ab data, current first + await current.pullAb(force: force, quiet: quiet); + addressbooks.forEach((key, value) async { + if (key != current.name()) { + return await value.pullAb(force: force, quiet: quiet); + } + }); + _saveCache(); + _allInitialized = true; + _syncAllFromRecent = true; + } catch (e) { + debugPrint("pullAb error: $e"); + } + // again in case of error happens + if (!addressbooks.containsKey(_currentName.value)) { + setCurrentName(_personalAddressBookName); + } + } + + Future _getAbSettings() async { + try { + final api = "${await bind.mainGetApiServer()}/api/ab/settings"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final resp = await http.post(Uri.parse(api), headers: headers); + if (resp.statusCode == 404) { + debugPrint("HTTP 404, api server doesn't support shared address book"); + return false; + } + Map json = + _jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode); + if (json.containsKey('error')) { + throw json['error']; + } + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; + } + _maxPeerOneAb = json['max_peer_one_ab'] ?? 0; + return true; + } catch (err) { + debugPrint('get ab settings err: ${err.toString()}'); } return false; } - Future pushAb( - {bool toastIfFail = true, - bool toastIfSucc = true, - bool isRetry = false}) async { - debugPrint( - "pushAb: toastIfFail:$toastIfFail, toastIfSucc:$toastIfSucc, isRetry:$isRetry"); - if (!gFFI.userModel.isLogin) return false; - pushError.value = ''; - if (isRetry) retrying.value = true; - DateTime startTime = DateTime.now(); - bool ret = false; + Future _getPersonalAbGuid() async { try { - // avoid double pushes in a row - _syncAllFromRecent = true; - await syncFromRecent(push: false); - //https: //stackoverflow.com/questions/68249333/flutter-getx-updating-item-in-children-list-is-not-reactive - peers.refresh(); - final api = "${await bind.mainGetApiServer()}/api/ab"; - var authHeaders = getHttpHeaders(); - authHeaders['Content-Type'] = "application/json"; - final body = jsonEncode({"data": jsonEncode(_serialize())}); - http.Response resp; - // support compression - if (licensedDevices > 0 && body.length > 1024) { - authHeaders['Content-Encoding'] = "gzip"; - resp = await http.post(Uri.parse(api), - headers: authHeaders, body: GZipCodec().encode(utf8.encode(body))); - } else { - resp = - await http.post(Uri.parse(api), headers: authHeaders, body: body); + final api = "${await bind.mainGetApiServer()}/api/ab/personal"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final resp = await http.post(Uri.parse(api), headers: headers); + if (resp.statusCode == 404) { + debugPrint("HTTP 404, api server doesn't support shared address book"); + return false; } - if (resp.statusCode == 200 && - (resp.body.isEmpty || resp.body.toLowerCase() == 'null')) { - ret = true; - _saveCache(); - } else { + Map json = + _jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode); + if (json.containsKey('error')) { + throw json['error']; + } + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; + } + _personalAbGuid = json['guid']; + return true; + } catch (err) { + debugPrint('get personal ab err: ${err.toString()}'); + } + return false; + } + + Future _getSharedAbProfiles(List tmpSharedAbs) async { + final api = "${await bind.mainGetApiServer()}/api/ab/shared/profiles"; + try { + var uri0 = Uri.parse(api); + final pageSize = 100; + var total = 0; + int current = 0; + do { + current += 1; + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': current.toString(), + 'pageSize': pageSize.toString(), + }); + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final resp = await http.post(uri, headers: headers); Map json = - _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode); + _jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode); if (json.containsKey('error')) { throw json['error']; - } else if (resp.statusCode == 200) { - ret = true; - _saveCache(); - } else { + } + if (resp.statusCode != 200) { throw 'HTTP ${resp.statusCode}'; } + if (json.containsKey('total')) { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final profile in data) { + final u = AbProfile.fromJson(profile); + int index = tmpSharedAbs.indexWhere((e) => e.name == u.name); + if (index < 0) { + tmpSharedAbs.add(u); + } else { + tmpSharedAbs[index] = u; + } + } + } + } + } + } while (current * pageSize < total); + return true; + } catch (err) { + debugPrint('_getSharedAbProfiles err: ${err.toString()}'); + } + return false; + } + + Future addSharedAb(String name, String note) async { + try { + if (addressbooks.containsKey(name)) { + return '$name already exists'; } + final api = "${await bind.mainGetApiServer()}/api/ab/shared/add"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + var v = { + 'name': name, + }; + if (note.isNotEmpty) { + v['note'] = note; + } + final body = jsonEncode(v); + final resp = + await http.post(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + return errMsg; } catch (e) { - pushError.value = - '${translate('push_ab_failed_tip')}: ${translate(e.toString())}'; + return e.toString(); + } + } + + Future updateSharedAb(String guid, String name, String note) async { + try { + final api = + "${await bind.mainGetApiServer()}/api/ab/shared/update/profile"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + var v = { + 'guid': guid, + 'name': name, + }; + if (note.isNotEmpty) { + v['note'] = note; + } + final body = jsonEncode(v); + final resp = await http.put(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + return errMsg; + } catch (e) { + return e.toString(); + } + } + + Future deleteSharedAb(String name) async { + try { + final guid = abProfiles.firstWhereOrNull((e) => e.name == name)?.guid; + if (guid == null) { + return '$name not found'; + } + final api = "${await bind.mainGetApiServer()}/api/ab/shared"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode([guid]); + final resp = + await http.delete(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + return errMsg; + } catch (e) { + return e.toString(); + } + } + +// #endregion + +// #region rule + List addressBooksCanWrite() { + List list = []; + addressbooks.forEach((key, value) async { + if (value.canWrite()) { + list.add(key); + } + }); + return list; + } + + Future> getAllRules() async { + try { + List res = []; + final abGuid = current.sharedProfile()?.guid; + if (abGuid == null) { + return res; + } + final api = "${await bind.mainGetApiServer()}/api/ab/rules"; + var uri0 = Uri.parse(api); + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final pageSize = 100; + var total = 0; + int currentPage = 0; + do { + currentPage += 1; + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': currentPage.toString(), + 'pageSize': pageSize.toString(), + 'ab': abGuid, + }); + final resp = await http.post(uri, headers: headers); + Map json = + _jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode); + if (resp.statusCode == 404) { + debugPrint( + "HTTP 404, api server doesn't support shared address book"); + return res; + } + if (json.containsKey('error')) { + throw json['error']; + } + + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; + } + if (json.containsKey('total')) { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final d in data) { + final t = AbRulePayload.fromJson(d); + res.add(t); + } + } + } + } + } while (currentPage * pageSize < total); + return res; + } catch (err) { + debugPrint('get all rules err: ${err.toString()}'); + } + return []; + } + + Future addRule(String name, int level, int rule) async { + try { + final abGuid = current.sharedProfile()?.guid; + if (abGuid == null) { + return "shared profile not found"; + } + final api = "${await bind.mainGetApiServer()}/api/ab/rule"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode({ + 'ab': abGuid, + 'name': name, + 'level': level, + 'rule': rule, + }); + final resp = + await http.post(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + return errMsg; + } + return null; + } catch (err) { + return err.toString(); + } + } + + Future updateRule(String ruleGuid, int rule) async { + try { + final abGuid = current.sharedProfile()?.guid; + if (abGuid == null) { + return "shared profile not found"; + } + final api = "${await bind.mainGetApiServer()}/api/ab/rule"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode({ + 'guid': ruleGuid, + 'rule': rule, + }); + final resp = + await http.patch(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + return errMsg; + } + return null; + } catch (err) { + return err.toString(); + } + } + + Future deleteRules(List ruleGuids) async { + try { + final abGuid = current.sharedProfile()?.guid; + if (abGuid == null) { + return "shared profile not found"; + } + final api = "${await bind.mainGetApiServer()}/api/ab/rules"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode(ruleGuids); + final resp = + await http.delete(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + return errMsg; + } + return null; + } catch (err) { + return err.toString(); + } + } + + Future>> getNamesTree() async { + Map> res = Map.fromEntries([]); + try { + final abGuid = current.sharedProfile()?.guid; + if (abGuid == null) { + return res; + } + final api = "${await bind.mainGetApiServer()}/api/ab/rule/tree/$abGuid"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final resp = await http.post(Uri.parse(api), headers: headers); + if (resp.statusCode == 404) { + debugPrint("HTTP 404, api server doesn't support shared address book"); + return res; + } + Map json = + _jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode); + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; + } + json.forEach((key, value) { + if (value is List) { + res[key] = value.map((e) => e.toString()).toList(); + } + }); + return res; + } catch (err) { + debugPrint('get name tree err: ${err.toString()}'); + } + return res; + } + +// #endregion + +// #region peer + Future addIdToCurrent( + String id, String alias, String password, List tags) async { + if (currentAbPeers.where((element) => element.id == id).isNotEmpty) { + return "$id already exists in address book $_currentName"; + } + Map peer = { + 'id': id, + 'alias': alias, + 'tags': tags, + }; + // avoid set existing password to empty + if (password.isNotEmpty) { + peer['password'] = password; + } + final ret = await addPeersTo([peer], _currentName.value); + _timerCounter = 0; + return ret; + } + + // Use Map rather than Peer to distinguish between empty and null + Future addPeersTo( + List> ps, + String name, + ) async { + final ab = addressbooks[name]; + if (ab == null) { + return 'no such addressbook: $name'; + } + String? errMsg = await ab.addPeers(ps); + await pullNonLegacyAfterChange(name: name); + if (name == _currentName.value) { + _refreshTab(); } _syncAllFromRecent = true; - if (isRetry) { - var ms = - (Duration(milliseconds: 200) - DateTime.now().difference(startTime)) - .inMilliseconds; - ms = ms > 0 ? ms : 0; - Future.delayed(Duration(milliseconds: ms), () { - retrying.value = false; - }); - } + _saveCache(); + return errMsg; + } - if (!ret && toastIfFail) { - BotToast.showText(contentColor: Colors.red, text: pushError.value); + Future changeTagForPeers(List ids, List tags) async { + bool ret = await current.changeTagForPeers(ids, tags); + await pullNonLegacyAfterChange(); + currentAbPeers.refresh(); + _saveCache(); + return ret; + } + + Future changeAlias({required String id, required String alias}) async { + bool res = await current.changeAlias(id: id, alias: alias); + await pullNonLegacyAfterChange(); + currentAbPeers.refresh(); + _saveCache(); + return res; + } + + Future changePersonalHashPassword(String id, String hash) async { + var ret = false; + final personalAb = addressbooks[_personalAddressBookName]; + if (personalAb != null) { + ret = await personalAb.changePersonalHashPassword(id, hash); + await pullNonLegacyAfterChange(); + } else { + final legacyAb = addressbooks[_legacyAddressBookName]; + if (legacyAb != null) { + ret = await legacyAb.changePersonalHashPassword(id, hash); + } } - if (ret && toastIfSucc) { - showToast(translate('Successful')); + _saveCache(); + return ret; + } + + Future changeSharedPassword( + String abName, String id, String password) async { + final ret = + await addressbooks[abName]?.changeSharedPassword(id, password) ?? false; + await pullNonLegacyAfterChange(); + return ret; + } + + Future deletePeers(List ids) async { + final ret = await current.deletePeers(ids); + await pullNonLegacyAfterChange(); + currentAbPeers.refresh(); + _refreshTab(); + _saveCache(); + if (legacyMode.value && current.isPersonal()) { + // non-legacy mode not add peers automatically + Future.delayed(Duration(seconds: 2), () async { + if (!shouldSyncAb()) return; + var hasSynced = false; + for (var id in ids) { + if (await bind.mainPeerExists(id: id)) { + hasSynced = true; + break; + } + } + if (hasSynced) { + BotToast.showText( + contentColor: Colors.lightBlue, + text: translate('synced_peer_readded_tip')); + _syncAllFromRecent = true; + } + }); } return ret; } - Peer? find(String id) { - return peers.firstWhereOrNull((e) => e.id == id); +// #endregion + +// #region tags + Future addTags(List tagList) async { + final ret = await current.addTags(tagList, {}); + await pullNonLegacyAfterChange(); + _saveCache(); + return ret; } - bool idContainBy(String id) { - return peers.where((element) => element.id == id).isNotEmpty; - } - - bool tagContainBy(String tag) { - return tags.where((element) => element == tag).isNotEmpty; - } - - void deletePeer(String id) { - peers.removeWhere((element) => element.id == id); - } - - void deletePeers(List ids) { - peers.removeWhere((e) => ids.contains(e.id)); - } - - void deleteTag(String tag) { - gFFI.abModel.selectedTags.remove(tag); - tags.removeWhere((element) => element == tag); - tagColors.remove(tag); - for (var peer in peers) { - if (peer.tags.isEmpty) { - continue; - } - if (peer.tags.contains(tag)) { - ((peer.tags)).remove(tag); - } - } - } - - void renameTag(String oldTag, String newTag) { - if (tags.contains(newTag)) return; - tags.value = tags.map((e) { - if (e == oldTag) { - return newTag; - } else { - return e; - } - }).toList(); + Future renameTag(String oldTag, String newTag) async { + final ret = await current.renameTag(oldTag, newTag); + await pullNonLegacyAfterChange(); selectedTags.value = selectedTags.map((e) { if (e == oldTag) { return newTag; @@ -332,61 +637,28 @@ class AbModel { return e; } }).toList(); - for (var peer in peers) { - peer.tags = peer.tags.map((e) { - if (e == oldTag) { - return newTag; - } else { - return e; - } - }).toList(); - } - int? oldColor = tagColors[oldTag]; - if (oldColor != null) { - tagColors.remove(oldTag); - tagColors.addAll({newTag: oldColor}); - } + _saveCache(); + return ret; } - void unsetSelectedTags() { - selectedTags.clear(); + Future setTagColor(String tag, Color color) async { + final ret = await current.setTagColor(tag, color); + await pullNonLegacyAfterChange(); + _saveCache(); + return ret; } - List getPeerTags(String id) { - final it = peers.where((p0) => p0.id == id); - if (it.isEmpty) { - return []; - } else { - return it.first.tags; - } + Future deleteTag(String tag) async { + final ret = await current.deleteTag(tag); + await pullNonLegacyAfterChange(); + _saveCache(); + return ret; } - Color getTagColor(String tag) { - int? colorValue = tagColors[tag]; - if (colorValue != null) { - return Color(colorValue); - } - return str2color2(tag, existing: tagColors.values.toList()); - } +// #endregion - setTagColor(String tag, Color color) { - if (tags.contains(tag)) { - tagColors[tag] = color.value; - } - } - - void merge(Peer r, Peer p) { - p.hash = r.hash.isEmpty ? p.hash : r.hash; - p.username = r.username.isEmpty ? p.username : r.username; - p.hostname = r.hostname.isEmpty ? p.hostname : r.hostname; - p.platform = r.platform.isEmpty ? p.platform : r.platform; - p.alias = p.alias.isEmpty ? r.alias : p.alias; - p.forceAlwaysRelay = r.forceAlwaysRelay; - p.rdpPort = r.rdpPort; - p.rdpUsername = r.rdpUsername; - } - - Future syncFromRecent({bool push = true}) async { +// #region sync from recent + Future _syncFromRecent({bool push = true}) async { if (!_syncFromRecentLock) { _syncFromRecentLock = true; await _syncFromRecentWithoutLock(push: push); @@ -395,14 +667,6 @@ class AbModel { } Future _syncFromRecentWithoutLock({bool push = true}) async { - bool peerSyncEqual(Peer a, Peer b) { - return a.hash == b.hash && - a.username == b.username && - a.platform == b.platform && - a.hostname == b.hostname && - a.alias == b.alias; - } - Future> getRecentPeers() async { try { List filteredPeerIDs; @@ -431,7 +695,7 @@ class AbModel { } return recents; } catch (e) { - debugPrint('getRecentPeers:$e'); + debugPrint('getRecentPeers: $e'); } return []; } @@ -440,82 +704,640 @@ class AbModel { if (!shouldSyncAb()) return; final recents = await getRecentPeers(); if (recents.isEmpty) return; - bool uiChanged = false; - bool needSync = false; - for (var i = 0; i < recents.length; i++) { - var r = recents[i]; - var index = peers.indexWhere((e) => e.id == r.id); - if (index < 0) { - if (!isFull(false)) { - peers.add(r); - uiChanged = true; - needSync = true; - } - } else { - Peer old = Peer.copy(peers[index]); - merge(r, peers[index]); - if (!peerSyncEqual(peers[index], old)) { - needSync = true; - } - if (!old.equal(peers[index])) { - uiChanged = true; - } + debugPrint("sync from recent, len: ${recents.length}"); + addressbooks.forEach((key, value) async { + if (value.canWrite()) { + await value.syncFromRecent(recents); } - } - // Be careful with loop calls - if (needSync && push) { - pushAb(toastIfSucc: false, toastIfFail: false); - } else if (uiChanged) { - peers.refresh(); - } + }); } catch (e) { - debugPrint('syncFromRecent:$e'); + debugPrint('_syncFromRecentWithoutLock: $e'); } } + void setShouldAsync(bool v) async { + await bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : ''); + _syncAllFromRecent = true; + _timerCounter = 0; + } + +// #endregion + +// #region cache _saveCache() { try { - var m = _serialize(); - m.addAll({ + var ab_entries = _serializeCache(); + Map m = { "access_token": bind.mainGetLocalOption(key: 'access_token'), - }); + "ab_entries": ab_entries, + }; bind.mainSaveAb(json: jsonEncode(m)); } catch (e) { debugPrint('ab save:$e'); } } + List _serializeCache() { + var res = []; + addressbooks.forEach((key, value) { + res.add({ + "guid": value.sharedProfile()?.guid ?? '', + "name": key, + "tags": value.tags, + "peers": value.peers + .map((e) => value.isPersonal() + ? e.toPersonalAbUploadJson(true) + : e.toSharedAbCacheJson()) + .toList(), + "tag_colors": jsonEncode(value.tagColors) + }); + }); + return res; + } + Future loadCache() async { try { - if (_cacheLoadOnceFlag || abLoading.value || initialized) return; + if (_cacheLoadOnceFlag || currentAbLoading.value) return; _cacheLoadOnceFlag = true; final access_token = bind.mainGetLocalOption(key: 'access_token'); if (access_token.isEmpty) return; final cache = await bind.mainLoadAb(); - if (abLoading.value) return; + if (currentAbLoading.value) return; final data = jsonDecode(cache); if (data == null || data['access_token'] != access_token) return; - _deserialize(data); + _deserializeCache(data); } catch (e) { debugPrint("load ab cache: $e"); } } - Map _jsonDecodeResp(String body, int statusCode) { - try { - Map json = jsonDecode(body); - return json; - } catch (e) { - final err = body.isNotEmpty && body.length < 128 ? body : e.toString(); - if (statusCode != 200) { - throw 'HTTP $statusCode, $err'; + _deserializeCache(dynamic data) { + if (data == null) return; + reset(); + final abEntries = data['ab_entries']; + if (abEntries is List) { + for (var i = 0; i < abEntries.length; i++) { + var abEntry = abEntries[i]; + if (abEntry is Map) { + var guid = abEntry['guid']; + var name = abEntry['name']; + final BaseAb ab; + if (name == _legacyAddressBookName) { + ab = LegacyAb(); + } else { + if (name == null || guid == null) { + continue; + } + ab = Ab(AbProfile(guid, name, '', '', ShareRule.read.value), + name == _personalAddressBookName); + } + addressbooks[name] = ab; + if (abEntry['tags'] is List) { + ab.tags.value = + (abEntry['tags'] as List).map((e) => e.toString()).toList(); + } + if (abEntry['peers'] is List) { + for (var peer in abEntry['peers']) { + ab.peers.add(Peer.fromJson(peer)); + } + } + if (abEntry['tag_colors'] is String) { + Map map = jsonDecode(abEntry['tag_colors']); + ab.tagColors.value = Map.from(map); + } + } } - throw err; } } +// #endregion + +// #region tools + Peer? find(String id) { + return currentAbPeers.firstWhereOrNull((e) => e.id == id); + } + + bool idContainByCurrent(String id) { + return currentAbPeers.where((element) => element.id == id).isNotEmpty; + } + + void unsetSelectedTags() { + selectedTags.clear(); + } + + List getPeerTags(String id) { + final it = currentAbPeers.where((p0) => p0.id == id); + if (it.isEmpty) { + return []; + } else { + return it.first.tags; + } + } + + Color getCurrentAbTagColor(String tag) { + int? colorValue = current.tagColors[tag]; + if (colorValue != null) { + return Color(colorValue); + } + return str2color2(tag, existing: current.tagColors.values.toList()); + } + + List addressBookNames() { + return addressbooks.keys.toList(); + } + + void setCurrentName(String name) { + if (addressbooks.containsKey(name)) { + _currentName.value = name; + } else { + if (addressbooks.containsKey(_personalAddressBookName)) { + _currentName.value = _personalAddressBookName; + } else if (addressbooks.containsKey(_legacyAddressBookName)) { + _currentName.value = _legacyAddressBookName; + } else { + _currentName.value = ''; + } + } + _refreshTab(); + } + + bool isCurrentAbFull(bool warn) { + return current.isFull(warn); + } + + void _refreshTab() { + platformFFI.tryHandle({'name': LoadEvent.addressBook}); + } + + // should not call this function in a loop call stack + Future pullNonLegacyAfterChange({String? name}) async { + if (name == null) { + if (current.name() != _legacyAddressBookName) { + return await current.pullAb(force: true, quiet: true); + } + } else if (name != _legacyAddressBookName) { + final ab = addressbooks[name]; + if (ab != null) { + return ab.pullAb(force: true, quiet: true); + } + } + } + + List idExistIn(String id) { + List v = []; + addressbooks.forEach((key, value) { + if (value.peers.any((e) => e.id == id)) { + v.add(key); + } + }); + return v; + } + + List allPeers() { + List v = []; + addressbooks.forEach((key, value) { + v.addAll(value.peers.map((e) => Peer.copy(e)).toList()); + }); + return v; + } + + String translatedName(String name) { + if (name == _personalAddressBookName || name == _legacyAddressBookName) { + return translate(name); + } else { + return name; + } + } + +// #endregion +} + +abstract class BaseAb { + final peers = List.empty(growable: true).obs; + final RxList tags = [].obs; + final RxMap tagColors = Map.fromEntries([]).obs; + final selectedTags = List.empty(growable: true).obs; + + final pullError = "".obs; + final pushError = "".obs; + final abLoading = false.obs; + + reset() { + pullError.value = ''; + pushError.value = ''; + tags.clear(); + peers.clear(); + } + + String name(); + + bool isPersonal() { + return name() == _personalAddressBookName || + name() == _legacyAddressBookName; + } + + Future pullAb({force = true, quiet = false}) async { + if (abLoading.value) return; + if (!quiet) { + abLoading.value = true; + pullError.value = ""; + } + final ret = pullAbImpl(force: force, quiet: quiet); + abLoading.value = false; + return ret; + } + + Future pullAbImpl({force = true, quiet = false}); + + Future addPeers(List> ps); + removeHash(Map p) { + p.remove('hash'); + } + + removePassword(Map p) { + p.remove('password'); + } + + Future changeTagForPeers(List ids, List tags); + + Future changeAlias({required String id, required String alias}); + + Future changePersonalHashPassword(String id, String hash); + + Future changeSharedPassword(String id, String password); + + Future deletePeers(List ids); + + Future addTags(List tagList, Map tagColorMap); + + bool tagContainBy(String tag) { + return tags.where((element) => element == tag).isNotEmpty; + } + + Future renameTag(String oldTag, String newTag); + + Future setTagColor(String tag, Color color); + + Future deleteTag(String tag); + + bool isFull(bool warn) { + bool res; + res = gFFI.abModel.licensedDevices > 0 && + peers.length >= gFFI.abModel.licensedDevices; + if (res && warn) { + BotToast.showText( + contentColor: Colors.red, text: translate("exceed_max_devices")); + } + return res; + } + + AbProfile? sharedProfile(); + + bool canWrite(); + + bool fullControl(); + + bool allowUpdateSettingsOrDelete(); + + Future syncFromRecent(List recents); +} + +class LegacyAb extends BaseAb { + final sortTags = shouldSortTags().obs; + final filterByIntersection = filterAbTagByIntersection().obs; + bool get emtpy => peers.isEmpty && tags.isEmpty; + + LegacyAb(); + + @override + AbProfile? sharedProfile() { + return null; + } + + @override + bool canWrite() { + return true; + } + + @override + bool fullControl() { + return true; + } + + @override + bool allowUpdateSettingsOrDelete() { + return false; + } + + @override + String name() { + return _legacyAddressBookName; + } + + @override + Future pullAbImpl({force = true, quiet = false}) async { + final api = "${await bind.mainGetApiServer()}/api/ab"; + int? statusCode; + try { + var authHeaders = getHttpHeaders(); + authHeaders['Content-Type'] = "application/json"; + authHeaders['Accept-Encoding'] = "gzip"; + final resp = await http.get(Uri.parse(api), headers: authHeaders); + statusCode = resp.statusCode; + if (resp.body.toLowerCase() == "null") { + // normal reply, emtpy ab return null + tags.clear(); + tagColors.clear(); + peers.clear(); + } else if (resp.body.isNotEmpty) { + Map json = + _jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode); + if (json.containsKey('error')) { + throw json['error']; + } else if (json.containsKey('data')) { + try { + gFFI.abModel.licensedDevices = json['licensed_devices']; + // ignore: empty_catches + } catch (e) {} + final data = jsonDecode(json['data']); + if (data != null) { + _deserialize(data); + } + } + } + } catch (err) { + if (!quiet) { + pullError.value = + '${translate('pull_ab_failed_tip')}: ${translate(err.toString())}'; + } + } finally { + if (pullError.isNotEmpty) { + if (statusCode == 401) { + gFFI.userModel.reset(resetOther: true); + } + } + } + } + + Future pushAb( + {bool toastIfFail = true, bool toastIfSucc = true}) async { + debugPrint("pushAb: toastIfFail:$toastIfFail, toastIfSucc:$toastIfSucc"); + if (!gFFI.userModel.isLogin) return false; + pushError.value = ''; + bool ret = false; + try { + //https: //stackoverflow.com/questions/68249333/flutter-getx-updating-item-in-children-list-is-not-reactive + peers.refresh(); + final api = "${await bind.mainGetApiServer()}/api/ab"; + var authHeaders = getHttpHeaders(); + authHeaders['Content-Type'] = "application/json"; + final body = jsonEncode({"data": jsonEncode(_serialize())}); + http.Response resp; + // support compression + if (gFFI.abModel.licensedDevices > 0 && body.length > 1024) { + authHeaders['Content-Encoding'] = "gzip"; + resp = await http.post(Uri.parse(api), + headers: authHeaders, body: GZipCodec().encode(utf8.encode(body))); + } else { + resp = + await http.post(Uri.parse(api), headers: authHeaders, body: body); + } + if (resp.statusCode == 200 && + (resp.body.isEmpty || resp.body.toLowerCase() == 'null')) { + ret = true; + } else { + Map json = + _jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode); + if (json.containsKey('error')) { + throw json['error']; + } else if (resp.statusCode == 200) { + ret = true; + } else { + throw 'HTTP ${resp.statusCode}'; + } + } + } catch (e) { + pushError.value = + '${translate('push_ab_failed_tip')}: ${translate(e.toString())}'; + } + + if (!ret && toastIfFail) { + BotToast.showText(contentColor: Colors.red, text: pushError.value); + } + if (ret && toastIfSucc) { + showToast(translate('Successful')); + } + return ret; + } + +// #region Peer + @override + Future addPeers(List> ps) async { + bool full = false; + for (var p in ps) { + if (!isFull(false)) { + p.remove('password'); // legacy ab ignore password + final index = peers.indexWhere((e) => e.id == p['id']); + if (index >= 0) { + _merge(Peer.fromJson(p), peers[index]); + _mergePeerFromGroup(peers[index]); + } else { + peers.add(Peer.fromJson(p)); + } + } else { + full = true; + break; + } + } + if (!await pushAb()) { + return "Failed to push to server"; + } else if (full) { + return translate("exceed_max_devices"); + } else { + return null; + } + } + + _mergePeerFromGroup(Peer p) { + final g = gFFI.groupModel.peers.firstWhereOrNull((e) => p.id == e.id); + if (g == null) return; + if (p.username.isEmpty) { + p.username = g.username; + } + if (p.hostname.isEmpty) { + p.hostname = g.hostname; + } + if (p.platform.isEmpty) { + p.platform = g.platform; + } + } + + @override + Future changeTagForPeers(List ids, List tags) async { + peers.map((e) { + if (ids.contains(e.id)) { + e.tags = tags; + } + }).toList(); + return await pushAb(); + } + + @override + Future changeAlias({required String id, required String alias}) async { + final it = peers.where((element) => element.id == id); + if (it.isEmpty) { + return false; + } + it.first.alias = alias; + return await pushAb(); + } + + @override + Future changeSharedPassword(String id, String password) async { + // no need to implement + return false; + } + + @override + Future syncFromRecent(List recents) async { + bool peerSyncEqual(Peer a, Peer b) { + return a.hash == b.hash && + a.username == b.username && + a.platform == b.platform && + a.hostname == b.hostname && + a.alias == b.alias; + } + + bool needSync = false; + for (var i = 0; i < recents.length; i++) { + var r = recents[i]; + var index = peers.indexWhere((e) => e.id == r.id); + if (index < 0) { + if (!isFull(false)) { + peers.add(r); + needSync = true; + } + } else { + Peer old = Peer.copy(peers[index]); + _merge(r, peers[index]); + if (!peerSyncEqual(peers[index], old)) { + needSync = true; + } + } + } + if (needSync) { + await pushAb(toastIfSucc: false, toastIfFail: false); + gFFI.abModel._refreshTab(); + } + // Pull cannot be used for sync to avoid cyclic sync. + } + + void _merge(Peer r, Peer p) { + p.hash = r.hash.isEmpty ? p.hash : r.hash; + p.username = r.username.isEmpty ? p.username : r.username; + p.hostname = r.hostname.isEmpty ? p.hostname : r.hostname; + p.platform = r.platform.isEmpty ? p.platform : r.platform; + p.alias = p.alias.isEmpty ? r.alias : p.alias; + p.forceAlwaysRelay = r.forceAlwaysRelay; + p.rdpPort = r.rdpPort; + p.rdpUsername = r.rdpUsername; + } + + @override + Future changePersonalHashPassword(String id, String hash) async { + bool changed = false; + final it = peers.where((element) => element.id == id); + if (it.isNotEmpty) { + if (it.first.hash != hash) { + it.first.hash = hash; + changed = true; + } + } + if (changed) { + return await pushAb(toastIfSucc: false, toastIfFail: false); + } + return true; + } + + @override + Future deletePeers(List ids) async { + peers.removeWhere((e) => ids.contains(e.id)); + return await pushAb(); + } +// #endregion + +// #region Tag + @override + Future addTags( + List tagList, Map tagColorMap) async { + for (var e in tagList) { + if (!tagContainBy(e)) { + tags.add(e); + } + } + return await pushAb(); + } + + @override + Future renameTag(String oldTag, String newTag) async { + if (tags.contains(newTag)) { + BotToast.showText( + contentColor: Colors.red, text: 'Tag $newTag already exists'); + return false; + } + tags.value = tags.map((e) { + if (e == oldTag) { + return newTag; + } else { + return e; + } + }).toList(); + for (var peer in peers) { + peer.tags = peer.tags.map((e) { + if (e == oldTag) { + return newTag; + } else { + return e; + } + }).toList(); + } + int? oldColor = tagColors[oldTag]; + if (oldColor != null) { + tagColors.remove(oldTag); + tagColors.addAll({newTag: oldColor}); + } + return await pushAb(); + } + + @override + Future setTagColor(String tag, Color color) async { + if (tags.contains(tag)) { + tagColors[tag] = color.value; + } + return await pushAb(); + } + + @override + Future deleteTag(String tag) async { + gFFI.abModel.selectedTags.remove(tag); + tags.removeWhere((element) => element == tag); + tagColors.remove(tag); + for (var peer in peers) { + if (peer.tags.isEmpty) { + continue; + } + if (peer.tags.contains(tag)) { + peer.tags.remove(tag); + } + } + return await pushAb(); + } + +// #endregion + Map _serialize() { - final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList(); + final peersJsonData = + peers.map((e) => e.toPersonalAbUploadJson(true)).toList(); final tagColorJsonData = jsonEncode(tagColors); return { "tags": tags, @@ -531,7 +1353,7 @@ class AbModel { tagColors.clear(); peers.clear(); if (data['tags'] is List) { - tags.value = data['tags']; + tags.value = (data['tags'] as List).map((e) => e.toString()).toList(); } if (data['peers'] is List) { for (final peer in data['peers']) { @@ -539,7 +1361,7 @@ class AbModel { } } if (isFull(false)) { - peers.removeRange(licensedDevices, peers.length); + peers.removeRange(gFFI.abModel.licensedDevices, peers.length); } // restore online peers @@ -557,39 +1379,614 @@ class AbModel { tagColors[t] = str2color2(t, existing: tagColors.values.toList()).value; } } +} - reSyncToast(Future future) { - if (!shouldSyncAb()) return; - Future.delayed(Duration.zero, () async { - final succ = await future; - if (succ) { - await Future.delayed(Duration(seconds: 2)); // success msg - BotToast.showText( - contentColor: Colors.lightBlue, - text: translate('synced_peer_readded_tip')); +class Ab extends BaseAb { + late final AbProfile profile; + late final bool personal; + final sortTags = shouldSortTags().obs; + final filterByIntersection = filterAbTagByIntersection().obs; + bool get emtpy => peers.isEmpty && tags.isEmpty; + + Ab(this.profile, this.personal); + + @override + String name() { + if (personal) { + return _personalAddressBookName; + } else { + return profile.name; + } + } + + @override + AbProfile? sharedProfile() { + return profile; + } + + bool creatorOrAdmin() { + return profile.owner == gFFI.userModel.userName.value || + gFFI.userModel.isAdmin.value; + } + + @override + bool canWrite() { + if (personal) { + return true; + } else { + return profile.rule == ShareRule.readWrite.value || + profile.rule == ShareRule.fullControl.value; + } + } + + @override + bool fullControl() { + if (personal) { + return true; + } else { + return profile.rule == ShareRule.fullControl.value; + } + } + + @override + bool allowUpdateSettingsOrDelete() { + if (personal) { + return false; + } else { + return creatorOrAdmin(); + } + } + + @override + Future pullAbImpl({force = true, quiet = false}) async { + List tmpPeers = []; + await _fetchPeers(tmpPeers); + peers.value = tmpPeers; + List tmpTags = []; + await _fetchTags(tmpTags); + tags.value = tmpTags.map((e) => e.name).toList(); + Map tmpTagColors = {}; + for (var t in tmpTags) { + tmpTagColors[t.name] = t.color; + } + tagColors.value = tmpTagColors; + } + + Future _fetchPeers(List tmpPeers) async { + final api = "${await bind.mainGetApiServer()}/api/ab/peers"; + try { + var uri0 = Uri.parse(api); + final pageSize = 100; + var total = 0; + int current = 0; + do { + current += 1; + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': current.toString(), + 'pageSize': pageSize.toString(), + 'ab': profile.guid, + }); + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final resp = await http.post(uri, headers: headers); + Map json = + _jsonDecodeRespMap(utf8.decode(resp.bodyBytes), resp.statusCode); + if (json.containsKey('error')) { + throw json['error']; + } + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; + } + if (json.containsKey('total')) { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final profile in data) { + final u = Peer.fromJson(profile); + int index = tmpPeers.indexWhere((e) => e.id == u.id); + if (index < 0) { + tmpPeers.add(u); + } else { + tmpPeers[index] = u; + } + } + } + } + } + } while (current * pageSize < total); + return true; + } catch (err) { + debugPrint('_fetchPeers err: ${err.toString()}'); + } + return false; + } + + Future _fetchTags(List tmpTags) async { + final api = "${await bind.mainGetApiServer()}/api/ab/tags/${profile.guid}"; + try { + var uri0 = Uri.parse(api); + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + ); + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final resp = await http.post(uri, headers: headers); + List json = + _jsonDecodeRespList(utf8.decode(resp.bodyBytes), resp.statusCode); + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; } - }); + + for (final d in json) { + final t = AbTag.fromJson(d); + int index = tmpTags.indexWhere((e) => e.name == t.name); + if (index < 0) { + tmpTags.add(t); + } else { + tmpTags[index] = t; + } + } + return true; + } catch (err) { + debugPrint('_fetchTags err: ${err.toString()}'); + } + return false; } - reset() async { - pullError.value = ''; - pushError.value = ''; - tags.clear(); - peers.clear(); - await bind.mainClearAb(); +// #region Peers + @override + Future addPeers(List> ps) async { + try { + final api = + "${await bind.mainGetApiServer()}/api/ab/peer/add/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + for (var p in ps) { + if (peers.firstWhereOrNull((e) => e.id == p['id']) != null) { + continue; + } + if (isFull(false)) { + return translate("exceed_max_devices"); + } + if (personal) { + removePassword(p); + } else { + removeHash(p); + } + String body = jsonEncode(p); + final resp = + await http.post(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + return errMsg; + } + } + } catch (err) { + return err.toString(); + } + return null; } - _mergePeerFromGroup(Peer p) { - final g = gFFI.groupModel.peers.firstWhereOrNull((e) => p.id == e.id); - if (g == null) return; - if (p.username.isEmpty) { - p.username = g.username; + @override + Future changeTagForPeers(List ids, List tags) async { + try { + final api = + "${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + var ret = true; + for (var id in ids) { + final body = jsonEncode({"id": id, "tags": tags}); + final resp = + await http.put(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + ret = false; + break; + } + } + return ret; + } catch (err) { + debugPrint('changeTagForPeers err: ${err.toString()}'); + return false; } - if (p.hostname.isEmpty) { - p.hostname = g.hostname; + } + + @override + Future changeAlias({required String id, required String alias}) async { + try { + final api = + "${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode({"id": id, "alias": alias}); + final resp = await http.put(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + return false; + } + return true; + } catch (err) { + debugPrint('changeAlias err: ${err.toString()}'); + return false; } - if (p.platform.isEmpty) { - p.platform = g.platform; + } + + Future _setPassword(Object bodyContent) async { + try { + final api = + "${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode(bodyContent); + final resp = await http.put(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + return false; + } + return true; + } catch (err) { + debugPrint('changeSharedPassword err: ${err.toString()}'); + return false; } } + + @override + Future changePersonalHashPassword(String id, String hash) async { + if (!personal) return false; + if (!peers.any((e) => e.id == id)) return false; + return _setPassword({"id": id, "hash": hash}); + } + + @override + Future changeSharedPassword(String id, String password) async { + if (personal) return false; + return _setPassword({"id": id, "password": password}); + } + + @override + Future syncFromRecent(List recents) async { + bool uiUpdate = false; + bool peerSyncEqual(Peer a, Peer b) { + return a.username == b.username && + a.platform == b.platform && + a.hostname == b.hostname; + } + + Future syncOnePeer(Peer p, Peer r) async { + p.username = r.username; + p.hostname = r.hostname; + p.platform = r.platform; + final api = + "${await bind.mainGetApiServer()}/api/ab/peer/update/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode({ + "id": p.id, + "username": r.username, + "hostname": r.hostname, + "platform": r.platform + }); + final resp = await http.put(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + debugPrint('syncOnePeer errMsg: $errMsg'); + return false; + } + uiUpdate = true; + return true; + } + + try { + /* Remove this because IDs that are not on the server can't be synced, then sync will happen every startup. + // Try add new peers to personal ab + if (personal) { + for (var r in recents) { + if (peers.length < gFFI.abModel._maxPeerOneAb) { + if (!peers.any((e) => e.id == r.id)) { + var err = await addPeers([r.toPersonalAbUploadJson(true)]); + if (err == null) { + peers.add(r); + uiUpdate = true; + } + } + } + } + } + */ + final syncPeers = peers.where((p0) => p0.sameServer != true); + for (var p in syncPeers) { + Peer? r = recents.firstWhereOrNull((e) => e.id == p.id); + if (r != null) { + if (!peerSyncEqual(p, r)) { + await syncOnePeer(p, r); + } + } + } + // Pull cannot be used for sync to avoid cyclic sync. + if (uiUpdate && gFFI.abModel.currentName.value == profile.name) { + peers.refresh(); + } + } catch (err) { + debugPrint('syncFromRecent err: ${err.toString()}'); + } + } + + @override + Future deletePeers(List ids) async { + try { + final api = + "${await bind.mainGetApiServer()}/api/ab/peer/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode(ids); + final resp = + await http.delete(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + return false; + } + return true; + } catch (err) { + debugPrint('deletePeers err: ${err.toString()}'); + return false; + } + } +// #endregion + +// #region Tags + @override + Future addTags( + List tagList, Map tagColorMap) async { + try { + final api = + "${await bind.mainGetApiServer()}/api/ab/tag/add/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + for (var t in tagList) { + final body = jsonEncode({ + "name": t, + "color": tagColorMap[t] ?? + str2color2(t, existing: tagColors.values.toList()).value, + }); + final resp = + await http.post(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + return false; + } + } + return true; + } catch (err) { + debugPrint('addTags err: ${err.toString()}'); + return false; + } + } + + @override + Future renameTag(String oldTag, String newTag) async { + if (tags.contains(newTag)) { + BotToast.showText( + contentColor: Colors.red, text: 'Tag $newTag already exists'); + return false; + } + try { + final api = + "${await bind.mainGetApiServer()}/api/ab/tag/rename/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode({ + "old": oldTag, + "new": newTag, + }); + final resp = await http.put(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + return false; + } + return true; + } catch (err) { + debugPrint('renameTag err: ${err.toString()}'); + return false; + } + } + + @override + Future setTagColor(String tag, Color color) async { + try { + final api = + "${await bind.mainGetApiServer()}/api/ab/tag/update/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode({ + "name": tag, + "color": color.value, + }); + final resp = await http.put(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + return false; + } + return true; + } catch (err) { + debugPrint('setTagColor err: ${err.toString()}'); + return false; + } + } + + @override + Future deleteTag(String tag) async { + try { + final api = "${await bind.mainGetApiServer()}/api/ab/tag/${profile.guid}"; + var headers = getHttpHeaders(); + headers['Content-Type'] = "application/json"; + final body = jsonEncode([tag]); + final resp = + await http.delete(Uri.parse(api), headers: headers, body: body); + final errMsg = _jsonDecodeActionResp(resp); + if (errMsg.isNotEmpty) { + BotToast.showText(contentColor: Colors.red, text: errMsg); + return false; + } + return true; + } catch (err) { + debugPrint('deleteTag err: ${err.toString()}'); + return false; + } + } + +// #endregion +} + +// DummyAb is for current ab is null +class DummyAb extends BaseAb { + @override + Future addPeers(List> ps) async { + return "Unreachable"; + } + + @override + Future addTags( + List tagList, Map tagColorMap) async { + return false; + } + + @override + bool canWrite() { + return false; + } + + @override + bool fullControl() { + return false; + } + + @override + bool allowUpdateSettingsOrDelete() { + return false; + } + + @override + Future changeAlias({required String id, required String alias}) async { + return false; + } + + @override + Future changePersonalHashPassword(String id, String hash) async { + return false; + } + + @override + Future changeSharedPassword(String id, String password) async { + return false; + } + + @override + Future changeTagForPeers(List ids, List tags) async { + return false; + } + + @override + Future deletePeers(List ids) async { + return false; + } + + @override + Future deleteTag(String tag) async { + return false; + } + + @override + String name() { + return "Unreachable"; + } + + @override + Future pullAbImpl({force = true, quiet = false}) async {} + + @override + Future renameTag(String oldTag, String newTag) async { + return false; + } + + @override + Future setTagColor(String tag, Color color) async { + return false; + } + + @override + AbProfile? sharedProfile() { + return null; + } + + @override + Future syncFromRecent(List recents) async {} +} + +Map _jsonDecodeRespMap(String body, int statusCode) { + try { + Map json = jsonDecode(body); + return json; + } catch (e) { + final err = body.isNotEmpty && body.length < 128 ? body : e.toString(); + if (statusCode != 200) { + throw 'HTTP $statusCode, $err'; + } + throw err; + } +} + +List _jsonDecodeRespList(String body, int statusCode) { + try { + List json = jsonDecode(body); + return json; + } catch (e) { + final err = body.isNotEmpty && body.length < 128 ? body : e.toString(); + if (statusCode != 200) { + throw 'HTTP $statusCode, $err'; + } + throw err; + } +} + +String _jsonDecodeActionResp(http.Response resp) { + var errMsg = ''; + if (resp.statusCode == 200 && resp.body.isEmpty) { + // ok + } else { + try { + errMsg = jsonDecode(resp.body)['error'].toString(); + } catch (_) {} + if (errMsg.isEmpty) { + if (resp.statusCode != 200) { + errMsg = 'HTTP ${resp.statusCode}'; + } + if (resp.body.isNotEmpty) { + if (errMsg.isNotEmpty) { + errMsg += ', '; + } + errMsg += resp.body; + } + if (errMsg.isEmpty) { + errMsg = "unknown error"; + } + } + } + return errMsg; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 79401de3c..fee48765f 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -355,10 +355,8 @@ class FfiModel with ChangeNotifier { final id = evt['id']; final password = evt['password']; if (id != null && password != null) { - if (gFFI.abModel - .changePassword(id.toString(), password.toString())) { - gFFI.abModel.pushAb(toastIfFail: false, toastIfSucc: false); - } + gFFI.abModel + .changePersonalHashPassword(id.toString(), password.toString()); } } } else if (name == "cm_file_transfer_log") { @@ -2179,6 +2177,7 @@ class FFI { bool isRdp = false, String? switchUuid, String? password, + bool? isSharedPassword, bool? forceRelay, int? tabWindowId, int? display, @@ -2212,6 +2211,7 @@ class FFI { switchUuid: switchUuid ?? '', forceRelay: forceRelay ?? false, password: password ?? '', + isSharedPassword: isSharedPassword ?? false, ); } else if (display != null) { if (displays == null) { diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 1ce8648ab..8b853b3fe 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -7,7 +7,8 @@ import 'package:collection/collection.dart'; class Peer { final String id; - String hash; + String hash; // personal ab hash password + String password; // shared ab password String username; // pc username String hostname; String platform; @@ -18,6 +19,7 @@ class Peer { String rdpUsername; bool online = false; String loginName; //login username + bool? sameServer; String getId() { if (alias != '') { @@ -29,6 +31,7 @@ class Peer { Peer.fromJson(Map json) : id = json['id'] ?? '', hash = json['hash'] ?? '', + password = json['password'] ?? '', username = json['username'] ?? '', hostname = json['hostname'] ?? '', platform = json['platform'] ?? '', @@ -37,12 +40,14 @@ class Peer { forceAlwaysRelay = json['forceAlwaysRelay'] == 'true', rdpPort = json['rdpPort'] ?? '', rdpUsername = json['rdpUsername'] ?? '', - loginName = json['loginName'] ?? ''; + loginName = json['loginName'] ?? '', + sameServer = json['same_server']; Map toJson() { return { "id": id, "hash": hash, + "password": password, "username": username, "hostname": hostname, "platform": platform, @@ -52,13 +57,43 @@ class Peer { "rdpPort": rdpPort, "rdpUsername": rdpUsername, 'loginName': loginName, + 'same_server': sameServer, }; } - Map toAbUploadJson() { + Map toPersonalAbUploadJson(bool includingHash) { + var res = { + "id": id, + "username": username, + "hostname": hostname, + "platform": platform, + "alias": alias, + "tags": tags, + }; + if (includingHash) { + res['hash'] = hash; + } + return res; + } + + Map toSharedAbUploadJson(bool includingPassword) { + var res = { + "id": id, + "username": username, + "hostname": hostname, + "platform": platform, + "alias": alias, + "tags": tags, + }; + if (includingPassword) { + res['password'] = password; + } + return res; + } + + Map toSharedAbCacheJson() { return { "id": id, - "hash": hash, "username": username, "hostname": hostname, "platform": platform, @@ -80,6 +115,7 @@ class Peer { Peer({ required this.id, required this.hash, + required this.password, required this.username, required this.hostname, required this.platform, @@ -89,12 +125,14 @@ class Peer { required this.rdpPort, required this.rdpUsername, required this.loginName, + this.sameServer, }); Peer.loading() : this( id: '...', hash: '', + password: '', username: '...', hostname: '...', platform: '...', @@ -108,6 +146,7 @@ class Peer { bool equal(Peer other) { return id == other.id && hash == other.hash && + password == other.password && username == other.username && hostname == other.hostname && platform == other.platform && @@ -121,33 +160,38 @@ class Peer { Peer.copy(Peer other) : this( - id: other.id, - hash: other.hash, - username: other.username, - hostname: other.hostname, - platform: other.platform, - alias: other.alias, - tags: other.tags.toList(), - forceAlwaysRelay: other.forceAlwaysRelay, - rdpPort: other.rdpPort, - rdpUsername: other.rdpUsername, - loginName: other.loginName, - ); + id: other.id, + hash: other.hash, + password: other.password, + username: other.username, + hostname: other.hostname, + platform: other.platform, + alias: other.alias, + tags: other.tags.toList(), + forceAlwaysRelay: other.forceAlwaysRelay, + rdpPort: other.rdpPort, + rdpUsername: other.rdpUsername, + loginName: other.loginName, + sameServer: other.sameServer); } enum UpdateEvent { online, load } +typedef GetInitPeers = RxList Function(); + class Peers extends ChangeNotifier { final String name; final String loadEvent; List peers = List.empty(growable: true); - final RxList? initPeers; + final GetInitPeers? getInitPeers; UpdateEvent event = UpdateEvent.load; static const _cbQueryOnlines = 'callback_query_onlines'; Peers( - {required this.name, required this.initPeers, required this.loadEvent}) { - peers = initPeers ?? []; + {required this.name, + required this.getInitPeers, + required this.loadEvent}) { + peers = getInitPeers?.call() ?? []; platformFFI.registerEventHandler(_cbQueryOnlines, name, (evt) async { _updateOnlineState(evt); }); @@ -198,8 +242,8 @@ class Peers extends ChangeNotifier { void _updatePeers(Map evt) { final onlineStates = _getOnlineStates(); - if (initPeers != null) { - peers = initPeers!; + if (getInitPeers != null) { + peers = getInitPeers?.call() ?? []; } else { peers = _decodePeers(evt['peers']); } diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 9ad8270e4..4c498023f 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -194,6 +194,7 @@ class RustDeskMultiWindowManager { bool? forceRelay, String? switchUuid, bool? isRDP, + bool? isSharedPassword, }) async { var params = { "type": type.index, @@ -207,6 +208,9 @@ class RustDeskMultiWindowManager { if (isRDP != null) { params['isRDP'] = isRDP; } + if (isSharedPassword != null) { + params['isSharedPassword'] = isSharedPassword; + } final msg = jsonEncode(params); // separate window for file transfer is not supported @@ -228,6 +232,7 @@ class RustDeskMultiWindowManager { Future newRemoteDesktop( String remoteId, { String? password, + bool? isSharedPassword, String? switchUuid, bool? forceRelay, }) async { @@ -239,11 +244,12 @@ class RustDeskMultiWindowManager { password: password, forceRelay: forceRelay, switchUuid: switchUuid, + isSharedPassword: isSharedPassword, ); } Future newFileTransfer(String remoteId, - {String? password, bool? forceRelay}) async { + {String? password, bool? isSharedPassword, bool? forceRelay}) async { return await newSession( WindowType.FileTransfer, kWindowEventNewFileTransfer, @@ -251,11 +257,12 @@ class RustDeskMultiWindowManager { _fileTransferWindows, password: password, forceRelay: forceRelay, + isSharedPassword: isSharedPassword, ); } Future newPortForward(String remoteId, bool isRDP, - {String? password, bool? forceRelay}) async { + {String? password, bool? isSharedPassword, bool? forceRelay}) async { return await newSession( WindowType.PortForward, kWindowEventNewPortForward, @@ -264,6 +271,7 @@ class RustDeskMultiWindowManager { password: password, forceRelay: forceRelay, isRDP: isRDP, + isSharedPassword: isSharedPassword, ); } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 68210ed79..e602e11e8 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -616,6 +616,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.80.1" + flutter_simple_treeview: + dependency: "direct main" + description: + name: flutter_simple_treeview + sha256: ad4978d2668dd078d3a09966832da111bef9102dd636e572c50c80133b7ff4d9 + url: "https://pub.dev" + source: hosted + version: "3.0.2" flutter_svg: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 0ec497700..487f3a232 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -104,6 +104,7 @@ dependencies: pull_down_button: ^0.9.3 device_info_plus: ^9.1.0 qr_flutter: ^4.1.0 + flutter_simple_treeview: ^3.0.2 dev_dependencies: icons_launcher: ^2.0.4 diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 0eabf8ad6..73f32cb95 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1678,13 +1678,19 @@ pub struct AbPeer { } #[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct Ab { +pub struct AbEntry { #[serde( default, deserialize_with = "deserialize_string", skip_serializing_if = "String::is_empty" )] - pub access_token: String, + pub guid: String, + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub name: String, #[serde(default, deserialize_with = "deserialize_vec_abpeer")] pub peers: Vec, #[serde(default, deserialize_with = "deserialize_vec_string")] @@ -1697,6 +1703,24 @@ pub struct Ab { pub tag_colors: String, } +impl AbEntry { + pub fn personal(&self) -> bool { + self.name == "My address book" || self.name == "Legacy address book" + } +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct Ab { + #[serde( + default, + deserialize_with = "deserialize_string", + skip_serializing_if = "String::is_empty" + )] + pub access_token: String, + #[serde(default, deserialize_with = "deserialize_vec_abentry")] + pub ab_entries: Vec, +} + impl Ab { fn path() -> PathBuf { let filename = format!("{}_ab", APP_NAME.read().unwrap().clone()); @@ -1709,6 +1733,7 @@ impl Ab { let max_len = 64 * 1024 * 1024; if data.len() > max_len { // maxlen of function decompress + log::error!("ab data too large, {} > {}", data.len(), max_len); return; } if let Ok(data) = symmetric_crypt(&data, true) { @@ -1858,6 +1883,7 @@ deserialize_default!(deserialize_vec_string, Vec); deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>); deserialize_default!(deserialize_vec_discoverypeer, Vec); deserialize_default!(deserialize_vec_abpeer, Vec); +deserialize_default!(deserialize_vec_abentry, Vec); deserialize_default!(deserialize_vec_groupuser, Vec); deserialize_default!(deserialize_vec_grouppeer, Vec); deserialize_default!(deserialize_keypair, KeyPair); diff --git a/src/cli.rs b/src/cli.rs index 4ab8e5547..8b062f3a7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -32,11 +32,14 @@ impl Session { password, lc: Default::default(), }; - session - .lc - .write() - .unwrap() - .initialize(id.to_owned(), ConnType::PORT_FORWARD, None); + session.lc.write().unwrap().initialize( + id.to_owned(), + ConnType::PORT_FORWARD, + None, + false, + None, + None, + ); session } } diff --git a/src/client.rs b/src/client.rs index ce9c3a5c0..e03b765b2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1151,6 +1151,7 @@ pub struct LoginConfigHandler { pub mark_unsupported: Vec, pub selected_windows_session_id: Option, pub peer_info: Option, + shared_password: Option, // used to distinguish whether it is connected with a shared password } impl Deref for LoginConfigHandler { @@ -1175,6 +1176,7 @@ impl LoginConfigHandler { switch_uuid: Option, mut force_relay: bool, adapter_luid: Option, + shared_password: Option, ) { let mut id = id; if id.contains("@") { @@ -1238,6 +1240,7 @@ impl LoginConfigHandler { self.switch_uuid = switch_uuid; self.adapter_luid = adapter_luid; self.selected_windows_session_id = None; + self.shared_password = shared_password; } /// Check if the client should auto login. @@ -1827,6 +1830,8 @@ impl LoginConfigHandler { platform: pi.platform.clone(), }; let mut config = self.load_config(); + let connected_with_shared_password = self.is_connected_with_shared_password(); + let old_config_password = config.password.clone(); config.info = serde; let password = self.password.clone(); let password0 = config.password.clone(); @@ -1859,15 +1864,17 @@ impl LoginConfigHandler { } #[cfg(feature = "flutter")] { - // sync ab password with PeerConfig password - let password = base64::encode(config.password.clone(), base64::Variant::Original); - let evt: HashMap<&str, String> = HashMap::from([ - ("name", "sync_peer_password_to_ab".to_string()), - ("id", self.id.clone()), - ("password", password), - ]); - let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned()); - crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt); + if !connected_with_shared_password && remember && !config.password.is_empty() { + // sync ab password with PeerConfig password + let password = base64::encode(config.password.clone(), base64::Variant::Original); + let evt: HashMap<&str, String> = HashMap::from([ + ("name", "sync_peer_password_to_ab".to_string()), + ("id", self.id.clone()), + ("password", password), + ]); + let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned()); + crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt); + } } if config.keyboard_mode.is_empty() { if is_keyboard_mode_supported( @@ -1887,12 +1894,27 @@ impl LoginConfigHandler { config.keyboard_mode = KeyboardMode::Legacy.to_string(); } } + // keep hash password unchanged if connected with shared password + if connected_with_shared_password { + config.password = old_config_password; + } // no matter if change, for update file time self.save_config(config); self.supported_encoding = pi.encoding.clone().unwrap_or_default(); log::info!("peer info supported_encoding:{:?}", self.supported_encoding); } + fn is_connected_with_shared_password(&self) -> bool { + if let Some(shared_password) = self.shared_password.as_ref() { + let mut hasher = Sha256::new(); + hasher.update(shared_password); + hasher.update(&self.hash.salt); + let res = hasher.finalize(); + return self.password.clone()[..] == res[..]; + } + false + } + pub fn get_remote_dir(&self) -> String { serde_json::from_str::>(&self.get_option("remote_dir")) .unwrap_or_default() @@ -2621,15 +2643,17 @@ pub async fn handle_hash( let ab = hbb_common::config::Ab::load(); if !access_token.is_empty() && access_token == ab.access_token { let id = lc.read().unwrap().id.clone(); - if let Some(p) = ab - .peers - .iter() - .find_map(|p| if p.id == id { Some(p) } else { None }) - { - if let Ok(hash) = base64::decode(p.hash.clone(), base64::Variant::Original) { - if !hash.is_empty() { - password = hash; - lc.write().unwrap().save_ab_password_to_recent = true; + if let Some(ab) = ab.ab_entries.iter().find(|a| a.personal()) { + if let Some(p) = ab + .peers + .iter() + .find_map(|p| if p.id == id { Some(p) } else { None }) + { + if let Ok(hash) = base64::decode(p.hash.clone(), base64::Variant::Original) { + if !hash.is_empty() { + password = hash; + lc.write().unwrap().save_ab_password_to_recent = true; + } } } } diff --git a/src/flutter.rs b/src/flutter.rs index 5f52b85d8..af150c3f5 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1025,6 +1025,7 @@ pub fn session_add( switch_uuid: &str, force_relay: bool, password: String, + is_shared_password: bool, ) -> ResultType { let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER @@ -1050,7 +1051,7 @@ pub fn session_add( LocalConfig::set_remote_id(&id); let session: Session = Session { - password, + password: password.clone(), server_keyboard_enabled: Arc::new(RwLock::new(true)), server_file_transfer_enabled: Arc::new(RwLock::new(true)), server_clipboard_enabled: Arc::new(RwLock::new(true)), @@ -1068,12 +1069,18 @@ pub fn session_add( #[cfg(not(feature = "gpucodec"))] let adapter_luid = None; + let shared_password = if is_shared_password { + Some(password) + } else { + None + }; session.lc.write().unwrap().initialize( id.to_owned(), conn_type, switch_uuid, force_relay, adapter_luid, + shared_password, ); let session = Arc::new(session.clone()); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 9e34102a4..2a886376d 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -122,6 +122,7 @@ pub fn session_add_sync( switch_uuid: String, force_relay: bool, password: String, + is_shared_password: bool, ) -> SyncReturn { if let Err(e) = session_add( &session_id, @@ -132,6 +133,7 @@ pub fn session_add_sync( &switch_uuid, force_relay, password, + is_shared_password, ) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { @@ -1018,14 +1020,6 @@ pub fn main_load_lan_peers_sync() -> SyncReturn { return SyncReturn(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); } -pub fn main_load_ab_sync() -> SyncReturn { - return SyncReturn(serde_json::to_string(&config::Ab::load()).unwrap_or_default()); -} - -pub fn main_load_group_sync() -> SyncReturn { - return SyncReturn(serde_json::to_string(&config::Group::load()).unwrap_or_default()); -} - pub fn main_load_recent_peers_for_ab(filter: String) -> String { let id_filters = serde_json::from_str::>(&filter).unwrap_or_default(); let id_filters = if id_filters.is_empty() { diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 12cb00278..a73a9c8cf 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index aef94d4e7..07c9fcf62 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 1d3a33923..86603eb5b 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 686a86ecf..1c4af2ed8 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "请选择您要连接的会话"), ("powered_by_me", "由 RustDesk 提供支持"), ("outgoing_only_desk_tip", "当前版本的软件是定制版本。\n您可以连接至其他设备,但是其他设备无法连接至您的设备。"), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", "添加共享地址簿"), + ("Update this address book", "更新此地址簿"), + ("Delete this address book", "删除此地址簿"), + ("Share this address book", "共享此地址簿"), + ("Are you sure you want to delete address book {}?", "确定要删除地址簿{}吗?"), + ("My address book", "我的地址簿"), + ("Personal", "个人的"), + ("Owner", "所有者"), + ("Set shared password", "设置共享密码"), + ("Exist in", "存在于"), + ("Read-only", "只读"), + ("Read/Write", "读写"), + ("Full Control", "完全控制"), + ("full_control_tip", "完全控制赋予其他人与地址簿所有者相同的权限。"), + ("share_warning_tip", "上述字段是共享的并且对其他人可见。"), + ("Only show existing", "只显示存在的"), + ("Everyone", "所有人"), + ("permission_priority_tip", "优先级:用户>组>所有人"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 21f194ee3..21088a509 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Vyberte relaci, ke které se chcete připojit"), ("powered_by_me", "Poháněno společností RustDesk"), ("outgoing_only_desk_tip", "Toto je přizpůsobená edice.\nMůžete se připojit k jiným zařízením, ale jiná zařízení se k vašemu zařízení připojit nemohou."), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 1cd99bdc1..e36ddd774 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index eaf424d7b..3b71aa8a8 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Bitte wählen Sie die Sitzung, mit der Sie sich verbinden möchten"), ("powered_by_me", "Unterstützt von RustDesk"), ("outgoing_only_desk_tip", "Dies ist eine benutzerdefinierte Ausgabe.\nSie können eine Verbindung zu anderen Geräten herstellen, aber andere Geräte können keine Verbindung zu Ihrem Gerät herstellen."), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index e50e09a38..ccb369092 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index b72b93316..5f7997ca2 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -216,5 +216,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("powered_by_me", "Powered by RustDesk"), ("outgoing_only_desk_tip", "This is a customized edition.\nYou can connect to other devices, but other devices cannot connect to your device."), ("preset_password_warning", "This customized edition comes with a preset password. Anyone knowing this password could gain full control of your device. If you did not expect this, uninstall the software immediately."), + ("full_control_tip", "Full Control gives others the same permissions as the address book owner."), + ("share_warning_tip", "The fields above are shared and visible to others."), + ("permission_priority_tip", "Priority: User > Group > Everyone") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index a3ec5bd72..35501eeea 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index f33103b45..c2206b264 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Por favor, seleccione la sesión a la que se desea conectar"), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 9ece9eaee..df9359594 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 7750be1ab..08984a39a 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "لطفاً جلسه ای را که می خواهید به آن متصل شوید انتخاب کنید"), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index bf1977bba..55f755168 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 633114dff..e5780f434 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -1,218 +1,612 @@ lazy_static::lazy_static! { - pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("desk_tip", "ניתן לגשת לשולחן העבודה שלך עם מזהה וסיסמה זו."), - ("outgoing_only_desk_tip", "זוהי מהדורה מותאמת אישית.\nניתן להתחבר למכשירים אחרים, אך מכשירים אחרים לא יכולים להתחבר אליך."), - ("connecting_status", "מתחבר לרשת RustDesk..."), - ("not_ready_status", "לא מוכן. בדוק את החיבור שלך"), - ("ID/Relay Server", "שרת מזהה/ריליי"), - ("id_change_tip", "מותרים רק תווים a-z, A-Z, 0-9 ו_ (קו תחתון). האות הראשונה חייבת להיות a-z, A-Z. אורך בין 6 ל-16."), - ("Slogan_tip", "נוצר בלב בעולם הזה הכאוטי!"), - ("Build Date", "תאריך בנייה"), - ("Audio Input", "קלט שמע"), - ("Hardware Codec", "קודק חומרה"), - ("ID Server", "שרת מזהה"), - ("Relay Server", "שרת ריליי"), - ("API Server", "שרת API"), - ("invalid_http", "חייב להתחיל עם http:// או https://"), - ("server_not_support", "עדיין לא נתמך על ידי השרת"), - ("Password Required", "נדרשת סיסמה"), - ("Wrong Password", "סיסמה שגויה"), - ("Connection Error", "שגיאת חיבור"), - ("Login Error", "שגיאת התחברות"), - ("Show Hidden Files", "הצג קבצים נסתרים"), - ("Refresh File", "רענן קובץ"), - ("Remote Computer", "מחשב מרוחק"), - ("Local Computer", "מחשב מקומי"), - ("Confirm Delete", "אשר מחיקה"), - ("Multi Select", "בחירה מרובה"), - ("Select All", "בחר הכל"), - ("Unselect All", "בטל בחירת הכל"), - ("Empty Directory", "תיקייה ריקה"), - ("Custom Image Quality", "איכות תמונה מותאמת אישית"), - ("Adjust Window", "התאם חלון"), - ("Insert Lock", "הוסף נעילה"), - ("Set Password", "הגדר סיסמה"), - ("OS Password", "סיסמת מערכת הפעלה"), - ("install_tip", "בגלל UAC, RustDesk לא יכול לפעול כראוי כצד מרוחק בחלק מהמקרים. כדי להימנע מ-UAC, אנא לחץ על הכפתור למטה כדי להתקין את RustDesk במערכת."), - ("config_acc", "כדי לשלוט מרחוק בשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"נגישות\"."), - ("config_screen", "כדי לגשת מרחוק לשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"הקלטת מסך\"."), - ("Installation Path", "נתיב התקנה"), - ("agreement_tip", "על ידי התחלת ההתקנה, אתה מקבל את הסכם הרישיון."), - ("Accept and Install", "קבל והתקן"), - ("not_close_tcp_tip", "אל תסגור חלון זה בזמן שאתה משתמש במנהרה"), - ("Remote Host", "מארח מרוחק"), - ("Remote Port", "פורט מרוחק"), - ("Local Port", "פורט מקומי"), - ("Local Address", "כתובת מקומית"), - ("Change Local Port", "שנה פורט מקומי"), - ("setup_server_tip", "לחיבור מהיר יותר, אנא הגדר שרת משלך"), - ("Enter Remote ID", "הזן מזהה מרוחק"), - ("Auto Login", "התחברות אוטומטית (תקפה רק אם הגדרת \"נעל לאחר סיום הסשן\")"), - ("Change Path", "שנה נתיב"), - ("Create Folder", "צור תיקייה"), - ("whitelist_tip", "רק IP ברשימה הלבנה יכול לגשת אלי"), - ("verification_tip", "קוד אימות נשלח לכתובת הדוא\"ל הרשומה, הזן את קוד האימות כדי להמשיך בהתחברות."), - ("whitelist_sep", "מופרד על ידי פסיק, נקודה פסיק, רווחים או שורה חדשה"), - ("Add Tag", "הוסף תג"), - ("Wrong credentials", "שם משתמש או סיסמה שגויים"), - ("Edit Tag", "ערוך תג"), - ("Forget Password", "שכחת סיסמה"), - ("Add to Favorites", "הוסף למועדפים"), - ("Remove from Favorites", "הסר מהמועדפים"), - ("Socks5 Proxy", "פרוקסי Socks5"), - ("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."), - ("Are you sure to close the connection?", "האם אתה בטוח שברצונך לסגור את החיבור?"), - ("One-Finger Tap", "הקשה באצבע אחת"), - ("Left Mouse", "עכבר שמאלי"), - ("One-Long Tap", "הקשה ארוכה באצבע אחת"), - ("Two-Finger Tap", "הקשה בשתי אצבעות"), - ("Right Mouse", "עכבר ימני"), - ("One-Finger Move", "הזזה באצבע אחת"), - ("Double Tap & Move", "הקשה כפולה והזזה"), - ("Mouse Drag", "גרירת עכבר"), - ("Three-Finger vertically", "שלוש אצבעות אנכית"), - ("Mouse Wheel", "גלגלת עכבר"), - ("Two-Finger Move", "הזזה בשתי אצבעות"), - ("Canvas Move", "הזזת בד"), - ("Pinch to Zoom", "צביטה לזום"), - ("Canvas Zoom", "זום בד"), - ("Share Screen", "שיתוף מסך"), - ("Screen Capture", "לכידת מסך"), - ("Input Control", "בקרת קלט"), - ("Audio Capture", "לכידת שמע"), - ("File Connection", "חיבור קובץ"), - ("Screen Connection", "חיבור מסך"), - ("Open System Setting", "פתח הגדרת מערכת"), - ("android_input_permission_tip1", "כדי שמכשיר מרוחק יוכל לשלוט במכשיר האנדרואיד שלך באמצעות עכבר או מגע, עליך לאפשר ל-RustDesk להשתמש בשירות \"נגישות\"."), - ("android_input_permission_tip2", "אנא עבור לדף ההגדרות של המערכת הבא, מצא והכנס ל[שירותים מותקנים], הפעל את שירות [RustDesk Input]."), - ("android_new_connection_tip", "בקשת שליטה חדשה התקבלה, שרוצה לשלוט במכשירך הנוכחי."), - ("android_service_will_start_tip", "הפעלת \"לכידת מסך\" תתחיל אוטומטית את השירות, מאפשרת למכשירים אחרים לבקש חיבור למכשיר שלך."), - ("android_stop_service_tip", "סגירת השירות תסגור אוטומטית את כל החיבורים המוקמים."), - ("android_version_audio_tip", "גרסת האנדרואיד הנוכחית אינה תומכת בלכידת שמע, אנא שדרג לאנדרואיד 10 או גבוה יותר."), - ("android_start_service_tip", "הקש על [התחל שירות] או אפשר הרשאת [לכידת מסך] כדי להתחיל את שירות שיתוף המסך."), - ("android_permission_may_not_change_tip", "הרשאות עבור חיבורים שנוצרו עשויות לא להשתנות מייד עד להתחברות מחדש."), - ("doc_mac_permission", "https://rustdesk.com/docs/en/client/mac/#enable-permissions"), - ("Ignore Battery Optimizations", "התעלם מאופטימיזציות סוללה"), - ("android_open_battery_optimizations_tip", "אם ברצונך לבטל תכונה זו, אנא עבור לדף ההגדרות של יישום RustDesk הבא, מצא והכנס ל[סוללה], הסר את הסימון מ-[לא מוגבל]"), - ("remote_restarting_tip", "המכשיר המרוחק מתחיל מחדש, אנא סגור את תיבת ההודעה הזו והתחבר מחדש עם סיסמה קבועה לאחר זמן מה"), - ("Exit Fullscreen", "יציאה ממסך מלא"), - ("Mobile Actions", "פעולות ניידות"), - ("Select Monitor", "בחר מסך"), - ("Control Actions", "פעולות בקרה"), - ("Display Settings", "הגדרות תצוגה"), - ("Image Quality", "איכות תמונה"), - ("Scroll Style", "סגנון גלילה"), - ("Show Toolbar", "הצג סרגל כלים"), - ("Hide Toolbar", "הסתר סרגל כלים"), - ("Direct Connection", "חיבור ישיר"), - ("Relay Connection", "חיבור ריליי"), - ("Secure Connection", "חיבור מאובטח"), - ("Insecure Connection", "חיבור לא מאובטח"), - ("Dark Theme", "ערכת נושא כהה"), - ("Light Theme", "ערכת נושא בהירה"), - ("Follow System", "עקוב אחר המערכת"), - ("Unlock Security Settings", "פתח הגדרות אבטחה"), - ("Unlock Network Settings", "פתח הגדרות רשת"), - ("Direct IP Access", "גישה ישירה ל-IP"), - ("Audio Input Device", "מכשיר קלט שמע"), - ("Use IP Whitelisting", "השתמש ברשימת לבנה של IP"), - ("Pin Toolbar", "נעץ סרגל כלים"), - ("Unpin Toolbar", "הסר נעיצת סרגל כלים"), - ("elevated_foreground_window_tip", "החלון הנוכחי של שולחן העבודה המרוחק דורש הרשאה גבוהה יותר לפעולה, לכן אי אפשר להשתמש בעכבר ובמקלדת באופן זמני. תוכל לבקש מהמשתמש המרוחק למזער את החלון הנוכחי, או ללחוץ על כפתור ההגבהה בחלון ניהול החיבור. כדי להימנע מבעיה זו, מומלץ להתקין את התוכנה במכשיר המרוחק."), - ("Keyboard Settings", "הגדרות מקלדת"), - ("Full Access", "גישה מלאה"), - ("Screen Share", "שיתוף מסך"), - ("JumpLink", "הצג"), - ("Please Select the screen to be shared(Operate on the peer side).", "אנא בחר את המסך לשיתוף (פעולה בצד העמית)."), - ("One-time Password", "סיסמה חד-פעמית"), - ("hide_cm_tip", "אפשר הסתרה רק אם מקבלים סשנים דרך סיסמה ומשתמשים בסיסמה קבועה"), - ("wayland_experiment_tip", "תמיכה ב-Wayland נמצאת בשלב ניסיוני, אנא השתמש ב-X11 אם אתה זקוק לגישה לא מלווה."), - ("software_render_tip", "אם אתה משתמש בכרטיס גרפיקה של Nvidia תחת Linux וחלון המרחוק נסגר מיד לאחר החיבור, החלפה למנהל ההתקן הפתוח Nouveau ובחירה בשימוש בעיבוד תוכנה עשויה לעזור. נדרשת הפעלה מחדש של התוכנה."), - ("config_input", "כדי לשלוט בשולחן העבודה המרוחק באמצעות מקלדת, עליך להעניק ל-RustDesk הרשאות \"מעקב אחרי קלט\"."), - ("config_microphone", "כדי לדבר מרחוק, עליך להעניק ל-RustDesk הרשאות \"הקלטת שמע\"."), - ("request_elevation_tip", "ניתן גם לבקש הגבהה אם יש מישהו בצד המרוחק."), - ("Elevation Error", "שגיאת הגבהה"), - ("still_click_uac_tip", "עדיין דורש מהמשתמש המרוחק ללחוץ OK בחלון ה-UAC של הרצת RustDesk."), - ("Request Elevation", "בקש הגבהה"), - ("wait_accept_uac_tip", "אנא המתן למשתמש המרוחק לקבל את דיאלוג ה-UAC."), - ("Switch Sides", "החלף צדדים"), - ("Default View Style", "סגנון תצוגה ברירת מחדל"), - ("Default Scroll Style", "סגנון גלילה ברירת מחדל"), - ("Default Image Quality", "איכות תמונה ברירת מחדל"), - ("Default Codec", "קודק ברירת מחדל"), - ("Other Default Options", "אפשרויות ברירת מחדל אחרות"), - ("relay_hint_tip", "ייתכן שלא ניתן להתחבר ישירות; ניתן לנסות להתחבר דרך ריליי. בנוסף, אם ברצונך להשתמש בריליי בניסיון הראשון שלך, תוכל להוסיף את הסיומת \"/r\" למזהה או לבחור באפשרות \"התחבר תמיד דרך ריליי\" בכרטיס של הסשנים האחרונים אם קיים."), - ("RDP Settings", "הגדרות RDP"), - ("New Connection", "חיבור חדש"), - ("Your Device", "המכשיר שלך"), - ("empty_recent_tip", "אופס, אין סשנים אחרונים!\nהגיע הזמן לתכנן חדש."), - ("empty_favorite_tip", "עדיין אין עמיתים מועדפים?\nבוא נמצא מישהו להתחבר אליו ונוסיף אותו למועדפים!"), - ("empty_lan_tip", "אוי לא, נראה שעדיין לא גילינו עמיתים."), - ("empty_address_book_tip", "אוי ואבוי, נראה שכרגע אין עמיתים בספר הכתובות שלך."), - ("Empty Username", "שם משתמש ריק"), - ("Empty Password", "סיסמה ריקה"), - ("identical_file_tip", "קובץ זה זהה לקובץ של העמית."), - ("show_monitors_tip", "הצג מסכים בסרגל כלים"), - ("View Mode", "מצב תצוגה"), - ("login_linux_tip", "עליך להתחבר לחשבון Linux מרוחק כדי לאפשר פעילות שולחן עבודה X"), - ("verify_rustdesk_password_tip", "אמת סיסמת RustDesk"), - ("remember_account_tip", "זכור חשבון זה"), - ("os_account_desk_tip", "חשבון זה משמש להתחברות למערכת ההפעלה המרוחקת ולאפשר פעילות שולחן עבודה במצב לא מקוון"), - ("OS Account", "חשבון מערכת הפעלה"), - ("another_user_login_title_tip", "משתמש אחר כבר התחבר"), - ("another_user_login_text_tip", "נתק"), - ("xorg_not_found_title_tip", "Xorg לא נמצא"), - ("xorg_not_found_text_tip", "אנא התקן Xorg"), - ("no_desktop_title_tip", "אין שולחן עבודה זמין"), - ("no_desktop_text_tip", "אנא התקן שולחן עבודה GNOME"), - ("System Sound", "צליל מערכת"), - ("Copy Fingerprint", "העתק טביעת אצבע"), - ("no fingerprints", "אין טביעות אצבע"), - ("resolution_original_tip", "רזולוציה מקורית"), - ("resolution_fit_local_tip", "התאם לרזולוציה מקומית"), - ("resolution_custom_tip", "רזולוציה מותאמת אישית"), - ("Accept and Elevate", "קבל והגבה"), - ("accept_and_elevate_btn_tooltip", "קבל את החיבור והגבה הרשאות UAC."), - ("clipboard_wait_response_timeout_tip", "המתנה לתגובת העתקה הסתיימה בזמן."), - ("logout_tip", "האם אתה בטוח שברצונך להתנתק?"), - ("exceed_max_devices", "הגעת למספר המקסימלי של מכשירים שניתן לנהל."), - ("Change Password", "שנה סיסמה"), - ("Refresh Password", "רענן סיסמה"), - ("Grid View", "תצוגת רשת"), - ("List View", "תצוגת רשימה"), - ("Toggle Tags", "החלף תגיות"), - ("pull_ab_failed_tip", "נכשל ברענון ספר הכתובות"), - ("push_ab_failed_tip", "נכשל בסנכרון ספר הכתובות לשרת"), - ("synced_peer_readded_tip", "המכשירים שהיו נוכחים בסשנים האחרונים יסונכרנו בחזרה לספר הכתובות."), - ("Change Color", "שנה צבע"), - ("Primary Color", "צבע עיקרי"), - ("HSV Color", "צבע HSV"), - ("Installation Successful!", "ההתקנה הצליחה!"), - ("scam_title", "ייתכן שאתה נפלת להונאה!"), - ("scam_text1", "אם אתה בשיחת טלפון עם מישהו שאינך מכיר ואינך סומך עליו שביקש ממך להשתמש ב-RustDesk ולהתחיל את השירות, אל תמשיך ונתק מיד."), - ("scam_text2", "סביר להניח שמדובר בהונאה שמנסה לגנוב ממך כסף או מידע פרטי אחר."), - ("auto_disconnect_option_tip", "סגור באופן אוטומטי סשנים נכנסים במקרה של חוסר פעילות של המשתמש"), - ("Connection failed due to inactivity", "התנתקות אוטומטית בגלל חוסר פעילות"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "אנא שדרג את RustDesk Server Pro לגרסה {} או חדשה יותר!"), - ("pull_group_failed_tip", "נכשל ברענון קבוצה"), - ("doc_fix_wayland", "https://rustdesk.com/docs/en/client/linux/#x11-required"), - ("display_is_plugged_out_msg", "המסך הופסק, החלף למסך הראשון."), - ("elevated_switch_display_msg", "מעבר למסך הראשי מכיוון שתמיכה במסכים מרובים אינה נתמכת במצב משתמש מוגבה."), - ("selinux_tip", "SELinux מופעל במכשיר שלך, מה שעלול למנוע מ-RustDesk לפעול כראוי כצד הנשלט."), - ("id_input_tip", "ניתן להזין מזהה, IP ישיר, או דומיין עם פורט (:).\nאם ברצונך לגשת למכשיר בשרת אחר, אנא הוסף את כתובת השרת (@?key=), לדוגמה,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nאם ברצונך לגשת למכשיר בשרת ציבורי, אנא הזן \"@public\", המפתח אינו נדרש לשרת ציבורי"), - ("privacy_mode_impl_mag_tip", "מצב 1"), - ("privacy_mode_impl_virtual_display_tip", "מצב 2"), - ("idd_not_support_under_win10_2004_tip", "נהג התצוגה העקיף אינו נתמך. נדרשת גרסת Windows 10, גרסה 2004 או חדשה יותר."), - ("switch_display_elevated_connections_tip", "מעבר למסך שאינו ראשי אינו נתמך במצב משתמש מוגבה כאשר יש מספר חיבורים. אנא נסה שוב לאחר התקנה אם ברצונך לשלוט במסכים מרובים."), - ("input_source_1_tip", "מקור קלט 1"), - ("input_source_2_tip", "מקור קלט 2"), - ("capture_display_elevated_connections_tip", "לכידת מסכים מרובים אינה נתמכת במצב משתמש מוגבה. אנא נסה שוב לאחר התקנה אם ברצונך לשלוט במסכים מרובים."), - ("swap-left-right-mouse", "החלף בין כפתור העכבר השמאלי לימני"), - ("2FA code", "קוד אימות דו-שלבי"), - ("enable-2fa-title", "הפעל אימות דו-שלבי"), - ("enable-2fa-desc", "אנא הגדר כעת את האפליקציה שלך לאימות. תוכל להשתמש באפליקציית אימות כגון Authy, Microsoft או Google Authenticator בטלפון או במחשב שלך.\n\nסרוק את קוד ה-QR עם האפליקציה שלך והזן את הקוד שהאפליקציה מציגה כדי להפעיל את אימות הדו-שלבי."), - ("wrong-2fa-code", "לא ניתן לאמת את הקוד. בדוק שהקוד והגדרות הזמן המקומיות נכונות"), - ("enter-2fa-title", "אימות דו-שלבי"), - ].iter().cloned().collect(); - } +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", ""), + ("Your Desktop", ""), + ("desk_tip", "ניתן לגשת לשולחן העבודה שלך עם מזהה וסיסמה זו."), + ("Password", ""), + ("Ready", ""), + ("Established", ""), + ("connecting_status", "מתחבר לרשת RustDesk..."), + ("Enable service", ""), + ("Start service", ""), + ("Service is running", ""), + ("Service is not running", ""), + ("not_ready_status", "לא מוכן. בדוק את החיבור שלך"), + ("Control Remote Desktop", ""), + ("Transfer file", ""), + ("Connect", ""), + ("Recent sessions", ""), + ("Address book", ""), + ("Confirmation", ""), + ("TCP tunneling", ""), + ("Remove", ""), + ("Refresh random password", ""), + ("Set your own password", ""), + ("Enable keyboard/mouse", ""), + ("Enable clipboard", ""), + ("Enable file transfer", ""), + ("Enable TCP tunneling", ""), + ("IP Whitelisting", ""), + ("ID/Relay Server", "שרת מזהה/ריליי"), + ("Import server config", ""), + ("Export Server Config", ""), + ("Import server configuration successfully", ""), + ("Export server configuration successfully", ""), + ("Invalid server configuration", ""), + ("Clipboard is empty", ""), + ("Stop service", ""), + ("Change ID", ""), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "מותרים רק תווים a-z, A-Z, 0-9 ו_ (קו תחתון). האות הראשונה חייבת להיות a-z, A-Z. אורך בין 6 ל-16."), + ("Website", ""), + ("About", ""), + ("Slogan_tip", "נוצר בלב בעולם הזה הכאוטי!"), + ("Privacy Statement", ""), + ("Mute", ""), + ("Build Date", "תאריך בנייה"), + ("Version", ""), + ("Home", ""), + ("Audio Input", "קלט שמע"), + ("Enhancements", ""), + ("Hardware Codec", "קודק חומרה"), + ("Adaptive bitrate", ""), + ("ID Server", "שרת מזהה"), + ("Relay Server", "שרת ריליי"), + ("API Server", "שרת API"), + ("invalid_http", "חייב להתחיל עם http:// או https://"), + ("Invalid IP", ""), + ("Invalid format", ""), + ("server_not_support", "עדיין לא נתמך על ידי השרת"), + ("Not available", ""), + ("Too frequent", ""), + ("Cancel", ""), + ("Skip", ""), + ("Close", ""), + ("Retry", ""), + ("OK", ""), + ("Password Required", "נדרשת סיסמה"), + ("Please enter your password", ""), + ("Remember password", ""), + ("Wrong Password", "סיסמה שגויה"), + ("Do you want to enter again?", ""), + ("Connection Error", "שגיאת חיבור"), + ("Error", ""), + ("Reset by the peer", ""), + ("Connecting...", ""), + ("Connection in progress. Please wait.", ""), + ("Please try 1 minute later", ""), + ("Login Error", "שגיאת התחברות"), + ("Successful", ""), + ("Connected, waiting for image...", ""), + ("Name", ""), + ("Type", ""), + ("Modified", ""), + ("Size", ""), + ("Show Hidden Files", "הצג קבצים נסתרים"), + ("Receive", ""), + ("Send", ""), + ("Refresh File", "רענן קובץ"), + ("Local", ""), + ("Remote", ""), + ("Remote Computer", "מחשב מרוחק"), + ("Local Computer", "מחשב מקומי"), + ("Confirm Delete", "אשר מחיקה"), + ("Delete", ""), + ("Properties", ""), + ("Multi Select", "בחירה מרובה"), + ("Select All", "בחר הכל"), + ("Unselect All", "בטל בחירת הכל"), + ("Empty Directory", "תיקייה ריקה"), + ("Not an empty directory", ""), + ("Are you sure you want to delete this file?", ""), + ("Are you sure you want to delete this empty directory?", ""), + ("Are you sure you want to delete the file of this directory?", ""), + ("Do this for all conflicts", ""), + ("This is irreversible!", ""), + ("Deleting", ""), + ("files", ""), + ("Waiting", ""), + ("Finished", ""), + ("Speed", ""), + ("Custom Image Quality", "איכות תמונה מותאמת אישית"), + ("Privacy mode", ""), + ("Block user input", ""), + ("Unblock user input", ""), + ("Adjust Window", "התאם חלון"), + ("Original", ""), + ("Shrink", ""), + ("Stretch", ""), + ("Scrollbar", ""), + ("ScrollAuto", ""), + ("Good image quality", ""), + ("Balanced", ""), + ("Optimize reaction time", ""), + ("Custom", ""), + ("Show remote cursor", ""), + ("Show quality monitor", ""), + ("Disable clipboard", ""), + ("Lock after session end", ""), + ("Insert", ""), + ("Insert Lock", "הוסף נעילה"), + ("Refresh", ""), + ("ID does not exist", ""), + ("Failed to connect to rendezvous server", ""), + ("Please try later", ""), + ("Remote desktop is offline", ""), + ("Key mismatch", ""), + ("Timeout", ""), + ("Failed to connect to relay server", ""), + ("Failed to connect via rendezvous server", ""), + ("Failed to connect via relay server", ""), + ("Failed to make direct connection to remote desktop", ""), + ("Set Password", "הגדר סיסמה"), + ("OS Password", "סיסמת מערכת הפעלה"), + ("install_tip", "בגלל UAC, RustDesk לא יכול לפעול כראוי כצד מרוחק בחלק מהמקרים. כדי להימנע מ-UAC, אנא לחץ על הכפתור למטה כדי להתקין את RustDesk במערכת."), + ("Click to upgrade", ""), + ("Click to download", ""), + ("Click to update", ""), + ("Configure", ""), + ("config_acc", "כדי לשלוט מרחוק בשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"נגישות\"."), + ("config_screen", "כדי לגשת מרחוק לשולחן העבודה שלך, עליך להעניק ל-RustDesk הרשאות \"הקלטת מסך\"."), + ("Installing ...", ""), + ("Install", ""), + ("Installation", ""), + ("Installation Path", "נתיב התקנה"), + ("Create start menu shortcuts", ""), + ("Create desktop icon", ""), + ("agreement_tip", "על ידי התחלת ההתקנה, אתה מקבל את הסכם הרישיון."), + ("Accept and Install", "קבל והתקן"), + ("End-user license agreement", ""), + ("Generating ...", ""), + ("Your installation is lower version.", ""), + ("not_close_tcp_tip", "אל תסגור חלון זה בזמן שאתה משתמש במנהרה"), + ("Listening ...", ""), + ("Remote Host", "מארח מרוחק"), + ("Remote Port", "פורט מרוחק"), + ("Action", ""), + ("Add", ""), + ("Local Port", "פורט מקומי"), + ("Local Address", "כתובת מקומית"), + ("Change Local Port", "שנה פורט מקומי"), + ("setup_server_tip", "לחיבור מהיר יותר, אנא הגדר שרת משלך"), + ("Too short, at least 6 characters.", ""), + ("The confirmation is not identical.", ""), + ("Permissions", ""), + ("Accept", ""), + ("Dismiss", ""), + ("Disconnect", ""), + ("Enable file copy and paste", ""), + ("Connected", ""), + ("Direct and encrypted connection", ""), + ("Relayed and encrypted connection", ""), + ("Direct and unencrypted connection", ""), + ("Relayed and unencrypted connection", ""), + ("Enter Remote ID", "הזן מזהה מרוחק"), + ("Enter your password", ""), + ("Logging in...", ""), + ("Enable RDP session sharing", ""), + ("Auto Login", "התחברות אוטומטית (תקפה רק אם הגדרת \"נעל לאחר סיום הסשן\")"), + ("Enable direct IP access", ""), + ("Rename", ""), + ("Space", ""), + ("Create desktop shortcut", ""), + ("Change Path", "שנה נתיב"), + ("Create Folder", "צור תיקייה"), + ("Please enter the folder name", ""), + ("Fix it", ""), + ("Warning", ""), + ("Login screen using Wayland is not supported", ""), + ("Reboot required", ""), + ("Unsupported display server", ""), + ("x11 expected", ""), + ("Port", ""), + ("Settings", ""), + ("Username", ""), + ("Invalid port", ""), + ("Closed manually by the peer", ""), + ("Enable remote configuration modification", ""), + ("Run without install", ""), + ("Connect via relay", ""), + ("Always connect via relay", ""), + ("whitelist_tip", "רק IP ברשימה הלבנה יכול לגשת אלי"), + ("Login", ""), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", "קוד אימות נשלח לכתובת הדוא\"ל הרשומה, הזן את קוד האימות כדי להמשיך בהתחברות."), + ("Logout", ""), + ("Tags", ""), + ("Search ID", ""), + ("whitelist_sep", "מופרד על ידי פסיק, נקודה פסיק, רווחים או שורה חדשה"), + ("Add ID", ""), + ("Add Tag", "הוסף תג"), + ("Unselect all tags", ""), + ("Network error", ""), + ("Username missed", ""), + ("Password missed", ""), + ("Wrong credentials", "שם משתמש או סיסמה שגויים"), + ("The verification code is incorrect or has expired", ""), + ("Edit Tag", "ערוך תג"), + ("Forget Password", "שכחת סיסמה"), + ("Favorites", ""), + ("Add to Favorites", "הוסף למועדפים"), + ("Remove from Favorites", "הסר מהמועדפים"), + ("Empty", ""), + ("Invalid folder name", ""), + ("Socks5 Proxy", "פרוקסי Socks5"), + ("Hostname", ""), + ("Discovered", ""), + ("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."), + ("Remote ID", ""), + ("Paste", ""), + ("Paste here?", ""), + ("Are you sure to close the connection?", "האם אתה בטוח שברצונך לסגור את החיבור?"), + ("Download new version", ""), + ("Touch mode", ""), + ("Mouse mode", ""), + ("One-Finger Tap", "הקשה באצבע אחת"), + ("Left Mouse", "עכבר שמאלי"), + ("One-Long Tap", "הקשה ארוכה באצבע אחת"), + ("Two-Finger Tap", "הקשה בשתי אצבעות"), + ("Right Mouse", "עכבר ימני"), + ("One-Finger Move", "הזזה באצבע אחת"), + ("Double Tap & Move", "הקשה כפולה והזזה"), + ("Mouse Drag", "גרירת עכבר"), + ("Three-Finger vertically", "שלוש אצבעות אנכית"), + ("Mouse Wheel", "גלגלת עכבר"), + ("Two-Finger Move", "הזזה בשתי אצבעות"), + ("Canvas Move", "הזזת בד"), + ("Pinch to Zoom", "צביטה לזום"), + ("Canvas Zoom", "זום בד"), + ("Reset canvas", ""), + ("No permission of file transfer", ""), + ("Note", ""), + ("Connection", ""), + ("Share Screen", "שיתוף מסך"), + ("Chat", ""), + ("Total", ""), + ("items", ""), + ("Selected", ""), + ("Screen Capture", "לכידת מסך"), + ("Input Control", "בקרת קלט"), + ("Audio Capture", "לכידת שמע"), + ("File Connection", "חיבור קובץ"), + ("Screen Connection", "חיבור מסך"), + ("Do you accept?", ""), + ("Open System Setting", "פתח הגדרת מערכת"), + ("How to get Android input permission?", ""), + ("android_input_permission_tip1", "כדי שמכשיר מרוחק יוכל לשלוט במכשיר האנדרואיד שלך באמצעות עכבר או מגע, עליך לאפשר ל-RustDesk להשתמש בשירות \"נגישות\"."), + ("android_input_permission_tip2", "אנא עבור לדף ההגדרות של המערכת הבא, מצא והכנס ל[שירותים מותקנים], הפעל את שירות [RustDesk Input]."), + ("android_new_connection_tip", "בקשת שליטה חדשה התקבלה, שרוצה לשלוט במכשירך הנוכחי."), + ("android_service_will_start_tip", "הפעלת \"לכידת מסך\" תתחיל אוטומטית את השירות, מאפשרת למכשירים אחרים לבקש חיבור למכשיר שלך."), + ("android_stop_service_tip", "סגירת השירות תסגור אוטומטית את כל החיבורים המוקמים."), + ("android_version_audio_tip", "גרסת האנדרואיד הנוכחית אינה תומכת בלכידת שמע, אנא שדרג לאנדרואיד 10 או גבוה יותר."), + ("android_start_service_tip", "הקש על [התחל שירות] או אפשר הרשאת [לכידת מסך] כדי להתחיל את שירות שיתוף המסך."), + ("android_permission_may_not_change_tip", "הרשאות עבור חיבורים שנוצרו עשויות לא להשתנות מייד עד להתחברות מחדש."), + ("Account", ""), + ("Overwrite", ""), + ("This file exists, skip or overwrite this file?", ""), + ("Quit", ""), + ("Help", ""), + ("Failed", ""), + ("Succeeded", ""), + ("Someone turns on privacy mode, exit", ""), + ("Unsupported", ""), + ("Peer denied", ""), + ("Please install plugins", ""), + ("Peer exit", ""), + ("Failed to turn off", ""), + ("Turned off", ""), + ("Language", ""), + ("Keep RustDesk background service", ""), + ("Ignore Battery Optimizations", "התעלם מאופטימיזציות סוללה"), + ("android_open_battery_optimizations_tip", "אם ברצונך לבטל תכונה זו, אנא עבור לדף ההגדרות של יישום RustDesk הבא, מצא והכנס ל[סוללה], הסר את הסימון מ-[לא מוגבל]"), + ("Start on boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", ""), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), + ("Use permanent password", ""), + ("Use both passwords", ""), + ("Set permanent password", ""), + ("Enable remote restart", ""), + ("Restart remote device", ""), + ("Are you sure you want to restart", ""), + ("Restarting remote device", ""), + ("remote_restarting_tip", "המכשיר המרוחק מתחיל מחדש, אנא סגור את תיבת ההודעה הזו והתחבר מחדש עם סיסמה קבועה לאחר זמן מה"), + ("Copied", ""), + ("Exit Fullscreen", "יציאה ממסך מלא"), + ("Fullscreen", ""), + ("Mobile Actions", "פעולות ניידות"), + ("Select Monitor", "בחר מסך"), + ("Control Actions", "פעולות בקרה"), + ("Display Settings", "הגדרות תצוגה"), + ("Ratio", ""), + ("Image Quality", "איכות תמונה"), + ("Scroll Style", "סגנון גלילה"), + ("Show Toolbar", "הצג סרגל כלים"), + ("Hide Toolbar", "הסתר סרגל כלים"), + ("Direct Connection", "חיבור ישיר"), + ("Relay Connection", "חיבור ריליי"), + ("Secure Connection", "חיבור מאובטח"), + ("Insecure Connection", "חיבור לא מאובטח"), + ("Scale original", ""), + ("Scale adaptive", ""), + ("General", ""), + ("Security", ""), + ("Theme", ""), + ("Dark Theme", "ערכת נושא כהה"), + ("Light Theme", "ערכת נושא בהירה"), + ("Dark", ""), + ("Light", ""), + ("Follow System", "עקוב אחר המערכת"), + ("Enable hardware codec", ""), + ("Unlock Security Settings", "פתח הגדרות אבטחה"), + ("Enable audio", ""), + ("Unlock Network Settings", "פתח הגדרות רשת"), + ("Server", ""), + ("Direct IP Access", "גישה ישירה ל-IP"), + ("Proxy", ""), + ("Apply", ""), + ("Disconnect all devices?", ""), + ("Clear", ""), + ("Audio Input Device", "מכשיר קלט שמע"), + ("Use IP Whitelisting", "השתמש ברשימת לבנה של IP"), + ("Network", ""), + ("Pin Toolbar", "נעץ סרגל כלים"), + ("Unpin Toolbar", "הסר נעיצת סרגל כלים"), + ("Recording", ""), + ("Directory", ""), + ("Automatically record incoming sessions", ""), + ("Change", ""), + ("Start session recording", ""), + ("Stop session recording", ""), + ("Enable recording session", ""), + ("Enable LAN discovery", ""), + ("Deny LAN discovery", ""), + ("Write a message", ""), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", "החלון הנוכחי של שולחן העבודה המרוחק דורש הרשאה גבוהה יותר לפעולה, לכן אי אפשר להשתמש בעכבר ובמקלדת באופן זמני. תוכל לבקש מהמשתמש המרוחק למזער את החלון הנוכחי, או ללחוץ על כפתור ההגבהה בחלון ניהול החיבור. כדי להימנע מבעיה זו, מומלץ להתקין את התוכנה במכשיר המרוחק."), + ("Disconnected", ""), + ("Other", ""), + ("Confirm before closing multiple tabs", ""), + ("Keyboard Settings", "הגדרות מקלדת"), + ("Full Access", "גישה מלאה"), + ("Screen Share", "שיתוף מסך"), + ("Wayland requires Ubuntu 21.04 or higher version.", ""), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), + ("JumpLink", "הצג"), + ("Please Select the screen to be shared(Operate on the peer side).", "אנא בחר את המסך לשיתוף (פעולה בצד העמית)."), + ("Show RustDesk", ""), + ("This PC", ""), + ("or", ""), + ("Continue with", ""), + ("Elevate", ""), + ("Zoom cursor", ""), + ("Accept sessions via password", ""), + ("Accept sessions via click", ""), + ("Accept sessions via both", ""), + ("Please wait for the remote side to accept your session request...", ""), + ("One-time Password", "סיסמה חד-פעמית"), + ("Use one-time password", ""), + ("One-time password length", ""), + ("Request access to your device", ""), + ("Hide connection management window", ""), + ("hide_cm_tip", "אפשר הסתרה רק אם מקבלים סשנים דרך סיסמה ומשתמשים בסיסמה קבועה"), + ("wayland_experiment_tip", "תמיכה ב-Wayland נמצאת בשלב ניסיוני, אנא השתמש ב-X11 אם אתה זקוק לגישה לא מלווה."), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to address book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", "אם אתה משתמש בכרטיס גרפיקה של Nvidia תחת Linux וחלון המרחוק נסגר מיד לאחר החיבור, החלפה למנהל ההתקן הפתוח Nouveau ובחירה בשימוש בעיבוד תוכנה עשויה לעזור. נדרשת הפעלה מחדש של התוכנה."), + ("Always use software rendering", ""), + ("config_input", "כדי לשלוט בשולחן העבודה המרוחק באמצעות מקלדת, עליך להעניק ל-RustDesk הרשאות \"מעקב אחרי קלט\"."), + ("config_microphone", "כדי לדבר מרחוק, עליך להעניק ל-RustDesk הרשאות \"הקלטת שמע\"."), + ("request_elevation_tip", "ניתן גם לבקש הגבהה אם יש מישהו בצד המרוחק."), + ("Wait", ""), + ("Elevation Error", "שגיאת הגבהה"), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", "עדיין דורש מהמשתמש המרוחק ללחוץ OK בחלון ה-UAC של הרצת RustDesk."), + ("Request Elevation", "בקש הגבהה"), + ("wait_accept_uac_tip", "אנא המתן למשתמש המרוחק לקבל את דיאלוג ה-UAC."), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", "החלף צדדים"), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", "סגנון תצוגה ברירת מחדל"), + ("Default Scroll Style", "סגנון גלילה ברירת מחדל"), + ("Default Image Quality", "איכות תמונה ברירת מחדל"), + ("Default Codec", "קודק ברירת מחדל"), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", "אפשרויות ברירת מחדל אחרות"), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", "ייתכן שלא ניתן להתחבר ישירות; ניתן לנסות להתחבר דרך ריליי. בנוסף, אם ברצונך להשתמש בריליי בניסיון הראשון שלך, תוכל להוסיף את הסיומת \"/r\" למזהה או לבחור באפשרות \"התחבר תמיד דרך ריליי\" בכרטיס של הסשנים האחרונים אם קיים."), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("RDP Settings", "הגדרות RDP"), + ("Sort by", ""), + ("New Connection", "חיבור חדש"), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", "המכשיר שלך"), + ("empty_recent_tip", "אופס, אין סשנים אחרונים!\nהגיע הזמן לתכנן חדש."), + ("empty_favorite_tip", "עדיין אין עמיתים מועדפים?\nבוא נמצא מישהו להתחבר אליו ונוסיף אותו למועדפים!"), + ("empty_lan_tip", "אוי לא, נראה שעדיין לא גילינו עמיתים."), + ("empty_address_book_tip", "אוי ואבוי, נראה שכרגע אין עמיתים בספר הכתובות שלך."), + ("eg: admin", ""), + ("Empty Username", "שם משתמש ריק"), + ("Empty Password", "סיסמה ריקה"), + ("Me", ""), + ("identical_file_tip", "קובץ זה זהה לקובץ של העמית."), + ("show_monitors_tip", "הצג מסכים בסרגל כלים"), + ("View Mode", "מצב תצוגה"), + ("login_linux_tip", "עליך להתחבר לחשבון Linux מרוחק כדי לאפשר פעילות שולחן עבודה X"), + ("verify_rustdesk_password_tip", "אמת סיסמת RustDesk"), + ("remember_account_tip", "זכור חשבון זה"), + ("os_account_desk_tip", "חשבון זה משמש להתחברות למערכת ההפעלה המרוחקת ולאפשר פעילות שולחן עבודה במצב לא מקוון"), + ("OS Account", "חשבון מערכת הפעלה"), + ("another_user_login_title_tip", "משתמש אחר כבר התחבר"), + ("another_user_login_text_tip", "נתק"), + ("xorg_not_found_title_tip", "Xorg לא נמצא"), + ("xorg_not_found_text_tip", "אנא התקן Xorg"), + ("no_desktop_title_tip", "אין שולחן עבודה זמין"), + ("no_desktop_text_tip", "אנא התקן שולחן עבודה GNOME"), + ("No need to elevate", ""), + ("System Sound", "צליל מערכת"), + ("Default", ""), + ("New RDP", ""), + ("Fingerprint", ""), + ("Copy Fingerprint", "העתק טביעת אצבע"), + ("no fingerprints", "אין טביעות אצבע"), + ("Select a peer", ""), + ("Select peers", ""), + ("Plugins", ""), + ("Uninstall", ""), + ("Update", ""), + ("Enable", ""), + ("Disable", ""), + ("Options", ""), + ("resolution_original_tip", "רזולוציה מקורית"), + ("resolution_fit_local_tip", "התאם לרזולוציה מקומית"), + ("resolution_custom_tip", "רזולוציה מותאמת אישית"), + ("Collapse toolbar", ""), + ("Accept and Elevate", "קבל והגבה"), + ("accept_and_elevate_btn_tooltip", "קבל את החיבור והגבה הרשאות UAC."), + ("clipboard_wait_response_timeout_tip", "המתנה לתגובת העתקה הסתיימה בזמן."), + ("Incoming connection", ""), + ("Outgoing connection", ""), + ("Exit", ""), + ("Open", ""), + ("logout_tip", "האם אתה בטוח שברצונך להתנתק?"), + ("Service", ""), + ("Start", ""), + ("Stop", ""), + ("exceed_max_devices", "הגעת למספר המקסימלי של מכשירים שניתן לנהל."), + ("Sync with recent sessions", ""), + ("Sort tags", ""), + ("Open connection in new tab", ""), + ("Move tab to new window", ""), + ("Can not be empty", ""), + ("Already exists", ""), + ("Change Password", "שנה סיסמה"), + ("Refresh Password", "רענן סיסמה"), + ("ID", ""), + ("Grid View", "תצוגת רשת"), + ("List View", "תצוגת רשימה"), + ("Select", ""), + ("Toggle Tags", "החלף תגיות"), + ("pull_ab_failed_tip", "נכשל ברענון ספר הכתובות"), + ("push_ab_failed_tip", "נכשל בסנכרון ספר הכתובות לשרת"), + ("synced_peer_readded_tip", "המכשירים שהיו נוכחים בסשנים האחרונים יסונכרנו בחזרה לספר הכתובות."), + ("Change Color", "שנה צבע"), + ("Primary Color", "צבע עיקרי"), + ("HSV Color", "צבע HSV"), + ("Installation Successful!", "ההתקנה הצליחה!"), + ("Installation failed!", ""), + ("Reverse mouse wheel", ""), + ("{} sessions", ""), + ("scam_title", "ייתכן שאתה נפלת להונאה!"), + ("scam_text1", "אם אתה בשיחת טלפון עם מישהו שאינך מכיר ואינך סומך עליו שביקש ממך להשתמש ב-RustDesk ולהתחיל את השירות, אל תמשיך ונתק מיד."), + ("scam_text2", "סביר להניח שמדובר בהונאה שמנסה לגנוב ממך כסף או מידע פרטי אחר."), + ("Don't show again", ""), + ("I Agree", ""), + ("Decline", ""), + ("Timeout in minutes", ""), + ("auto_disconnect_option_tip", "סגור באופן אוטומטי סשנים נכנסים במקרה של חוסר פעילות של המשתמש"), + ("Connection failed due to inactivity", "התנתקות אוטומטית בגלל חוסר פעילות"), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", "אנא שדרג את RustDesk Server Pro לגרסה {} או חדשה יותר!"), + ("pull_group_failed_tip", "נכשל ברענון קבוצה"), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", "המסך הופסק, החלף למסך הראשון."), + ("No displays", ""), + ("elevated_switch_display_msg", "מעבר למסך הראשי מכיוון שתמיכה במסכים מרובים אינה נתמכת במצב משתמש מוגבה."), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", "SELinux מופעל במכשיר שלך, מה שעלול למנוע מ-RustDesk לפעול כראוי כצד הנשלט."), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", "ניתן להזין מזהה, IP ישיר, או דומיין עם פורט (:).\nאם ברצונך לגשת למכשיר בשרת אחר, אנא הוסף את כתובת השרת (@?key=), לדוגמה,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nאם ברצונך לגשת למכשיר בשרת ציבורי, אנא הזן \"@public\", המפתח אינו נדרש לשרת ציבורי"), + ("privacy_mode_impl_mag_tip", "מצב 1"), + ("privacy_mode_impl_virtual_display_tip", "מצב 2"), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), + ("idd_not_support_under_win10_2004_tip", "נהג התצוגה העקיף אינו נתמך. נדרשת גרסת Windows 10, גרסה 2004 או חדשה יותר."), + ("switch_display_elevated_connections_tip", "מעבר למסך שאינו ראשי אינו נתמך במצב משתמש מוגבה כאשר יש מספר חיבורים. אנא נסה שוב לאחר התקנה אם ברצונך לשלוט במסכים מרובים."), + ("input_source_1_tip", "מקור קלט 1"), + ("input_source_2_tip", "מקור קלט 2"), + ("capture_display_elevated_connections_tip", "לכידת מסכים מרובים אינה נתמכת במצב משתמש מוגבה. אנא נסה שוב לאחר התקנה אם ברצונך לשלוט במסכים מרובים."), + ("Swap control-command key", ""), + ("swap-left-right-mouse", "החלף בין כפתור העכבר השמאלי לימני"), + ("2FA code", "קוד אימות דו-שלבי"), + ("More", ""), + ("enable-2fa-title", "הפעל אימות דו-שלבי"), + ("enable-2fa-desc", "אנא הגדר כעת את האפליקציה שלך לאימות. תוכל להשתמש באפליקציית אימות כגון Authy, Microsoft או Google Authenticator בטלפון או במחשב שלך.\n\nסרוק את קוד ה-QR עם האפליקציה שלך והזן את הקוד שהאפליקציה מציגה כדי להפעיל את אימות הדו-שלבי."), + ("wrong-2fa-code", "לא ניתן לאמת את הקוד. בדוק שהקוד והגדרות הזמן המקומיות נכונות"), + ("enter-2fa-title", "אימות דו-שלבי"), + ("Email verification code must be 6 characters.", ""), + ("2FA code must be 6 digits.", ""), + ("Multiple Windows sessions found", ""), + ("Please select the session you want to connect to", ""), + ("powered_by_me", ""), + ("outgoing_only_desk_tip", "זוהי מהדורה מותאמת אישית.\nניתן להתחבר למכשירים אחרים, אך מכשירים אחרים לא יכולים להתחבר אליך."), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 302dcdbc2..8b97f2b0f 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 68b62ba41..fd4a48e71 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index c573ac10e..856a83a96 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -588,7 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Seleziona la sessione a cui connetterti"), ("powered_by_me", "Alimentato da RustDesk"), ("outgoing_only_desk_tip", "Questa è un'edizione personalizzata.\nPuoi connetterti ad altri dispositivi, ma gli altri dispositivi non possono connettersi a questo dispositivo."), - ("preset_password_warning", "Questa edizione personalizzata viene fornita con una password preimpostata. Chiunque conosca questa password potrebbe ottenere il pieno controllo del dispositivo. Se non te lo aspettavi, disinstalla immediatamente il software."), + ("preset_password_warning", ""), ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index a0fc9e5f6..60603f651 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index daec15775..d98bb7f3a 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "연결하려는 세션을 선택하세요."), ("powered_by_me", "RustDesk 제공"), ("outgoing_only_desk_tip", "이것은 맞춤형 버전입니다.\n다른 장치에 연결할 수 있지만 다른 장치는 귀하의 장치에 연결할 수 없습니다."), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index df126bc14..dfcc505f7 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index e5eb49c99..3b97ef22d 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 77bb48eed..41548ce81 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Lūdzu, atlasiet sesiju, ar kuru vēlaties izveidot savienojumu"), ("powered_by_me", "Darbojas ar RustDesk"), ("outgoing_only_desk_tip", "Šis ir pielāgots izdevums.\nVarat izveidot savienojumu ar citām ierīcēm, taču citas ierīces nevar izveidot savienojumu ar jūsu ierīci."), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index dc3378248..8dd09e3ff 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 4458cd56d..4a6f3c39c 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Selecteer de sessie waarmee je verbinding wilt maken"), ("powered_by_me", "Werkt met Rustdesk"), ("outgoing_only_desk_tip", "Je kan verbinding maken met andere apparaten, maar andere apparaten kunnen geen verbinding maken met dit apparaat."), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index fd14da4d0..008a0e72c 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Wybierz sesję, do której chcesz się podłączyć"), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0a2a4e448..4d9051b96 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 1c356dbcb..a402e93e8 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index e25815e2e..9eff2f144 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 932b26973..bb53fb1ed 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -590,5 +590,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("outgoing_only_desk_tip", "Это специализированная версия.\nВы можете подключаться к другим устройствам, но другие устройства не могут подключиться к вашему."), ("preset_password_warning", "Это специализированная версия с предустановленным паролем. Любой, кто знает этот пароль, может получить полный контроль над вашим устройством. Если это для вас неожиданно, немедленно удалите данное программное обеспечение."), ("Security Alert", "Предупреждение о безопасности"), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index a752a8095..856dc6ac6 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Vyberte reláciu, ku ktorej sa chcete pripojiť"), ("powered_by_me", "Poháňané aplikáciou RustDesk"), ("outgoing_only_desk_tip", "Toto je prispôsobené vydanie.\nMôžete sa pripojiť k iným zariadeniam, ale iné zariadenia sa k vášmu zariadeniu pripojiť nemôžu."), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index bcbe5f876..779f6d97d 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 1f1dab848..e79811a51 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index cd86e6d9f..cb747ceb0 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 9685fc116..5929c088a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 8e20292ae..4a3e59817 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -590,5 +590,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("outgoing_only_desk_tip", ""), ("preset_password_warning", ""), ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index ebcb1b44d..287aa66d9 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index d7db35a88..42a609750 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index c8b9d9420..49fdc774e 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "請選擇您想要連結的工作階段"), ("powered_by_me", "由 RustDesk 提供支援"), ("outgoing_only_desk_tip", "目前版本的軟體是自定義版本。\n您可以連接至其他設備,但是其他設備無法連接至您的設備。"), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 7f2680eb4..37c20867f 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Будь ласка, оберіть сеанс, до якого ви хочете підключитися"), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 8e85a5cc2..528aeecbf 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -588,5 +588,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), + ("preset_password_warning", ""), + ("Security Alert", ""), + ("Add shared address book", ""), + ("Update this address book", ""), + ("Delete this address book", ""), + ("Share this address book", ""), + ("Are you sure you want to delete address book {}?", ""), + ("My address book", ""), + ("Personal", ""), + ("Owner", ""), + ("Set shared password", ""), + ("Exist in", ""), + ("Read-only", ""), + ("Read/Write", ""), + ("Full Control", ""), + ("full_control_tip", ""), + ("share_warning_tip", ""), + ("Only show existing", ""), + ("Everyone", ""), + ("permission_priority_tip", ""), ].iter().cloned().collect(); } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index f7b10d2a0..c2e01c9f5 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -519,7 +519,7 @@ impl SciterSession { .lc .write() .unwrap() - .initialize(id, conn_type, None, force_relay, None); + .initialize(id, conn_type, None, force_relay, None, None); Self(session) }