1130 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			1130 lines
		
	
	
		
			37 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
// original cm window in Sciter version.
 | 
						|
 | 
						|
import 'dart:async';
 | 
						|
import 'dart:io';
 | 
						|
import 'dart:math';
 | 
						|
 | 
						|
import 'package:flutter/material.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/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(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 (Platform.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) => Container(
 | 
						|
          decoration: BoxDecoration(
 | 
						|
              border: Border.all(color: MyTheme.color(context).border!)),
 | 
						|
          child: Scaffold(
 | 
						|
            backgroundColor: Theme.of(context).scaffoldBackgroundColor,
 | 
						|
            body: ConnectionManager(),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  bool get wantKeepAlive => true;
 | 
						|
}
 | 
						|
 | 
						|
class ConnectionManager extends StatefulWidget {
 | 
						|
  @override
 | 
						|
  State<StatefulWidget> createState() => ConnectionManagerState();
 | 
						|
}
 | 
						|
 | 
						|
class ConnectionManagerState extends State<ConnectionManager> {
 | 
						|
  @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;
 | 
						|
    super.initState();
 | 
						|
  }
 | 
						|
 | 
						|
  @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: buildScrollJumper(),
 | 
						|
              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: buildRemoteBlock(
 | 
						|
                                  child: Container(
 | 
						|
                                      decoration: BoxDecoration(
 | 
						|
                                          border: Border(
 | 
						|
                                              right: BorderSide(
 | 
						|
                                                  color: Theme.of(context)
 | 
						|
                                                      .dividerColor))),
 | 
						|
                                      child: buildSidePage()),
 | 
						|
                                ),
 | 
						|
                              )),
 | 
						|
                    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 opt = "enable-confirm-closing-tabs";
 | 
						|
      final bool res;
 | 
						|
      if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
 | 
						|
        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: SvgPicture.asset(
 | 
						|
        'assets/logo.svg',
 | 
						|
        width: 30,
 | 
						|
        height: 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.toggleCMSidePage();
 | 
						|
                } 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"}",
 | 
						|
      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,
 | 
						|
                  size: 32,
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return Container(
 | 
						|
      width: double.infinity,
 | 
						|
      height: 200.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: 3,
 | 
						|
              padding: EdgeInsets.symmetric(horizontal: 20.0),
 | 
						|
              mainAxisSpacing: 20.0,
 | 
						|
              crossAxisSpacing: 20.0,
 | 
						|
              children: [
 | 
						|
                buildPermissionIcon(
 | 
						|
                  client.keyboard,
 | 
						|
                  Icons.keyboard,
 | 
						|
                  (enabled) {
 | 
						|
                    bind.cmSwitchPermission(
 | 
						|
                        connId: client.id, name: "keyboard", enabled: enabled);
 | 
						|
                    setState(() {
 | 
						|
                      client.keyboard = enabled;
 | 
						|
                    });
 | 
						|
                  },
 | 
						|
                  translate('Allow using keyboard and mouse'),
 | 
						|
                ),
 | 
						|
                buildPermissionIcon(
 | 
						|
                  client.clipboard,
 | 
						|
                  Icons.assignment_rounded,
 | 
						|
                  (enabled) {
 | 
						|
                    bind.cmSwitchPermission(
 | 
						|
                        connId: client.id, name: "clipboard", enabled: enabled);
 | 
						|
                    setState(() {
 | 
						|
                      client.clipboard = enabled;
 | 
						|
                    });
 | 
						|
                  },
 | 
						|
                  translate('Allow using clipboard'),
 | 
						|
                ),
 | 
						|
                buildPermissionIcon(
 | 
						|
                  client.audio,
 | 
						|
                  Icons.volume_up_rounded,
 | 
						|
                  (enabled) {
 | 
						|
                    bind.cmSwitchPermission(
 | 
						|
                        connId: client.id, name: "audio", enabled: enabled);
 | 
						|
                    setState(() {
 | 
						|
                      client.audio = enabled;
 | 
						|
                    });
 | 
						|
                  },
 | 
						|
                  translate('Allow hearing sound'),
 | 
						|
                ),
 | 
						|
                buildPermissionIcon(
 | 
						|
                  client.file,
 | 
						|
                  Icons.upload_file_rounded,
 | 
						|
                  (enabled) {
 | 
						|
                    bind.cmSwitchPermission(
 | 
						|
                        connId: client.id, name: "file", enabled: enabled);
 | 
						|
                    setState(() {
 | 
						|
                      client.file = enabled;
 | 
						|
                    });
 | 
						|
                  },
 | 
						|
                  translate('Allow 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('Allow remote restart'),
 | 
						|
                ),
 | 
						|
                buildPermissionIcon(
 | 
						|
                  client.recording,
 | 
						|
                  Icons.videocam_rounded,
 | 
						|
                  (enabled) {
 | 
						|
                    bind.cmSwitchPermission(
 | 
						|
                        connId: client.id, name: "recording", enabled: enabled);
 | 
						|
                    setState(() {
 | 
						|
                      client.recording = enabled;
 | 
						|
                    });
 | 
						|
                  },
 | 
						|
                  translate('Allow recording session'),
 | 
						|
                )
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
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: 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,
 | 
						|
      required Function() onClick,
 | 
						|
      Icon? icon,
 | 
						|
      BoxBorder? border,
 | 
						|
      required String text,
 | 
						|
      required Color? textColor,
 | 
						|
      String? tooltip}) {
 | 
						|
    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: () => checkClickTime(client.id, onClick),
 | 
						|
        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 {
 | 
						|
  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();
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  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<JobProgress> 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: Column(
 | 
						|
                                      children: [
 | 
						|
                                        Transform.rotate(
 | 
						|
                                          angle: item.isRemoteToLocal ? 0 : pi,
 | 
						|
                                          child: SvgPicture.asset(
 | 
						|
                                            "assets/arrow.svg",
 | 
						|
                                            color: Theme.of(context)
 | 
						|
                                                .tabBarTheme
 | 
						|
                                                .labelColor,
 | 
						|
                                          ),
 | 
						|
                                        ),
 | 
						|
                                        Text(item.isRemoteToLocal
 | 
						|
                                            ? translate('Send')
 | 
						|
                                            : translate('Receive'))
 | 
						|
                                      ],
 | 
						|
                                    ),
 | 
						|
                                  ).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.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",
 | 
						|
                              color: Theme.of(context).tabBarTheme.labelColor,
 | 
						|
                              height: 40,
 | 
						|
                            ).paddingOnly(bottom: 10),
 | 
						|
                            Text(
 | 
						|
                              translate("No transfers in progress"),
 | 
						|
                              textAlign: TextAlign.center,
 | 
						|
                              textScaleFactor: 1.20,
 | 
						|
                              style: TextStyle(
 | 
						|
                                  color:
 | 
						|
                                      Theme.of(context).tabBarTheme.labelColor),
 | 
						|
                            ),
 | 
						|
                          ],
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    )
 | 
						|
                  : statusListView(jobTable);
 | 
						|
            },
 | 
						|
          )),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |