device group (#10781)

1. Rename `Group` tab to `Accessible devices`
2. Add accessible device groups at the top of search list
3. option `preset-device-group-name` and command line `--assign --device_group_name`

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2025-02-15 12:13:11 +08:00
committed by GitHub
parent 8f545491a2
commit cefda0dec1
57 changed files with 269 additions and 33 deletions

Binary file not shown.

View File

@@ -103,6 +103,8 @@ enum DesktopType {
class IconFont { class IconFont {
static const _family1 = 'Tabbar'; static const _family1 = 'Tabbar';
static const _family2 = 'PeerSearchbar'; static const _family2 = 'PeerSearchbar';
static const _family3 = 'AddressBook';
static const _family4 = 'DeviceGroup';
IconFont._(); IconFont._();
static const IconData max = IconData(0xe606, fontFamily: _family1); static const IconData max = IconData(0xe606, fontFamily: _family1);
@@ -113,8 +115,11 @@ class IconFont {
static const IconData menu = IconData(0xe628, fontFamily: _family1); static const IconData menu = IconData(0xe628, fontFamily: _family1);
static const IconData search = IconData(0xe6a4, fontFamily: _family2); static const IconData search = IconData(0xe6a4, fontFamily: _family2);
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2); static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
static const IconData addressBook = static const IconData addressBook = IconData(0xe602, fontFamily: _family3);
IconData(0xe602, fontFamily: "AddressBook"); static const IconData deviceGroupOutline =
IconData(0xe623, fontFamily: _family4);
static const IconData deviceGroupFill =
IconData(0xe748, fontFamily: _family4);
} }
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> { class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {

View File

@@ -67,6 +67,7 @@ class PeerPayload {
int? status; int? status;
String user = ''; String user = '';
String user_name = ''; String user_name = '';
String? device_group_name;
String note = ''; String note = '';
PeerPayload.fromJson(Map<String, dynamic> json) PeerPayload.fromJson(Map<String, dynamic> json)
@@ -75,6 +76,7 @@ class PeerPayload {
status = json['status'], status = json['status'],
user = json['user'] ?? '', user = json['user'] ?? '',
user_name = json['user_name'] ?? '', user_name = json['user_name'] ?? '',
device_group_name = json['device_group_name'] ?? '',
note = json['note'] ?? ''; note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) { static Peer toPeer(PeerPayload p) {
@@ -84,6 +86,7 @@ class PeerPayload {
"username": p.info['username'] ?? '', "username": p.info['username'] ?? '',
"platform": _platform(p.info['os']), "platform": _platform(p.info['os']),
"hostname": p.info['device_name'], "hostname": p.info['device_name'],
"device_group_name": p.device_group_name,
}); });
} }
@@ -265,3 +268,19 @@ class AbTag {
: name = json['name'] ?? '', : name = json['name'] ?? '',
color = json['color'] ?? ''; color = json['color'] ?? '';
} }
class DeviceGroupPayload {
String name;
DeviceGroupPayload(this.name);
DeviceGroupPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '';
Map<String, dynamic> toGroupCacheJson() {
final Map<String, dynamic> map = {
'name': name,
};
return map;
}
}

View File

@@ -20,8 +20,11 @@ class MyGroup extends StatefulWidget {
} }
class _MyGroupState extends State<MyGroup> { class _MyGroupState extends State<MyGroup> {
RxString get selectedUser => gFFI.groupModel.selectedUser; RxBool get isSelectedDeviceGroup => gFFI.groupModel.isSelectedDeviceGroup;
RxString get searchUserText => gFFI.groupModel.searchUserText; RxString get selectedAccessibleItemName =>
gFFI.groupModel.selectedAccessibleItemName;
RxString get searchAccessibleItemNameText =>
gFFI.groupModel.searchAccessibleItemNameText;
static TextEditingController searchUserController = TextEditingController(); static TextEditingController searchUserController = TextEditingController();
@override @override
@@ -72,7 +75,7 @@ class _MyGroupState extends State<MyGroup> {
child: Container( child: Container(
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
child: _buildUserContacts(), child: _buildLeftList(),
), ),
) )
], ],
@@ -105,7 +108,7 @@ class _MyGroupState extends State<MyGroup> {
_buildLeftHeader(), _buildLeftHeader(),
Container( Container(
width: double.infinity, width: double.infinity,
child: _buildUserContacts(), child: _buildLeftList(),
) )
], ],
), ),
@@ -130,7 +133,7 @@ class _MyGroupState extends State<MyGroup> {
child: TextField( child: TextField(
controller: searchUserController, controller: searchUserController,
onChanged: (value) { onChanged: (value) {
searchUserText.value = value; searchAccessibleItemNameText.value = value;
}, },
textAlignVertical: TextAlignVertical.center, textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: fontSize), style: TextStyle(fontSize: fontSize),
@@ -150,20 +153,30 @@ class _MyGroupState extends State<MyGroup> {
); );
} }
Widget _buildUserContacts() { Widget _buildLeftList() {
return Obx(() { return Obx(() {
final items = gFFI.groupModel.users.where((p0) { final userItems = gFFI.groupModel.users.where((p0) {
if (searchUserText.isNotEmpty) { if (searchAccessibleItemNameText.isNotEmpty) {
return p0.name return p0.name
.toLowerCase() .toLowerCase()
.contains(searchUserText.value.toLowerCase()); .contains(searchAccessibleItemNameText.value.toLowerCase());
}
return true;
}).toList();
final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) {
if (searchAccessibleItemNameText.isNotEmpty) {
return p0.name
.toLowerCase()
.contains(searchAccessibleItemNameText.value.toLowerCase());
} }
return true; return true;
}).toList(); }).toList();
listView(bool isPortrait) => ListView.builder( listView(bool isPortrait) => ListView.builder(
shrinkWrap: isPortrait, shrinkWrap: isPortrait,
itemCount: items.length, itemCount: deviceGroupItems.length + userItems.length,
itemBuilder: (context, index) => _buildUserItem(items[index])); itemBuilder: (context, index) => index < deviceGroupItems.length
? _buildDeviceGroupItem(deviceGroupItems[index])
: _buildUserItem(userItems[index - deviceGroupItems.length]));
var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0);
return Obx(() => stateGlobal.isPortrait.isFalse return Obx(() => stateGlobal.isPortrait.isFalse
? listView(false) ? listView(false)
@@ -174,14 +187,16 @@ class _MyGroupState extends State<MyGroup> {
Widget _buildUserItem(UserPayload user) { Widget _buildUserItem(UserPayload user) {
final username = user.name; final username = user.name;
return InkWell(onTap: () { return InkWell(onTap: () {
if (selectedUser.value != username) { isSelectedDeviceGroup.value = false;
selectedUser.value = username; if (selectedAccessibleItemName.value != username) {
selectedAccessibleItemName.value = username;
} else { } else {
selectedUser.value = ''; selectedAccessibleItemName.value = '';
} }
}, child: Obx( }, child: Obx(
() { () {
bool selected = selectedUser.value == username; bool selected = !isSelectedDeviceGroup.value &&
selectedAccessibleItemName.value == username;
final isMe = username == gFFI.userModel.userName.value; final isMe = username == gFFI.userModel.userName.value;
final colorMe = MyTheme.color(context).me!; final colorMe = MyTheme.color(context).me!;
return Container( return Container(
@@ -238,4 +253,43 @@ class _MyGroupState extends State<MyGroup> {
}, },
)).marginSymmetric(horizontal: 12).marginOnly(bottom: 6); )).marginSymmetric(horizontal: 12).marginOnly(bottom: 6);
} }
Widget _buildDeviceGroupItem(DeviceGroupPayload deviceGroup) {
final name = deviceGroup.name;
return InkWell(onTap: () {
isSelectedDeviceGroup.value = true;
if (selectedAccessibleItemName.value != name) {
selectedAccessibleItemName.value = name;
} else {
selectedAccessibleItemName.value = '';
}
}, child: Obx(
() {
bool selected = isSelectedDeviceGroup.value &&
selectedAccessibleItemName.value == name;
return Container(
decoration: BoxDecoration(
color: selected ? MyTheme.color(context).highlight : null,
border: Border(
bottom: BorderSide(
width: 0.7,
color: Theme.of(context).dividerColor.withOpacity(0.1))),
),
child: Container(
child: Row(
children: [
Container(
width: 20,
height: 20,
child: Icon(IconFont.deviceGroupOutline,
color: MyTheme.accent, size: 19),
).marginOnly(right: 4),
Expanded(child: Text(name)),
],
).paddingSymmetric(vertical: 4),
),
);
},
)).marginSymmetric(horizontal: 12).marginOnly(bottom: 6);
}
} }

View File

@@ -562,14 +562,23 @@ class MyGroupPeerView extends BasePeersView {
); );
static bool filter(Peer peer) { static bool filter(Peer peer) {
if (gFFI.groupModel.searchUserText.isNotEmpty) { if (gFFI.groupModel.searchAccessibleItemNameText.isNotEmpty) {
if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) { if (!peer.loginName
.contains(gFFI.groupModel.searchAccessibleItemNameText)) {
return false; return false;
} }
} }
if (gFFI.groupModel.selectedUser.isNotEmpty) { if (gFFI.groupModel.selectedAccessibleItemName.isNotEmpty) {
if (gFFI.groupModel.selectedUser.value != peer.loginName) { if (gFFI.groupModel.isSelectedDeviceGroup.value) {
return false; if (gFFI.groupModel.selectedAccessibleItemName.value !=
peer.device_group_name) {
return false;
}
} else {
if (gFFI.groupModel.selectedAccessibleItemName.value !=
peer.loginName) {
return false;
}
} }
} }
return true; return true;

View File

@@ -350,6 +350,7 @@ class _ConnectionPageState extends State<ConnectionPage>
rdpPort: '', rdpPort: '',
rdpUsername: '', rdpUsername: '',
loginName: '', loginName: '',
device_group_name: '',
); );
_autocompleteOpts = [emptyPeer]; _autocompleteOpts = [emptyPeer];
} else { } else {

View File

@@ -174,6 +174,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
rdpPort: '', rdpPort: '',
rdpUsername: '', rdpUsername: '',
loginName: '', loginName: '',
device_group_name: '',
); );
_autocompleteOpts = [emptyPeer]; _autocompleteOpts = [emptyPeer];
} else { } else {

View File

@@ -12,16 +12,18 @@ import '../utils/http_service.dart' as http;
class GroupModel { class GroupModel {
final RxBool groupLoading = false.obs; final RxBool groupLoading = false.obs;
final RxString groupLoadError = "".obs; final RxString groupLoadError = "".obs;
final RxList<DeviceGroupPayload> deviceGroups = RxList.empty(growable: true);
final RxList<UserPayload> users = RxList.empty(growable: true); final RxList<UserPayload> users = RxList.empty(growable: true);
final RxList<Peer> peers = RxList.empty(growable: true); final RxList<Peer> peers = RxList.empty(growable: true);
final RxString selectedUser = ''.obs; final RxBool isSelectedDeviceGroup = false.obs;
final RxString searchUserText = ''.obs; final RxString selectedAccessibleItemName = ''.obs;
final RxString searchAccessibleItemNameText = ''.obs;
WeakReference<FFI> parent; WeakReference<FFI> parent;
var initialized = false; var initialized = false;
var _cacheLoadOnceFlag = false; var _cacheLoadOnceFlag = false;
var _statusCode = 200; var _statusCode = 200;
bool get emtpy => users.isEmpty && peers.isEmpty; bool get emtpy => deviceGroups.isEmpty && users.isEmpty && peers.isEmpty;
late final Peers peersModel; late final Peers peersModel;
@@ -55,6 +57,12 @@ class GroupModel {
} }
Future<void> _pull() async { Future<void> _pull() async {
List<DeviceGroupPayload> tmpDeviceGroups = List.empty(growable: true);
if (!await _getDeviceGroups(tmpDeviceGroups)) {
// old hbbs doesn't support this api
// return;
}
tmpDeviceGroups.sort((a, b) => a.name.compareTo(b.name));
List<UserPayload> tmpUsers = List.empty(growable: true); List<UserPayload> tmpUsers = List.empty(growable: true);
if (!await _getUsers(tmpUsers)) { if (!await _getUsers(tmpUsers)) {
return; return;
@@ -63,6 +71,7 @@ class GroupModel {
if (!await _getPeers(tmpPeers)) { if (!await _getPeers(tmpPeers)) {
return; return;
} }
deviceGroups.value = tmpDeviceGroups;
// me first // me first
var index = tmpUsers var index = tmpUsers
.indexWhere((user) => user.name == gFFI.userModel.userName.value); .indexWhere((user) => user.name == gFFI.userModel.userName.value);
@@ -71,8 +80,9 @@ class GroupModel {
tmpUsers.insert(0, user); tmpUsers.insert(0, user);
} }
users.value = tmpUsers; users.value = tmpUsers;
if (!users.any((u) => u.name == selectedUser.value)) { if (!users.any((u) => u.name == selectedAccessibleItemName.value) &&
selectedUser.value = ''; !deviceGroups.any((d) => d.name == selectedAccessibleItemName.value)) {
selectedAccessibleItemName.value = '';
} }
// recover online // recover online
final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList(); final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
@@ -84,6 +94,63 @@ class GroupModel {
groupLoadError.value = ''; groupLoadError.value = '';
} }
Future<bool> _getDeviceGroups(
List<DeviceGroupPayload> tmpDeviceGroups) async {
final api = "${await bind.mainGetApiServer()}/api/device-group/accessible";
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(),
});
final resp = await http.get(uri, headers: getHttpHeaders());
_statusCode = resp.statusCode;
Map<String, dynamic> json =
_jsonDecodeResp(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 user in data) {
final u = DeviceGroupPayload.fromJson(user);
int index = tmpDeviceGroups.indexWhere((e) => e.name == u.name);
if (index < 0) {
tmpDeviceGroups.add(u);
} else {
tmpDeviceGroups[index] = u;
}
}
}
}
}
} while (current * pageSize < total);
return true;
} catch (err) {
debugPrint('get accessible device groups: $err');
// old hbbs doesn't support this api
// groupLoadError.value =
// '${translate('pull_group_failed_tip')}: ${translate(err.toString())}';
}
return false;
}
Future<bool> _getUsers(List<UserPayload> tmpUsers) async { Future<bool> _getUsers(List<UserPayload> tmpUsers) async {
final api = "${await bind.mainGetApiServer()}/api/users"; final api = "${await bind.mainGetApiServer()}/api/users";
try { try {
@@ -225,6 +292,7 @@ class GroupModel {
try { try {
final map = (<String, dynamic>{ final map = (<String, dynamic>{
"access_token": bind.mainGetLocalOption(key: 'access_token'), "access_token": bind.mainGetLocalOption(key: 'access_token'),
"device_groups": deviceGroups.map((e) => e.toGroupCacheJson()).toList(),
"users": users.map((e) => e.toGroupCacheJson()).toList(), "users": users.map((e) => e.toGroupCacheJson()).toList(),
'peers': peers.map((e) => e.toGroupCacheJson()).toList() 'peers': peers.map((e) => e.toGroupCacheJson()).toList()
}); });
@@ -244,8 +312,14 @@ class GroupModel {
if (groupLoading.value) return; if (groupLoading.value) return;
final data = jsonDecode(cache); final data = jsonDecode(cache);
if (data == null || data['access_token'] != access_token) return; if (data == null || data['access_token'] != access_token) return;
deviceGroups.clear();
users.clear(); users.clear();
peers.clear(); peers.clear();
if (data['device_groups'] is List) {
for (var u in data['device_groups']) {
deviceGroups.add(DeviceGroupPayload.fromJson(u));
}
}
if (data['users'] is List) { if (data['users'] is List) {
for (var u in data['users']) { for (var u in data['users']) {
users.add(UserPayload.fromJson(u)); users.add(UserPayload.fromJson(u));
@@ -263,9 +337,10 @@ class GroupModel {
reset() async { reset() async {
groupLoadError.value = ''; groupLoadError.value = '';
deviceGroups.clear();
users.clear(); users.clear();
peers.clear(); peers.clear();
selectedUser.value = ''; selectedAccessibleItemName.value = '';
await bind.mainClearGroup(); await bind.mainClearGroup();
} }
} }

View File

@@ -19,6 +19,7 @@ class Peer {
String rdpUsername; String rdpUsername;
bool online = false; bool online = false;
String loginName; //login username String loginName; //login username
String device_group_name;
bool? sameServer; bool? sameServer;
String getId() { String getId() {
@@ -41,6 +42,7 @@ class Peer {
rdpPort = json['rdpPort'] ?? '', rdpPort = json['rdpPort'] ?? '',
rdpUsername = json['rdpUsername'] ?? '', rdpUsername = json['rdpUsername'] ?? '',
loginName = json['loginName'] ?? '', loginName = json['loginName'] ?? '',
device_group_name = json['device_group_name'] ?? '',
sameServer = json['same_server']; sameServer = json['same_server'];
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@@ -57,6 +59,7 @@ class Peer {
"rdpPort": rdpPort, "rdpPort": rdpPort,
"rdpUsername": rdpUsername, "rdpUsername": rdpUsername,
'loginName': loginName, 'loginName': loginName,
'device_group_name': device_group_name,
'same_server': sameServer, 'same_server': sameServer,
}; };
} }
@@ -83,6 +86,7 @@ class Peer {
"hostname": hostname, "hostname": hostname,
"platform": platform, "platform": platform,
"login_name": loginName, "login_name": loginName,
"device_group_name": device_group_name,
}; };
} }
@@ -99,6 +103,7 @@ class Peer {
required this.rdpPort, required this.rdpPort,
required this.rdpUsername, required this.rdpUsername,
required this.loginName, required this.loginName,
required this.device_group_name,
this.sameServer, this.sameServer,
}); });
@@ -116,6 +121,7 @@ class Peer {
rdpPort: '', rdpPort: '',
rdpUsername: '', rdpUsername: '',
loginName: '', loginName: '',
device_group_name: '',
); );
bool equal(Peer other) { bool equal(Peer other) {
return id == other.id && return id == other.id &&
@@ -129,6 +135,7 @@ class Peer {
forceAlwaysRelay == other.forceAlwaysRelay && forceAlwaysRelay == other.forceAlwaysRelay &&
rdpPort == other.rdpPort && rdpPort == other.rdpPort &&
rdpUsername == other.rdpUsername && rdpUsername == other.rdpUsername &&
device_group_name == other.device_group_name &&
loginName == other.loginName; loginName == other.loginName;
} }
@@ -146,6 +153,7 @@ class Peer {
rdpPort: other.rdpPort, rdpPort: other.rdpPort,
rdpUsername: other.rdpUsername, rdpUsername: other.rdpUsername,
loginName: other.loginName, loginName: other.loginName,
device_group_name: other.device_group_name,
sameServer: other.sameServer); sameServer: other.sameServer);
} }

View File

@@ -28,14 +28,14 @@ class PeerTabModel with ChangeNotifier {
'Favorites', 'Favorites',
'Discovered', 'Discovered',
'Address book', 'Address book',
'Group', 'Accessible devices',
]; ];
static const List<IconData> icons = [ static const List<IconData> icons = [
Icons.access_time_filled, Icons.access_time_filled,
Icons.star, Icons.star,
Icons.explore, Icons.explore,
IconFont.addressBook, IconFont.addressBook,
Icons.group, IconFont.deviceGroupFill,
]; ];
List<bool> isEnabled = List.from([ List<bool> isEnabled = List.from([
true, true,

View File

@@ -161,6 +161,9 @@ flutter:
- family: AddressBook - family: AddressBook
fonts: fonts:
- asset: assets/address_book.ttf - asset: assets/address_book.ttf
- family: DeviceGroup
fonts:
- asset: assets/device_group.ttf
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware. # https://flutter.dev/assets-and-images/#resolution-aware.

View File

@@ -427,15 +427,26 @@ pub fn core_main() -> Option<Vec<String>> {
if pos < max { if pos < max {
address_book_tag = Some(args[pos + 1].to_owned()); address_book_tag = Some(args[pos + 1].to_owned());
} }
let mut device_group_name = None;
let pos = args
.iter()
.position(|x| x == "--device_group_name")
.unwrap_or(max);
if pos < max {
device_group_name = Some(args[pos + 1].to_owned());
}
let mut body = serde_json::json!({ let mut body = serde_json::json!({
"id": id, "id": id,
"uuid": uuid, "uuid": uuid,
}); });
let header = "Authorization: Bearer ".to_owned() + &token; let header = "Authorization: Bearer ".to_owned() + &token;
if user_name.is_none() && strategy_name.is_none() && address_book_name.is_none() if user_name.is_none()
&& strategy_name.is_none()
&& address_book_name.is_none()
&& device_group_name.is_none()
{ {
println!( println!(
"--user_name or --strategy_name or --address_book_name is required!" "--user_name or --strategy_name or --address_book_name or --device_group_name is required!"
); );
} else { } else {
if let Some(name) = user_name { if let Some(name) = user_name {
@@ -450,6 +461,9 @@ pub fn core_main() -> Option<Vec<String>> {
body["address_book_tag"] = serde_json::json!(name); body["address_book_tag"] = serde_json::json!(name);
} }
} }
if let Some(name) = device_group_name {
body["device_group_name"] = serde_json::json!(name);
}
let url = crate::ui_interface::get_api_server() + "/api/devices/cli"; let url = crate::ui_interface::get_api_server() + "/api/devices/cli";
match crate::post_request_sync(url, body.to_string(), &header) { match crate::post_request_sync(url, body.to_string(), &header) {
Err(err) => println!("{}", err), Err(err) => println!("{}", err),

View File

@@ -99,6 +99,10 @@ async fn start_hbbs_sync_async() {
if !strategy_name.is_empty() { if !strategy_name.is_empty() {
v[keys::OPTION_PRESET_STRATEGY_NAME] = json!(strategy_name); v[keys::OPTION_PRESET_STRATEGY_NAME] = json!(strategy_name);
} }
let device_group_name = get_builtin_option(keys::OPTION_PRESET_DEVICE_GROUP_NAME);
if !device_group_name.is_empty() {
v[keys::OPTION_PRESET_DEVICE_GROUP_NAME] = json!(device_group_name);
}
match crate::post_request(url.replace("heartbeat", "sysinfo"), v.to_string(), "").await { match crate::post_request(url.replace("heartbeat", "sysinfo"), v.to_string(), "").await {
Ok(x) => { Ok(x) => {
if x == "SYSINFO_UPDATED" { if x == "SYSINFO_UPDATED" {

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "更新客户端的粘贴板"), ("Update client clipboard", "更新客户端的粘贴板"),
("Untagged", "无标签"), ("Untagged", "无标签"),
("new-version-of-{}-tip", "{} 版本更新"), ("new-version-of-{}-tip", "{} 版本更新"),
("Accessible devices", "可访问的设备"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Client-Zwischenablage aktualisieren"), ("Update client clipboard", "Client-Zwischenablage aktualisieren"),
("Untagged", "Unmarkiert"), ("Untagged", "Unmarkiert"),
("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Ενημέρωση απομακρισμένου προχείρου"), ("Update client clipboard", "Ενημέρωση απομακρισμένου προχείρου"),
("Untagged", "Χωρίς ετικέτα"), ("Untagged", "Χωρίς ετικέτα"),
("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Actualizar portapapeles del cliente"), ("Update client clipboard", "Actualizar portapapeles del cliente"),
("Untagged", "Sin itiquetar"), ("Untagged", "Sin itiquetar"),
("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "A kliens vágólapjának frissítése"), ("Update client clipboard", "A kliens vágólapjának frissítése"),
("Untagged", "Címkézetlen"), ("Untagged", "Címkézetlen"),
("new-version-of-{}-tip", "A(z) {} új verziója"), ("new-version-of-{}-tip", "A(z) {} új verziója"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Aggiorna appunti client"), ("Update client clipboard", "Aggiorna appunti client"),
("Untagged", "Senza tag"), ("Untagged", "Senza tag"),
("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "클라이언트 클립보드 업데이트"), ("Update client clipboard", "클라이언트 클립보드 업데이트"),
("Untagged", "태그 없음"), ("Untagged", "태그 없음"),
("new-version-of-{}-tip", "{} 의 새로운 버전이 출시되었습니다."), ("new-version-of-{}-tip", "{} 의 새로운 버전이 출시되었습니다."),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Atjaunināt klienta starpliktuvi"), ("Update client clipboard", "Atjaunināt klienta starpliktuvi"),
("Untagged", "Neatzīmēts"), ("Untagged", "Neatzīmēts"),
("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Klembord van client bijwerken"), ("Update client clipboard", "Klembord van client bijwerken"),
("Untagged", "Ongemarkeerd"), ("Untagged", "Ongemarkeerd"),
("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Uaktualnij schowek klienta"), ("Update client clipboard", "Uaktualnij schowek klienta"),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Обновить буфер обмена клиента"), ("Update client clipboard", "Обновить буфер обмена клиента"),
("Untagged", "Без метки"), ("Untagged", "Без метки"),
("new-version-of-{}-tip", "Доступна новая версия {}"), ("new-version-of-{}-tip", "Доступна новая версия {}"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Osveži odjemalčevo odložišče"), ("Update client clipboard", "Osveži odjemalčevo odložišče"),
("Untagged", "Neoznačeno"), ("Untagged", "Neoznačeno"),
("new-version-of-{}-tip", "Na voljo je nova različica {}"), ("new-version-of-{}-tip", "Na voljo je nova različica {}"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "更新客戶端的剪貼簿"), ("Update client clipboard", "更新客戶端的剪貼簿"),
("Untagged", "無標籤"), ("Untagged", "無標籤"),
("new-version-of-{}-tip", "有新版本的 {} 可用"), ("new-version-of-{}-tip", "有新版本的 {} 可用"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", "Оновити буфер обміну клієнта"), ("Update client clipboard", "Оновити буфер обміну клієнта"),
("Untagged", "Без міток"), ("Untagged", "Без міток"),
("new-version-of-{}-tip", "Доступна нова версія {}"), ("new-version-of-{}-tip", "Доступна нова версія {}"),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""), ("Update client clipboard", ""),
("Untagged", ""), ("Untagged", ""),
("new-version-of-{}-tip", ""), ("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }