mirror of
https://github.com/rustdesk/rustdesk.git
synced 2024-12-22 17:27:50 +08:00
30afe4f779
Signed-off-by: fufesou <linlong1266@gmail.com>
1288 lines
43 KiB
Dart
1288 lines
43 KiB
Dart
// original cm window in Sciter version.
|
|
|
|
import 'dart:async';
|
|
import 'dart:math';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
|
import 'package:flutter_hbb/consts.dart';
|
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
|
import 'package:flutter_hbb/models/chat_model.dart';
|
|
import 'package:flutter_hbb/models/cm_file_model.dart';
|
|
import 'package:flutter_hbb/utils/platform_channel.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:percent_indicator/linear_percent_indicator.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:window_manager/window_manager.dart';
|
|
import 'package:flutter_svg/flutter_svg.dart';
|
|
|
|
import '../../common.dart';
|
|
import '../../common/widgets/chat_page.dart';
|
|
import '../../models/file_model.dart';
|
|
import '../../models/platform_model.dart';
|
|
import '../../models/server_model.dart';
|
|
|
|
class DesktopServerPage extends StatefulWidget {
|
|
const DesktopServerPage({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<DesktopServerPage> createState() => _DesktopServerPageState();
|
|
}
|
|
|
|
class _DesktopServerPageState extends State<DesktopServerPage>
|
|
with WindowListener, AutomaticKeepAliveClientMixin {
|
|
final tabController = gFFI.serverModel.tabController;
|
|
@override
|
|
void initState() {
|
|
gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
|
|
windowManager.addListener(this);
|
|
Get.put<DesktopTabController>(tabController);
|
|
tabController.onRemoved = (_, id) {
|
|
onRemoveId(id);
|
|
};
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
windowManager.removeListener(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void onWindowClose() {
|
|
Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
|
|
if (isMacOS) {
|
|
RdPlatformChannel.instance.terminate();
|
|
} else {
|
|
windowManager.setPreventClose(false);
|
|
windowManager.close();
|
|
}
|
|
});
|
|
super.onWindowClose();
|
|
}
|
|
|
|
void onRemoveId(String id) {
|
|
if (tabController.state.value.tabs.isEmpty) {
|
|
windowManager.close();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return MultiProvider(
|
|
providers: [
|
|
ChangeNotifierProvider.value(value: gFFI.serverModel),
|
|
ChangeNotifierProvider.value(value: gFFI.chatModel),
|
|
],
|
|
child: Consumer<ServerModel>(
|
|
builder: (context, serverModel, child) {
|
|
final body = Scaffold(
|
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
|
body: ConnectionManager(),
|
|
);
|
|
return isLinux
|
|
? buildVirtualWindowFrame(context, body)
|
|
: Container(
|
|
decoration: BoxDecoration(
|
|
border:
|
|
Border.all(color: MyTheme.color(context).border!)),
|
|
child: body,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|
|
|
|
class ConnectionManager extends StatefulWidget {
|
|
@override
|
|
State<StatefulWidget> createState() => ConnectionManagerState();
|
|
}
|
|
|
|
class ConnectionManagerState extends State<ConnectionManager>
|
|
with WidgetsBindingObserver {
|
|
final RxBool _block = false.obs;
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
super.didChangeAppLifecycleState(state);
|
|
if (state == AppLifecycleState.resumed) {
|
|
if (!allowRemoteCMModification()) {
|
|
shouldBeBlocked(_block, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
gFFI.serverModel.updateClientState();
|
|
gFFI.serverModel.tabController.onSelected = (client_id_str) {
|
|
final client_id = int.tryParse(client_id_str);
|
|
if (client_id != null) {
|
|
final client =
|
|
gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == client_id);
|
|
if (client != null) {
|
|
gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
|
|
if (client.unreadChatMessageCount.value > 0) {
|
|
Future.delayed(Duration.zero, () {
|
|
client.unreadChatMessageCount.value = 0;
|
|
gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
|
|
});
|
|
}
|
|
windowManager.setTitle(getWindowNameWithId(client.peerId));
|
|
gFFI.cmFileModel.updateCurrentClientId(client.id);
|
|
}
|
|
}
|
|
};
|
|
gFFI.chatModel.isConnManager = true;
|
|
WidgetsBinding.instance.addObserver(this);
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final serverModel = Provider.of<ServerModel>(context);
|
|
pointerHandler(PointerEvent e) {
|
|
if (serverModel.cmHiddenTimer != null) {
|
|
serverModel.cmHiddenTimer!.cancel();
|
|
serverModel.cmHiddenTimer = null;
|
|
debugPrint("CM hidden timer has been canceled");
|
|
}
|
|
}
|
|
|
|
return serverModel.clients.isEmpty
|
|
? Column(
|
|
children: [
|
|
buildTitleBar(),
|
|
Expanded(
|
|
child: Center(
|
|
child: Text(translate("Waiting")),
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: Listener(
|
|
onPointerDown: pointerHandler,
|
|
onPointerMove: pointerHandler,
|
|
child: DesktopTab(
|
|
showTitle: false,
|
|
showMaximize: false,
|
|
showMinimize: true,
|
|
showClose: true,
|
|
onWindowCloseButton: handleWindowCloseButton,
|
|
controller: serverModel.tabController,
|
|
selectedBorderColor: MyTheme.accent,
|
|
maxLabelWidth: 100,
|
|
tail: null, //buildScrollJumper(),
|
|
blockTab: allowRemoteCMModification() ? null : _block,
|
|
selectedTabBackgroundColor:
|
|
Theme.of(context).hintColor.withOpacity(0),
|
|
tabBuilder: (key, icon, label, themeConf) {
|
|
final client = serverModel.clients
|
|
.firstWhereOrNull((client) => client.id.toString() == key);
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Tooltip(
|
|
message: key,
|
|
waitDuration: Duration(seconds: 1),
|
|
child: label),
|
|
unreadMessageCountBuilder(client?.unreadChatMessageCount)
|
|
.marginOnly(left: 4),
|
|
],
|
|
);
|
|
},
|
|
pageViewBuilder: (pageView) => LayoutBuilder(
|
|
builder: (context, constrains) {
|
|
var borderWidth = 0.0;
|
|
if (constrains.maxWidth >
|
|
kConnectionManagerWindowSizeClosedChat.width) {
|
|
borderWidth = kConnectionManagerWindowSizeOpenChat.width -
|
|
constrains.maxWidth;
|
|
} else {
|
|
borderWidth = kConnectionManagerWindowSizeClosedChat.width -
|
|
constrains.maxWidth;
|
|
}
|
|
if (borderWidth < 0 || borderWidth > 50) {
|
|
borderWidth = 0;
|
|
}
|
|
final realClosedWidth =
|
|
kConnectionManagerWindowSizeClosedChat.width -
|
|
borderWidth;
|
|
final realChatPageWidth =
|
|
constrains.maxWidth - realClosedWidth;
|
|
return Row(children: [
|
|
if (constrains.maxWidth >
|
|
kConnectionManagerWindowSizeClosedChat.width)
|
|
Consumer<ChatModel>(
|
|
builder: (_, model, child) => SizedBox(
|
|
width: realChatPageWidth,
|
|
child: allowRemoteCMModification()
|
|
? buildSidePage()
|
|
: buildRemoteBlock(
|
|
child: buildSidePage(),
|
|
block: _block,
|
|
mask: true),
|
|
)),
|
|
SizedBox(
|
|
width: realClosedWidth,
|
|
child:
|
|
SizedBox(width: realClosedWidth, child: pageView)),
|
|
]);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildSidePage() {
|
|
final selected = gFFI.serverModel.tabController.state.value.selected;
|
|
if (selected < 0 || selected >= gFFI.serverModel.clients.length) {
|
|
return Offstage();
|
|
}
|
|
final clientType = gFFI.serverModel.clients[selected].type_();
|
|
if (clientType == ClientType.file) {
|
|
return _FileTransferLogPage();
|
|
} else {
|
|
return ChatPage(type: ChatPageType.desktopCM);
|
|
}
|
|
}
|
|
|
|
Widget buildTitleBar() {
|
|
return SizedBox(
|
|
height: kDesktopRemoteTabBarHeight,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
const _AppIcon(),
|
|
Expanded(
|
|
child: GestureDetector(
|
|
onPanStart: (d) {
|
|
windowManager.startDragging();
|
|
},
|
|
child: Container(
|
|
color: Theme.of(context).colorScheme.background,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(
|
|
width: 4.0,
|
|
),
|
|
const _CloseButton()
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildScrollJumper() {
|
|
final offstage = gFFI.serverModel.clients.length < 2;
|
|
final sc = gFFI.serverModel.tabController.state.value.scrollController;
|
|
return Offstage(
|
|
offstage: offstage,
|
|
child: Row(
|
|
children: [
|
|
ActionIcon(
|
|
icon: Icons.arrow_left, iconSize: 22, onTap: sc.backward),
|
|
ActionIcon(
|
|
icon: Icons.arrow_right, iconSize: 22, onTap: sc.forward),
|
|
],
|
|
));
|
|
}
|
|
|
|
Future<bool> handleWindowCloseButton() async {
|
|
var tabController = gFFI.serverModel.tabController;
|
|
final connLength = tabController.length;
|
|
if (connLength <= 1) {
|
|
windowManager.close();
|
|
return true;
|
|
} else {
|
|
final bool res;
|
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
|
res = true;
|
|
} else {
|
|
res = await closeConfirmDialog();
|
|
}
|
|
if (res) {
|
|
windowManager.close();
|
|
}
|
|
return res;
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget buildConnectionCard(Client client) {
|
|
return Consumer<ServerModel>(
|
|
builder: (context, value, child) => Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
key: ValueKey(client.id),
|
|
children: [
|
|
_CmHeader(client: client),
|
|
client.type_() != ClientType.remote || client.disconnected
|
|
? Offstage()
|
|
: _PrivilegeBoard(client: client),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.bottomCenter,
|
|
child: _CmControlPanel(client: client),
|
|
),
|
|
)
|
|
],
|
|
).paddingSymmetric(vertical: 4.0, horizontal: 8.0),
|
|
);
|
|
}
|
|
|
|
class _AppIcon extends StatelessWidget {
|
|
const _AppIcon({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
margin: EdgeInsets.symmetric(horizontal: 4.0),
|
|
child: loadIcon(30),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CloseButton extends StatelessWidget {
|
|
const _CloseButton({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return IconButton(
|
|
onPressed: () {
|
|
windowManager.close();
|
|
},
|
|
icon: const Icon(
|
|
IconFont.close,
|
|
size: 18,
|
|
),
|
|
splashColor: Colors.transparent,
|
|
hoverColor: Colors.transparent,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CmHeader extends StatefulWidget {
|
|
final Client client;
|
|
|
|
const _CmHeader({Key? key, required this.client}) : super(key: key);
|
|
|
|
@override
|
|
State<_CmHeader> createState() => _CmHeaderState();
|
|
}
|
|
|
|
class _CmHeaderState extends State<_CmHeader>
|
|
with AutomaticKeepAliveClientMixin {
|
|
Client get client => widget.client;
|
|
|
|
final _time = 0.obs;
|
|
Timer? _timer;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_timer = Timer.periodic(Duration(seconds: 1), (_) {
|
|
if (client.authorized && !client.disconnected) {
|
|
_time.value = _time.value + 1;
|
|
}
|
|
});
|
|
gFFI.serverModel.tabController.onSelected?.call(client.id.toString());
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_timer?.cancel();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topRight,
|
|
end: Alignment.bottomLeft,
|
|
colors: [
|
|
Color(0xff00bfe1),
|
|
Color(0xff0071ff),
|
|
],
|
|
),
|
|
),
|
|
margin: EdgeInsets.symmetric(horizontal: 5.0, vertical: 10.0),
|
|
padding: EdgeInsets.only(
|
|
top: 10.0,
|
|
bottom: 10.0,
|
|
left: 10.0,
|
|
right: 5.0,
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 70,
|
|
height: 70,
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: str2color(client.name),
|
|
borderRadius: BorderRadius.circular(15.0),
|
|
),
|
|
child: Text(
|
|
client.name[0],
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
fontSize: 55,
|
|
),
|
|
),
|
|
).marginOnly(right: 10.0),
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
FittedBox(
|
|
child: Text(
|
|
client.name,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 20,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
maxLines: 1,
|
|
)),
|
|
FittedBox(
|
|
child: Text(
|
|
"(${client.peerId})",
|
|
style: TextStyle(color: Colors.white, fontSize: 14),
|
|
),
|
|
).marginOnly(bottom: 10.0),
|
|
FittedBox(
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
client.authorized
|
|
? client.disconnected
|
|
? translate("Disconnected")
|
|
: translate("Connected")
|
|
: "${translate("Request access to your device")}...",
|
|
style: TextStyle(color: Colors.white),
|
|
).marginOnly(right: 8.0),
|
|
if (client.authorized)
|
|
Obx(
|
|
() => Text(
|
|
formatDurationToTime(
|
|
Duration(seconds: _time.value),
|
|
),
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
)
|
|
],
|
|
))
|
|
],
|
|
),
|
|
),
|
|
Offstage(
|
|
offstage: !client.authorized ||
|
|
(client.type_() != ClientType.remote &&
|
|
client.type_() != ClientType.file),
|
|
child: IconButton(
|
|
onPressed: () => checkClickTime(client.id, () {
|
|
if (client.type_() == ClientType.file) {
|
|
gFFI.chatModel.toggleCMFilePage();
|
|
} else {
|
|
gFFI.chatModel
|
|
.toggleCMChatPage(MessageKey(client.peerId, client.id));
|
|
}
|
|
}),
|
|
icon: SvgPicture.asset(client.type_() == ClientType.file
|
|
? 'assets/file_transfer.svg'
|
|
: 'assets/chat2.svg'),
|
|
splashRadius: kDesktopIconButtonSplashRadius,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
}
|
|
|
|
class _PrivilegeBoard extends StatefulWidget {
|
|
final Client client;
|
|
|
|
const _PrivilegeBoard({Key? key, required this.client}) : super(key: key);
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _PrivilegeBoardState();
|
|
}
|
|
|
|
class _PrivilegeBoardState extends State<_PrivilegeBoard> {
|
|
late final client = widget.client;
|
|
Widget buildPermissionIcon(bool enabled, IconData iconData,
|
|
Function(bool)? onTap, String tooltipText) {
|
|
return Tooltip(
|
|
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
|
|
waitDuration: Duration.zero,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: enabled ? MyTheme.accent : Colors.grey[700],
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
),
|
|
padding: EdgeInsets.all(8.0),
|
|
child: InkWell(
|
|
onTap: () =>
|
|
checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
Expanded(
|
|
child: Icon(
|
|
iconData,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final crossAxisCount = 4;
|
|
final spacing = 10.0;
|
|
return Container(
|
|
width: double.infinity,
|
|
height: 160.0,
|
|
margin: EdgeInsets.all(5.0),
|
|
padding: EdgeInsets.all(5.0),
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(10.0),
|
|
color: Theme.of(context).colorScheme.background,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.2),
|
|
spreadRadius: 1,
|
|
blurRadius: 1,
|
|
offset: Offset(0, 1.5),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
translate("Permissions"),
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
textAlign: TextAlign.center,
|
|
).marginOnly(left: 4.0, bottom: 8.0),
|
|
Expanded(
|
|
child: GridView.count(
|
|
crossAxisCount: crossAxisCount,
|
|
padding: EdgeInsets.symmetric(horizontal: spacing),
|
|
mainAxisSpacing: spacing,
|
|
crossAxisSpacing: spacing,
|
|
children: [
|
|
buildPermissionIcon(
|
|
client.keyboard,
|
|
Icons.keyboard,
|
|
(enabled) {
|
|
bind.cmSwitchPermission(
|
|
connId: client.id, name: "keyboard", enabled: enabled);
|
|
setState(() {
|
|
client.keyboard = enabled;
|
|
});
|
|
},
|
|
translate('Enable keyboard/mouse'),
|
|
),
|
|
buildPermissionIcon(
|
|
client.clipboard,
|
|
Icons.assignment_rounded,
|
|
(enabled) {
|
|
bind.cmSwitchPermission(
|
|
connId: client.id, name: "clipboard", enabled: enabled);
|
|
setState(() {
|
|
client.clipboard = enabled;
|
|
});
|
|
},
|
|
translate('Enable clipboard'),
|
|
),
|
|
buildPermissionIcon(
|
|
client.audio,
|
|
Icons.volume_up_rounded,
|
|
(enabled) {
|
|
bind.cmSwitchPermission(
|
|
connId: client.id, name: "audio", enabled: enabled);
|
|
setState(() {
|
|
client.audio = enabled;
|
|
});
|
|
},
|
|
translate('Enable audio'),
|
|
),
|
|
buildPermissionIcon(
|
|
client.file,
|
|
Icons.upload_file_rounded,
|
|
(enabled) {
|
|
bind.cmSwitchPermission(
|
|
connId: client.id, name: "file", enabled: enabled);
|
|
setState(() {
|
|
client.file = enabled;
|
|
});
|
|
},
|
|
translate('Enable file copy and paste'),
|
|
),
|
|
buildPermissionIcon(
|
|
client.restart,
|
|
Icons.restart_alt_rounded,
|
|
(enabled) {
|
|
bind.cmSwitchPermission(
|
|
connId: client.id, name: "restart", enabled: enabled);
|
|
setState(() {
|
|
client.restart = enabled;
|
|
});
|
|
},
|
|
translate('Enable remote restart'),
|
|
),
|
|
buildPermissionIcon(
|
|
client.recording,
|
|
Icons.videocam_rounded,
|
|
(enabled) {
|
|
bind.cmSwitchPermission(
|
|
connId: client.id, name: "recording", enabled: enabled);
|
|
setState(() {
|
|
client.recording = enabled;
|
|
});
|
|
},
|
|
translate('Enable recording session'),
|
|
),
|
|
// only windows support block input
|
|
if (isWindows)
|
|
buildPermissionIcon(
|
|
client.blockInput,
|
|
Icons.block,
|
|
(enabled) {
|
|
bind.cmSwitchPermission(
|
|
connId: client.id,
|
|
name: "block_input",
|
|
enabled: enabled);
|
|
setState(() {
|
|
client.blockInput = enabled;
|
|
});
|
|
},
|
|
translate('Enable blocking user input'),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
const double buttonBottomMargin = 8;
|
|
|
|
class _CmControlPanel extends StatelessWidget {
|
|
final Client client;
|
|
|
|
const _CmControlPanel({Key? key, required this.client}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return client.authorized
|
|
? client.disconnected
|
|
? buildDisconnected(context)
|
|
: buildAuthorized(context)
|
|
: buildUnAuthorized(context);
|
|
}
|
|
|
|
buildAuthorized(BuildContext context) {
|
|
final bool canElevate = bind.cmCanElevate();
|
|
final model = Provider.of<ServerModel>(context);
|
|
final showElevation = canElevate &&
|
|
model.showElevation &&
|
|
client.type_() == ClientType.remote;
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Offstage(
|
|
offstage: !client.inVoiceCall,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: buildButton(context,
|
|
color: MyTheme.accent,
|
|
onClick: null, onTapDown: (details) async {
|
|
final devicesInfo = await AudioInput.getDevicesInfo(true, true);
|
|
List<String> devices = devicesInfo['devices'] as List<String>;
|
|
if (devices.isEmpty) {
|
|
msgBox(
|
|
gFFI.sessionId,
|
|
'custom-nocancel-info',
|
|
'Prompt',
|
|
'no_audio_input_device_tip',
|
|
'',
|
|
gFFI.dialogManager,
|
|
);
|
|
return;
|
|
}
|
|
|
|
String currentDevice = devicesInfo['current'] as String;
|
|
final x = details.globalPosition.dx;
|
|
final y = details.globalPosition.dy;
|
|
final position = RelativeRect.fromLTRB(x, y, x, y);
|
|
showMenu(
|
|
context: context,
|
|
position: position,
|
|
items: devices
|
|
.map((d) => PopupMenuItem<String>(
|
|
value: d,
|
|
height: 18,
|
|
padding: EdgeInsets.zero,
|
|
onTap: () => AudioInput.setDevice(d, true, true),
|
|
child: IgnorePointer(
|
|
child: RadioMenuButton(
|
|
value: d,
|
|
groupValue: currentDevice,
|
|
onChanged: (v) {
|
|
if (v != null) AudioInput.setDevice(v, true, true);
|
|
},
|
|
child: Container(
|
|
child: Text(
|
|
d,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
),
|
|
constraints: BoxConstraints(
|
|
maxWidth:
|
|
kConnectionManagerWindowSizeClosedChat
|
|
.width -
|
|
80),
|
|
),
|
|
)),
|
|
))
|
|
.toList(),
|
|
);
|
|
},
|
|
icon: Icon(
|
|
Icons.call_rounded,
|
|
color: Colors.white,
|
|
size: 14,
|
|
),
|
|
text: "Audio input",
|
|
textColor: Colors.white),
|
|
),
|
|
Expanded(
|
|
child: buildButton(
|
|
context,
|
|
color: Colors.red,
|
|
onClick: () => closeVoiceCall(),
|
|
icon: Icon(
|
|
Icons.call_end_rounded,
|
|
color: Colors.white,
|
|
size: 14,
|
|
),
|
|
text: "Stop voice call",
|
|
textColor: Colors.white,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
Offstage(
|
|
offstage: !client.incomingVoiceCall,
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: buildButton(context,
|
|
color: MyTheme.accent,
|
|
onClick: () => handleVoiceCall(true),
|
|
icon: Icon(
|
|
Icons.call_rounded,
|
|
color: Colors.white,
|
|
size: 14,
|
|
),
|
|
text: "Accept",
|
|
textColor: Colors.white),
|
|
),
|
|
Expanded(
|
|
child: buildButton(
|
|
context,
|
|
color: Colors.red,
|
|
onClick: () => handleVoiceCall(false),
|
|
icon: Icon(
|
|
Icons.phone_disabled_rounded,
|
|
color: Colors.white,
|
|
size: 14,
|
|
),
|
|
text: "Dismiss",
|
|
textColor: Colors.white,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
Offstage(
|
|
offstage: !client.fromSwitch,
|
|
child: buildButton(context,
|
|
color: Colors.purple,
|
|
onClick: () => handleSwitchBack(context),
|
|
icon: Icon(Icons.reply, color: Colors.white),
|
|
text: "Switch Sides",
|
|
textColor: Colors.white),
|
|
),
|
|
Offstage(
|
|
offstage: !showElevation,
|
|
child: buildButton(
|
|
context,
|
|
color: MyTheme.accent,
|
|
onClick: () {
|
|
handleElevate(context);
|
|
windowManager.minimize();
|
|
},
|
|
icon: Icon(
|
|
Icons.security_rounded,
|
|
color: Colors.white,
|
|
size: 14,
|
|
),
|
|
text: 'Elevate',
|
|
textColor: Colors.white,
|
|
),
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: buildButton(context,
|
|
color: Colors.redAccent,
|
|
onClick: handleDisconnect,
|
|
text: 'Disconnect',
|
|
icon: Icon(
|
|
Icons.link_off_rounded,
|
|
color: Colors.white,
|
|
size: 14,
|
|
),
|
|
textColor: Colors.white),
|
|
),
|
|
],
|
|
)
|
|
],
|
|
).marginOnly(bottom: buttonBottomMargin);
|
|
}
|
|
|
|
buildDisconnected(BuildContext context) {
|
|
return Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Expanded(
|
|
child: buildButton(context,
|
|
color: MyTheme.accent,
|
|
onClick: handleClose,
|
|
text: 'Close',
|
|
textColor: Colors.white)),
|
|
],
|
|
).marginOnly(bottom: buttonBottomMargin);
|
|
}
|
|
|
|
buildUnAuthorized(BuildContext context) {
|
|
final bool canElevate = bind.cmCanElevate();
|
|
final model = Provider.of<ServerModel>(context);
|
|
final showElevation = canElevate &&
|
|
model.showElevation &&
|
|
client.type_() == ClientType.remote;
|
|
final showAccept = model.approveMode != 'password';
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Offstage(
|
|
offstage: !showElevation || !showAccept,
|
|
child: buildButton(context, color: Colors.green[700], onClick: () {
|
|
handleAccept(context);
|
|
handleElevate(context);
|
|
windowManager.minimize();
|
|
},
|
|
text: 'Accept and Elevate',
|
|
icon: Icon(
|
|
Icons.security_rounded,
|
|
color: Colors.white,
|
|
size: 14,
|
|
),
|
|
textColor: Colors.white,
|
|
tooltip: 'accept_and_elevate_btn_tooltip'),
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
if (showAccept)
|
|
Expanded(
|
|
child: Column(
|
|
children: [
|
|
buildButton(
|
|
context,
|
|
color: MyTheme.accent,
|
|
onClick: () {
|
|
handleAccept(context);
|
|
windowManager.minimize();
|
|
},
|
|
text: 'Accept',
|
|
textColor: Colors.white,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: buildButton(
|
|
context,
|
|
color: Colors.transparent,
|
|
border: Border.all(color: Colors.grey),
|
|
onClick: handleDisconnect,
|
|
text: 'Cancel',
|
|
textColor: null,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
).marginOnly(bottom: buttonBottomMargin);
|
|
}
|
|
|
|
Widget buildButton(BuildContext context,
|
|
{required Color? color,
|
|
GestureTapCallback? onClick,
|
|
Widget? icon,
|
|
BoxBorder? border,
|
|
required String text,
|
|
required Color? textColor,
|
|
String? tooltip,
|
|
GestureTapDownCallback? onTapDown}) {
|
|
assert(!(onClick == null && onTapDown == null));
|
|
Widget textWidget;
|
|
if (icon != null) {
|
|
textWidget = Text(
|
|
translate(text),
|
|
style: TextStyle(color: textColor),
|
|
textAlign: TextAlign.center,
|
|
);
|
|
} else {
|
|
textWidget = Expanded(
|
|
child: Text(
|
|
translate(text),
|
|
style: TextStyle(color: textColor),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
final borderRadius = BorderRadius.circular(10.0);
|
|
final btn = Container(
|
|
height: 28,
|
|
decoration: BoxDecoration(
|
|
color: color, borderRadius: borderRadius, border: border),
|
|
child: InkWell(
|
|
borderRadius: borderRadius,
|
|
onTap: () {
|
|
if (onClick == null) return;
|
|
checkClickTime(client.id, onClick);
|
|
},
|
|
onTapDown: (details) {
|
|
if (onTapDown == null) return;
|
|
checkClickTime(client.id, () {
|
|
onTapDown.call(details);
|
|
});
|
|
},
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Offstage(offstage: icon == null, child: icon).marginOnly(right: 5),
|
|
textWidget,
|
|
],
|
|
),
|
|
),
|
|
);
|
|
return (tooltip != null
|
|
? Tooltip(
|
|
message: translate(tooltip),
|
|
child: btn,
|
|
)
|
|
: btn)
|
|
.marginAll(4);
|
|
}
|
|
|
|
void handleDisconnect() {
|
|
bind.cmCloseConnection(connId: client.id);
|
|
}
|
|
|
|
void handleAccept(BuildContext context) {
|
|
final model = Provider.of<ServerModel>(context, listen: false);
|
|
model.sendLoginResponse(client, true);
|
|
}
|
|
|
|
void handleElevate(BuildContext context) {
|
|
final model = Provider.of<ServerModel>(context, listen: false);
|
|
model.setShowElevation(false);
|
|
bind.cmElevatePortable(connId: client.id);
|
|
}
|
|
|
|
void handleClose() async {
|
|
await bind.cmRemoveDisconnectedConnection(connId: client.id);
|
|
if (await bind.cmGetClientsLength() == 0) {
|
|
windowManager.close();
|
|
}
|
|
}
|
|
|
|
void handleSwitchBack(BuildContext context) {
|
|
bind.cmSwitchBack(connId: client.id);
|
|
}
|
|
|
|
void handleVoiceCall(bool accept) {
|
|
bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
|
|
}
|
|
|
|
void closeVoiceCall() {
|
|
bind.cmCloseVoiceCall(id: client.id);
|
|
}
|
|
}
|
|
|
|
void checkClickTime(int id, Function() callback) async {
|
|
if (allowRemoteCMModification()) {
|
|
callback();
|
|
return;
|
|
}
|
|
var clickCallbackTime = DateTime.now().millisecondsSinceEpoch;
|
|
await bind.cmCheckClickTime(connId: id);
|
|
Timer(const Duration(milliseconds: 120), () async {
|
|
var d = clickCallbackTime - await bind.cmGetClickTime();
|
|
if (d > 120) callback();
|
|
});
|
|
}
|
|
|
|
bool allowRemoteCMModification() {
|
|
return option2bool(kOptionAllowRemoteCmModification,
|
|
bind.mainGetLocalOption(key: kOptionAllowRemoteCmModification));
|
|
}
|
|
|
|
class _FileTransferLogPage extends StatefulWidget {
|
|
_FileTransferLogPage({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
State<_FileTransferLogPage> createState() => __FileTransferLogPageState();
|
|
}
|
|
|
|
class __FileTransferLogPageState extends State<_FileTransferLogPage> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return statusList();
|
|
}
|
|
|
|
Widget generateCard(Widget child) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: BorderRadius.all(
|
|
Radius.circular(15.0),
|
|
),
|
|
),
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
iconLabel(CmFileLog item) {
|
|
switch (item.action) {
|
|
case CmFileAction.none:
|
|
return Container();
|
|
case CmFileAction.localToRemote:
|
|
case CmFileAction.remoteToLocal:
|
|
return Column(
|
|
children: [
|
|
Transform.rotate(
|
|
angle: item.action == CmFileAction.remoteToLocal ? 0 : pi,
|
|
child: SvgPicture.asset(
|
|
"assets/arrow.svg",
|
|
colorFilter: svgColor(Theme.of(context).tabBarTheme.labelColor),
|
|
),
|
|
),
|
|
Text(item.action == CmFileAction.remoteToLocal
|
|
? translate('Send')
|
|
: translate('Receive'))
|
|
],
|
|
);
|
|
case CmFileAction.remove:
|
|
return Column(
|
|
children: [
|
|
Icon(
|
|
Icons.delete,
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
Text(translate('Delete'))
|
|
],
|
|
);
|
|
case CmFileAction.createDir:
|
|
return Column(
|
|
children: [
|
|
Icon(
|
|
Icons.create_new_folder,
|
|
color: Theme.of(context).tabBarTheme.labelColor,
|
|
),
|
|
Text(translate('Create Folder'))
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget statusList() {
|
|
return PreferredSize(
|
|
preferredSize: const Size(200, double.infinity),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Obx(
|
|
() {
|
|
final jobTable = gFFI.cmFileModel.currentJobTable;
|
|
statusListView(List<CmFileLog> jobs) => ListView.builder(
|
|
controller: ScrollController(),
|
|
itemBuilder: (BuildContext context, int index) {
|
|
final item = jobs[index];
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 5),
|
|
child: generateCard(
|
|
Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
SizedBox(
|
|
width: 50,
|
|
child: iconLabel(item),
|
|
).paddingOnly(left: 15),
|
|
const SizedBox(
|
|
width: 16.0,
|
|
),
|
|
Expanded(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
item.fileName,
|
|
).paddingSymmetric(vertical: 10),
|
|
if (item.totalSize > 0)
|
|
Text(
|
|
'${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: MyTheme.darkGray,
|
|
),
|
|
),
|
|
if (item.totalSize > 0)
|
|
Offstage(
|
|
offstage: item.state !=
|
|
JobState.inProgress,
|
|
child: Text(
|
|
'${translate("Speed")} ${readableFileSize(item.speed)}/s',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: MyTheme.darkGray,
|
|
),
|
|
),
|
|
),
|
|
Offstage(
|
|
offstage: !(item.isTransfer() &&
|
|
item.state !=
|
|
JobState.inProgress),
|
|
child: Text(
|
|
translate(
|
|
item.display(),
|
|
),
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
color: MyTheme.darkGray,
|
|
),
|
|
),
|
|
),
|
|
if (item.totalSize > 0)
|
|
Offstage(
|
|
offstage: item.state !=
|
|
JobState.inProgress,
|
|
child: LinearPercentIndicator(
|
|
padding:
|
|
EdgeInsets.only(right: 15),
|
|
animateFromLastPercent: true,
|
|
center: Text(
|
|
'${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
|
|
),
|
|
barRadius: Radius.circular(15),
|
|
percent: item.finishedSize /
|
|
item.totalSize,
|
|
progressColor: MyTheme.accent,
|
|
backgroundColor:
|
|
Theme.of(context).hoverColor,
|
|
lineHeight:
|
|
kDesktopFileTransferRowHeight,
|
|
).paddingSymmetric(vertical: 15),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [],
|
|
),
|
|
],
|
|
),
|
|
],
|
|
).paddingSymmetric(vertical: 10),
|
|
),
|
|
);
|
|
},
|
|
itemCount: jobTable.length,
|
|
);
|
|
|
|
return jobTable.isEmpty
|
|
? generateCard(
|
|
Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
SvgPicture.asset(
|
|
"assets/transfer.svg",
|
|
colorFilter: svgColor(
|
|
Theme.of(context).tabBarTheme.labelColor),
|
|
height: 40,
|
|
).paddingOnly(bottom: 10),
|
|
Text(
|
|
translate("No transfers in progress"),
|
|
textAlign: TextAlign.center,
|
|
textScaler: TextScaler.linear(1.20),
|
|
style: TextStyle(
|
|
color:
|
|
Theme.of(context).tabBarTheme.labelColor),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
: statusListView(jobTable);
|
|
},
|
|
)),
|
|
);
|
|
}
|
|
}
|