Merge pull request #2538 from 21pages/group_card

add group peer card
This commit is contained in:
RustDesk 2022-12-14 11:42:33 +08:00 committed by GitHub
commit ac14e462f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 777 additions and 195 deletions

View File

@ -99,22 +99,28 @@ class IconFont {
class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
const ColorThemeExtension({
required this.border,
required this.highlight,
});
final Color? border;
final Color? highlight;
static const light = ColorThemeExtension(
border: Color(0xFFCCCCCC),
highlight: Color(0xFFE5E5E5),
);
static const dark = ColorThemeExtension(
border: Color(0xFF555555),
highlight: Color(0xFF3F3F3F),
);
@override
ThemeExtension<ColorThemeExtension> copyWith({Color? border}) {
ThemeExtension<ColorThemeExtension> copyWith(
{Color? border, Color? highlight}) {
return ColorThemeExtension(
border: border ?? this.border,
highlight: highlight ?? this.highlight,
);
}
@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
}
return ColorThemeExtension(
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
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb());
}
@override
@ -45,11 +44,7 @@ class _AddressBookState extends State<AddressBook> {
handleLogin() {
// TODO refactor login dialog for desktop and mobile
if (isDesktop) {
loginDialog().then((success) {
if (success) {
gFFI.abModel.pullAb();
}
});
loginDialog();
} else {
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,
lan,
ab,
grp,
}
abstract class BasePeerCard extends StatelessWidget {
@ -684,6 +685,9 @@ abstract class BasePeerCard extends StatelessWidget {
case CardType.ab:
gFFI.abModel.pullAb();
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 {
String port, username;
if (card == CardType.ab) {

View File

@ -4,6 +4,7 @@ import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.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/peer_card.dart';
import 'package:flutter_hbb/consts.dart';
@ -16,6 +17,151 @@ import 'package:get/get.dart';
import '../../common.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 {
const PeerTabPage({Key? key}) : super(key: key);
@override
@ -23,10 +169,9 @@ class PeerTabPage extends StatefulWidget {
}
class _TabEntry {
final String name;
final Widget widget;
final Function() load;
_TabEntry(this.name, this.widget, this.load);
_TabEntry(this.widget, this.load);
}
EdgeInsets? _menuPadding() {
@ -35,65 +180,36 @@ EdgeInsets? _menuPadding() {
class _PeerTabPageState extends State<PeerTabPage>
with SingleTickerProviderStateMixin {
late final RxInt tabHiddenFlag;
late final RxString currentTab;
late final RxList<String> visibleOrderedTabs;
final List<_TabEntry> entries = [
_TabEntry(
'Recent Sessions',
RecentPeersView(
menuPadding: _menuPadding(),
),
bind.mainLoadRecentPeers),
_TabEntry(
'Favorites',
FavoritePeersView(
menuPadding: _menuPadding(),
),
bind.mainLoadFavPeers),
_TabEntry(
'Discovered',
DiscoveredPeersView(
menuPadding: _menuPadding(),
),
bind.mainDiscover),
_TabEntry(
'Address Book',
AddressBook(
menuPadding: _menuPadding(),
),
() => {}),
_TabEntry(
MyGroup(
menuPadding: _menuPadding(),
),
() => {}),
];
@override
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();
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
@ -105,10 +221,11 @@ class _PeerTabPageState extends State<PeerTabPage>
super.initState();
}
Future<void> handleTabSelection(String tabName) async {
currentTab.value = tabName;
await bind.setLocalFlutterConfig(k: 'current-peer-tab', v: tabName);
entries.firstWhereOrNull((e) => e.name == tabName)?.load();
Future<void> handleTabSelection(int tabIndex) async {
if (tabIndex < entries.length) {
statePeerTab.currentTab.value = tabIndex;
entries[tabIndex].load();
}
}
@override
@ -148,25 +265,26 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createSwitchBar(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
statePeerTab.visibleTabOrder
.removeWhere((e) => !StatePeerTab.tabIndexs.contains(e));
return Obx(() {
int indexCounter = -1;
return ReorderableListView(
buildDefaultDragHandles: false,
onReorder: (oldIndex, newIndex) {
var list = visibleOrderedTabs.toList();
var list = statePeerTab.visibleTabOrder.toList();
if (oldIndex < newIndex) {
newIndex -= 1;
}
final String item = list.removeAt(oldIndex);
final int item = list.removeAt(oldIndex);
list.insert(newIndex, item);
bind.setLocalFlutterConfig(
k: 'peer-tab-order', v: jsonEncode(list));
visibleOrderedTabs.value = list;
statePeerTab.visibleTabOrder.value = list;
statePeerTab.saveTabOrder();
},
scrollDirection: Axis.horizontal,
shrinkWrap: true,
scrollController: ScrollController(),
children: visibleOrderedTabs.map((t) {
children: statePeerTab.visibleTabOrder.map((t) {
indexCounter++;
return ReorderableDragStartListener(
key: ValueKey(t),
@ -175,7 +293,7 @@ class _PeerTabPageState extends State<PeerTabPage>
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: currentTab.value == t
color: statePeerTab.currentTab.value == t
? Theme.of(context).backgroundColor
: null,
borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
@ -183,16 +301,22 @@ class _PeerTabPageState extends State<PeerTabPage>
child: Align(
alignment: Alignment.center,
child: Text(
translate(t),
statePeerTab.tabNames[t], // TODO
textAlign: TextAlign.center,
style: TextStyle(
height: 1,
fontSize: 14,
color: currentTab.value == t ? textColor : textColor
color: statePeerTab.currentTab.value == t
? textColor
: textColor
?..withOpacity(0.5)),
),
)),
onTap: () async => await handleTabSelection(t),
onTap: () async {
await handleTabSelection(t);
await bind.setLocalFlutterConfig(
k: 'peer-tab-index', v: t.toString());
},
),
);
}).toList());
@ -201,13 +325,24 @@ class _PeerTabPageState extends State<PeerTabPage>
Widget _createPeersView() {
final verticalMargin = isDesktop ? 12.0 : 6.0;
statePeerTab.visibleTabOrder
.removeWhere((e) => !StatePeerTab.tabIndexs.contains(e));
return Expanded(
child: Obx(() =>
entries.firstWhereOrNull((e) => e.name == currentTab.value)?.widget ??
visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')),
))).marginSymmetric(vertical: verticalMargin),
);
child: Obx(() {
if (statePeerTab.visibleTabOrder.isEmpty) {
return visibleContextMenuListener(Center(
child: Text(translate('Right click to select tabs')),
));
} 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) {
@ -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() {
if (visibleOrderedTabs.isNotEmpty) {
if (!visibleOrderedTabs.contains(currentTab.value)) {
handleTabSelection(visibleOrderedTabs[0]);
if (statePeerTab.visibleTabOrder.isNotEmpty) {
if (!statePeerTab.visibleTabOrder
.contains(statePeerTab.currentTab.value)) {
handleTabSelection(statePeerTab.visibleTabOrder[0]);
}
} else {
currentTab.value = '';
statePeerTab.currentTab.value = 0;
}
}
@ -278,47 +405,53 @@ class _PeerTabPageState extends State<PeerTabPage>
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
final List<MenuEntryBase> menu = entries.asMap().entries.map((e) {
int bitMask = 1 << e.key;
return MenuEntrySwitch(
switchType: SwitchType.scheckbox,
text: translate(e.value.name),
getter: () async {
return tabHiddenFlag.value & bitMask == 0;
},
setter: (show) async {
if (show) {
tabHiddenFlag.value &= ~bitMask;
} else {
tabHiddenFlag.value |= bitMask;
}
await bind.setLocalFlutterConfig(
k: 'hidden-peer-card', v: tabHiddenFlag.value.toRadixString(2));
visibleOrderedTabs.removeWhere((e) => isTabHidden(e));
visibleOrderedTabs.addAll(entries
.where((e) =>
!visibleOrderedTabs.contains(e.name) &&
!isTabHidden(e.name))
.map((e) => e.name)
.toList());
await bind.setLocalFlutterConfig(
k: 'peer-tab-order', v: jsonEncode(visibleOrderedTabs));
cancelFunc();
adjustTab();
});
}).toList();
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList(),
);
return Obx(() {
final List<MenuEntryBase> menu = List.empty(growable: true);
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,
text: statePeerTab.tabNames[i],
getter: () async {
return statePeerTab.tabHiddenFlag & bitMask == 0;
},
setter: (show) async {
if (show) {
statePeerTab.tabHiddenFlag &= ~bitMask;
} else {
statePeerTab.tabHiddenFlag |= bitMask;
}
await bind.setLocalFlutterConfig(
k: 'hidden-peer-card',
v: statePeerTab.tabHiddenFlag.toRadixString(2));
statePeerTab.visibleTabOrder
.removeWhere((e) => statePeerTab.isTabHidden(e));
for (int j = 0; j < statePeerTab.tabNames.length; j++) {
if (!statePeerTab.visibleTabOrder.contains(j) &&
!statePeerTab.isTabHidden(j)) {
statePeerTab.visibleTabOrder.add(j);
}
}
statePeerTab.saveTabOrder();
cancelFunc();
adjustTab();
}));
}
return mod_menu.PopupMenu(
items: menu
.map((entry) => entry.build(
context,
const MenuConfig(
commonColor: MyTheme.accent,
height: 20.0,
dividerHeight: 12.0,
)))
.expand((i) => i)
.toList());
});
}
}

View File

@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView {
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() {
return _futureBuilder(future: () async {
return await gFFI.userModel.getUserName();
}(), hasData: (_) {
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog().then((success) {
if (success) {
gFFI.abModel.pullAb();
}
})
: gFFI.userModel.logOut()
}));
});
return Obx(() => _Button(
gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout',
() => {
gFFI.userModel.userName.value.isEmpty
? loginDialog()
: gFFI.userModel.logOut()
}));
}
}

View File

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

View File

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

View File

@ -21,10 +21,8 @@ class AbModel {
AbModel(this.parent);
FFI? get _ffi => parent.target;
Future<dynamic> pullAb() async {
if (_ffi!.userModel.userName.isEmpty) return;
if (gFFI.userModel.userName.isEmpty) return;
abLoading.value = true;
abError.value = "";
final api = "${await bind.mainGetApiServer()}/api/ab/get";
@ -63,7 +61,8 @@ class AbModel {
return null;
}
void reset() {
Future<void> reset() async {
await bind.mainSetLocalOption(key: "selected-tags", value: '');
tags.clear();
peers.clear();
}
@ -188,9 +187,4 @@ class AbModel {
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/chat_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/user_model.dart';
import 'package:flutter_hbb/models/state_model.dart';
@ -1221,6 +1222,7 @@ class FFI {
late final ChatModel chatModel; // session
late final FileModel fileModel; // session
late final AbModel abModel; // global
late final GroupModel groupModel; // global
late final UserModel userModel; // global
late final QualityMonitorModel qualityMonitorModel; // session
late final RecordingModel recordingModel; // recording
@ -1234,8 +1236,9 @@ class FFI {
serverModel = ServerModel(WeakReference(this));
chatModel = ChatModel(WeakReference(this));
fileModel = FileModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
userModel = UserModel(WeakReference(this));
abModel = AbModel(WeakReference(this));
groupModel = GroupModel(WeakReference(this));
qualityMonitorModel = QualityMonitorModel(WeakReference(this));
recordingModel = RecordingModel(WeakReference(this));
inputModel = InputModel(WeakReference(this));

View File

@ -1,7 +1,8 @@
import 'dart:async';
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:http/http.dart' as http;
@ -10,17 +11,19 @@ import 'model.dart';
import 'platform_model.dart';
class UserModel {
var userName = ''.obs;
final RxString userName = ''.obs;
final RxString groupName = ''.obs;
final RxBool isAdmin = false.obs;
WeakReference<FFI> parent;
UserModel(this.parent) {
refreshCurrentUser();
}
UserModel(this.parent);
void refreshCurrentUser() async {
await getUserName();
final token = bind.mainGetLocalOption(key: 'access_token');
if (token == '') return;
if (token == '') {
await _updateOtherModels();
return;
}
final url = await bind.mainGetApiServer();
final body = {
'id': await bind.mainGetMyId(),
@ -35,55 +38,42 @@ class UserModel {
body: json.encode(body));
final status = response.statusCode;
if (status == 401 || status == 400) {
resetToken();
reset();
return;
}
await _parseResp(response.body);
final data = json.decode(response.body);
final error = data['error'];
if (error != null) {
throw error;
}
await _parseUserInfo(data);
} catch (e) {
print('Failed to refreshCurrentUser: $e');
} finally {
await _updateOtherModels();
}
}
void resetToken() async {
Future<void> reset() async {
await bind.mainSetLocalOption(key: 'access_token', value: '');
await bind.mainSetLocalOption(key: 'user_info', value: '');
await gFFI.abModel.reset();
await gFFI.groupModel.reset();
userName.value = '';
groupName.value = '';
statePeerTab.check();
}
Future<String> _parseResp(String body) async {
final data = json.decode(body);
final error = data['error'];
if (error != null) {
return error!;
}
final token = data['access_token'];
if (token != null) {
await bind.mainSetLocalOption(key: 'access_token', value: token);
}
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<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<String> getUserName() async {
if (userName.isNotEmpty) {
return userName.value;
}
final userInfo = bind.mainGetLocalOption(key: 'user_info');
if (userInfo.trim().isEmpty) {
return '';
}
final m = jsonDecode(userInfo);
if (m == null) {
userName.value = '';
} else {
userName.value = m['name'] ?? '';
}
return userName.value;
Future<void> _updateOtherModels() async {
await gFFI.abModel.pullAb();
await gFFI.groupModel.pull();
}
Future<void> logOut() async {
@ -95,13 +85,7 @@ class UserModel {
'uuid': await bind.mainGetUuid(),
},
headers: await getHttpHeaders());
await Future.wait([
bind.mainSetLocalOption(key: 'access_token', value: ''),
bind.mainSetLocalOption(key: 'user_info', value: ''),
bind.mainSetLocalOption(key: 'selected-tags', value: ''),
]);
parent.target?.abModel.clear();
userName.value = '';
await reset();
gFFI.dialogManager.dismissByTag(tag);
}
@ -119,12 +103,12 @@ class UserModel {
final body = jsonDecode(resp.body);
bind.mainSetLocalOption(
key: 'access_token', value: body['access_token'] ?? '');
bind.mainSetLocalOption(
key: 'user_info', value: jsonEncode(body['user']));
this.userName.value = body['user']?['name'] ?? '';
await _parseUserInfo(body['user']);
return body;
} catch (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", ""),
("Right click to select tabs", ""),
("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].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", "右键选择选项卡"),
("Skipped", "已跳过"),
("Add to Address Book", "添加到地址簿"),
("Group", "小组"),
("Search", "搜索"),
].iter().cloned().collect();
}

View File

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

View File

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

View File

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

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""),
("Right click to select tabs", ""),
("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].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."),
("Right click to select tabs", "Clic derecho para seleccionar pestañas"),
("Add to Address Book", "Añadir a la libreta de direcciones"),
("Group", ""),
("Search", ""),
].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 استفاده کنید."),
("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"),
("Add to Address Book", "افزودن به دفترچه آدرس"),
("Group", ""),
("Search", ""),
].iter().cloned().collect();
}

View File

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

View File

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

View File

@ -400,5 +400,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("wayland_experiment_tip", ""),
("Right click to select tabs", ""),
("Add to Address Book", ""),
("Group", ""),
("Search", ""),
].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."),
("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"),
("Add to Address Book", "Aggiungi alla rubrica"),
("Group", ""),
("Search", ""),
].iter().cloned().collect();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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