add group peer card

Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
21pages 2022-12-11 21:40:35 +08:00
parent 5ee3e3f347
commit 880a0d4209
42 changed files with 777 additions and 195 deletions

View File

@ -99,22 +99,28 @@ class IconFont {
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> { class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
const ColorThemeExtension({ const ColorThemeExtension({
required this.border, required this.border,
required this.highlight,
}); });
final Color? border; final Color? border;
final Color? highlight;
static const light = ColorThemeExtension( static const light = ColorThemeExtension(
border: Color(0xFFCCCCCC), border: Color(0xFFCCCCCC),
highlight: Color(0xFFE5E5E5),
); );
static const dark = ColorThemeExtension( static const dark = ColorThemeExtension(
border: Color(0xFF555555), border: Color(0xFF555555),
highlight: Color(0xFF3F3F3F),
); );
@override @override
ThemeExtension<ColorThemeExtension> copyWith({Color? border}) { ThemeExtension<ColorThemeExtension> copyWith(
{Color? border, Color? highlight}) {
return ColorThemeExtension( return ColorThemeExtension(
border: border ?? this.border, border: border ?? this.border,
highlight: highlight ?? this.highlight,
); );
} }
@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
} }
return ColorThemeExtension( return ColorThemeExtension(
border: Color.lerp(border, other.border, t), border: Color.lerp(border, other.border, t),
highlight: Color.lerp(highlight, other.highlight, t),
); );
} }
} }

View File

@ -0,0 +1,39 @@
import 'package:flutter_hbb/models/peer_model.dart';
class UserPayload {
String name = '';
String email = '';
String note = '';
int? status;
String grp = '';
bool is_admin = false;
UserPayload.fromJson(Map<String, dynamic> json)
: name = json['name'] ?? '',
email = json['email'] ?? '',
note = json['note'] ?? '',
status = json['status'],
grp = json['grp'] ?? '',
is_admin = json['is_admin'] == true;
}
class PeerPayload {
String id = '';
String info = '';
int? status;
String user = '';
String user_name = '';
String note = '';
PeerPayload.fromJson(Map<String, dynamic> json)
: id = json['id'] ?? '',
info = json['info'] ?? '',
status = json['status'],
user = json['user'] ?? '',
user_name = json['user_name'] ?? '',
note = json['note'] ?? '';
static Peer toPeer(PeerPayload p) {
return Peer.fromJson({"id": p.id});
}
}

View File

@ -28,7 +28,6 @@ class _AddressBookState extends State<AddressBook> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb());
} }
@override @override
@ -45,11 +44,7 @@ class _AddressBookState extends State<AddressBook> {
handleLogin() { handleLogin() {
// TODO refactor login dialog for desktop and mobile // TODO refactor login dialog for desktop and mobile
if (isDesktop) { if (isDesktop) {
loginDialog().then((success) { loginDialog();
if (success) {
gFFI.abModel.pullAb();
}
});
} else { } else {
showLogin(gFFI.dialogManager); showLogin(gFFI.dialogManager);
} }

View File

@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:get/get.dart';
import '../../common.dart';
class MyGroup extends StatefulWidget {
final EdgeInsets? menuPadding;
const MyGroup({Key? key, this.menuPadding}) : super(key: key);
@override
State<StatefulWidget> createState() {
return _MyGroupState();
}
}
class _MyGroupState extends State<MyGroup> {
static final RxString selectedUser = ''.obs;
static final RxString searchUserText = ''.obs;
static TextEditingController searchUserController = TextEditingController();
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) => FutureBuilder<Widget>(
future: buildBody(context),
builder: (context, snapshot) {
if (snapshot.hasData) {
return snapshot.data!;
} else {
return const Offstage();
}
});
Future<Widget> buildBody(BuildContext context) async {
return Obx(() {
if (gFFI.groupModel.userLoading.value) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (gFFI.groupModel.userLoadError.isNotEmpty) {
return _buildShowError(gFFI.groupModel.userLoadError.value);
}
return Row(
children: [
_buildLeftDesktop(),
Expanded(
child: Align(
alignment: Alignment.topLeft,
child: MyGroupPeerView(
menuPadding: widget.menuPadding,
initPeers: gFFI.groupModel.peersShow.value)),
)
],
);
});
}
Widget _buildShowError(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(translate(error)),
TextButton(
onPressed: () {
gFFI.groupModel.pull();
},
child: Text(translate("Retry")))
],
));
}
Widget _buildLeftDesktop() {
return Row(
children: [
Card(
margin: EdgeInsets.symmetric(horizontal: 4.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side:
BorderSide(color: Theme.of(context).scaffoldBackgroundColor)),
child: Container(
width: 200,
height: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
child: Column(
children: [
_buildLeftHeader(),
Expanded(
child: Container(
width: double.infinity,
height: double.infinity,
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(2)),
child: _buildUserContacts(),
).marginSymmetric(vertical: 8.0),
)
],
),
),
).marginOnly(right: 8.0),
],
);
}
Widget _buildLeftHeader() {
return Row(
children: [
Expanded(
child: TextField(
controller: searchUserController,
onChanged: (value) {
searchUserText.value = value;
},
decoration: InputDecoration(
prefixIcon: Icon(
Icons.search_rounded,
color: Theme.of(context).hintColor,
),
contentPadding: const EdgeInsets.symmetric(vertical: 10),
hintText: translate("Search"),
hintStyle:
TextStyle(fontSize: 14, color: Theme.of(context).hintColor),
border: InputBorder.none,
isDense: true,
),
)),
],
);
}
Widget _buildUserContacts() {
return Obx(() {
return Column(
children: gFFI.groupModel.users
.where((p0) {
if (searchUserText.isNotEmpty) {
return p0.name.contains(searchUserText.value);
}
return true;
})
.map((e) => _buildUserItem(e.name))
.toList());
});
}
Widget _buildUserItem(String username) {
return InkWell(onTap: () {
if (selectedUser.value != username) {
selectedUser.value = username;
gFFI.groupModel.pullUserPeers(username);
}
}, child: Obx(
() {
bool selected = selectedUser.value == username;
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: [
Icon(Icons.person_outline_rounded, color: Colors.grey, size: 16)
.marginOnly(right: 4),
Expanded(child: Text(username)),
],
).paddingSymmetric(vertical: 4),
),
);
},
)).marginSymmetric(horizontal: 12);
}
}

View File

@ -321,6 +321,7 @@ enum CardType {
fav, fav,
lan, lan,
ab, ab,
grp,
} }
abstract class BasePeerCard extends StatelessWidget { abstract class BasePeerCard extends StatelessWidget {
@ -684,6 +685,9 @@ abstract class BasePeerCard extends StatelessWidget {
case CardType.ab: case CardType.ab:
gFFI.abModel.pullAb(); gFFI.abModel.pullAb();
break; break;
case CardType.grp:
gFFI.groupModel.pull();
break;
} }
} }
} }
@ -937,6 +941,41 @@ class AddressBookPeerCard extends BasePeerCard {
} }
} }
class MyGroupPeerCard extends BasePeerCard {
MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
: super(
peer: peer,
cardType: CardType.grp,
menuPadding: menuPadding,
key: key);
@override
Future<List<MenuEntryBase<String>>> _buildMenuItems(
BuildContext context) async {
final List<MenuEntryBase<String>> menuItems = [
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
menuItems.add(await _forceAlwaysRelayAction(peer.id));
if (peer.platform == 'Windows') {
menuItems.add(_rdpAction(context, peer.id));
}
menuItems.add(_wolAction(peer.id));
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
return menuItems;
}
}
void _rdpDialog(String id, CardType card) async { void _rdpDialog(String id, CardType card) async {
String port, username; String port, username;
if (card == CardType.ab) { if (card == CardType.ab) {

View File

@ -4,6 +4,7 @@ import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart';
import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart';
import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart';
import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/consts.dart';
@ -16,6 +17,151 @@ import 'package:get/get.dart';
import '../../common.dart'; import '../../common.dart';
import '../../models/platform_model.dart'; import '../../models/platform_model.dart';
const int groupTabIndex = 4;
class StatePeerTab {
final RxInt currentTab = 0.obs;
static const List<int> tabIndexs = [0, 1, 2, 3, 4];
List<int> tabOrder = List.empty(growable: true);
final RxList<int> visibleTabOrder = RxList.empty(growable: true);
int tabHiddenFlag = 0;
final RxList<String> tabNames = [
translate('Recent Sessions'),
translate('Favorites'),
translate('Discovered'),
translate('Address Book'),
translate('Group'),
].obs;
StatePeerTab._() {
tabHiddenFlag = (int.tryParse(
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
radix: 2) ??
0);
currentTab.value =
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
if (!tabIndexs.contains(currentTab.value)) {
currentTab.value = tabIndexs[0];
}
tabOrder = tabIndexs.toList();
try {
final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order');
if (conf.isNotEmpty) {
final json = jsonDecode(conf);
if (json is List) {
final List<int> list =
json.map((e) => int.tryParse(e.toString()) ?? -1).toList();
if (list.length == tabOrder.length &&
tabOrder.every((e) => list.contains(e))) {
tabOrder = list;
}
}
}
} catch (e) {
debugPrintStack(label: '$e');
}
visibleTabOrder.value = tabOrder.where((e) => !isTabHidden(e)).toList();
visibleTabOrder.remove(groupTabIndex);
}
static final StatePeerTab instance = StatePeerTab._();
check() {
List<int> oldOrder = visibleTabOrder;
if (filterGroupCard()) {
visibleTabOrder.remove(groupTabIndex);
if (currentTab.value == groupTabIndex) {
currentTab.value =
visibleTabOrder.firstWhereOrNull((e) => e != groupTabIndex) ?? 0;
bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: currentTab.value.toString());
}
} else {
if (gFFI.userModel.isAdmin.isFalse &&
gFFI.userModel.groupName.isNotEmpty) {
tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
} else {
tabNames[groupTabIndex] = translate('Group');
}
if (isTabHidden(groupTabIndex)) {
visibleTabOrder.remove(groupTabIndex);
} else {
if (!visibleTabOrder.contains(groupTabIndex)) {
addTabInOrder(visibleTabOrder, groupTabIndex);
}
}
if (visibleTabOrder.contains(groupTabIndex) &&
int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
groupTabIndex) {
currentTab.value = groupTabIndex;
}
}
if (oldOrder != visibleTabOrder) {
saveTabOrder();
}
}
bool isTabHidden(int tabindex) {
return tabHiddenFlag & (1 << tabindex) != 0;
}
bool filterGroupCard() {
if (gFFI.groupModel.users.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
return true;
} else {
return false;
}
}
addTabInOrder(List<int> list, int tabIndex) {
if (!tabOrder.contains(tabIndex) || list.contains(tabIndex)) {
return;
}
bool sameOrder = true;
int lastIndex = -1;
for (int i = 0; i < list.length; i++) {
var index = tabOrder.lastIndexOf(list[i]);
if (index > lastIndex) {
lastIndex = index;
continue;
} else {
sameOrder = false;
break;
}
}
if (sameOrder) {
var indexInTabOrder = tabOrder.indexOf(tabIndex);
var left = List.empty(growable: true);
for (int i = 0; i < indexInTabOrder; i++) {
left.add(tabOrder[i]);
}
int insertIndex = list.lastIndexWhere((e) => left.contains(e));
if (insertIndex < 0) {
insertIndex = 0;
} else {
insertIndex += 1;
}
list.insert(insertIndex, tabIndex);
} else {
list.add(tabIndex);
}
}
saveTabOrder() {
var list = statePeerTab.visibleTabOrder.toList();
var left = tabOrder
.where((e) => !statePeerTab.visibleTabOrder.contains(e))
.toList();
for (var t in left) {
addTabInOrder(list, t);
}
statePeerTab.tabOrder = list;
bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(list));
}
}
final statePeerTab = StatePeerTab.instance;
class PeerTabPage extends StatefulWidget { class PeerTabPage extends StatefulWidget {
const PeerTabPage({Key? key}) : super(key: key); const PeerTabPage({Key? key}) : super(key: key);
@override @override
@ -23,10 +169,9 @@ class PeerTabPage extends StatefulWidget {
} }
class _TabEntry { class _TabEntry {
final String name;
final Widget widget; final Widget widget;
final Function() load; final Function() load;
_TabEntry(this.name, this.widget, this.load); _TabEntry(this.widget, this.load);
} }
EdgeInsets? _menuPadding() { EdgeInsets? _menuPadding() {
@ -35,65 +180,36 @@ EdgeInsets? _menuPadding() {
class _PeerTabPageState extends State<PeerTabPage> class _PeerTabPageState extends State<PeerTabPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final RxInt tabHiddenFlag;
late final RxString currentTab;
late final RxList<String> visibleOrderedTabs;
final List<_TabEntry> entries = [ final List<_TabEntry> entries = [
_TabEntry( _TabEntry(
'Recent Sessions',
RecentPeersView( RecentPeersView(
menuPadding: _menuPadding(), menuPadding: _menuPadding(),
), ),
bind.mainLoadRecentPeers), bind.mainLoadRecentPeers),
_TabEntry( _TabEntry(
'Favorites',
FavoritePeersView( FavoritePeersView(
menuPadding: _menuPadding(), menuPadding: _menuPadding(),
), ),
bind.mainLoadFavPeers), bind.mainLoadFavPeers),
_TabEntry( _TabEntry(
'Discovered',
DiscoveredPeersView( DiscoveredPeersView(
menuPadding: _menuPadding(), menuPadding: _menuPadding(),
), ),
bind.mainDiscover), bind.mainDiscover),
_TabEntry( _TabEntry(
'Address Book',
AddressBook( AddressBook(
menuPadding: _menuPadding(), menuPadding: _menuPadding(),
), ),
() => {}), () => {}),
_TabEntry(
MyGroup(
menuPadding: _menuPadding(),
),
() => {}),
]; ];
@override @override
void initState() { void initState() {
tabHiddenFlag = (int.tryParse(
bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
radix: 2) ??
0)
.obs;
currentTab = bind.getLocalFlutterConfig(k: 'current-peer-tab').obs;
visibleOrderedTabs = entries
.where((e) => !isTabHidden(e.name))
.map((e) => e.name)
.toList()
.obs;
try {
final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order');
if (conf.isNotEmpty) {
final json = jsonDecode(conf);
if (json is List) {
final List<String> list = json.map((e) => e.toString()).toList();
if (list.length == visibleOrderedTabs.length &&
visibleOrderedTabs.every((e) => list.contains(e))) {
visibleOrderedTabs.value = list;
}
}
}
} catch (e) {
debugPrintStack(label: '$e');
}
adjustTab(); adjustTab();
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
@ -105,10 +221,11 @@ class _PeerTabPageState extends State<PeerTabPage>
super.initState(); super.initState();
} }
Future<void> handleTabSelection(String tabName) async { Future<void> handleTabSelection(int tabIndex) async {
currentTab.value = tabName; if (tabIndex < entries.length) {
await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName); statePeerTab.currentTab.value = tabIndex;
entries.firstWhereOrNull((e) => e.name == tabName)?.load(); entries[tabIndex].load();
}
} }
@override @override
@ -148,25 +265,26 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createSwitchBar(BuildContext context) { Widget _createSwitchBar(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color; final textColor = Theme.of(context).textTheme.titleLarge?.color;
statePeerTab.visibleTabOrder
.removeWhere((e) => !StatePeerTab.tabIndexs.contains(e));
return Obx(() { return Obx(() {
int indexCounter = -1; int indexCounter = -1;
return ReorderableListView( return ReorderableListView(
buildDefaultDragHandles: false, buildDefaultDragHandles: false,
onReorder: (oldIndex, newIndex) { onReorder: (oldIndex, newIndex) {
var list = visibleOrderedTabs.toList(); var list = statePeerTab.visibleTabOrder.toList();
if (oldIndex < newIndex) { if (oldIndex < newIndex) {
newIndex -= 1; newIndex -= 1;
} }
final String item = list.removeAt(oldIndex); final int item = list.removeAt(oldIndex);
list.insert(newIndex, item); list.insert(newIndex, item);
bind.setLocalFlutterConfig( statePeerTab.visibleTabOrder.value = list;
k: 'peer-tab-order', v: jsonEncode(list)); statePeerTab.saveTabOrder();
visibleOrderedTabs.value = list;
}, },
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
shrinkWrap: true, shrinkWrap: true,
scrollController: ScrollController(), scrollController: ScrollController(),
children: visibleOrderedTabs.map((t) { children: statePeerTab.visibleTabOrder.map((t) {
indexCounter++; indexCounter++;
return ReorderableDragStartListener( return ReorderableDragStartListener(
key: ValueKey(t), key: ValueKey(t),
@ -175,7 +293,7 @@ class _PeerTabPageState extends State<PeerTabPage>
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: currentTab.value == t color: statePeerTab.currentTab.value == t
? Theme.of(context).backgroundColor ? Theme.of(context).backgroundColor
: null, : null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
@ -183,16 +301,22 @@ class _PeerTabPageState extends State<PeerTabPage>
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
translate(t), statePeerTab.tabNames[t], // TODO
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
height: 1, height: 1,
fontSize: 14, fontSize: 14,
color: currentTab.value == t ? textColor : textColor color: statePeerTab.currentTab.value == t
? textColor
: textColor
?..withOpacity(0.5)), ?..withOpacity(0.5)),
), ),
)), )),
onTap: () async => await handleTabSelection(t), onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: t.toString());
},
), ),
); );
}).toList()); }).toList());
@ -201,13 +325,24 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createPeersView() { Widget _createPeersView() {
final verticalMargin = isDesktop ? 12.0 : 6.0; final verticalMargin = isDesktop ? 12.0 : 6.0;
statePeerTab.visibleTabOrder
.removeWhere((e) => !StatePeerTab.tabIndexs.contains(e));
return Expanded( return Expanded(
child: Obx(() => child: Obx(() {
entries.firstWhereOrNull((e) => e.name == currentTab.value)?.widget ?? if (statePeerTab.visibleTabOrder.isEmpty) {
visibleContextMenuListener(Center( return visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')), child: Text(translate('Right click to select tabs')),
))).marginSymmetric(vertical: verticalMargin), ));
); } else {
if (statePeerTab.visibleTabOrder
.contains(statePeerTab.currentTab.value)) {
return entries[statePeerTab.currentTab.value].widget;
} else {
statePeerTab.currentTab.value = statePeerTab.visibleTabOrder[0];
return entries[statePeerTab.currentTab.value].widget;
}
}
}).marginSymmetric(vertical: verticalMargin));
} }
Widget _createPeerViewTypeSwitch(BuildContext context) { Widget _createPeerViewTypeSwitch(BuildContext context) {
@ -240,22 +375,14 @@ class _PeerTabPageState extends State<PeerTabPage>
); );
} }
bool isTabHidden(String name) {
int index = entries.indexWhere((e) => e.name == name);
if (index >= 0) {
return tabHiddenFlag & (1 << index) != 0;
}
assert(false);
return false;
}
adjustTab() { adjustTab() {
if (visibleOrderedTabs.isNotEmpty) { if (statePeerTab.visibleTabOrder.isNotEmpty) {
if (!visibleOrderedTabs.contains(currentTab.value)) { if (!statePeerTab.visibleTabOrder
handleTabSelection(visibleOrderedTabs[0]); .contains(statePeerTab.currentTab.value)) {
handleTabSelection(statePeerTab.visibleTabOrder[0]);
} }
} else { } else {
currentTab.value = ''; statePeerTab.currentTab.value = 0;
} }
} }
@ -278,35 +405,41 @@ class _PeerTabPageState extends State<PeerTabPage>
} }
Widget visibleContextMenu(CancelFunc cancelFunc) { Widget visibleContextMenu(CancelFunc cancelFunc) {
final List<MenuEntryBase> menu = entries.asMap().entries.map((e) { return Obx(() {
int bitMask = 1 << e.key; final List<MenuEntryBase> menu = List.empty(growable: true);
return MenuEntrySwitch( for (int i = 0; i < statePeerTab.tabNames.length; i++) {
if (i == groupTabIndex && statePeerTab.filterGroupCard()) {
continue;
}
int bitMask = 1 << i;
menu.add(MenuEntrySwitch(
switchType: SwitchType.scheckbox, switchType: SwitchType.scheckbox,
text: translate(e.value.name), text: statePeerTab.tabNames[i],
getter: () async { getter: () async {
return tabHiddenFlag.value & bitMask == 0; return statePeerTab.tabHiddenFlag & bitMask == 0;
}, },
setter: (show) async { setter: (show) async {
if (show) { if (show) {
tabHiddenFlag.value &= ~bitMask; statePeerTab.tabHiddenFlag &= ~bitMask;
} else { } else {
tabHiddenFlag.value |= bitMask; statePeerTab.tabHiddenFlag |= bitMask;
} }
await bind.setLocalFlutterConfig( await bind.setLocalFlutterConfig(
k: 'hidden-peer-card', v: tabHiddenFlag.value.toRadixString(2)); k: 'hidden-peer-card',
visibleOrderedTabs.removeWhere((e) => isTabHidden(e)); v: statePeerTab.tabHiddenFlag.toRadixString(2));
visibleOrderedTabs.addAll(entries statePeerTab.visibleTabOrder
.where((e) => .removeWhere((e) => statePeerTab.isTabHidden(e));
!visibleOrderedTabs.contains(e.name) && for (int j = 0; j < statePeerTab.tabNames.length; j++) {
!isTabHidden(e.name)) if (!statePeerTab.visibleTabOrder.contains(j) &&
.map((e) => e.name) !statePeerTab.isTabHidden(j)) {
.toList()); statePeerTab.visibleTabOrder.add(j);
await bind.setLocalFlutterConfig( }
k: 'peer-tab-order', v: jsonEncode(visibleOrderedTabs)); }
statePeerTab.saveTabOrder();
cancelFunc(); cancelFunc();
adjustTab(); adjustTab();
}); }));
}).toList(); }
return mod_menu.PopupMenu( return mod_menu.PopupMenu(
items: menu items: menu
.map((entry) => entry.build( .map((entry) => entry.build(
@ -317,8 +450,8 @@ class _PeerTabPageState extends State<PeerTabPage>
dividerHeight: 12.0, dividerHeight: 12.0,
))) )))
.expand((i) => i) .expand((i) => i)
.toList(), .toList());
); });
} }
} }

View File

@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView {
return true; return true;
} }
} }
class MyGroupPeerView extends BasePeersView {
MyGroupPeerView(
{Key? key,
EdgeInsets? menuPadding,
ScrollController? scrollController,
required List<Peer> initPeers})
: super(
key: key,
name: 'my group peer',
loadEvent: 'load_my_group_peers',
peerCardBuilder: (Peer peer) => MyGroupPeerCard(
peer: peer,
menuPadding: menuPadding,
),
initPeers: initPeers,
);
}

View File

@ -1059,21 +1059,13 @@ class _AccountState extends State<_Account> {
} }
Widget accountAction() { Widget accountAction() {
return _futureBuilder(future: () async {
return await gFFI.userModel.getUserName();
}(), hasData: (_) {
return Obx(() => _Button( return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
() => { () => {
gFFI.userModel.userName.value.isEmpty gFFI.userModel.userName.value.isEmpty
? loginDialog().then((success) { ? loginDialog()
if (success) {
gFFI.abModel.pullAb();
}
})
: gFFI.userModel.logOut() : gFFI.userModel.logOut()
})); }));
});
} }
} }

View File

@ -117,6 +117,7 @@ void runMainApp(bool startService) async {
// await windowManager.ensureInitialized(); // await windowManager.ensureInitialized();
gFFI.serverModel.startService(); gFFI.serverModel.startService();
} }
gFFI.userModel.refreshCurrentUser();
runApp(App()); runApp(App());
// restore the location of the main window before window hide or show // restore the location of the main window before window hide or show
await restoreWindowPosition(WindowType.Main); await restoreWindowPosition(WindowType.Main);

View File

@ -547,7 +547,6 @@ void showLogin(OverlayDialogManager dialogManager) {
error = resp['error']; error = resp['error'];
return; return;
} }
gFFI.abModel.pullAb();
} }
close(); close();
}, },

View File

@ -21,10 +21,8 @@ class AbModel {
AbModel(this.parent); AbModel(this.parent);
FFI? get _ffi => parent.target;
Future<dynamic> pullAb() async { Future<dynamic> pullAb() async {
if (_ffi!.userModel.userName.isEmpty) return; if (gFFI.userModel.userName.isEmpty) return;
abLoading.value = true; abLoading.value = true;
abError.value = ""; abError.value = "";
final api = "${await bind.mainGetApiServer()}/api/ab/get"; final api = "${await bind.mainGetApiServer()}/api/ab/get";
@ -63,7 +61,8 @@ class AbModel {
return null; return null;
} }
void reset() { Future<void> reset() async {
await bind.mainSetLocalOption(key: "selected-tags", value: '');
tags.clear(); tags.clear();
peers.clear(); peers.clear();
} }
@ -188,9 +187,4 @@ class AbModel {
await pushAb(); await pushAb();
} }
} }
void clear() {
peers.clear();
tags.clear();
}
} }

View File

@ -0,0 +1,139 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:get/get.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
class GroupModel {
final RxBool userLoading = false.obs;
final RxString userLoadError = "".obs;
final RxBool peerLoading = false.obs; //to-do: not used
final RxString peerLoadError = "".obs;
final RxList<UserPayload> users = RxList.empty(growable: true);
final RxList<PeerPayload> peerPayloads = RxList.empty(growable: true);
final RxList<Peer> peersShow = RxList.empty(growable: true);
WeakReference<FFI> parent;
GroupModel(this.parent);
Future<void> reset() async {
userLoading.value = false;
userLoadError.value = "";
peerLoading.value = false;
peerLoadError.value = "";
users.clear();
peerPayloads.clear();
peersShow.clear();
}
Future<void> pull() async {
await reset();
if (gFFI.userModel.userName.isEmpty ||
(gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
statePeerTab.check();
return;
}
userLoading.value = true;
userLoadError.value = "";
final api = "${await bind.mainGetApiServer()}/api/users";
try {
var uri0 = Uri.parse(api);
final pageSize = 20;
var total = 0;
int current = 1;
do {
var uri = Uri(
scheme: uri0.scheme,
host: uri0.host,
path: uri0.path,
port: uri0.port,
queryParameters: {
'current': current.toString(),
'pageSize': pageSize.toString(),
if (gFFI.userModel.isAdmin.isFalse)
'grp': gFFI.userModel.groupName.value,
});
current += pageSize;
final resp = await http.get(uri, headers: await getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {
throw json['error'];
} else {
total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final user in data) {
users.add(UserPayload.fromJson(user));
}
}
}
}
}
} while (current < total);
} catch (err) {
debugPrint('$err');
userLoadError.value = err.toString();
} finally {
userLoading.value = false;
statePeerTab.check();
}
}
Future<void> pullUserPeers(String username) async {
peerPayloads.clear();
peersShow.clear();
peerLoading.value = true;
peerLoadError.value = "";
final api = "${await bind.mainGetApiServer()}/api/peers";
try {
var uri0 = Uri.parse(api);
final pageSize = 20;
var total = 0;
int current = 1;
do {
var uri = Uri(
scheme: uri0.scheme,
host: uri0.host,
path: uri0.path,
port: uri0.port,
queryParameters: {
'current': current.toString(),
'pageSize': pageSize.toString(),
'user_name': username
});
current += pageSize;
final resp = await http.get(uri, headers: await getHttpHeaders());
if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
Map<String, dynamic> json = jsonDecode(resp.body);
if (json.containsKey('error')) {
throw json['error'];
} else {
total = json['total'];
if (json.containsKey('data')) {
final data = json['data'];
if (data is List) {
for (final p in data) {
final peer = PeerPayload.fromJson(p);
peerPayloads.add(peer);
peersShow.add(PeerPayload.toPeer(peer));
}
}
}
}
}
} while (current < total);
} catch (err) {
debugPrint('$err');
peerLoadError.value = err.toString();
} finally {
peerLoading.value = false;
}
}
}

