arblant
firefox-vpn-client / app / lib / ui / port_dialog.dart
port_dialog.dart135 lines · 3.8 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import '../core/ports.dart';
import '../l10n/app_localizations.dart';

/// Asks for a port: in place of [taken], with a free one filled in, or to
/// change the [current] one, starting from it. The chosen port, or null
/// when the user cancels or keeps the current one.
Future<int?> showPortDialog(
  BuildContext context, {
  String? taken,
  String? current,
}) => showDialog<int>(
  context: context,
  builder: (_) => _PortDialog(taken: taken, current: current),
);

class _PortDialog extends StatefulWidget {
  const _PortDialog({this.taken, this.current});

  /// The taken address or port, as the core reported it.
  final String? taken;

  /// The address or port in use now, to change.
  final String? current;

  @override
  State<_PortDialog> createState() => _PortDialogState();
}

class _PortDialogState extends State<_PortDialog> {
  final _field = TextEditingController();
  String? _error;
  bool _checking = false;

  bool get _changing => widget.current != null;

  /// The taken port, or the current one when changing it.
  int get _takenPort {
    final given = widget.current ?? widget.taken ?? '';
    return portOf(given) ?? int.tryParse(given) ?? 10810;
  }

  @override
  void initState() {
    super.initState();
    if (_changing) {
      _field.text = '$_takenPort';
      _field.selection = TextSelection(
        baseOffset: 0,
        extentOffset: _field.text.length,
      );
      return;
    }
    findFreePort(_takenPort).then((port) {
      if (mounted && _field.text.isEmpty) _field.text = '$port';
    });
  }

  @override
  void dispose() {
    _field.dispose();
    super.dispose();
  }

  Future<void> _submit() async {
    final l = AppLocalizations.of(context);
    final port = int.tryParse(_field.text.trim());
    if (port == null || !validPort(port)) {
      setState(() => _error = l.portInvalid);
      return;
    }
    // The current port stays: nothing to change.
    if (_changing && port == _takenPort) {
      Navigator.of(context).pop();
      return;
    }
    setState(() => _checking = true);
    final free = port == _takenPort ? false : await isFree(port);
    if (!mounted) return;
    setState(() => _checking = false);
    if (!free) {
      setState(() => _error = _changing ? l.portInUse : l.portBusy);
      return;
    }
    Navigator.of(context).pop(port);
  }

  @override
  Widget build(BuildContext context) {
    final l = AppLocalizations.of(context);
    return AlertDialog(
      icon: const Icon(Icons.settings_ethernet_rounded),
      title: Text(l.portDialogTitle),
      content: SizedBox(
        width: 380,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Text(
              _changing ? l.portChangeBody : l.portDialogBody('$_takenPort'),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _field,
              autofocus: true,
              keyboardType: TextInputType.number,
              textDirection: TextDirection.ltr,
              inputFormatters: [FilteringTextInputFormatter.digitsOnly],
              decoration: InputDecoration(
                labelText: l.portLabel,
                errorText: _error,
                border: const OutlineInputBorder(),
              ),
              onChanged: (_) => setState(() => _error = null),
              onSubmitted: (_) => _submit(),
            ),
          ],
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: Text(l.cancel),
        ),
        FilledButton(
          onPressed: _checking ? null : _submit,
          child: Text(l.save),
        ),
      ],
    );
  }
}