add group peer card
Signed-off-by: 21pages <pages21@163.com>
This commit is contained in:
parent
5ee3e3f347
commit
880a0d4209
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
flutter/lib/common/hbbs/hbbs.dart
Normal file
39
flutter/lib/common/hbbs/hbbs.dart
Normal 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});
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
183
flutter/lib/common/widgets/my_group.dart
Normal file
183
flutter/lib/common/widgets/my_group.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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());
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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()
|
||||||
}));
|
}));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -547,7 +547,6 @@ void showLogin(OverlayDialogManager dialogManager) {
|
|||||||
error = resp['error'];
|
error = resp['error'];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
gFFI.abModel.pullAb();
|
|
||||||
}
|
}
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
139
flutter/lib/models/group_model.dart
Normal file
139
flutter/lib/models/group_model.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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));
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user