882 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			882 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:async';
 | 
						|
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:flutter/services.dart';
 | 
						|
import 'package:flutter_hbb/mobile/widgets/dialog.dart';
 | 
						|
import 'package:flutter_hbb/models/chat_model.dart';
 | 
						|
import 'package:get/get.dart';
 | 
						|
import 'package:provider/provider.dart';
 | 
						|
 | 
						|
import '../../common.dart';
 | 
						|
import '../../common/widgets/dialog.dart';
 | 
						|
import '../../consts.dart';
 | 
						|
import '../../models/platform_model.dart';
 | 
						|
import '../../models/server_model.dart';
 | 
						|
import 'home_page.dart';
 | 
						|
 | 
						|
class ServerPage extends StatefulWidget implements PageShape {
 | 
						|
  @override
 | 
						|
  final title = translate("Share Screen");
 | 
						|
 | 
						|
  @override
 | 
						|
  final icon = const Icon(Icons.mobile_screen_share);
 | 
						|
 | 
						|
  @override
 | 
						|
  final appBarActions = [
 | 
						|
    PopupMenuButton<String>(
 | 
						|
        tooltip: "",
 | 
						|
        icon: const Icon(Icons.more_vert),
 | 
						|
        itemBuilder: (context) {
 | 
						|
          listTile(String text, bool checked) {
 | 
						|
            return ListTile(
 | 
						|
                title: Text(translate(text)),
 | 
						|
                trailing: Icon(
 | 
						|
                  Icons.check,
 | 
						|
                  color: checked ? null : Colors.transparent,
 | 
						|
                ));
 | 
						|
          }
 | 
						|
 | 
						|
          final approveMode = gFFI.serverModel.approveMode;
 | 
						|
          final verificationMethod = gFFI.serverModel.verificationMethod;
 | 
						|
          final showPasswordOption = approveMode != 'click';
 | 
						|
          final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
 | 
						|
          return [
 | 
						|
            PopupMenuItem(
 | 
						|
              enabled: gFFI.serverModel.connectStatus > 0,
 | 
						|
              value: "changeID",
 | 
						|
              child: Text(translate("Change ID")),
 | 
						|
            ),
 | 
						|
            const PopupMenuDivider(),
 | 
						|
            PopupMenuItem(
 | 
						|
              value: 'AcceptSessionsViaPassword',
 | 
						|
              child: listTile(
 | 
						|
                  'Accept sessions via password', approveMode == 'password'),
 | 
						|
              enabled: !isApproveModeFixed,
 | 
						|
            ),
 | 
						|
            PopupMenuItem(
 | 
						|
              value: 'AcceptSessionsViaClick',
 | 
						|
              child:
 | 
						|
                  listTile('Accept sessions via click', approveMode == 'click'),
 | 
						|
              enabled: !isApproveModeFixed,
 | 
						|
            ),
 | 
						|
            PopupMenuItem(
 | 
						|
              value: "AcceptSessionsViaBoth",
 | 
						|
              child: listTile("Accept sessions via both",
 | 
						|
                  approveMode != 'password' && approveMode != 'click'),
 | 
						|
              enabled: !isApproveModeFixed,
 | 
						|
            ),
 | 
						|
            if (showPasswordOption) const PopupMenuDivider(),
 | 
						|
            if (showPasswordOption &&
 | 
						|
                verificationMethod != kUseTemporaryPassword)
 | 
						|
              PopupMenuItem(
 | 
						|
                value: "setPermanentPassword",
 | 
						|
                child: Text(translate("Set permanent password")),
 | 
						|
              ),
 | 
						|
            if (showPasswordOption &&
 | 
						|
                verificationMethod != kUsePermanentPassword)
 | 
						|
              PopupMenuItem(
 | 
						|
                value: "setTemporaryPasswordLength",
 | 
						|
                child: Text(translate("One-time password length")),
 | 
						|
              ),
 | 
						|
            if (showPasswordOption) const PopupMenuDivider(),
 | 
						|
            if (showPasswordOption)
 | 
						|
              PopupMenuItem(
 | 
						|
                value: kUseTemporaryPassword,
 | 
						|
                child: listTile('Use one-time password',
 | 
						|
                    verificationMethod == kUseTemporaryPassword),
 | 
						|
              ),
 | 
						|
            if (showPasswordOption)
 | 
						|
              PopupMenuItem(
 | 
						|
                value: kUsePermanentPassword,
 | 
						|
                child: listTile('Use permanent password',
 | 
						|
                    verificationMethod == kUsePermanentPassword),
 | 
						|
              ),
 | 
						|
            if (showPasswordOption)
 | 
						|
              PopupMenuItem(
 | 
						|
                value: kUseBothPasswords,
 | 
						|
                child: listTile(
 | 
						|
                    'Use both passwords',
 | 
						|
                    verificationMethod != kUseTemporaryPassword &&
 | 
						|
                        verificationMethod != kUsePermanentPassword),
 | 
						|
              ),
 | 
						|
          ];
 | 
						|
        },
 | 
						|
        onSelected: (value) {
 | 
						|
          if (value == "changeID") {
 | 
						|
            changeIdDialog();
 | 
						|
          } else if (value == "setPermanentPassword") {
 | 
						|
            setPermanentPasswordDialog(gFFI.dialogManager);
 | 
						|
          } else if (value == "setTemporaryPasswordLength") {
 | 
						|
            setTemporaryPasswordLengthDialog(gFFI.dialogManager);
 | 
						|
          } else if (value == kUsePermanentPassword ||
 | 
						|
              value == kUseTemporaryPassword ||
 | 
						|
              value == kUseBothPasswords) {
 | 
						|
            bind.mainSetOption(key: kOptionVerificationMethod, value: value);
 | 
						|
            gFFI.serverModel.updatePasswordModel();
 | 
						|
          } else if (value.startsWith("AcceptSessionsVia")) {
 | 
						|
            value = value.substring("AcceptSessionsVia".length);
 | 
						|
            if (value == "Password") {
 | 
						|
              gFFI.serverModel.setApproveMode('password');
 | 
						|
            } else if (value == "Click") {
 | 
						|
              gFFI.serverModel.setApproveMode('click');
 | 
						|
            } else {
 | 
						|
              gFFI.serverModel.setApproveMode(defaultOptionApproveMode);
 | 
						|
            }
 | 
						|
          }
 | 
						|
        })
 | 
						|
  ];
 | 
						|
 | 
						|
  ServerPage({Key? key}) : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  State<StatefulWidget> createState() => _ServerPageState();
 | 
						|
}
 | 
						|
 | 
						|
