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 {
static const _family1 = 'Tabbar';
static const _family2 = 'PeerSearchbar';
static const _family3 = 'AddressBook';
static const _family4 = 'DeviceGroup';
IconFont._();
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 search = IconData(0xe6a4, fontFamily: _family2);
static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
static const IconData addressBook =
IconData(0xe602, fontFamily: "AddressBook");
static const IconData addressBook = IconData(0xe602, fontFamily: _family3);
static const IconData deviceGroupOutline =
IconData(0xe623, fontFamily: _family4);
static const IconData deviceGroupFill =
IconData(0xe748, fontFamily: _family4);
}
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {

View File

@@ -67,6 +67,7 @@ class PeerPayload {
int? status;
String user = '';
String user_name = '';
String? device_group_name;
String note = '';
PeerPayload.fromJson(Map<String, dynamic> json)
@@ -75,6 +76,7 @@ class PeerPayload {
status = json['status'],
user = json['user'] ?? '',
user_name = json['user_name'] ?? '',
device_group_name = json['device_group_name'] ?? '',
note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) {
@@ -84,6 +86,7 @@ class PeerPayload {
"username": p.info['username'] ?? '',
"platform": _platform(p.info['os']),
"hostname": p.info['device_name'],
"device_group_name": p.device_group_name,
});
}
@@ -265,3 +268,19 @@ class AbTag {
: name = json['name'] ?? '',
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> {
RxString get selectedUser => gFFI.groupModel.selectedUser;
RxString get searchUserText => gFFI.groupModel.searchUserText;
RxBool get isSelectedDeviceGroup => gFFI.groupModel.isSelectedDeviceGroup;
RxString get selectedAccessibleItemName =>
gFFI.groupModel.selectedAccessibleItemName;
RxString get searchAccessibleItemNameText =>
gFFI.groupModel.searchAccessibleItemNameText;
static TextEditingController searchUserController = TextEditingController();
@override
@@ -72,7 +75,7 @@ class _MyGroupState extends State<MyGroup> {
child: Container(
width: double.infinity,
height: double.infinity,
child: _buildUserContacts(),
child: _buildLeftList(),
),
)
],
@@ -105,7 +108,7 @@ class _MyGroupState extends State<MyGroup> {
_buildLeftHeader(),
Container(
width: double.infinity,
child: _buildUserContacts(),
child: _buildLeftList(),
)
],
),
@@ -130,7 +133,7 @@ class _MyGroupState extends State<MyGroup> {
child: TextField(
controller: searchUserController,
onChanged: (value) {
searchUserText.value = value;
searchAccessibleItemNameText.value = value;
},
textAlignVertical: TextAlignVertical.center,
style: TextStyle(fontSize: fontSize),
@@ -150,20 +153,30 @@ class _MyGroupState extends State<MyGroup> {
);
}
Widget _buildUserContacts() {
Widget _buildLeftList() {
return Obx(() {
final items = gFFI.groupModel.users.where((p0) {
if (searchUserText.isNotEmpty) {
final userItems = gFFI.groupModel.users.where((p0) {
if (searchAccessibleItemNameText.isNotEmpty) {
return p0.name
.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;
}).toList();
listView(bool isPortrait) => ListView.builder(
shrinkWrap: isPortrait,
itemCount: items.length,
itemBuilder: (context, index) => _buildUserItem(items[index]));
itemCount: deviceGroupItems.length + userItems.length,
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);
return Obx(() => stateGlobal.isPortrait.isFalse
? listView(false)
@@ -174,14 +187,16 @@ class _MyGroupState extends State<MyGroup> {
Widget _buildUserItem(UserPayload user) {
final username = user.name;
return InkWell(onTap: () {
if (selectedUser.value != username) {
selectedUser.value = username;
isSelectedDeviceGroup.value = false;
if (selectedAccessibleItemName.value != username) {
selectedAccessibleItemName.value = username;
} else {
selectedUser.value = '';
selectedAccessibleItemName.value = '';
}
}, child: Obx(
() {
bool selected = selectedUser.value == username;
bool selected = !isSelectedDeviceGroup.value &&
selectedAccessibleItemName.value == username;
final isMe = username == gFFI.userModel.userName.value;
final colorMe = MyTheme.color(context).me!;
return Container(
@@ -238,4 +253,43 @@ class _MyGroupState extends State<MyGroup> {
},
)).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,15 +562,24 @@ class MyGroupPeerView extends BasePeersView {
);
static bool filter(Peer peer) {
if (gFFI.groupModel.searchUserText.isNotEmpty) {
if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) {
if (gFFI.groupModel.searchAccessibleItemNameText.isNotEmpty) {
if (!peer.loginName
.contains(gFFI.groupModel.searchAccessibleItemNameText)) {
return false;
}
}
if (gFFI.groupModel.selectedUser.isNotEmpty) {
if (gFFI.groupModel.selectedUser.value != peer.loginName) {
if (gFFI.groupModel.selectedAccessibleItemName.isNotEmpty) {
if (gFFI.groupModel.isSelectedDeviceGroup.value) {
if (gFFI.groupModel.selectedAccessibleItemName.value !=
peer.device_group_name) {
return false;
}
} else {
if (gFFI.groupModel.selectedAccessibleItemName.value !=
peer.loginName) {
return false;
}
}
}
return true;
}

View File

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

View File

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

View File

@@ -12,16 +12,18 @@ import '../utils/http_service.dart' as http;
class GroupModel {
final RxBool groupLoading = false.obs;
final RxString groupLoadError = "".obs;
final RxList<DeviceGroupPayload> deviceGroups = RxList.empty(growable: true);
final RxList<UserPayload> users = RxList.empty(growable: true);
final RxList<Peer> peers = RxList.empty(growable: true);
final RxString selectedUser = ''.obs;
final RxString searchUserText = ''.obs;
final RxBool isSelectedDeviceGroup = false.obs;
final RxString selectedAccessibleItemName = ''.obs;
final RxString searchAccessibleItemNameText = ''.obs;
WeakReference<FFI> parent;
var initialized = false;
var _cacheLoadOnceFlag = false;
var _statusCode = 200;
bool get emtpy => users.isEmpty && peers.isEmpty;
bool get emtpy => deviceGroups.isEmpty && users.isEmpty && peers.isEmpty;
late final Peers peersModel;
@@ -55,6 +57,12 @@ class GroupModel {
}
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);
if (!await _getUsers(tmpUsers)) {
return;
@@ -63,6 +71,7 @@ class GroupModel {
if (!await _getPeers(tmpPeers)) {
return;
}
deviceGroups.value = tmpDeviceGroups;
// me first
var index = tmpUsers
.indexWhere((user) => user.name == gFFI.userModel.userName.value);
@@ -71,8 +80,9 @@ class GroupModel {
tmpUsers.insert(0, user);
}
users.value = tmpUsers;
if (!users.any((u) => u.name == selectedUser.value)) {
selectedUser.value = '';
if (!users.any((u) => u.name == selectedAccessibleItemName.value) &&
!deviceGroups.any((d) => d.name == selectedAccessibleItemName.value)) {
selectedAccessibleItemName.value = '';
}
// recover online
final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
@@ -84,6 +94,63 @@ class GroupModel {
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 {
final api = "${await bind.mainGetApiServer()}/api/users";
try {
@@ -225,6 +292,7 @@ class GroupModel {
try {
final map = (<String, dynamic>{
"access_token": bind.mainGetLocalOption(key: 'access_token'),
"device_groups": deviceGroups.map((e) => e.toGroupCacheJson()).toList(),
"users": users.map((e) => e.toGroupCacheJson()).toList(),
'peers': peers.map((e) => e.toGroupCacheJson()).toList()
});
@@ -244,8 +312,14 @@ class GroupModel {
if (groupLoading.value) return;
final data = jsonDecode(cache);
if (data == null || data['access_token'] != access_token) return;
deviceGroups.clear();
users.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) {
for (var u in data['users']) {
users.add(UserPayload.fromJson(u));
@@ -263,9 +337,10 @@ class GroupModel {
reset() async {
groupLoadError.value = '';
deviceGroups.clear();
users.clear();
peers.clear();
selectedUser.value = '';
selectedAccessibleItemName.value = '';
await bind.mainClearGroup();
}
}

View File

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

View File

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

View File

@@ -161,6 +161,9 @@ flutter:
- family: AddressBook
fonts:
- 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
# https://flutter.dev/assets-and-images/#resolution-aware.

View File

@@ -427,15 +427,26 @@ pub fn core_main() -> Option<Vec<String>> {
if pos < max {
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!({
"id": id,
"uuid": uuid,
});
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!(
"--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 {
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);
}
}
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";
match crate::post_request_sync(url, body.to_string(), &header) {
Err(err) => println!("{}", err),

View File

@@ -99,6 +99,10 @@ async fn start_hbbs_sync_async() {
if !strategy_name.is_empty() {
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 {
Ok(x) => {
if x == "SYSINFO_UPDATED" {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""),
("Untagged", ""),
("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].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"),
("Untagged", "Címkézetlen"),
("new-version-of-{}-tip", "A(z) {} új verziója"),
("Accessible devices", ""),
].iter().cloned().collect();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -656,5 +656,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update client clipboard", ""),
("Untagged", ""),
("new-version-of-{}-tip", ""),
("Accessible devices", ""),
].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"),
("Untagged", "Neoznačeno"),
("new-version-of-{}-tip", "Na voljo je nova različica {}"),
("Accessible devices", ""),
].iter().cloned().collect();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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