View File

@ -12,6 +12,7 @@ import 'package:flutter_hbb/generated_bridge.dart';
import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/ab_model.dart';
import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/chat_model.dart';
import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/group_model.dart';
import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/server_model.dart';
import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/state_model.dart';
@ -1221,6 +1222,7 @@ class FFI {
late final ChatModel chatModel; // session late final ChatModel chatModel; // session
late final FileModel fileModel; // session late final FileModel fileModel; // session
late final AbModel abModel; // global late final AbModel abModel; // global
late final GroupModel groupModel; // global
late final UserModel userModel; // global late final UserModel userModel; // global
late final QualityMonitorModel qualityMonitorModel; // session late final QualityMonitorModel qualityMonitorModel; // session
late final RecordingModel recordingModel; // recording late final RecordingModel recordingModel; // recording
@ -1234,8 +1236,9 @@ class FFI {
serverModel = ServerModel(WeakReference(this)); serverModel = ServerModel(WeakReference(this));
chatModel = ChatModel(WeakReference(this)); chatModel = ChatModel(WeakReference(this));
fileModel = FileModel(WeakReference(this)); fileModel = FileModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
userModel = UserModel(WeakReference(this)); userModel = UserModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
groupModel = GroupModel(WeakReference(this));
qualityMonitorModel = QualityMonitorModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this));
recordingModel = RecordingModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this));
inputModel = InputModel(WeakReference(this)); inputModel = InputModel(WeakReference(this));

View File

@ -1,7 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart';
import 'package:flutter_hbb/common/widgets/peer_tab_page.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
@ -10,17 +11,19 @@ import 'model.dart';
import 'platform_model.dart'; import 'platform_model.dart';
class UserModel { class UserModel {
var userName = ''.obs; final RxString userName = ''.obs;
final RxString groupName = ''.obs;
final RxBool isAdmin = false.obs;
WeakReference<FFI> parent; WeakReference<FFI> parent;
UserModel(this.parent) { UserModel(this.parent);
refreshCurrentUser();
}
void refreshCurrentUser() async { void refreshCurrentUser() async {
await getUserName();
final token = bind.mainGetLocalOption(key: 'access_token'); final token = bind.mainGetLocalOption(key: 'access_token');
if (token == '') return; if (token == '') {
await _updateOtherModels();
return;
}
final url = await bind.mainGetApiServer(); final url = await bind.mainGetApiServer();
final body = { final body = {
'id': await bind.mainGetMyId(), 'id': await bind.mainGetMyId(),
@ -35,55 +38,42 @@ class UserModel {
body: json.encode(body)); body: json.encode(body));
final status = response.statusCode; final status = response.statusCode;
if (status == 401 || status == 400) { if (status == 401 || status == 400) {
resetToken(); reset();
return; return;
} }
await _parseResp(response.body); final data = json.decode(response.body);
} catch (e) {
print('Failed to refreshCurrentUser: $e');
}
}
void resetToken() async {
await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: '');
userName.value = '';
}
Future<String> _parseResp(String body) async {
final data = json.decode(body);
final error = data['error']; final error = data['error'];
if (error != null) { if (error != null) {
return error!; throw error;
} }
final token = data['access_token']; await _parseUserInfo(data);
if (token != null) { } catch (e) {
await bind.mainSetLocalOption(key: 'access_token', value: token); print('Failed to refreshCurrentUser: $e');
} finally {
await _updateOtherModels();
} }
final info = data['user'];
if (info != null) {
final value = json.encode(info);
await bind.mainSetOption(key: 'user_info', value: value);
userName.value = info['name'];
}
return '';
} }
Future<String> getUserName() async { Future<void> reset() async {
if (userName.isNotEmpty) { await bind.mainSetLocalOption(key: 'access_token', value: '');
return userName.value; await bind.mainSetLocalOption(key: 'user_info', value: '');
} await gFFI.abModel.reset();
final userInfo = bind.mainGetLocalOption(key: 'user_info'); await gFFI.groupModel.reset();
if (userInfo.trim().isEmpty) {
return '';
}
final m = jsonDecode(userInfo);
if (m == null) {
userName.value = ''; userName.value = '';
} else { groupName.value = '';
userName.value = m['name'] ?? ''; statePeerTab.check();
} }
return userName.value;
Future<void> _parseUserInfo(dynamic userinfo) async {
bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(userinfo));
userName.value = userinfo['name'] ?? '';
groupName.value = userinfo['grp'] ?? '';
isAdmin.value = userinfo['is_admin'] == true;
}
Future<void> _updateOtherModels() async {
await gFFI.abModel.pullAb();
await gFFI.groupModel.pull();
} }
Future<void> logOut() async { Future<void> logOut() async {
@ -95,13 +85,7 @@ class UserModel {
'uuid': await bind.mainGetUuid(), 'uuid': await bind.mainGetUuid(),
}, },
headers: await getHttpHeaders()); headers: await getHttpHeaders());
await Future.wait([ await reset();
bind.mainSetLocalOption(key: 'access_token', value: ''),
bind.mainSetLocalOption(key: 'user_info', value: ''),
bind.mainSetLocalOption(key: 'selected-tags', value: ''),
]);
parent.target?.abModel.clear();
userName.value = '';
gFFI.dialogManager.dismissByTag(tag); gFFI.dialogManager.dismissByTag(tag);
} }
@ -119,12 +103,12 @@ class UserModel {
final body = jsonDecode(resp.body); final body = jsonDecode(resp.body);
bind.mainSetLocalOption( bind.mainSetLocalOption(
key: 'access_token', value: body['access_token'] ?? ''); key: 'access_token', value: body['access_token'] ?? '');
bind.mainSetLocalOption( await _parseUserInfo(body['user']);
key: 'user_info', value: jsonEncode(body['user']));
this.userName.value = body['user']?['name'] ?? '';
return body; return body;
} catch (err) { } catch (err) {
return {'error': '$err'}; return {'error': '$err'};
} finally {
await _updateOtherModels();
} }
} }
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -401,5 +401,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Right click to select tabs", "右键选择选项卡"), ("Right click to select tabs", "右键选择选项卡"),
("Skipped", "已跳过"), ("Skipped", "已跳过"),
("Add to Address Book", "添加到地址簿"), ("Add to Address Book", "添加到地址簿"),
("Group", "小组"),
("Search", "搜索"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"),
("Add to Address Book", "Zum Adressbuch hinzufügen"), ("Add to Address Book", "Zum Adressbuch hinzufügen"),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."),
("Right click to select tabs", "Clic derecho para seleccionar pestañas"), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"),
("Add to Address Book", "Añadir a la libreta de direcciones"), ("Add to Address Book", "Añadir a la libreta de direcciones"),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."), ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."),
("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"), ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"),
("Add to Address Book", "افزودن به دفترچه آدرس"), ("Add to Address Book", "افزودن به دفترچه آدرس"),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."),
("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."),
("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"),
("Add to Address Book", "Aggiungi alla rubrica"), ("Add to Address Book", "Aggiungi alla rubrica"),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."),
("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"),
("Add to Address Book", "Добавить в адресную книгу"), ("Add to Address Book", "Добавить в адресную книгу"),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", "右鍵選擇選項卡"), ("Right click to select tabs", "右鍵選擇選項卡"),
("Add to Address Book", "添加到地址簿"), ("Add to Address Book", "添加到地址簿"),
("Group", "小組"),
("Search", "搜索"),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""), ("wayland_experiment_tip", ""),
("Right click to select tabs", ""), ("Right click to select tabs", ""),
("Add to Address Book", ""), ("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].iter().cloned().collect(); ].iter().cloned().collect();
} }