class _ServerPageState extends State<ServerPage> {
 | 
						|
  Timer? _updateTimer;
 | 
						|
 | 
						|
  @override
 | 
						|
  void initState() {
 | 
						|
    super.initState();
 | 
						|
    _updateTimer = periodic_immediate(const Duration(seconds: 3), () async {
 | 
						|
      await gFFI.serverModel.fetchID();
 | 
						|
    });
 | 
						|
    gFFI.serverModel.checkAndroidPermission();
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void dispose() {
 | 
						|
    _updateTimer?.cancel();
 | 
						|
    super.dispose();
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    checkService();
 | 
						|
    return ChangeNotifierProvider.value(
 | 
						|
        value: gFFI.serverModel,
 | 
						|
        child: Consumer<ServerModel>(
 | 
						|
            builder: (context, serverModel, child) => SingleChildScrollView(
 | 
						|
                  controller: gFFI.serverModel.controller,
 | 
						|
                  child: Center(
 | 
						|
                    child: Column(
 | 
						|
                      mainAxisAlignment: MainAxisAlignment.start,
 | 
						|
                      children: [
 | 
						|
                        buildPresetPasswordWarning(),
 | 
						|
                        gFFI.serverModel.isStart
 | 
						|
                            ? ServerInfo()
 | 
						|
                            : ServiceNotRunningNotification(),
 | 
						|
                        const ConnectionManager(),
 | 
						|
                        const PermissionChecker(),
 | 
						|
                        SizedBox.fromSize(size: const Size(0, 15.0)),
 | 
						|
                      ],
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                )));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void checkService() async {
 | 
						|
  gFFI.invokeMethod("check_service");
 | 
						|
  // for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page
 | 
						|
  if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) {
 | 
						|
    AndroidPermissionManager.complete(kManageExternalStorage,
 | 
						|
        await AndroidPermissionManager.check(kManageExternalStorage));
 | 
						|
    debugPrint("file permission finished");
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ServiceNotRunningNotification extends StatelessWidget {
 | 
						|
  ServiceNotRunningNotification({Key? key}) : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final serverModel = Provider.of<ServerModel>(context);
 | 
						|
 | 
						|
    return PaddingCard(
 | 
						|
        title: translate("Service is not running"),
 | 
						|
        titleIcon:
 | 
						|
            const Icon(Icons.warning_amber_sharp, color: Colors.redAccent),
 | 
						|
        child: Column(
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
          children: [
 | 
						|
            Text(translate("android_start_service_tip"),
 | 
						|
                    style:
 | 
						|
                        const TextStyle(fontSize: 12, color: MyTheme.darkGray))
 | 
						|
                .marginOnly(bottom: 8),
 | 
						|
            ElevatedButton.icon(
 | 
						|
                icon: const Icon(Icons.play_arrow),
 | 
						|
                onPressed: () {
 | 
						|
                  if (gFFI.userModel.userName.value.isEmpty &&
 | 
						|
                      bind.mainGetLocalOption(key: "show-scam-warning") !=
 | 
						|
                          "N") {
 | 
						|
                    showScamWarning(context, serverModel);
 | 
						|
                  } else {
 | 
						|
                    serverModel.toggleService();
 | 
						|
                  }
 | 
						|
                },
 | 
						|
                label: Text(translate("Start service")))
 | 
						|
          ],
 | 
						|
        ));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ScamWarningDialog extends StatefulWidget {
 | 
						|
  final ServerModel serverModel;
 | 
						|
 | 
						|
  ScamWarningDialog({required this.serverModel});
 | 
						|
 | 
						|
  @override
 | 
						|
  ScamWarningDialogState createState() => ScamWarningDialogState();
 | 
						|
}
 | 
						|
 | 
						|
class ScamWarningDialogState extends State<ScamWarningDialog> {
 | 
						|
  int _countdown = bind.isCustomClient() ? 0 : 12;
 | 
						|
  bool show_warning = false;
 | 
						|
  late Timer _timer;
 | 
						|
  late ServerModel _serverModel;
 | 
						|
 | 
						|
  @override
 | 
						|
  void initState() {
 | 
						|
    super.initState();
 | 
						|
    _serverModel = widget.serverModel;
 | 
						|
    startCountdown();
 | 
						|
  }
 | 
						|
 | 
						|
  void startCountdown() {
 | 
						|
    const oneSecond = Duration(seconds: 1);
 | 
						|
    _timer = Timer.periodic(oneSecond, (timer) {
 | 
						|
      setState(() {
 | 
						|
        _countdown--;
 | 
						|
        if (_countdown <= 0) {
 | 
						|
          timer.cancel();
 | 
						|
        }
 | 
						|
      });
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void dispose() {
 | 
						|
    _timer.cancel();
 | 
						|
    super.dispose();
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final isButtonLocked = _countdown > 0;
 | 
						|
 | 
						|
    return AlertDialog(
 | 
						|
      content: ClipRRect(
 | 
						|
        borderRadius: BorderRadius.circular(20.0),
 | 
						|
        child: SingleChildScrollView(
 | 
						|
          child: Container(
 | 
						|
            decoration: BoxDecoration(
 | 
						|
              gradient: LinearGradient(
 | 
						|
                begin: Alignment.topRight,
 | 
						|
                end: Alignment.bottomLeft,
 | 
						|
                colors: [
 | 
						|
                  Color(0xffe242bc),
 | 
						|
                  Color(0xfff4727c),
 | 
						|
                ],
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            padding: EdgeInsets.all(25.0),
 | 
						|
            child: Column(
 | 
						|
              mainAxisSize: MainAxisSize.min,
 | 
						|
              crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
              children: [
 | 
						|
                Row(
 | 
						|
                  children: [
 | 
						|
                    Icon(
 | 
						|
                      Icons.warning_amber_sharp,
 | 
						|
                      color: Colors.white,
 | 
						|
                    ),
 | 
						|
                    SizedBox(width: 10),
 | 
						|
                    Text(
 | 
						|
                      translate("Warning"),
 | 
						|
                      style: TextStyle(
 | 
						|
                        color: Colors.white,
 | 
						|
                        fontWeight: FontWeight.bold,
 | 
						|
                        fontSize: 20.0,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
                SizedBox(height: 20),
 | 
						|
                Center(
 | 
						|
                  child: Image.asset(
 | 
						|
                    'assets/scam.png',
 | 
						|
                    width: 180,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                SizedBox(height: 18),
 | 
						|
                Text(
 | 
						|
                  translate("scam_title"),
 | 
						|
                  textAlign: TextAlign.center,
 | 
						|
                  style: TextStyle(
 | 
						|
                    color: Colors.white,
 | 
						|
                    fontWeight: FontWeight.bold,
 | 
						|
                    fontSize: 22.0,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                SizedBox(height: 18),
 | 
						|
                Text(
 | 
						|
                  "${translate("scam_text1")}\n\n${translate("scam_text2")}\n",
 | 
						|
                  style: TextStyle(
 | 
						|
                    color: Colors.white,
 | 
						|
                    fontWeight: FontWeight.bold,
 | 
						|
                    fontSize: 16.0,
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                Row(
 | 
						|
                  children: <Widget>[
 | 
						|
                    Checkbox(
 | 
						|
                      value: show_warning,
 | 
						|
                      onChanged: (value) {
 | 
						|
                        setState(() {
 | 
						|
                          show_warning = value!;
 | 
						|
                        });
 | 
						|
                      },
 | 
						|
                    ),
 | 
						|
                    Text(
 | 
						|
                      translate("Don't show again"),
 | 
						|
                      style: TextStyle(
 | 
						|
                        color: Colors.white,
 | 
						|
                        fontWeight: FontWeight.bold,
 | 
						|
                        fontSize: 15.0,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
                Row(
 | 
						|
                  mainAxisAlignment: MainAxisAlignment.end,
 | 
						|
                  children: [
 | 
						|
                    Container(
 | 
						|
                      constraints: BoxConstraints(maxWidth: 150),
 | 
						|
                      child: ElevatedButton(
 | 
						|
                        onPressed: isButtonLocked
 | 
						|
                            ? null
 | 
						|
                            : () {
 | 
						|
                                Navigator.of(context).pop();
 | 
						|
                                _serverModel.toggleService();
 | 
						|
                                if (show_warning) {
 | 
						|
                                  bind.mainSetLocalOption(
 | 
						|
                                      key: "show-scam-warning", value: "N");
 | 
						|
                                }
 | 
						|
                              },
 | 
						|
                        style: ElevatedButton.styleFrom(
 | 
						|
                          backgroundColor: Colors.blueAccent,
 | 
						|
                        ),
 | 
						|
                        child: Text(
 | 
						|
                          isButtonLocked
 | 
						|
                              ? "${translate("I Agree")} (${_countdown}s)"
 | 
						|
                              : translate("I Agree"),
 | 
						|
                          style: TextStyle(
 | 
						|
                            fontWeight: FontWeight.bold,
 | 
						|
                            fontSize: 13.0,
 | 
						|
                          ),
 | 
						|
                          maxLines: 2,
 | 
						|
                          overflow: TextOverflow.ellipsis,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    SizedBox(width: 15),
 | 
						|
                    Container(
 | 
						|
                      constraints: BoxConstraints(maxWidth: 150),
 | 
						|
                      child: ElevatedButton(
 | 
						|
                        onPressed: () {
 | 
						|
                          Navigator.of(context).pop();
 | 
						|
                        },
 | 
						|
                        style: ElevatedButton.styleFrom(
 | 
						|
                          backgroundColor: Colors.blueAccent,
 | 
						|
                        ),
 | 
						|
                        child: Text(
 | 
						|
                          translate("Decline"),
 | 
						|
                          style: TextStyle(
 | 
						|
                            fontWeight: FontWeight.bold,
 | 
						|
                            fontSize: 13.0,
 | 
						|
                          ),
 | 
						|
                          maxLines: 2,
 | 
						|
                          overflow: TextOverflow.ellipsis,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      contentPadding: EdgeInsets.all(0.0),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ServerInfo extends StatelessWidget {
 | 
						|
  final model = gFFI.serverModel;
 | 
						|
  final emptyController = TextEditingController(text: "-");
 | 
						|
 | 
						|
  ServerInfo({Key? key}) : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final isPermanent = model.verificationMethod == kUsePermanentPassword;
 | 
						|
    final serverModel = Provider.of<ServerModel>(context);
 | 
						|
 | 
						|
    const Color colorPositive = Colors.green;
 | 
						|
    const Color colorNegative = Colors.red;
 | 
						|
    const double iconMarginRight = 15;
 | 
						|
    const double iconSize = 24;
 | 
						|
    const TextStyle textStyleHeading = TextStyle(
 | 
						|
        fontSize: 16.0, fontWeight: FontWeight.bold, color: Colors.grey);
 | 
						|
    const TextStyle textStyleValue =
 | 
						|
        TextStyle(fontSize: 25.0, fontWeight: FontWeight.bold);
 | 
						|
 | 
						|
    void copyToClipboard(String value) {
 | 
						|
      Clipboard.setData(ClipboardData(text: value));
 | 
						|
      showToast(translate('Copied'));
 | 
						|
    }
 | 
						|
 | 
						|
    Widget ConnectionStateNotification() {
 | 
						|
      if (serverModel.connectStatus == -1) {
 | 
						|
        return Row(children: [
 | 
						|
          const Icon(Icons.warning_amber_sharp,
 | 
						|
                  color: colorNegative, size: iconSize)
 | 
						|
              .marginOnly(right: iconMarginRight),
 | 
						|
          Expanded(child: Text(translate('not_ready_status')))
 | 
						|
        ]);
 | 
						|
      } else if (serverModel.connectStatus == 0) {
 | 
						|
        return Row(children: [
 | 
						|
          SizedBox(width: 20, height: 20, child: CircularProgressIndicator())
 | 
						|
              .marginOnly(left: 4, right: iconMarginRight),
 | 
						|
          Expanded(child: Text(translate('connecting_status')))
 | 
						|
        ]);
 | 
						|
      } else {
 | 
						|
        return Row(children: [
 | 
						|
          const Icon(Icons.check, color: colorPositive, size: iconSize)
 | 
						|
              .marginOnly(right: iconMarginRight),
 | 
						|
          Expanded(child: Text(translate('Ready')))
 | 
						|
        ]);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return PaddingCard(
 | 
						|
        title: translate('Your Device'),
 | 
						|
        child: Column(
 | 
						|
          // ID
 | 
						|
          children: [
 | 
						|
            Row(children: [
 | 
						|
              const Icon(Icons.perm_identity,
 | 
						|
                      color: Colors.grey, size: iconSize)
 | 
						|
                  .marginOnly(right: iconMarginRight),
 | 
						|
              Text(
 | 
						|
                translate('ID'),
 | 
						|
                style: textStyleHeading,
 | 
						|
              )
 | 
						|
            ]),
 | 
						|
            Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
 | 
						|
              Text(
 | 
						|
                model.serverId.value.text,
 | 
						|
                style: textStyleValue,
 | 
						|
              ),
 | 
						|
              IconButton(
 | 
						|
                  visualDensity: VisualDensity.compact,
 | 
						|
                  icon: Icon(Icons.copy_outlined),
 | 
						|
                  onPressed: () {
 | 
						|
                    copyToClipboard(model.serverId.value.text.trim());
 | 
						|
                  })
 | 
						|
            ]).marginOnly(left: 39, bottom: 10),
 | 
						|
            // Password
 | 
						|
            Row(children: [
 | 
						|
              const Icon(Icons.lock_outline, color: Colors.grey, size: iconSize)
 | 
						|
                  .marginOnly(right: iconMarginRight),
 | 
						|
              Text(
 | 
						|
                translate('One-time Password'),
 | 
						|
                style: textStyleHeading,
 | 
						|
              )
 | 
						|
            ]),
 | 
						|
            Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
 | 
						|
              Text(
 | 
						|
                isPermanent ? '-' : model.serverPasswd.value.text,
 | 
						|
                style: textStyleValue,
 | 
						|
              ),
 | 
						|
              isPermanent
 | 
						|
                  ? SizedBox.shrink()
 | 
						|
                  : Row(children: [
 | 
						|
                      IconButton(
 | 
						|
                          visualDensity: VisualDensity.compact,
 | 
						|
                          icon: const Icon(Icons.refresh),
 | 
						|
                          onPressed: () => bind.mainUpdateTemporaryPassword()),
 | 
						|
                      IconButton(
 | 
						|
                          visualDensity: VisualDensity.compact,
 | 
						|
                          icon: Icon(Icons.copy_outlined),
 | 
						|
                          onPressed: () {
 | 
						|
                            copyToClipboard(
 | 
						|
                                model.serverPasswd.value.text.trim());
 | 
						|
                          })
 | 
						|
                    ])
 | 
						|
            ]).marginOnly(left: 40, bottom: 15),
 | 
						|
            ConnectionStateNotification()
 | 
						|
          ],
 | 
						|
        ));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class PermissionChecker extends StatefulWidget {
 | 
						|
  const PermissionChecker({Key? key}) : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  State<PermissionChecker> createState() => _PermissionCheckerState();
 | 
						|
}
 | 
						|
 | 
						|
class _PermissionCheckerState extends State<PermissionChecker> {
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final serverModel = Provider.of<ServerModel>(context);
 | 
						|
    final hasAudioPermission = androidVersion >= 30;
 | 
						|
    return PaddingCard(
 | 
						|
        title: translate("Permissions"),
 | 
						|
        child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
 | 
						|
          serverModel.mediaOk
 | 
						|
              ? ElevatedButton.icon(
 | 
						|
                      style: ButtonStyle(
 | 
						|
                          backgroundColor:
 | 
						|
                              MaterialStateProperty.all(Colors.red)),
 | 
						|
                      icon: const Icon(Icons.stop),
 | 
						|
                      onPressed: serverModel.toggleService,
 | 
						|
                      label: Text(translate("Stop service")))
 | 
						|
                  .marginOnly(bottom: 8)
 | 
						|
              : SizedBox.shrink(),
 | 
						|
          PermissionRow(
 | 
						|
              translate("Screen Capture"),
 | 
						|
              serverModel.mediaOk,
 | 
						|
              !serverModel.mediaOk &&
 | 
						|
                      gFFI.userModel.userName.value.isEmpty &&
 | 
						|
                      bind.mainGetLocalOption(key: "show-scam-warning") != "N"
 | 
						|
                  ? () => showScamWarning(context, serverModel)
 | 
						|
                  : serverModel.toggleService),
 | 
						|
          PermissionRow(translate("Input Control"), serverModel.inputOk,
 | 
						|
              serverModel.toggleInput),
 | 
						|
          PermissionRow(translate("Transfer file"), serverModel.fileOk,
 | 
						|
              serverModel.toggleFile),
 | 
						|
          hasAudioPermission
 | 
						|
              ? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
 | 
						|
                  serverModel.toggleAudio)
 | 
						|
              : Row(children: [
 | 
						|
                  Icon(Icons.info_outline).marginOnly(right: 15),
 | 
						|
                  Expanded(
 | 
						|
                      child: Text(
 | 
						|
                    translate("android_version_audio_tip"),
 | 
						|
                    style: const TextStyle(color: MyTheme.darkGray),
 | 
						|
                  ))
 | 
						|
                ])
 | 
						|
        ]));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class PermissionRow extends StatelessWidget {
 | 
						|
  const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key})
 | 
						|
      : super(key: key);
 | 
						|
 | 
						|
  final String name;
 | 
						|
  final bool isOk;
 | 
						|
  final VoidCallback onPressed;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return SwitchListTile(
 | 
						|
        visualDensity: VisualDensity.compact,
 | 
						|
        contentPadding: EdgeInsets.all(0),
 | 
						|
        title: Text(name),
 | 
						|
        value: isOk,
 | 
						|
        onChanged: (bool value) {
 | 
						|
          onPressed();
 | 
						|
        });
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ConnectionManager extends StatelessWidget {
 | 
						|
  const ConnectionManager({Key? key}) : super(key: key);
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final serverModel = Provider.of<ServerModel>(context);
 | 
						|
    return Column(
 | 
						|
        children: serverModel.clients
 | 
						|
            .map((client) => PaddingCard(
 | 
						|
                title: translate(client.isFileTransfer
 | 
						|
                    ? "File Connection"
 | 
						|
                    : "Screen Connection"),
 | 
						|
                titleIcon: client.isFileTransfer
 | 
						|
                    ? Icon(Icons.folder_outlined)
 | 
						|
                    : Icon(Icons.mobile_screen_share),
 | 
						|
                child: Column(children: [
 | 
						|
                  Row(
 | 
						|
                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
						|
                    children: [
 | 
						|
                      Expanded(child: ClientInfo(client)),
 | 
						|
                      Expanded(
 | 
						|
                          flex: -1,
 | 
						|
                          child: client.isFileTransfer || !client.authorized
 | 
						|
                              ? const SizedBox.shrink()
 | 
						|
                              : IconButton(
 | 
						|
                                  onPressed: () {
 | 
						|
                                    gFFI.chatModel.changeCurrentKey(
 | 
						|
                                        MessageKey(client.peerId, client.id));
 | 
						|
                                    final bar = navigationBarKey.currentWidget;
 | 
						|
                                    if (bar != null) {
 | 
						|
                                      bar as BottomNavigationBar;
 | 
						|
                                      bar.onTap!(1);
 | 
						|
                                    }
 | 
						|
                                  },
 | 
						|
                                  icon: unreadTopRightBuilder(
 | 
						|
                                      client.unreadChatMessageCount)))
 | 
						|
                    ],
 | 
						|
                  ),
 | 
						|
                  client.authorized
 | 
						|
                      ? const SizedBox.shrink()
 | 
						|
                      : Text(
 | 
						|
                          translate("android_new_connection_tip"),
 | 
						|
                          style: Theme.of(context).textTheme.bodyMedium,
 | 
						|
                        ).marginOnly(bottom: 5),
 | 
						|
                  client.authorized
 | 
						|
                      ? _buildDisconnectButton(client)
 | 
						|
                      : _buildNewConnectionHint(serverModel, client),
 | 
						|
                  if (client.incomingVoiceCall && !client.inVoiceCall)
 | 
						|
                    ..._buildNewVoiceCallHint(context, serverModel, client),
 | 
						|
                ])))
 | 
						|
            .toList());
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildDisconnectButton(Client client) {
 | 
						|
    final disconnectButton = ElevatedButton.icon(
 | 
						|
      style: ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.red)),
 | 
						|
      icon: const Icon(Icons.close),
 | 
						|
      onPressed: () {
 | 
						|
        bind.cmCloseConnection(connId: client.id);
 | 
						|
        gFFI.invokeMethod("cancel_notification", client.id);
 | 
						|
      },
 | 
						|
      label: Text(translate("Disconnect")),
 | 
						|
    );
 | 
						|
    final buttons = [disconnectButton];
 | 
						|
    if (client.inVoiceCall) {
 | 
						|
      buttons.insert(
 | 
						|
        0,
 | 
						|
        ElevatedButton.icon(
 | 
						|
          style: ButtonStyle(
 | 
						|
              backgroundColor: MaterialStatePropertyAll(Colors.red)),
 | 
						|
          icon: const Icon(Icons.phone),
 | 
						|
          label: Text(translate("Stop")),
 | 
						|
          onPressed: () {
 | 
						|
            bind.cmCloseVoiceCall(id: client.id);
 | 
						|
            gFFI.invokeMethod("cancel_notification", client.id);
 | 
						|
          },
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (buttons.length == 1) {
 | 
						|
      return Container(
 | 
						|
        alignment: Alignment.centerRight,
 | 
						|
        child: disconnectButton,
 | 
						|
      );
 | 
						|
    } else {
 | 
						|
      return Row(
 | 
						|
        children: buttons,
 | 
						|
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  Widget _buildNewConnectionHint(ServerModel serverModel, Client client) {
 | 
						|
    return Row(mainAxisAlignment: MainAxisAlignment.end, children: [
 | 
						|
      TextButton(
 | 
						|
          child: Text(translate("Dismiss")),
 | 
						|
          onPressed: () {
 | 
						|
            serverModel.sendLoginResponse(client, false);
 | 
						|
          }).marginOnly(right: 15),
 | 
						|
      if (serverModel.approveMode != 'password')
 | 
						|
        ElevatedButton.icon(
 | 
						|
            icon: const Icon(Icons.check),
 | 
						|
            label: Text(translate("Accept")),
 | 
						|
            onPressed: () {
 | 
						|
              serverModel.sendLoginResponse(client, true);
 | 
						|
            }),
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
 | 
						|
  List<Widget> _buildNewVoiceCallHint(
 | 
						|
      BuildContext context, ServerModel serverModel, Client client) {
 | 
						|
    return [
 | 
						|
      Text(
 | 
						|
        translate("android_new_voice_call_tip"),
 | 
						|
        style: Theme.of(context).textTheme.bodyMedium,
 | 
						|
      ).marginOnly(bottom: 5),
 | 
						|
      Row(mainAxisAlignment: MainAxisAlignment.end, children: [
 | 
						|
        TextButton(
 | 
						|
            child: Text(translate("Dismiss")),
 | 
						|
            onPressed: () {
 | 
						|
              serverModel.handleVoiceCall(client, false);
 | 
						|
            }).marginOnly(right: 15),
 | 
						|
        if (serverModel.approveMode != 'password')
 | 
						|
          ElevatedButton.icon(
 | 
						|
              icon: const Icon(Icons.check),
 | 
						|
              label: Text(translate("Accept")),
 | 
						|
              onPressed: () {
 | 
						|
                serverModel.handleVoiceCall(client, true);
 | 
						|
              }),
 | 
						|
      ])
 | 
						|
    ];
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class PaddingCard extends StatelessWidget {
 | 
						|
  const PaddingCard({Key? key, required this.child, this.title, this.titleIcon})
 | 
						|
      : super(key: key);
 | 
						|
 | 
						|
  final String? title;
 | 
						|
  final Icon? titleIcon;
 | 
						|
  final Widget child;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final children = [child];
 | 
						|
    if (title != null) {
 | 
						|
      children.insert(
 | 
						|
          0,
 | 
						|
          Padding(
 | 
						|
              padding: const EdgeInsets.fromLTRB(0, 5, 0, 8),
 | 
						|
              child: Row(
 | 
						|
                children: [
 | 
						|
                  titleIcon?.marginOnly(right: 10) ?? const SizedBox.shrink(),
 | 
						|
                  Expanded(
 | 
						|
                    child: Text(title!,
 | 
						|
                        style: Theme.of(context)
 | 
						|
                            .textTheme
 | 
						|
                            .titleLarge
 | 
						|
                            ?.merge(TextStyle(fontWeight: FontWeight.bold))),
 | 
						|
                  )
 | 
						|
                ],
 | 
						|
              )));
 | 
						|
    }
 | 
						|
    return SizedBox(
 | 
						|
        width: double.maxFinite,
 | 
						|
        child: Card(
 | 
						|
          shape: RoundedRectangleBorder(
 | 
						|
            borderRadius: BorderRadius.circular(13),
 | 
						|
          ),
 | 
						|
          margin: const EdgeInsets.fromLTRB(12.0, 10.0, 12.0, 0),
 | 
						|
          child: Padding(
 | 
						|
            padding:
 | 
						|
                const EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0),
 | 
						|
            child: Column(
 | 
						|
              children: children,
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class ClientInfo extends StatelessWidget {
 | 
						|
  final Client client;
 | 
						|
  ClientInfo(this.client);
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return Padding(
 | 
						|
        padding: const EdgeInsets.symmetric(vertical: 8),
 | 
						|
        child: Column(children: [
 | 
						|
          Row(
 | 
						|
            children: [
 | 
						|
              Expanded(
 | 
						|
                  flex: -1,
 | 
						|
                  child: Padding(
 | 
						|
                      padding: const EdgeInsets.only(right: 12),
 | 
						|
                      child: CircleAvatar(
 | 
						|
                          backgroundColor: str2color(
 | 
						|
                              client.name,
 | 
						|
                              Theme.of(context).brightness == Brightness.light
 | 
						|
                                  ? 255
 | 
						|
                                  : 150),
 | 
						|
                          child: Text(client.name[0])))),
 | 
						|
              Expanded(
 | 
						|
                  child: Column(
 | 
						|
                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                      children: [
 | 
						|
                    Text(client.name, style: const TextStyle(fontSize: 18)),
 | 
						|
                    const SizedBox(width: 8),
 | 
						|
                    Text(client.peerId, style: const TextStyle(fontSize: 10))
 | 
						|
                  ]))
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
        ]));
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void androidChannelInit() {
 | 
						|
  gFFI.setMethodCallHandler((method, arguments) {
 | 
						|
    debugPrint("flutter got android msg,$method,$arguments");
 | 
						|
    try {
 | 
						|
      switch (method) {
 | 
						|
        case "start_capture":
 | 
						|
          {
 | 
						|
            gFFI.dialogManager.dismissAll();
 | 
						|
            gFFI.serverModel.updateClientState();
 | 
						|
            break;
 | 
						|
          }
 | 
						|
        case "on_state_changed":
 | 
						|
          {
 | 
						|
            var name = arguments["name"] as String;
 | 
						|
            var value = arguments["value"] as String == "true";
 | 
						|
            debugPrint("from jvm:on_state_changed,$name:$value");
 | 
						|
            gFFI.serverModel.changeStatue(name, value);
 | 
						|
            break;
 | 
						|
          }
 | 
						|
        case "on_android_permission_result":
 | 
						|
          {
 | 
						|
            var type = arguments["type"] as String;
 | 
						|
            var result = arguments["result"] as bool;
 | 
						|
            AndroidPermissionManager.complete(type, result);
 | 
						|
            break;
 | 
						|
          }
 | 
						|
        case "on_media_projection_canceled":
 | 
						|
          {
 | 
						|
            gFFI.serverModel.stopService();
 | 
						|
            break;
 | 
						|
          }
 | 
						|
        case "msgbox":
 | 
						|
          {
 | 
						|
            var type = arguments["type"] as String;
 | 
						|
            var title = arguments["title"] as String;
 | 
						|
            var text = arguments["text"] as String;
 | 
						|
            var link = (arguments["link"] ?? '') as String;
 | 
						|
            msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
 | 
						|
            break;
 | 
						|
          }
 | 
						|
        case "stop_service":
 | 
						|
          {
 | 
						|
            print(
 | 
						|
                "stop_service by kotlin, isStart:${gFFI.serverModel.isStart}");
 | 
						|
            if (gFFI.serverModel.isStart) {
 | 
						|
              gFFI.serverModel.stopService();
 | 
						|
            }
 | 
						|
            break;
 | 
						|
          }
 | 
						|
      }
 | 
						|
    } catch (e) {
 | 
						|
      debugPrintStack(label: "MethodCallHandler err:$e");
 | 
						|
    }
 | 
						|
    return "";
 | 
						|
  });
 | 
						|
}
 | 
						|
 | 
						|
void showScamWarning(BuildContext context, ServerModel serverModel) {
 | 
						|
  showDialog(
 | 
						|
    context: context,
 | 
						|
    builder: (BuildContext context) {
 | 
						|
      return ScamWarningDialog(serverModel: serverModel);
 | 
						|
    },
 | 
						|
  );
 | 
						|
}
 |