mirror of
https://github.com/DefinedNet/mobile_nebula.git
synced 2025-02-15 16:25:26 +00:00
Flutter formatting changes (#252)
* `flutter fmt lib/` * Re-enable formatting in CI
This commit is contained in:
parent
b2ebe0289a
commit
ed348ab126
57 changed files with 2397 additions and 2125 deletions
5
.github/workflows/flutterfmt.yml
vendored
5
.github/workflows/flutterfmt.yml
vendored
|
@ -23,6 +23,5 @@ jobs:
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
|
||||||
# Disabled for a single PR, this will be re-enabled in the PR that has lots of formatting changes.
|
- name: Check formating
|
||||||
# - name: Check formating
|
run: dart format -l120 lib/ --set-exit-if-changed --suppress-analytics --output none
|
||||||
# run: dart format -l120 lib/ --set-exit-if-changed --suppress-analytics --output none
|
|
||||||
|
|
|
@ -53,9 +53,10 @@ class _CIDRFieldState extends State<CIDRField> {
|
||||||
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
|
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
child: Row(children: <Widget>[
|
child: Row(
|
||||||
Expanded(
|
children: <Widget>[
|
||||||
child: Padding(
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
padding: EdgeInsets.fromLTRB(6, 6, 2, 6),
|
padding: EdgeInsets.fromLTRB(6, 6, 2, 6),
|
||||||
child: IPField(
|
child: IPField(
|
||||||
help: widget.ipHelp,
|
help: widget.ipHelp,
|
||||||
|
@ -74,30 +75,35 @@ class _CIDRFieldState extends State<CIDRField> {
|
||||||
widget.onChanged!(cidr);
|
widget.onChanged!(cidr);
|
||||||
},
|
},
|
||||||
controller: widget.ipController,
|
controller: widget.ipController,
|
||||||
))),
|
),
|
||||||
Text("/"),
|
),
|
||||||
Container(
|
),
|
||||||
width: Utils.textSize("bits", textStyle).width + 12,
|
Text("/"),
|
||||||
padding: EdgeInsets.fromLTRB(2, 6, 6, 6),
|
Container(
|
||||||
child: SpecialTextField(
|
width: Utils.textSize("bits", textStyle).width + 12,
|
||||||
keyboardType: TextInputType.number,
|
padding: EdgeInsets.fromLTRB(2, 6, 6, 6),
|
||||||
focusNode: bitsFocus,
|
child: SpecialTextField(
|
||||||
nextFocusNode: widget.nextFocusNode,
|
keyboardType: TextInputType.number,
|
||||||
controller: widget.bitsController,
|
focusNode: bitsFocus,
|
||||||
onChanged: (val) {
|
nextFocusNode: widget.nextFocusNode,
|
||||||
if (widget.onChanged == null) {
|
controller: widget.bitsController,
|
||||||
return;
|
onChanged: (val) {
|
||||||
}
|
if (widget.onChanged == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cidr.bits = int.tryParse(val) ?? 0;
|
cidr.bits = int.tryParse(val) ?? 0;
|
||||||
widget.onChanged!(cidr);
|
widget.onChanged!(cidr);
|
||||||
},
|
},
|
||||||
maxLength: 2,
|
maxLength: 2,
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
textInputAction: widget.textInputAction ?? TextInputAction.done,
|
textInputAction: widget.textInputAction ?? TextInputAction.done,
|
||||||
placeholder: 'bits',
|
placeholder: 'bits',
|
||||||
))
|
),
|
||||||
]));
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -18,51 +18,57 @@ class CIDRFormField extends FormField<CIDR> {
|
||||||
this.ipController,
|
this.ipController,
|
||||||
this.bitsController,
|
this.bitsController,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
onSaved: onSaved,
|
onSaved: onSaved,
|
||||||
validator: (cidr) {
|
validator: (cidr) {
|
||||||
if (cidr == null) {
|
if (cidr == null) {
|
||||||
return "Please fill out this field";
|
return "Please fill out this field";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ipValidator(cidr.ip, enableIPV6)) {
|
if (!ipValidator(cidr.ip, enableIPV6)) {
|
||||||
return 'Please enter a valid ip address';
|
return 'Please enter a valid ip address';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cidr.bits > 32 || cidr.bits < 0) {
|
if (cidr.bits > 32 || cidr.bits < 0) {
|
||||||
return "Please enter a valid number of bits";
|
return "Please enter a valid number of bits";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
builder: (FormFieldState<CIDR> field) {
|
builder: (FormFieldState<CIDR> field) {
|
||||||
final _CIDRFormField state = field as _CIDRFormField;
|
final _CIDRFormField state = field as _CIDRFormField;
|
||||||
|
|
||||||
void onChangedHandler(CIDR value) {
|
void onChangedHandler(CIDR value) {
|
||||||
if (onChanged != null) {
|
if (onChanged != null) {
|
||||||
onChanged(value);
|
onChanged(value);
|
||||||
}
|
}
|
||||||
field.didChange(value);
|
field.didChange(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(crossAxisAlignment: CrossAxisAlignment.end, children: <Widget>[
|
return Column(
|
||||||
CIDRField(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
autoFocus: autoFocus,
|
children: <Widget>[
|
||||||
focusNode: focusNode,
|
CIDRField(
|
||||||
nextFocusNode: nextFocusNode,
|
autoFocus: autoFocus,
|
||||||
onChanged: onChangedHandler,
|
focusNode: focusNode,
|
||||||
textInputAction: textInputAction,
|
nextFocusNode: nextFocusNode,
|
||||||
ipController: state._effectiveIPController,
|
onChanged: onChangedHandler,
|
||||||
bitsController: state._effectiveBitsController,
|
textInputAction: textInputAction,
|
||||||
),
|
ipController: state._effectiveIPController,
|
||||||
field.hasError
|
bitsController: state._effectiveBitsController,
|
||||||
? Text(field.errorText ?? "Unknown error",
|
),
|
||||||
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
|
field.hasError
|
||||||
textAlign: TextAlign.end)
|
? Text(
|
||||||
: Container(height: 0)
|
field.errorText ?? "Unknown error",
|
||||||
]);
|
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
|
||||||
});
|
textAlign: TextAlign.end,
|
||||||
|
)
|
||||||
|
: Container(height: 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final TextEditingController? ipController;
|
final TextEditingController? ipController;
|
||||||
final TextEditingController? bitsController;
|
final TextEditingController? bitsController;
|
||||||
|
|
|
@ -13,18 +13,24 @@ class DangerButton extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
return FilledButton(
|
return FilledButton(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: child,
|
child: child,
|
||||||
style: FilledButton.styleFrom(
|
style: FilledButton.styleFrom(
|
||||||
backgroundColor: Theme.of(context).colorScheme.error,
|
backgroundColor: Theme.of(context).colorScheme.error,
|
||||||
foregroundColor: Theme.of(context).colorScheme.onError));
|
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Workaround for https://github.com/flutter/flutter/issues/161590
|
// Workaround for https://github.com/flutter/flutter/issues/161590
|
||||||
final themeData = CupertinoTheme.of(context);
|
final themeData = CupertinoTheme.of(context);
|
||||||
return CupertinoTheme(
|
return CupertinoTheme(
|
||||||
data: themeData.copyWith(primaryColor: CupertinoColors.white),
|
data: themeData.copyWith(primaryColor: CupertinoColors.white),
|
||||||
child: CupertinoButton(
|
child: CupertinoButton(
|
||||||
child: child, onPressed: onPressed, color: CupertinoColors.systemRed.resolveFrom(context)));
|
child: child,
|
||||||
|
onPressed: onPressed,
|
||||||
|
color: CupertinoColors.systemRed.resolveFrom(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,15 +5,15 @@ import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
/// SimplePage with a form and built in validation and confirmation to discard changes if any are made
|
/// SimplePage with a form and built in validation and confirmation to discard changes if any are made
|
||||||
class FormPage extends StatefulWidget {
|
class FormPage extends StatefulWidget {
|
||||||
const FormPage(
|
const FormPage({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.onSave,
|
required this.onSave,
|
||||||
required this.changed,
|
required this.changed,
|
||||||
this.hideSave = false,
|
this.hideSave = false,
|
||||||
this.scrollController})
|
this.scrollController,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final Function onSave;
|
final Function onSave;
|
||||||
|
@ -39,42 +39,61 @@ class _FormPageState extends State<FormPage> {
|
||||||
changed = widget.changed || changed;
|
changed = widget.changed || changed;
|
||||||
|
|
||||||
return PopScope<Object?>(
|
return PopScope<Object?>(
|
||||||
canPop: !changed,
|
canPop: !changed,
|
||||||
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
onPopInvokedWithResult: (bool didPop, Object? result) async {
|
||||||
if (didPop) {
|
if (didPop) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final NavigatorState navigator = Navigator.of(context);
|
final NavigatorState navigator = Navigator.of(context);
|
||||||
|
|
||||||
Utils.confirmDelete(context, 'Discard changes?', () {
|
Utils.confirmDelete(
|
||||||
|
context,
|
||||||
|
'Discard changes?',
|
||||||
|
() {
|
||||||
navigator.pop();
|
navigator.pop();
|
||||||
}, deleteLabel: 'Yes', cancelLabel: 'No');
|
},
|
||||||
},
|
deleteLabel: 'Yes',
|
||||||
child: SimplePage(
|
cancelLabel: 'No',
|
||||||
leadingAction: _buildLeader(context),
|
);
|
||||||
trailingActions: _buildTrailer(context),
|
},
|
||||||
scrollController: widget.scrollController,
|
child: SimplePage(
|
||||||
title: Text(widget.title),
|
leadingAction: _buildLeader(context),
|
||||||
child: Form(
|
trailingActions: _buildTrailer(context),
|
||||||
key: _formKey,
|
scrollController: widget.scrollController,
|
||||||
onChanged: () => setState(() {
|
title: Text(widget.title),
|
||||||
changed = true;
|
child: Form(
|
||||||
}),
|
key: _formKey,
|
||||||
child: widget.child),
|
onChanged:
|
||||||
));
|
() => setState(() {
|
||||||
|
changed = true;
|
||||||
|
}),
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLeader(BuildContext context) {
|
Widget _buildLeader(BuildContext context) {
|
||||||
return Utils.leadingBackWidget(context, label: changed ? 'Cancel' : 'Back', onPressed: () {
|
return Utils.leadingBackWidget(
|
||||||
if (changed) {
|
context,
|
||||||
Utils.confirmDelete(context, 'Discard changes?', () {
|
label: changed ? 'Cancel' : 'Back',
|
||||||
changed = false;
|
onPressed: () {
|
||||||
|
if (changed) {
|
||||||
|
Utils.confirmDelete(
|
||||||
|
context,
|
||||||
|
'Discard changes?',
|
||||||
|
() {
|
||||||
|
changed = false;
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
deleteLabel: 'Yes',
|
||||||
|
cancelLabel: 'No',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}, deleteLabel: 'Yes', cancelLabel: 'No');
|
}
|
||||||
} else {
|
},
|
||||||
Navigator.pop(context);
|
);
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildTrailer(BuildContext context) {
|
List<Widget> _buildTrailer(BuildContext context) {
|
||||||
|
@ -83,21 +102,18 @@ class _FormPageState extends State<FormPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Utils.trailingSaveWidget(
|
Utils.trailingSaveWidget(context, () {
|
||||||
context,
|
if (_formKey.currentState == null) {
|
||||||
() {
|
return;
|
||||||
if (_formKey.currentState == null) {
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_formKey.currentState!.validate()) {
|
if (!_formKey.currentState!.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_formKey.currentState!.save();
|
_formKey.currentState!.save();
|
||||||
widget.onSave();
|
widget.onSave();
|
||||||
},
|
}),
|
||||||
)
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,9 +59,10 @@ class _IPAndPortFieldState extends State<IPAndPortField> {
|
||||||
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
|
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
child: Row(children: <Widget>[
|
child: Row(
|
||||||
Expanded(
|
children: <Widget>[
|
||||||
child: Padding(
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
padding: EdgeInsets.fromLTRB(6, 6, 2, 6),
|
padding: EdgeInsets.fromLTRB(6, 6, 2, 6),
|
||||||
child: IPField(
|
child: IPField(
|
||||||
help: widget.ipHelp,
|
help: widget.ipHelp,
|
||||||
|
@ -76,26 +77,31 @@ class _IPAndPortFieldState extends State<IPAndPortField> {
|
||||||
},
|
},
|
||||||
textAlign: widget.ipTextAlign,
|
textAlign: widget.ipTextAlign,
|
||||||
controller: widget.ipController,
|
controller: widget.ipController,
|
||||||
))),
|
),
|
||||||
Text(":"),
|
),
|
||||||
Container(
|
),
|
||||||
width: Utils.textSize("00000", textStyle).width + 12,
|
Text(":"),
|
||||||
padding: EdgeInsets.fromLTRB(2, 6, 6, 6),
|
Container(
|
||||||
child: SpecialTextField(
|
width: Utils.textSize("00000", textStyle).width + 12,
|
||||||
keyboardType: TextInputType.number,
|
padding: EdgeInsets.fromLTRB(2, 6, 6, 6),
|
||||||
focusNode: _portFocus,
|
child: SpecialTextField(
|
||||||
nextFocusNode: widget.nextFocusNode,
|
keyboardType: TextInputType.number,
|
||||||
controller: widget.portController,
|
focusNode: _portFocus,
|
||||||
onChanged: (val) {
|
nextFocusNode: widget.nextFocusNode,
|
||||||
_ipAndPort.port = int.tryParse(val);
|
controller: widget.portController,
|
||||||
widget.onChanged(_ipAndPort);
|
onChanged: (val) {
|
||||||
},
|
_ipAndPort.port = int.tryParse(val);
|
||||||
maxLength: 5,
|
widget.onChanged(_ipAndPort);
|
||||||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
},
|
||||||
textInputAction: TextInputAction.done,
|
maxLength: 5,
|
||||||
placeholder: 'port',
|
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||||
))
|
textInputAction: TextInputAction.done,
|
||||||
]));
|
placeholder: 'port',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -24,54 +24,59 @@ class IPAndPortFormField extends FormField<IPAndPort> {
|
||||||
this.ipController,
|
this.ipController,
|
||||||
this.portController,
|
this.portController,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
onSaved: onSaved,
|
onSaved: onSaved,
|
||||||
validator: (ipAndPort) {
|
validator: (ipAndPort) {
|
||||||
if (ipAndPort == null) {
|
if (ipAndPort == null) {
|
||||||
return "Please fill out this field";
|
return "Please fill out this field";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ipValidator(ipAndPort.ip, enableIPV6) && (!ipOnly && !dnsValidator(ipAndPort.ip))) {
|
if (!ipValidator(ipAndPort.ip, enableIPV6) && (!ipOnly && !dnsValidator(ipAndPort.ip))) {
|
||||||
return ipOnly ? 'Please enter a valid ip address' : 'Please enter a valid ip address or dns name';
|
return ipOnly ? 'Please enter a valid ip address' : 'Please enter a valid ip address or dns name';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ipAndPort.port == null || ipAndPort.port! > 65535 || ipAndPort.port! < 0) {
|
if (ipAndPort.port == null || ipAndPort.port! > 65535 || ipAndPort.port! < 0) {
|
||||||
return "Please enter a valid port";
|
return "Please enter a valid port";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
builder: (FormFieldState<IPAndPort> field) {
|
builder: (FormFieldState<IPAndPort> field) {
|
||||||
final _IPAndPortFormField state = field as _IPAndPortFormField;
|
final _IPAndPortFormField state = field as _IPAndPortFormField;
|
||||||
|
|
||||||
void onChangedHandler(IPAndPort value) {
|
void onChangedHandler(IPAndPort value) {
|
||||||
if (onChanged != null) {
|
if (onChanged != null) {
|
||||||
onChanged(value);
|
onChanged(value);
|
||||||
}
|
}
|
||||||
field.didChange(value);
|
field.didChange(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(children: <Widget>[
|
return Column(
|
||||||
IPAndPortField(
|
children: <Widget>[
|
||||||
ipOnly: ipOnly,
|
IPAndPortField(
|
||||||
ipHelp: ipHelp,
|
ipOnly: ipOnly,
|
||||||
autoFocus: autoFocus,
|
ipHelp: ipHelp,
|
||||||
focusNode: focusNode,
|
autoFocus: autoFocus,
|
||||||
nextFocusNode: nextFocusNode,
|
focusNode: focusNode,
|
||||||
onChanged: onChangedHandler,
|
nextFocusNode: nextFocusNode,
|
||||||
textInputAction: textInputAction,
|
onChanged: onChangedHandler,
|
||||||
ipController: state._effectiveIPController,
|
textInputAction: textInputAction,
|
||||||
portController: state._effectivePortController,
|
ipController: state._effectiveIPController,
|
||||||
noBorder: noBorder,
|
portController: state._effectivePortController,
|
||||||
ipTextAlign: ipTextAlign,
|
noBorder: noBorder,
|
||||||
),
|
ipTextAlign: ipTextAlign,
|
||||||
field.hasError
|
),
|
||||||
? Text(field.errorText!,
|
field.hasError
|
||||||
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13))
|
? Text(
|
||||||
: Container(height: 0)
|
field.errorText!,
|
||||||
]);
|
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
|
||||||
});
|
)
|
||||||
|
: Container(height: 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final TextEditingController? ipController;
|
final TextEditingController? ipController;
|
||||||
final TextEditingController? portController;
|
final TextEditingController? portController;
|
||||||
|
@ -109,8 +114,10 @@ class _IPAndPortFormField extends FormFieldState<IPAndPort> {
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(IPAndPortFormField oldWidget) {
|
void didUpdateWidget(IPAndPortFormField oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
var update =
|
var update = IPAndPort(
|
||||||
IPAndPort(ip: widget.ipController?.text, port: int.tryParse(widget.portController?.text ?? "") ?? null);
|
ip: widget.ipController?.text,
|
||||||
|
port: int.tryParse(widget.portController?.text ?? "") ?? null,
|
||||||
|
);
|
||||||
bool shouldUpdate = false;
|
bool shouldUpdate = false;
|
||||||
|
|
||||||
if (widget.ipController != oldWidget.ipController) {
|
if (widget.ipController != oldWidget.ipController) {
|
||||||
|
|
|
@ -16,19 +16,19 @@ class IPField extends StatelessWidget {
|
||||||
final controller;
|
final controller;
|
||||||
final textAlign;
|
final textAlign;
|
||||||
|
|
||||||
const IPField(
|
const IPField({
|
||||||
{Key? key,
|
Key? key,
|
||||||
this.ipOnly = false,
|
this.ipOnly = false,
|
||||||
this.help = "ip address",
|
this.help = "ip address",
|
||||||
this.autoFocus = false,
|
this.autoFocus = false,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.nextFocusNode,
|
this.nextFocusNode,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.textPadding = const EdgeInsets.all(6.0),
|
this.textPadding = const EdgeInsets.all(6.0),
|
||||||
this.textInputAction,
|
this.textInputAction,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.textAlign = TextAlign.center})
|
this.textAlign = TextAlign.center,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -36,21 +36,22 @@ class IPField extends StatelessWidget {
|
||||||
final double? ipWidth = ipOnly ? Utils.textSize("000000000000000", textStyle).width + 12 : null;
|
final double? ipWidth = ipOnly ? Utils.textSize("000000000000000", textStyle).width + 12 : null;
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: ipWidth,
|
width: ipWidth,
|
||||||
child: SpecialTextField(
|
child: SpecialTextField(
|
||||||
keyboardType: ipOnly ? TextInputType.numberWithOptions(decimal: true, signed: true) : null,
|
keyboardType: ipOnly ? TextInputType.numberWithOptions(decimal: true, signed: true) : null,
|
||||||
textAlign: textAlign,
|
textAlign: textAlign,
|
||||||
autofocus: autoFocus,
|
autofocus: autoFocus,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
nextFocusNode: nextFocusNode,
|
nextFocusNode: nextFocusNode,
|
||||||
controller: controller,
|
controller: controller,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
maxLength: ipOnly ? 15 : null,
|
maxLength: ipOnly ? 15 : null,
|
||||||
maxLengthEnforcement: ipOnly ? MaxLengthEnforcement.enforced : MaxLengthEnforcement.none,
|
maxLengthEnforcement: ipOnly ? MaxLengthEnforcement.enforced : MaxLengthEnforcement.none,
|
||||||
inputFormatters: ipOnly ? [IPTextInputFormatter()] : [FilteringTextInputFormatter.allow(RegExp(r'[^\s]+'))],
|
inputFormatters: ipOnly ? [IPTextInputFormatter()] : [FilteringTextInputFormatter.allow(RegExp(r'[^\s]+'))],
|
||||||
textInputAction: this.textInputAction,
|
textInputAction: this.textInputAction,
|
||||||
placeholder: help,
|
placeholder: help,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,16 +60,13 @@ class IPTextInputFormatter extends TextInputFormatter {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||||
return _selectionAwareTextManipulation(
|
return _selectionAwareTextManipulation(newValue, (String substring) {
|
||||||
newValue,
|
return whitelistedPattern
|
||||||
(String substring) {
|
.allMatches(substring)
|
||||||
return whitelistedPattern
|
.map<String>((Match match) => match.group(0)!)
|
||||||
.allMatches(substring)
|
.join()
|
||||||
.map<String>((Match match) => match.group(0)!)
|
.replaceAll(RegExp(r','), '.');
|
||||||
.join()
|
});
|
||||||
.replaceAll(RegExp(r','), '.');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,52 +25,57 @@ class IPFormField extends FormField<String> {
|
||||||
crossAxisAlignment = CrossAxisAlignment.center,
|
crossAxisAlignment = CrossAxisAlignment.center,
|
||||||
textAlign = TextAlign.center,
|
textAlign = TextAlign.center,
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
onSaved: onSaved,
|
onSaved: onSaved,
|
||||||
validator: (ip) {
|
validator: (ip) {
|
||||||
if (ip == null || ip == "") {
|
if (ip == null || ip == "") {
|
||||||
return "Please fill out this field";
|
return "Please fill out this field";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ipValidator(ip, enableIPV6) || (!ipOnly && !dnsValidator(ip))) {
|
if (!ipValidator(ip, enableIPV6) || (!ipOnly && !dnsValidator(ip))) {
|
||||||
print(ip);
|
print(ip);
|
||||||
return ipOnly ? 'Please enter a valid ip address' : 'Please enter a valid ip address or dns name';
|
return ipOnly ? 'Please enter a valid ip address' : 'Please enter a valid ip address or dns name';
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
builder: (FormFieldState<String> field) {
|
builder: (FormFieldState<String> field) {
|
||||||
final _IPFormField state = field as _IPFormField;
|
final _IPFormField state = field as _IPFormField;
|
||||||
|
|
||||||
void onChangedHandler(String value) {
|
void onChangedHandler(String value) {
|
||||||
if (onChanged != null) {
|
if (onChanged != null) {
|
||||||
onChanged(value);
|
onChanged(value);
|
||||||
}
|
}
|
||||||
field.didChange(value);
|
field.didChange(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(crossAxisAlignment: crossAxisAlignment, children: <Widget>[
|
return Column(
|
||||||
IPField(
|
crossAxisAlignment: crossAxisAlignment,
|
||||||
ipOnly: ipOnly,
|
children: <Widget>[
|
||||||
help: help,
|
IPField(
|
||||||
autoFocus: autoFocus,
|
ipOnly: ipOnly,
|
||||||
focusNode: focusNode,
|
help: help,
|
||||||
nextFocusNode: nextFocusNode,
|
autoFocus: autoFocus,
|
||||||
onChanged: onChangedHandler,
|
focusNode: focusNode,
|
||||||
textPadding: textPadding,
|
nextFocusNode: nextFocusNode,
|
||||||
textInputAction: textInputAction,
|
onChanged: onChangedHandler,
|
||||||
controller: state._effectiveController,
|
textPadding: textPadding,
|
||||||
textAlign: textAlign),
|
textInputAction: textInputAction,
|
||||||
field.hasError
|
controller: state._effectiveController,
|
||||||
? Text(
|
textAlign: textAlign,
|
||||||
field.errorText!,
|
),
|
||||||
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
|
field.hasError
|
||||||
textAlign: textAlign,
|
? Text(
|
||||||
)
|
field.errorText!,
|
||||||
: Container(height: 0)
|
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
|
||||||
]);
|
textAlign: textAlign,
|
||||||
});
|
)
|
||||||
|
: Container(height: 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
|
|
||||||
|
|
|
@ -5,81 +5,86 @@ import 'package:mobile_nebula/components/SpecialTextField.dart';
|
||||||
|
|
||||||
class PlatformTextFormField extends FormField<String> {
|
class PlatformTextFormField extends FormField<String> {
|
||||||
//TODO: auto-validate, enabled?
|
//TODO: auto-validate, enabled?
|
||||||
PlatformTextFormField(
|
PlatformTextFormField({
|
||||||
{Key? key,
|
Key? key,
|
||||||
widgetKey,
|
widgetKey,
|
||||||
this.controller,
|
this.controller,
|
||||||
focusNode,
|
focusNode,
|
||||||
nextFocusNode,
|
nextFocusNode,
|
||||||
TextInputType? keyboardType,
|
TextInputType? keyboardType,
|
||||||
textInputAction,
|
textInputAction,
|
||||||
List<TextInputFormatter>? inputFormatters,
|
List<TextInputFormatter>? inputFormatters,
|
||||||
textAlign,
|
textAlign,
|
||||||
autofocus,
|
autofocus,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
maxLength,
|
maxLength,
|
||||||
maxLengthEnforcement,
|
maxLengthEnforcement,
|
||||||
onChanged,
|
onChanged,
|
||||||
keyboardAppearance,
|
keyboardAppearance,
|
||||||
minLines,
|
minLines,
|
||||||
expands,
|
expands,
|
||||||
suffix,
|
suffix,
|
||||||
textAlignVertical,
|
textAlignVertical,
|
||||||
String? initialValue,
|
String? initialValue,
|
||||||
String? placeholder,
|
String? placeholder,
|
||||||
FormFieldValidator<String>? validator,
|
FormFieldValidator<String>? validator,
|
||||||
ValueChanged<String?>? onSaved})
|
ValueChanged<String?>? onSaved,
|
||||||
: super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
initialValue: controller != null ? controller.text : (initialValue ?? ''),
|
initialValue: controller != null ? controller.text : (initialValue ?? ''),
|
||||||
onSaved: onSaved,
|
onSaved: onSaved,
|
||||||
validator: (str) {
|
validator: (str) {
|
||||||
if (validator != null) {
|
if (validator != null) {
|
||||||
return validator(str);
|
return validator(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
builder: (FormFieldState<String> field) {
|
builder: (FormFieldState<String> field) {
|
||||||
final _PlatformTextFormFieldState state = field as _PlatformTextFormFieldState;
|
final _PlatformTextFormFieldState state = field as _PlatformTextFormFieldState;
|
||||||
|
|
||||||
void onChangedHandler(String value) {
|
void onChangedHandler(String value) {
|
||||||
if (onChanged != null) {
|
if (onChanged != null) {
|
||||||
onChanged(value);
|
onChanged(value);
|
||||||
}
|
}
|
||||||
field.didChange(value);
|
field.didChange(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(crossAxisAlignment: CrossAxisAlignment.end, children: <Widget>[
|
return Column(
|
||||||
SpecialTextField(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
key: widgetKey,
|
children: <Widget>[
|
||||||
controller: state._effectiveController,
|
SpecialTextField(
|
||||||
focusNode: focusNode,
|
key: widgetKey,
|
||||||
nextFocusNode: nextFocusNode,
|
controller: state._effectiveController,
|
||||||
keyboardType: keyboardType,
|
focusNode: focusNode,
|
||||||
textInputAction: textInputAction,
|
nextFocusNode: nextFocusNode,
|
||||||
textAlign: textAlign,
|
keyboardType: keyboardType,
|
||||||
autofocus: autofocus,
|
textInputAction: textInputAction,
|
||||||
maxLines: maxLines,
|
textAlign: textAlign,
|
||||||
maxLength: maxLength,
|
autofocus: autofocus,
|
||||||
maxLengthEnforcement: maxLengthEnforcement,
|
maxLines: maxLines,
|
||||||
onChanged: onChangedHandler,
|
maxLength: maxLength,
|
||||||
keyboardAppearance: keyboardAppearance,
|
maxLengthEnforcement: maxLengthEnforcement,
|
||||||
minLines: minLines,
|
onChanged: onChangedHandler,
|
||||||
expands: expands,
|
keyboardAppearance: keyboardAppearance,
|
||||||
textAlignVertical: textAlignVertical,
|
minLines: minLines,
|
||||||
placeholder: placeholder,
|
expands: expands,
|
||||||
inputFormatters: inputFormatters,
|
textAlignVertical: textAlignVertical,
|
||||||
suffix: suffix),
|
placeholder: placeholder,
|
||||||
field.hasError
|
inputFormatters: inputFormatters,
|
||||||
? Text(
|
suffix: suffix,
|
||||||
field.errorText!,
|
),
|
||||||
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
|
field.hasError
|
||||||
textAlign: textAlign,
|
? Text(
|
||||||
)
|
field.errorText!,
|
||||||
: Container(height: 0)
|
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
|
||||||
]);
|
textAlign: textAlign,
|
||||||
});
|
)
|
||||||
|
: Container(height: 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
|
|
||||||
|
|
|
@ -2,29 +2,24 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||||
|
|
||||||
enum SimpleScrollable {
|
enum SimpleScrollable { none, vertical, horizontal, both }
|
||||||
none,
|
|
||||||
vertical,
|
|
||||||
horizontal,
|
|
||||||
both,
|
|
||||||
}
|
|
||||||
|
|
||||||
class SimplePage extends StatelessWidget {
|
class SimplePage extends StatelessWidget {
|
||||||
const SimplePage(
|
const SimplePage({
|
||||||
{Key? key,
|
Key? key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.leadingAction,
|
this.leadingAction,
|
||||||
this.trailingActions = const [],
|
this.trailingActions = const [],
|
||||||
this.scrollable = SimpleScrollable.vertical,
|
this.scrollable = SimpleScrollable.vertical,
|
||||||
this.scrollbar = true,
|
this.scrollbar = true,
|
||||||
this.scrollController,
|
this.scrollController,
|
||||||
this.bottomBar,
|
this.bottomBar,
|
||||||
this.onRefresh,
|
this.onRefresh,
|
||||||
this.onLoading,
|
this.onLoading,
|
||||||
this.alignment,
|
this.alignment,
|
||||||
this.refreshController})
|
this.refreshController,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Widget title;
|
final Widget title;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
@ -52,9 +47,10 @@ class SimplePage extends StatelessWidget {
|
||||||
|
|
||||||
if (scrollable == SimpleScrollable.vertical || scrollable == SimpleScrollable.both) {
|
if (scrollable == SimpleScrollable.vertical || scrollable == SimpleScrollable.both) {
|
||||||
realChild = SingleChildScrollView(
|
realChild = SingleChildScrollView(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
child: realChild,
|
child: realChild,
|
||||||
controller: refreshController == null ? scrollController : null);
|
controller: refreshController == null ? scrollController : null,
|
||||||
|
);
|
||||||
addScrollbar = true;
|
addScrollbar = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,19 +61,20 @@ class SimplePage extends StatelessWidget {
|
||||||
|
|
||||||
if (refreshController != null) {
|
if (refreshController != null) {
|
||||||
realChild = RefreshConfiguration(
|
realChild = RefreshConfiguration(
|
||||||
headerTriggerDistance: 100,
|
headerTriggerDistance: 100,
|
||||||
footerTriggerDistance: -100,
|
footerTriggerDistance: -100,
|
||||||
maxUnderScrollExtent: 100,
|
maxUnderScrollExtent: 100,
|
||||||
child: SmartRefresher(
|
child: SmartRefresher(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
onRefresh: onRefresh,
|
onRefresh: onRefresh,
|
||||||
onLoading: onLoading,
|
onLoading: onLoading,
|
||||||
controller: refreshController!,
|
controller: refreshController!,
|
||||||
child: realChild,
|
child: realChild,
|
||||||
enablePullUp: onLoading != null,
|
enablePullUp: onLoading != null,
|
||||||
enablePullDown: onRefresh != null,
|
enablePullDown: onRefresh != null,
|
||||||
footer: ClassicFooter(loadStyle: LoadStyle.ShowWhenLoading),
|
footer: ClassicFooter(loadStyle: LoadStyle.ShowWhenLoading),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
addScrollbar = true;
|
addScrollbar = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,24 +87,24 @@ class SimplePage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bottomBar != null) {
|
if (bottomBar != null) {
|
||||||
realChild = Column(children: [
|
realChild = Column(children: [Expanded(child: realChild), bottomBar!]);
|
||||||
Expanded(child: realChild),
|
|
||||||
bottomBar!,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return PlatformScaffold(
|
return PlatformScaffold(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
appBar: PlatformAppBar(
|
appBar: PlatformAppBar(
|
||||||
title: title,
|
title: title,
|
||||||
leading: leadingAction,
|
leading: leadingAction,
|
||||||
trailingActions: trailingActions,
|
trailingActions: trailingActions,
|
||||||
cupertino: (_, __) => CupertinoNavigationBarData(
|
cupertino:
|
||||||
|
(_, __) => CupertinoNavigationBarData(
|
||||||
transitionBetweenRoutes: false,
|
transitionBetweenRoutes: false,
|
||||||
// TODO: set title on route, show here instead of just "Back"
|
// TODO: set title on route, show here instead of just "Back"
|
||||||
previousPageTitle: 'Back',
|
previousPageTitle: 'Back',
|
||||||
padding: EdgeInsetsDirectional.only(end: 8.0)),
|
padding: EdgeInsetsDirectional.only(end: 8.0),
|
||||||
),
|
),
|
||||||
body: SafeArea(child: realChild));
|
),
|
||||||
|
body: SafeArea(child: realChild),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,17 +13,19 @@ class SiteItem extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final borderColor = site.errors.length > 0
|
final borderColor =
|
||||||
? CupertinoColors.systemRed.resolveFrom(context)
|
site.errors.length > 0
|
||||||
: site.connected
|
? CupertinoColors.systemRed.resolveFrom(context)
|
||||||
|
: site.connected
|
||||||
? CupertinoColors.systemGreen.resolveFrom(context)
|
? CupertinoColors.systemGreen.resolveFrom(context)
|
||||||
: CupertinoColors.systemGrey2.resolveFrom(context);
|
: CupertinoColors.systemGrey2.resolveFrom(context);
|
||||||
final border = BorderSide(color: borderColor, width: 10);
|
final border = BorderSide(color: borderColor, width: 10);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: EdgeInsets.symmetric(vertical: 6),
|
margin: EdgeInsets.symmetric(vertical: 6),
|
||||||
decoration: BoxDecoration(border: Border(left: border)),
|
decoration: BoxDecoration(border: Border(left: border)),
|
||||||
child: _buildContent(context));
|
child: _buildContent(context),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(BuildContext context) {
|
Widget _buildContent(BuildContext context) {
|
||||||
|
@ -32,21 +34,25 @@ class SiteItem extends StatelessWidget {
|
||||||
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
|
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
|
||||||
|
|
||||||
return SpecialButton(
|
return SpecialButton(
|
||||||
decoration:
|
decoration: BoxDecoration(
|
||||||
BoxDecoration(border: Border(top: border, bottom: border), color: Utils.configItemBackground(context)),
|
border: Border(top: border, bottom: border),
|
||||||
onPressed: onPressed,
|
color: Utils.configItemBackground(context),
|
||||||
child: Padding(
|
),
|
||||||
padding: EdgeInsets.fromLTRB(10, 10, 5, 10),
|
onPressed: onPressed,
|
||||||
child: Row(
|
child: Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
padding: EdgeInsets.fromLTRB(10, 10, 5, 10),
|
||||||
children: <Widget>[
|
child: Row(
|
||||||
site.managed
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
|
children: <Widget>[
|
||||||
: Container(),
|
site.managed
|
||||||
Expanded(child: Text(site.name, style: TextStyle(fontWeight: FontWeight.bold))),
|
? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
|
||||||
Padding(padding: EdgeInsets.only(right: 10)),
|
: Container(),
|
||||||
Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18)
|
Expanded(child: Text(site.name, style: TextStyle(fontWeight: FontWeight.bold))),
|
||||||
],
|
Padding(padding: EdgeInsets.only(right: 10)),
|
||||||
)));
|
Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,17 +14,17 @@ class SiteTitle extends StatelessWidget {
|
||||||
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
|
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
|
||||||
|
|
||||||
return IntrinsicWidth(
|
return IntrinsicWidth(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Row(children: [
|
child: Row(
|
||||||
site.managed
|
children: [
|
||||||
? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
|
site.managed
|
||||||
: Container(),
|
? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
|
||||||
Expanded(
|
: Container(),
|
||||||
child: Text(
|
Expanded(child: Text(site.name, overflow: TextOverflow.ellipsis)),
|
||||||
site.name,
|
],
|
||||||
overflow: TextOverflow.ellipsis,
|
),
|
||||||
))
|
),
|
||||||
])));
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import 'package:flutter/material.dart';
|
||||||
// This is a button that pushes the bare minimum onto you, it doesn't even respect button themes - unless you tell it to
|
// This is a button that pushes the bare minimum onto you, it doesn't even respect button themes - unless you tell it to
|
||||||
class SpecialButton extends StatefulWidget {
|
class SpecialButton extends StatefulWidget {
|
||||||
const SpecialButton({Key? key, this.child, this.color, this.onPressed, this.useButtonTheme = false, this.decoration})
|
const SpecialButton({Key? key, this.child, this.color, this.onPressed, this.useButtonTheme = false, this.decoration})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
final Color? color;
|
final Color? color;
|
||||||
|
@ -32,14 +32,13 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
textStyle: textStyle,
|
textStyle: textStyle,
|
||||||
child: Ink(
|
child: Ink(
|
||||||
decoration: widget.decoration,
|
decoration: widget.decoration,
|
||||||
color: widget.color,
|
color: widget.color,
|
||||||
child: InkWell(
|
child: InkWell(child: widget.child, onTap: widget.onPressed),
|
||||||
child: widget.child,
|
),
|
||||||
onTap: widget.onPressed,
|
);
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGeneric() {
|
Widget _buildGeneric() {
|
||||||
|
@ -49,21 +48,22 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: widget.decoration,
|
decoration: widget.decoration,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTapDown: _handleTapDown,
|
onTapDown: _handleTapDown,
|
||||||
onTapUp: _handleTapUp,
|
onTapUp: _handleTapUp,
|
||||||
onTapCancel: _handleTapCancel,
|
onTapCancel: _handleTapCancel,
|
||||||
onTap: widget.onPressed,
|
onTap: widget.onPressed,
|
||||||
child: Semantics(
|
child: Semantics(
|
||||||
button: true,
|
button: true,
|
||||||
child: FadeTransition(
|
child: FadeTransition(
|
||||||
opacity: _opacityAnimation!,
|
opacity: _opacityAnimation!,
|
||||||
child: DefaultTextStyle(style: textStyle, child: Container(child: widget.child, color: widget.color)),
|
child: DefaultTextStyle(style: textStyle, child: Container(child: widget.child, color: widget.color)),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eyeballed values. Feel free to tweak.
|
// Eyeballed values. Feel free to tweak.
|
||||||
|
@ -77,11 +77,7 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_animationController = AnimationController(
|
_animationController = AnimationController(duration: const Duration(milliseconds: 200), value: 0.0, vsync: this);
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
value: 0.0,
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_opacityAnimation = _animationController!.drive(CurveTween(curve: Curves.decelerate)).drive(_opacityTween);
|
_opacityAnimation = _animationController!.drive(CurveTween(curve: Curves.decelerate)).drive(_opacityTween);
|
||||||
_setTween();
|
_setTween();
|
||||||
}
|
}
|
||||||
|
@ -131,9 +127,10 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool wasHeldDown = _buttonHeldDown;
|
final bool wasHeldDown = _buttonHeldDown;
|
||||||
final TickerFuture ticker = _buttonHeldDown
|
final TickerFuture ticker =
|
||||||
? _animationController!.animateTo(1.0, duration: kFadeOutDuration)
|
_buttonHeldDown
|
||||||
: _animationController!.animateTo(0.0, duration: kFadeInDuration);
|
? _animationController!.animateTo(1.0, duration: kFadeOutDuration)
|
||||||
|
: _animationController!.animateTo(0.0, duration: kFadeInDuration);
|
||||||
|
|
||||||
ticker.then<void>((void value) {
|
ticker.then<void>((void value) {
|
||||||
if (mounted && wasHeldDown != _buttonHeldDown) {
|
if (mounted && wasHeldDown != _buttonHeldDown) {
|
||||||
|
|
|
@ -4,31 +4,31 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||||
|
|
||||||
/// A normal TextField or CupertinoTextField that looks the same on all platforms
|
/// A normal TextField or CupertinoTextField that looks the same on all platforms
|
||||||
class SpecialTextField extends StatefulWidget {
|
class SpecialTextField extends StatefulWidget {
|
||||||
const SpecialTextField(
|
const SpecialTextField({
|
||||||
{Key? key,
|
Key? key,
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.suffix,
|
this.suffix,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
this.nextFocusNode,
|
this.nextFocusNode,
|
||||||
this.autocorrect,
|
this.autocorrect,
|
||||||
this.minLines,
|
this.minLines,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
this.maxLength,
|
this.maxLength,
|
||||||
this.maxLengthEnforcement,
|
this.maxLengthEnforcement,
|
||||||
this.style,
|
this.style,
|
||||||
this.keyboardType,
|
this.keyboardType,
|
||||||
this.textInputAction,
|
this.textInputAction,
|
||||||
this.textCapitalization,
|
this.textCapitalization,
|
||||||
this.textAlign,
|
this.textAlign,
|
||||||
this.autofocus,
|
this.autofocus,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.enabled,
|
this.enabled,
|
||||||
this.expands,
|
this.expands,
|
||||||
this.keyboardAppearance,
|
this.keyboardAppearance,
|
||||||
this.textAlignVertical,
|
this.textAlignVertical,
|
||||||
this.inputFormatters})
|
this.inputFormatters,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final String? placeholder;
|
final String? placeholder;
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
|
@ -76,42 +76,48 @@ class _SpecialTextFieldState extends State<SpecialTextField> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PlatformTextField(
|
return PlatformTextField(
|
||||||
autocorrect: widget.autocorrect,
|
autocorrect: widget.autocorrect,
|
||||||
minLines: widget.minLines,
|
minLines: widget.minLines,
|
||||||
maxLines: widget.maxLines,
|
maxLines: widget.maxLines,
|
||||||
maxLength: widget.maxLength,
|
maxLength: widget.maxLength,
|
||||||
maxLengthEnforcement: widget.maxLengthEnforcement,
|
maxLengthEnforcement: widget.maxLengthEnforcement,
|
||||||
keyboardType: widget.keyboardType,
|
keyboardType: widget.keyboardType,
|
||||||
keyboardAppearance: widget.keyboardAppearance,
|
keyboardAppearance: widget.keyboardAppearance,
|
||||||
textInputAction: widget.textInputAction,
|
textInputAction: widget.textInputAction,
|
||||||
textCapitalization: widget.textCapitalization,
|
textCapitalization: widget.textCapitalization,
|
||||||
textAlign: widget.textAlign,
|
textAlign: widget.textAlign,
|
||||||
textAlignVertical: widget.textAlignVertical,
|
textAlignVertical: widget.textAlignVertical,
|
||||||
autofocus: widget.autofocus,
|
autofocus: widget.autofocus,
|
||||||
focusNode: widget.focusNode,
|
focusNode: widget.focusNode,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
enabled: widget.enabled ?? true,
|
enabled: widget.enabled ?? true,
|
||||||
onSubmitted: (_) {
|
onSubmitted: (_) {
|
||||||
if (widget.nextFocusNode != null) {
|
if (widget.nextFocusNode != null) {
|
||||||
FocusScope.of(context).requestFocus(widget.nextFocusNode);
|
FocusScope.of(context).requestFocus(widget.nextFocusNode);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
expands: widget.expands,
|
expands: widget.expands,
|
||||||
inputFormatters: formatters,
|
inputFormatters: formatters,
|
||||||
material: (_, __) => MaterialTextFieldData(
|
material:
|
||||||
|
(_, __) => MaterialTextFieldData(
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
hintText: widget.placeholder,
|
hintText: widget.placeholder,
|
||||||
counterText: '',
|
counterText: '',
|
||||||
suffix: widget.suffix)),
|
suffix: widget.suffix,
|
||||||
cupertino: (_, __) => CupertinoTextFieldData(
|
),
|
||||||
|
),
|
||||||
|
cupertino:
|
||||||
|
(_, __) => CupertinoTextFieldData(
|
||||||
decoration: BoxDecoration(),
|
decoration: BoxDecoration(),
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
placeholder: widget.placeholder,
|
placeholder: widget.placeholder,
|
||||||
suffix: widget.suffix),
|
suffix: widget.suffix,
|
||||||
style: widget.style,
|
),
|
||||||
controller: widget.controller);
|
style: widget.style,
|
||||||
|
controller: widget.controller,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,16 +13,21 @@ class PrimaryButton extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
return FilledButton(
|
return FilledButton(
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: child,
|
child: child,
|
||||||
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.primary));
|
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.primary),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Workaround for https://github.com/flutter/flutter/issues/161590
|
// Workaround for https://github.com/flutter/flutter/issues/161590
|
||||||
final themeData = CupertinoTheme.of(context);
|
final themeData = CupertinoTheme.of(context);
|
||||||
return CupertinoTheme(
|
return CupertinoTheme(
|
||||||
data: themeData.copyWith(primaryColor: CupertinoColors.white),
|
data: themeData.copyWith(primaryColor: CupertinoColors.white),
|
||||||
child: CupertinoButton(
|
child: CupertinoButton(
|
||||||
child: child, onPressed: onPressed, color: CupertinoColors.secondaryLabel.resolveFrom(context)));
|
child: child,
|
||||||
|
onPressed: onPressed,
|
||||||
|
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,13 @@ class ConfigButtonItem extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SpecialButton(
|
return SpecialButton(
|
||||||
color: Utils.configItemBackground(context),
|
color: Utils.configItemBackground(context),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
useButtonTheme: true,
|
useButtonTheme: true,
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
|
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
|
||||||
child: Center(child: content),
|
child: Center(child: content),
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,14 @@ import 'package:mobile_nebula/components/SpecialButton.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
class ConfigCheckboxItem extends StatelessWidget {
|
class ConfigCheckboxItem extends StatelessWidget {
|
||||||
const ConfigCheckboxItem(
|
const ConfigCheckboxItem({
|
||||||
{Key? key, this.label, this.content, this.labelWidth = 100, this.onChanged, this.checked = false})
|
Key? key,
|
||||||
: super(key: key);
|
this.label,
|
||||||
|
this.content,
|
||||||
|
this.labelWidth = 100,
|
||||||
|
this.onChanged,
|
||||||
|
this.checked = false,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final Widget? label;
|
final Widget? label;
|
||||||
final Widget? content;
|
final Widget? content;
|
||||||
|
@ -16,18 +21,19 @@ class ConfigCheckboxItem extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget item = Container(
|
Widget item = Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||||
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
|
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
label != null ? Container(width: labelWidth, child: label) : Container(),
|
label != null ? Container(width: labelWidth, child: label) : Container(),
|
||||||
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
|
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
|
||||||
checked
|
checked
|
||||||
? Icon(CupertinoIcons.check_mark, color: CupertinoColors.systemBlue.resolveFrom(context))
|
? Icon(CupertinoIcons.check_mark, color: CupertinoColors.systemBlue.resolveFrom(context))
|
||||||
: Container()
|
: Container(),
|
||||||
],
|
],
|
||||||
));
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (onChanged != null) {
|
if (onChanged != null) {
|
||||||
return SpecialButton(
|
return SpecialButton(
|
||||||
|
|
|
@ -20,10 +20,9 @@ class ConfigHeader extends StatelessWidget {
|
||||||
padding: const EdgeInsets.only(left: 10.0, top: 30.0, bottom: 5.0, right: 10.0),
|
padding: const EdgeInsets.only(left: 10.0, top: 30.0, bottom: 5.0, right: 10.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: basicTextStyle(context).copyWith(
|
style: basicTextStyle(
|
||||||
color: color ?? CupertinoColors.secondaryLabel.resolveFrom(context),
|
context,
|
||||||
fontSize: _headerFontSize,
|
).copyWith(color: color ?? CupertinoColors.secondaryLabel.resolveFrom(context), fontSize: _headerFontSize),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
class ConfigItem extends StatelessWidget {
|
class ConfigItem extends StatelessWidget {
|
||||||
const ConfigItem(
|
const ConfigItem({
|
||||||
{Key? key,
|
Key? key,
|
||||||
this.label,
|
this.label,
|
||||||
required this.content,
|
required this.content,
|
||||||
this.labelWidth = 100,
|
this.labelWidth = 100,
|
||||||
this.crossAxisAlignment = CrossAxisAlignment.center})
|
this.crossAxisAlignment = CrossAxisAlignment.center,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Widget? label;
|
final Widget? label;
|
||||||
final Widget content;
|
final Widget content;
|
||||||
|
@ -28,15 +28,16 @@ class ConfigItem extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
color: Utils.configItemBackground(context),
|
color: Utils.configItemBackground(context),
|
||||||
padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15),
|
padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15),
|
||||||
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize),
|
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: crossAxisAlignment,
|
crossAxisAlignment: crossAxisAlignment,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Container(width: labelWidth, child: DefaultTextStyle(style: textStyle, child: Container(child: label))),
|
Container(width: labelWidth, child: DefaultTextStyle(style: textStyle, child: Container(child: label))),
|
||||||
Expanded(child: DefaultTextStyle(style: textStyle, child: Container(child: content))),
|
Expanded(child: DefaultTextStyle(style: textStyle, child: Container(child: content))),
|
||||||
],
|
],
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,15 @@ import 'package:mobile_nebula/components/SpecialButton.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
class ConfigPageItem extends StatelessWidget {
|
class ConfigPageItem extends StatelessWidget {
|
||||||
const ConfigPageItem(
|
const ConfigPageItem({
|
||||||
{Key? key,
|
Key? key,
|
||||||
this.label,
|
this.label,
|
||||||
this.content,
|
this.content,
|
||||||
this.labelWidth = 100,
|
this.labelWidth = 100,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.disabled = false,
|
this.disabled = false,
|
||||||
this.crossAxisAlignment = CrossAxisAlignment.center})
|
this.crossAxisAlignment = CrossAxisAlignment.center,
|
||||||
: super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
final Widget? label;
|
final Widget? label;
|
||||||
final Widget? content;
|
final Widget? content;
|
||||||
|
@ -30,8 +30,10 @@ class ConfigPageItem extends StatelessWidget {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
final origTheme = Theme.of(context);
|
final origTheme = Theme.of(context);
|
||||||
theme = origTheme.copyWith(
|
theme = origTheme.copyWith(
|
||||||
textTheme: origTheme.textTheme
|
textTheme: origTheme.textTheme.copyWith(
|
||||||
.copyWith(labelLarge: origTheme.textTheme.labelLarge!.copyWith(fontWeight: FontWeight.normal)));
|
labelLarge: origTheme.textTheme.labelLarge!.copyWith(fontWeight: FontWeight.normal),
|
||||||
|
),
|
||||||
|
);
|
||||||
return Theme(data: theme, child: _buildContent(context));
|
return Theme(data: theme, child: _buildContent(context));
|
||||||
} else {
|
} else {
|
||||||
final origTheme = CupertinoTheme.of(context);
|
final origTheme = CupertinoTheme.of(context);
|
||||||
|
@ -45,18 +47,19 @@ class ConfigPageItem extends StatelessWidget {
|
||||||
onPressed: this.disabled ? null : onPressed,
|
onPressed: this.disabled ? null : onPressed,
|
||||||
color: Utils.configItemBackground(context),
|
color: Utils.configItemBackground(context),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15),
|
padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15),
|
||||||
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
|
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: crossAxisAlignment,
|
crossAxisAlignment: crossAxisAlignment,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
label != null ? Container(width: labelWidth, child: label) : Container(),
|
label != null ? Container(width: labelWidth, child: label) : Container(),
|
||||||
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
|
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
|
||||||
this.disabled
|
this.disabled
|
||||||
? Container()
|
? Container()
|
||||||
: Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18)
|
: Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18),
|
||||||
],
|
],
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'ConfigHeader.dart';
|
||||||
|
|
||||||
class ConfigSection extends StatelessWidget {
|
class ConfigSection extends StatelessWidget {
|
||||||
const ConfigSection({Key? key, this.label, required this.children, this.borderColor, this.labelColor})
|
const ConfigSection({Key? key, this.label, required this.children, this.borderColor, this.labelColor})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
final List<Widget> children;
|
final List<Widget> children;
|
||||||
final String? label;
|
final String? label;
|
||||||
|
@ -27,19 +27,27 @@ class ConfigSection extends StatelessWidget {
|
||||||
if (children[i + 1].runtimeType.toString() == 'ConfigButtonItem') {
|
if (children[i + 1].runtimeType.toString() == 'ConfigButtonItem') {
|
||||||
pad = 0;
|
pad = 0;
|
||||||
}
|
}
|
||||||
_children.add(Padding(
|
_children.add(
|
||||||
child: Divider(height: 1, color: Utils.configSectionBorder(context)), padding: EdgeInsets.only(left: pad)));
|
Padding(
|
||||||
|
child: Divider(height: 1, color: Utils.configSectionBorder(context)),
|
||||||
|
padding: EdgeInsets.only(left: pad),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
return Column(
|
||||||
label != null ? ConfigHeader(label: label!, color: labelColor) : Container(height: 20),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Container(
|
children: [
|
||||||
decoration:
|
label != null ? ConfigHeader(label: label!, color: labelColor) : Container(height: 20),
|
||||||
BoxDecoration(border: Border(top: border, bottom: border), color: Utils.configItemBackground(context)),
|
Container(
|
||||||
child: Column(
|
decoration: BoxDecoration(
|
||||||
children: _children,
|
border: Border(top: border, bottom: border),
|
||||||
))
|
color: Utils.configItemBackground(context),
|
||||||
]);
|
),
|
||||||
|
child: Column(children: _children),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
class ConfigTextItem extends StatelessWidget {
|
class ConfigTextItem extends StatelessWidget {
|
||||||
const ConfigTextItem(
|
const ConfigTextItem({
|
||||||
{Key? key, this.placeholder, this.controller, this.style = const TextStyle(fontFamily: 'RobotoMono')})
|
Key? key,
|
||||||
: super(key: key);
|
this.placeholder,
|
||||||
|
this.controller,
|
||||||
|
this.style = const TextStyle(fontFamily: 'RobotoMono'),
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
final String? placeholder;
|
final String? placeholder;
|
||||||
final TextEditingController? controller;
|
final TextEditingController? controller;
|
||||||
|
|
|
@ -19,16 +19,13 @@ Future<void> main() async {
|
||||||
|
|
||||||
var settings = Settings();
|
var settings = Settings();
|
||||||
if (settings.trackErrors) {
|
if (settings.trackErrors) {
|
||||||
await SentryFlutter.init(
|
await SentryFlutter.init((options) {
|
||||||
(options) {
|
options.dsn = 'https://96106df405ade3f013187dfc8e4200e7@o920269.ingest.us.sentry.io/4508132321001472';
|
||||||
options.dsn = 'https://96106df405ade3f013187dfc8e4200e7@o920269.ingest.us.sentry.io/4508132321001472';
|
// Capture all traces. May need to adjust if overwhelming
|
||||||
// Capture all traces. May need to adjust if overwhelming
|
options.tracesSampleRate = 1.0;
|
||||||
options.tracesSampleRate = 1.0;
|
// For each trace, capture all profiles
|
||||||
// For each trace, capture all profiles
|
options.profilesSampleRate = 1.0;
|
||||||
options.profilesSampleRate = 1.0;
|
}, appRunner: () => runApp(Main()));
|
||||||
},
|
|
||||||
appRunner: () => runApp(Main()),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
runApp(Main());
|
runApp(Main());
|
||||||
}
|
}
|
||||||
|
@ -91,41 +88,41 @@ class _AppState extends State<App> {
|
||||||
|
|
||||||
return PlatformProvider(
|
return PlatformProvider(
|
||||||
settings: PlatformSettingsData(iosUsesMaterialWidgets: true),
|
settings: PlatformSettingsData(iosUsesMaterialWidgets: true),
|
||||||
builder: (context) => PlatformApp(
|
builder:
|
||||||
debugShowCheckedModeBanner: false,
|
(context) => PlatformApp(
|
||||||
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
|
debugShowCheckedModeBanner: false,
|
||||||
DefaultMaterialLocalizations.delegate,
|
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
|
||||||
DefaultWidgetsLocalizations.delegate,
|
DefaultMaterialLocalizations.delegate,
|
||||||
DefaultCupertinoLocalizations.delegate,
|
DefaultWidgetsLocalizations.delegate,
|
||||||
],
|
DefaultCupertinoLocalizations.delegate,
|
||||||
title: 'Nebula',
|
],
|
||||||
material: (_, __) {
|
title: 'Nebula',
|
||||||
return new MaterialAppData(
|
material: (_, __) {
|
||||||
themeMode: brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark,
|
return new MaterialAppData(
|
||||||
theme: brightness == Brightness.light ? theme.light() : theme.dark(),
|
themeMode: brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark,
|
||||||
);
|
theme: brightness == Brightness.light ? theme.light() : theme.dark(),
|
||||||
},
|
);
|
||||||
cupertino: (_, __) => CupertinoAppData(
|
},
|
||||||
theme: CupertinoThemeData(brightness: brightness),
|
cupertino: (_, __) => CupertinoAppData(theme: CupertinoThemeData(brightness: brightness)),
|
||||||
),
|
onGenerateRoute: (settings) {
|
||||||
onGenerateRoute: (settings) {
|
if (settings.name == '/') {
|
||||||
if (settings.name == '/') {
|
return platformPageRoute(context: context, builder: (context) => MainScreen(this.dnEnrolled));
|
||||||
return platformPageRoute(context: context, builder: (context) => MainScreen(this.dnEnrolled));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
final uri = Uri.parse(settings.name!);
|
final uri = Uri.parse(settings.name!);
|
||||||
if (uri.path == EnrollmentScreen.routeName) {
|
if (uri.path == EnrollmentScreen.routeName) {
|
||||||
// TODO: maybe implement this as a dialog instead of a page, you can stack multiple enrollment screens which is annoying in dev
|
// TODO: maybe implement this as a dialog instead of a page, you can stack multiple enrollment screens which is annoying in dev
|
||||||
return platformPageRoute(
|
return platformPageRoute(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) =>
|
builder:
|
||||||
EnrollmentScreen(code: EnrollmentScreen.parseCode(settings.name!), stream: this.dnEnrolled),
|
(context) =>
|
||||||
);
|
EnrollmentScreen(code: EnrollmentScreen.parseCode(settings.name!), stream: this.dnEnrolled),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,9 +19,6 @@ class CIDR {
|
||||||
throw 'Invalid CIDR string';
|
throw 'Invalid CIDR string';
|
||||||
}
|
}
|
||||||
|
|
||||||
return CIDR(
|
return CIDR(ip: parts[0], bits: int.parse(parts[1]));
|
||||||
ip: parts[0],
|
|
||||||
bits: int.parse(parts[1]),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,13 @@ class CertificateInfo {
|
||||||
CertificateValidity? validity;
|
CertificateValidity? validity;
|
||||||
|
|
||||||
CertificateInfo.debug({this.rawCert = ""})
|
CertificateInfo.debug({this.rawCert = ""})
|
||||||
: this.cert = Certificate.debug(),
|
: this.cert = Certificate.debug(),
|
||||||
this.validity = CertificateValidity.debug();
|
this.validity = CertificateValidity.debug();
|
||||||
|
|
||||||
CertificateInfo.fromJson(Map<String, dynamic> json)
|
CertificateInfo.fromJson(Map<String, dynamic> json)
|
||||||
: cert = Certificate.fromJson(json['Cert']),
|
: cert = Certificate.fromJson(json['Cert']),
|
||||||
rawCert = json['RawCert'],
|
rawCert = json['RawCert'],
|
||||||
validity = CertificateValidity.fromJson(json['Validity']);
|
validity = CertificateValidity.fromJson(json['Validity']);
|
||||||
|
|
||||||
CertificateInfo({required this.cert, this.rawCert, this.validity});
|
CertificateInfo({required this.cert, this.rawCert, this.validity});
|
||||||
|
|
||||||
|
@ -24,15 +24,12 @@ class Certificate {
|
||||||
String fingerprint;
|
String fingerprint;
|
||||||
String signature;
|
String signature;
|
||||||
|
|
||||||
Certificate.debug()
|
Certificate.debug() : this.details = CertificateDetails.debug(), this.fingerprint = "DEBUG", this.signature = "DEBUG";
|
||||||
: this.details = CertificateDetails.debug(),
|
|
||||||
this.fingerprint = "DEBUG",
|
|
||||||
this.signature = "DEBUG";
|
|
||||||
|
|
||||||
Certificate.fromJson(Map<String, dynamic> json)
|
Certificate.fromJson(Map<String, dynamic> json)
|
||||||
: details = CertificateDetails.fromJson(json['details']),
|
: details = CertificateDetails.fromJson(json['details']),
|
||||||
fingerprint = json['fingerprint'],
|
fingerprint = json['fingerprint'],
|
||||||
signature = json['signature'];
|
signature = json['signature'];
|
||||||
}
|
}
|
||||||
|
|
||||||
class CertificateDetails {
|
class CertificateDetails {
|
||||||
|
@ -47,37 +44,33 @@ class CertificateDetails {
|
||||||
String issuer;
|
String issuer;
|
||||||
|
|
||||||
CertificateDetails.debug()
|
CertificateDetails.debug()
|
||||||
: this.name = "DEBUG",
|
: this.name = "DEBUG",
|
||||||
notBefore = DateTime.now(),
|
notBefore = DateTime.now(),
|
||||||
notAfter = DateTime.now(),
|
notAfter = DateTime.now(),
|
||||||
publicKey = "",
|
publicKey = "",
|
||||||
groups = [],
|
groups = [],
|
||||||
ips = [],
|
ips = [],
|
||||||
subnets = [],
|
subnets = [],
|
||||||
isCa = false,
|
isCa = false,
|
||||||
issuer = "DEBUG";
|
issuer = "DEBUG";
|
||||||
|
|
||||||
CertificateDetails.fromJson(Map<String, dynamic> json)
|
CertificateDetails.fromJson(Map<String, dynamic> json)
|
||||||
: name = json['name'],
|
: name = json['name'],
|
||||||
notBefore = DateTime.parse(json['notBefore']),
|
notBefore = DateTime.parse(json['notBefore']),
|
||||||
notAfter = DateTime.parse(json['notAfter']),
|
notAfter = DateTime.parse(json['notAfter']),
|
||||||
publicKey = json['publicKey'],
|
publicKey = json['publicKey'],
|
||||||
groups = List<String>.from(json['groups']),
|
groups = List<String>.from(json['groups']),
|
||||||
ips = List<String>.from(json['ips']),
|
ips = List<String>.from(json['ips']),
|
||||||
subnets = List<String>.from(json['subnets']),
|
subnets = List<String>.from(json['subnets']),
|
||||||
isCa = json['isCa'],
|
isCa = json['isCa'],
|
||||||
issuer = json['issuer'];
|
issuer = json['issuer'];
|
||||||
}
|
}
|
||||||
|
|
||||||
class CertificateValidity {
|
class CertificateValidity {
|
||||||
bool valid;
|
bool valid;
|
||||||
String reason;
|
String reason;
|
||||||
|
|
||||||
CertificateValidity.debug()
|
CertificateValidity.debug() : this.valid = true, this.reason = "";
|
||||||
: this.valid = true,
|
|
||||||
this.reason = "";
|
|
||||||
|
|
||||||
CertificateValidity.fromJson(Map<String, dynamic> json)
|
CertificateValidity.fromJson(Map<String, dynamic> json) : valid = json['Valid'], reason = json['Reason'];
|
||||||
: valid = json['Valid'],
|
|
||||||
reason = json['Reason'];
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,10 +52,7 @@ class UDPAddress {
|
||||||
String ip;
|
String ip;
|
||||||
int port;
|
int port;
|
||||||
|
|
||||||
UDPAddress({
|
UDPAddress({required this.ip, required this.port});
|
||||||
required this.ip,
|
|
||||||
required this.port,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|
|
@ -21,9 +21,6 @@ class IPAndPort {
|
||||||
//TODO: Uri.parse is as close as I could get to parsing both ipv4 and v6 addresses with a port without bringing a whole mess of code into here
|
//TODO: Uri.parse is as close as I could get to parsing both ipv4 and v6 addresses with a port without bringing a whole mess of code into here
|
||||||
final uri = Uri.parse("ugh://$val");
|
final uri = Uri.parse("ugh://$val");
|
||||||
|
|
||||||
return IPAndPort(
|
return IPAndPort(ip: uri.host, port: uri.port);
|
||||||
ip: uri.host,
|
|
||||||
port: uri.port,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,19 +94,22 @@ class Site {
|
||||||
this.lastManagedUpdate = lastManagedUpdate;
|
this.lastManagedUpdate = lastManagedUpdate;
|
||||||
|
|
||||||
_updates = EventChannel('net.defined.nebula/${this.id}');
|
_updates = EventChannel('net.defined.nebula/${this.id}');
|
||||||
_updates.receiveBroadcastStream().listen((d) {
|
_updates.receiveBroadcastStream().listen(
|
||||||
try {
|
(d) {
|
||||||
_updateFromJson(d);
|
try {
|
||||||
_change.add(null);
|
_updateFromJson(d);
|
||||||
} catch (err) {
|
_change.add(null);
|
||||||
//TODO: handle the error
|
} catch (err) {
|
||||||
print(err);
|
//TODO: handle the error
|
||||||
}
|
print(err);
|
||||||
}, onError: (err) {
|
}
|
||||||
_updateFromJson(err.details);
|
},
|
||||||
var error = err as PlatformException;
|
onError: (err) {
|
||||||
_change.addError(error.message ?? 'An unexpected error occurred');
|
_updateFromJson(err.details);
|
||||||
});
|
var error = err as PlatformException;
|
||||||
|
_change.addError(error.message ?? 'An unexpected error occurred');
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory Site.fromJson(Map<String, dynamic> json) {
|
factory Site.fromJson(Map<String, dynamic> json) {
|
||||||
|
@ -220,9 +223,11 @@ class Site {
|
||||||
'id': id,
|
'id': id,
|
||||||
'staticHostmap': staticHostmap,
|
'staticHostmap': staticHostmap,
|
||||||
'unsafeRoutes': unsafeRoutes,
|
'unsafeRoutes': unsafeRoutes,
|
||||||
'ca': ca.map((cert) {
|
'ca': ca
|
||||||
return cert.rawCert;
|
.map((cert) {
|
||||||
}).join('\n'),
|
return cert.rawCert;
|
||||||
|
})
|
||||||
|
.join('\n'),
|
||||||
'cert': certInfo?.rawCert,
|
'cert': certInfo?.rawCert,
|
||||||
'key': key,
|
'key': key,
|
||||||
'lhDuration': lhDuration,
|
'lhDuration': lhDuration,
|
||||||
|
@ -341,8 +346,11 @@ class Site {
|
||||||
|
|
||||||
Future<HostInfo?> getHostInfo(String vpnIp, bool pending) async {
|
Future<HostInfo?> getHostInfo(String vpnIp, bool pending) async {
|
||||||
try {
|
try {
|
||||||
var ret = await platform
|
var ret = await platform.invokeMethod("active.getHostInfo", <String, dynamic>{
|
||||||
.invokeMethod("active.getHostInfo", <String, dynamic>{"id": id, "vpnIp": vpnIp, "pending": pending});
|
"id": id,
|
||||||
|
"vpnIp": vpnIp,
|
||||||
|
"pending": pending,
|
||||||
|
});
|
||||||
final h = jsonDecode(ret);
|
final h = jsonDecode(ret);
|
||||||
if (h == null) {
|
if (h == null) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -358,8 +366,11 @@ class Site {
|
||||||
|
|
||||||
Future<HostInfo?> setRemoteForTunnel(String vpnIp, String addr) async {
|
Future<HostInfo?> setRemoteForTunnel(String vpnIp, String addr) async {
|
||||||
try {
|
try {
|
||||||
var ret = await platform
|
var ret = await platform.invokeMethod("active.setRemoteForTunnel", <String, dynamic>{
|
||||||
.invokeMethod("active.setRemoteForTunnel", <String, dynamic>{"id": id, "vpnIp": vpnIp, "addr": addr});
|
"id": id,
|
||||||
|
"vpnIp": vpnIp,
|
||||||
|
"addr": addr,
|
||||||
|
});
|
||||||
final h = jsonDecode(ret);
|
final h = jsonDecode(ret);
|
||||||
if (h == null) {
|
if (h == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -14,16 +14,10 @@ class StaticHost {
|
||||||
result.add(IPAndPort.fromString(item));
|
result.add(IPAndPort.fromString(item));
|
||||||
});
|
});
|
||||||
|
|
||||||
return StaticHost(
|
return StaticHost(lighthouse: json['lighthouse'], destinations: result);
|
||||||
lighthouse: json['lighthouse'],
|
|
||||||
destinations: result,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {'lighthouse': lighthouse, 'destinations': destinations};
|
||||||
'lighthouse': lighthouse,
|
|
||||||
'destinations': destinations,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,10 @@ class UnsafeRoute {
|
||||||
UnsafeRoute({this.route, this.via});
|
UnsafeRoute({this.route, this.via});
|
||||||
|
|
||||||
factory UnsafeRoute.fromJson(Map<String, dynamic> json) {
|
factory UnsafeRoute.fromJson(Map<String, dynamic> json) {
|
||||||
return UnsafeRoute(
|
return UnsafeRoute(route: json['route'], via: json['via']);
|
||||||
route: json['route'],
|
|
||||||
via: json['via'],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {'route': route, 'via': via};
|
||||||
'route': route,
|
|
||||||
'via': via,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,52 +37,67 @@ class _AboutScreenState extends State<AboutScreen> {
|
||||||
// packageInfo is null until ready is true
|
// packageInfo is null until ready is true
|
||||||
if (!ready) {
|
if (!ready) {
|
||||||
return Center(
|
return Center(
|
||||||
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
|
child: PlatformCircularProgressIndicator(
|
||||||
return CupertinoProgressIndicatorData(radius: 50);
|
cupertino: (_, __) {
|
||||||
}),
|
return CupertinoProgressIndicatorData(radius: 50);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SimplePage(
|
return SimplePage(
|
||||||
title: Text('About'),
|
title: Text('About'),
|
||||||
child: Column(children: [
|
child: Column(
|
||||||
ConfigSection(children: <Widget>[
|
children: [
|
||||||
ConfigItem(
|
ConfigSection(
|
||||||
label: Text('App version'),
|
children: <Widget>[
|
||||||
labelWidth: 150,
|
ConfigItem(
|
||||||
content: _buildText('${packageInfo!.version}-${packageInfo!.buildNumber} (sha: $gitSha)')),
|
label: Text('App version'),
|
||||||
ConfigItem(
|
labelWidth: 150,
|
||||||
label: Text('Nebula version'), labelWidth: 150, content: _buildText('$nebulaVersion ($goVersion)')),
|
content: _buildText('${packageInfo!.version}-${packageInfo!.buildNumber} (sha: $gitSha)'),
|
||||||
ConfigItem(
|
),
|
||||||
label: Text('Flutter version'),
|
ConfigItem(
|
||||||
labelWidth: 150,
|
label: Text('Nebula version'),
|
||||||
content: _buildText(flutterVersion['frameworkVersion'] ?? 'Unknown')),
|
labelWidth: 150,
|
||||||
ConfigItem(
|
content: _buildText('$nebulaVersion ($goVersion)'),
|
||||||
label: Text('Dart version'),
|
),
|
||||||
labelWidth: 150,
|
ConfigItem(
|
||||||
content: _buildText(flutterVersion['dartSdkVersion'] ?? 'Unknown')),
|
label: Text('Flutter version'),
|
||||||
]),
|
labelWidth: 150,
|
||||||
ConfigSection(children: <Widget>[
|
content: _buildText(flutterVersion['frameworkVersion'] ?? 'Unknown'),
|
||||||
//TODO: wire up these other pages
|
),
|
||||||
// ConfigPageItem(label: Text('Changelog'), labelWidth: 300, onPressed: () => Utils.launchUrl('https://defined.net/mobile/changelog', context)),
|
ConfigItem(
|
||||||
ConfigPageItem(
|
label: Text('Dart version'),
|
||||||
label: Text('Privacy policy'),
|
labelWidth: 150,
|
||||||
labelWidth: 300,
|
content: _buildText(flutterVersion['dartSdkVersion'] ?? 'Unknown'),
|
||||||
onPressed: () => Utils.launchUrl('https://www.defined.net/privacy/', context)),
|
),
|
||||||
ConfigPageItem(
|
],
|
||||||
label: Text('Licenses'),
|
),
|
||||||
labelWidth: 300,
|
ConfigSection(
|
||||||
onPressed: () => Utils.openPage(context, (context) {
|
children: <Widget>[
|
||||||
return LicensesScreen();
|
//TODO: wire up these other pages
|
||||||
})),
|
// ConfigPageItem(label: Text('Changelog'), labelWidth: 300, onPressed: () => Utils.launchUrl('https://defined.net/mobile/changelog', context)),
|
||||||
]),
|
ConfigPageItem(
|
||||||
Padding(
|
label: Text('Privacy policy'),
|
||||||
|
labelWidth: 300,
|
||||||
|
onPressed: () => Utils.launchUrl('https://www.defined.net/privacy/', context),
|
||||||
|
),
|
||||||
|
ConfigPageItem(
|
||||||
|
label: Text('Licenses'),
|
||||||
|
labelWidth: 300,
|
||||||
|
onPressed:
|
||||||
|
() => Utils.openPage(context, (context) {
|
||||||
|
return LicensesScreen();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
padding: EdgeInsets.only(top: 20),
|
padding: EdgeInsets.only(top: 20),
|
||||||
child: Text(
|
child: Text('Copyright © 2024 Defined Networking, Inc', textAlign: TextAlign.center),
|
||||||
'Copyright © 2024 Defined Networking, Inc',
|
),
|
||||||
textAlign: TextAlign.center,
|
],
|
||||||
)),
|
),
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -94,65 +94,78 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
|
||||||
} else {
|
} else {
|
||||||
// No code, show the error
|
// No code, show the error
|
||||||
child = Padding(
|
child = Padding(
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No valid enrollment code was found.\n\nContact your administrator to obtain a new enrollment code.',
|
'No valid enrollment code was found.\n\nContact your administrator to obtain a new enrollment code.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
)),
|
),
|
||||||
padding: EdgeInsets.only(top: 20));
|
),
|
||||||
|
padding: EdgeInsets.only(top: 20),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else if (this.error != null) {
|
} else if (this.error != null) {
|
||||||
// Error while enrolling, display it
|
// Error while enrolling, display it
|
||||||
child = Center(
|
child = Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
child: SelectableText(
|
child: SelectableText(
|
||||||
'There was an issue while attempting to enroll this device. Contact your administrator to obtain a new enrollment code.'),
|
'There was an issue while attempting to enroll this device. Contact your administrator to obtain a new enrollment code.',
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20)),
|
),
|
||||||
Padding(
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||||
child: SelectableText.rich(TextSpan(children: [
|
),
|
||||||
TextSpan(text: 'If the problem persists, please let us know at '),
|
Padding(
|
||||||
|
child: SelectableText.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'support@defined.net',
|
children: [
|
||||||
style: bodyTextStyle.apply(color: colorScheme.primary),
|
TextSpan(text: 'If the problem persists, please let us know at '),
|
||||||
recognizer: TapGestureRecognizer()
|
TextSpan(
|
||||||
..onTap = () async {
|
text: 'support@defined.net',
|
||||||
if (await canLaunchUrl(contactUri)) {
|
style: bodyTextStyle.apply(color: colorScheme.primary),
|
||||||
print(await launchUrl(contactUri));
|
recognizer:
|
||||||
}
|
TapGestureRecognizer()
|
||||||
},
|
..onTap = () async {
|
||||||
|
if (await canLaunchUrl(contactUri)) {
|
||||||
|
print(await launchUrl(contactUri));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextSpan(text: ' and provide the following error:'),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
TextSpan(text: ' and provide the following error:'),
|
),
|
||||||
])),
|
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10)),
|
),
|
||||||
Container(
|
Container(
|
||||||
child: Padding(child: SelectableText(this.error!), padding: EdgeInsets.all(16)),
|
child: Padding(child: SelectableText(this.error!), padding: EdgeInsets.all(16)),
|
||||||
color: Theme.of(context).colorScheme.errorContainer,
|
color: Theme.of(context).colorScheme.errorContainer,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
} else if (this.enrolled) {
|
} else if (this.enrolled) {
|
||||||
// Enrollment complete!
|
// Enrollment complete!
|
||||||
child = Padding(
|
child = Padding(
|
||||||
child: Center(
|
child: Center(child: Text('Enrollment complete! 🎉', textAlign: TextAlign.center)),
|
||||||
child: Text(
|
padding: EdgeInsets.only(top: 20),
|
||||||
'Enrollment complete! 🎉',
|
);
|
||||||
textAlign: TextAlign.center,
|
|
||||||
)),
|
|
||||||
padding: EdgeInsets.only(top: 20));
|
|
||||||
} else {
|
} else {
|
||||||
// Have a code and actively enrolling
|
// Have a code and actively enrolling
|
||||||
alignment = Alignment.center;
|
alignment = Alignment.center;
|
||||||
child = Center(
|
child = Center(
|
||||||
child: Column(children: [
|
child: Column(
|
||||||
Padding(child: Text('Contacting DN for enrollment'), padding: EdgeInsets.only(bottom: 25)),
|
children: [
|
||||||
PlatformCircularProgressIndicator(cupertino: (_, __) {
|
Padding(child: Text('Contacting DN for enrollment'), padding: EdgeInsets.only(bottom: 25)),
|
||||||
return CupertinoProgressIndicatorData(radius: 50);
|
PlatformCircularProgressIndicator(
|
||||||
})
|
cupertino: (_, __) {
|
||||||
]));
|
return CupertinoProgressIndicatorData(radius: 50);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SimplePage(title: Text('Enroll with Managed Nebula'), child: child, alignment: alignment);
|
return SimplePage(title: Text('Enroll with Managed Nebula'), child: child, alignment: alignment);
|
||||||
|
@ -182,32 +195,26 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final input = Padding(
|
final input = Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: PlatformTextFormField(
|
child: PlatformTextFormField(
|
||||||
controller: enrollInput,
|
controller: enrollInput,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
hintText: 'from admin.defined.net',
|
hintText: 'from admin.defined.net',
|
||||||
cupertino: (_, __) => CupertinoTextFormFieldData(
|
cupertino: (_, __) => CupertinoTextFormFieldData(prefix: Text("Code or link")),
|
||||||
prefix: Text("Code or link"),
|
material: (_, __) => MaterialTextFormFieldData(decoration: const InputDecoration(labelText: 'Code or link')),
|
||||||
),
|
),
|
||||||
material: (_, __) => MaterialTextFormFieldData(
|
|
||||||
decoration: const InputDecoration(labelText: 'Code or link'),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
final form = Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Platform.isAndroid ? input : ConfigSection(children: [input]),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return Column(children: [
|
final form = Form(key: _formKey, child: Platform.isAndroid ? input : ConfigSection(children: [input]));
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 32),
|
return Column(
|
||||||
child: form,
|
children: [
|
||||||
),
|
Padding(padding: EdgeInsets.symmetric(vertical: 32), child: form),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
child: Row(children: [Expanded(child: PrimaryButton(child: Text('Submit'), onPressed: onSubmit))]))
|
child: Row(children: [Expanded(child: PrimaryButton(child: Text('Submit'), onPressed: onSubmit))]),
|
||||||
]);
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,49 +53,66 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
||||||
final title = widget.pending ? 'Pending' : 'Active';
|
final title = widget.pending ? 'Pending' : 'Active';
|
||||||
|
|
||||||
return SimplePage(
|
return SimplePage(
|
||||||
title: Text('$title Host Info'),
|
title: Text('$title Host Info'),
|
||||||
refreshController: refreshController,
|
refreshController: refreshController,
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await _getHostInfo();
|
await _getHostInfo();
|
||||||
refreshController.refreshCompleted();
|
refreshController.refreshCompleted();
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [_buildMain(), _buildDetails(), _buildRemotes(), !widget.pending ? _buildClose() : Container()]));
|
children: [_buildMain(), _buildDetails(), _buildRemotes(), !widget.pending ? _buildClose() : Container()],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMain() {
|
Widget _buildMain() {
|
||||||
return ConfigSection(children: [
|
return ConfigSection(
|
||||||
ConfigItem(label: Text('VPN IP'), labelWidth: 150, content: SelectableText(hostInfo.vpnIp)),
|
children: [
|
||||||
hostInfo.cert != null
|
ConfigItem(label: Text('VPN IP'), labelWidth: 150, content: SelectableText(hostInfo.vpnIp)),
|
||||||
? ConfigPageItem(
|
hostInfo.cert != null
|
||||||
|
? ConfigPageItem(
|
||||||
label: Text('Certificate'),
|
label: Text('Certificate'),
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
content: Text(hostInfo.cert!.details.name),
|
content: Text(hostInfo.cert!.details.name),
|
||||||
onPressed: () => Utils.openPage(
|
onPressed:
|
||||||
context,
|
() => Utils.openPage(
|
||||||
(context) => CertificateDetailsScreen(
|
context,
|
||||||
certInfo: CertificateInfo(cert: hostInfo.cert!),
|
(context) => CertificateDetailsScreen(
|
||||||
supportsQRScanning: widget.supportsQRScanning,
|
certInfo: CertificateInfo(cert: hostInfo.cert!),
|
||||||
)))
|
supportsQRScanning: widget.supportsQRScanning,
|
||||||
: Container(),
|
),
|
||||||
]);
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDetails() {
|
Widget _buildDetails() {
|
||||||
return ConfigSection(children: <Widget>[
|
return ConfigSection(
|
||||||
ConfigItem(
|
children: <Widget>[
|
||||||
label: Text('Lighthouse'), labelWidth: 150, content: SelectableText(widget.isLighthouse ? 'Yes' : 'No')),
|
ConfigItem(
|
||||||
ConfigItem(label: Text('Local Index'), labelWidth: 150, content: SelectableText('${hostInfo.localIndex}')),
|
label: Text('Lighthouse'),
|
||||||
ConfigItem(label: Text('Remote Index'), labelWidth: 150, content: SelectableText('${hostInfo.remoteIndex}')),
|
labelWidth: 150,
|
||||||
ConfigItem(
|
content: SelectableText(widget.isLighthouse ? 'Yes' : 'No'),
|
||||||
label: Text('Message Counter'), labelWidth: 150, content: SelectableText('${hostInfo.messageCounter}')),
|
),
|
||||||
]);
|
ConfigItem(label: Text('Local Index'), labelWidth: 150, content: SelectableText('${hostInfo.localIndex}')),
|
||||||
|
ConfigItem(label: Text('Remote Index'), labelWidth: 150, content: SelectableText('${hostInfo.remoteIndex}')),
|
||||||
|
ConfigItem(
|
||||||
|
label: Text('Message Counter'),
|
||||||
|
labelWidth: 150,
|
||||||
|
content: SelectableText('${hostInfo.messageCounter}'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRemotes() {
|
Widget _buildRemotes() {
|
||||||
if (hostInfo.remoteAddresses.length == 0) {
|
if (hostInfo.remoteAddresses.length == 0) {
|
||||||
return ConfigSection(
|
return ConfigSection(
|
||||||
label: 'REMOTES', children: [ConfigItem(content: Text('No remote addresses yet'), labelWidth: 0)]);
|
label: 'REMOTES',
|
||||||
|
children: [ConfigItem(content: Text('No remote addresses yet'), labelWidth: 0)],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return widget.pending ? _buildStaticRemotes() : _buildEditRemotes();
|
return widget.pending ? _buildStaticRemotes() : _buildEditRemotes();
|
||||||
|
@ -109,26 +126,28 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
||||||
|
|
||||||
hostInfo.remoteAddresses.forEach((remoteObj) {
|
hostInfo.remoteAddresses.forEach((remoteObj) {
|
||||||
String remote = remoteObj.toString();
|
String remote = remoteObj.toString();
|
||||||
items.add(ConfigCheckboxItem(
|
items.add(
|
||||||
key: Key(remote),
|
ConfigCheckboxItem(
|
||||||
label: Text(remote), //TODO: need to do something to adjust the font size in the event we have an ipv6 address
|
key: Key(remote),
|
||||||
labelWidth: ipWidth,
|
label: Text(remote), //TODO: need to do something to adjust the font size in the event we have an ipv6 address
|
||||||
checked: currentRemote == remote,
|
labelWidth: ipWidth,
|
||||||
onChanged: () async {
|
checked: currentRemote == remote,
|
||||||
if (remote == currentRemote) {
|
onChanged: () async {
|
||||||
return;
|
if (remote == currentRemote) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
try {
|
|
||||||
final h = await widget.site.setRemoteForTunnel(hostInfo.vpnIp, remote);
|
|
||||||
if (h != null) {
|
|
||||||
_setHostInfo(h);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
Utils.popError(context, 'Error while changing the remote', err.toString());
|
try {
|
||||||
}
|
final h = await widget.site.setRemoteForTunnel(hostInfo.vpnIp, remote);
|
||||||
},
|
if (h != null) {
|
||||||
));
|
_setHostInfo(h);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Utils.popError(context, 'Error while changing the remote', err.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return ConfigSection(label: items.length > 0 ? 'Tap to change the active address' : null, children: items);
|
return ConfigSection(label: items.length > 0 ? 'Tap to change the active address' : null, children: items);
|
||||||
|
@ -142,12 +161,14 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
||||||
|
|
||||||
hostInfo.remoteAddresses.forEach((remoteObj) {
|
hostInfo.remoteAddresses.forEach((remoteObj) {
|
||||||
String remote = remoteObj.toString();
|
String remote = remoteObj.toString();
|
||||||
items.add(ConfigCheckboxItem(
|
items.add(
|
||||||
key: Key(remote),
|
ConfigCheckboxItem(
|
||||||
label: Text(remote), //TODO: need to do something to adjust the font size in the event we have an ipv6 address
|
key: Key(remote),
|
||||||
labelWidth: ipWidth,
|
label: Text(remote), //TODO: need to do something to adjust the font size in the event we have an ipv6 address
|
||||||
checked: currentRemote == remote,
|
labelWidth: ipWidth,
|
||||||
));
|
checked: currentRemote == remote,
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return ConfigSection(label: items.length > 0 ? 'REMOTES' : null, children: items);
|
return ConfigSection(label: items.length > 0 ? 'REMOTES' : null, children: items);
|
||||||
|
@ -155,22 +176,26 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
||||||
|
|
||||||
Widget _buildClose() {
|
Widget _buildClose() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: DangerButton(
|
child: DangerButton(
|
||||||
child: Text('Close Tunnel'),
|
child: Text('Close Tunnel'),
|
||||||
onPressed: () => Utils.confirmDelete(context, 'Close Tunnel?', () async {
|
onPressed:
|
||||||
try {
|
() => Utils.confirmDelete(context, 'Close Tunnel?', () async {
|
||||||
await widget.site.closeTunnel(hostInfo.vpnIp);
|
try {
|
||||||
if (widget.onChanged != null) {
|
await widget.site.closeTunnel(hostInfo.vpnIp);
|
||||||
widget.onChanged!();
|
if (widget.onChanged != null) {
|
||||||
}
|
widget.onChanged!();
|
||||||
Navigator.pop(context);
|
}
|
||||||
} catch (err) {
|
Navigator.pop(context);
|
||||||
Utils.popError(context, 'Error while trying to close the tunnel', err.toString());
|
} catch (err) {
|
||||||
}
|
Utils.popError(context, 'Error while trying to close the tunnel', err.toString());
|
||||||
}, deleteLabel: 'Close'))));
|
}
|
||||||
|
}, deleteLabel: 'Close'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_getHostInfo() async {
|
_getHostInfo() async {
|
||||||
|
|
|
@ -24,20 +24,13 @@ class LicensesScreen extends StatelessWidget {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
child: PlatformListTile(
|
child: PlatformListTile(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Utils.openPage(
|
Utils.openPage(context, (_) => LicenceDetailPage(title: capitalize(dep.name), licence: dep.license!));
|
||||||
context,
|
},
|
||||||
(_) => LicenceDetailPage(
|
title: Text(capitalize(dep.name)),
|
||||||
title: capitalize(dep.name),
|
subtitle: Text(dep.description),
|
||||||
licence: dep.license!,
|
trailing: Icon(context.platformIcons.forward, size: 18),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
title: Text(
|
|
||||||
capitalize(dep.name),
|
|
||||||
),
|
|
||||||
subtitle: Text(dep.description),
|
|
||||||
trailing: Icon(context.platformIcons.forward, size: 18)),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -62,14 +55,7 @@ class LicenceDetailPage extends StatelessWidget {
|
||||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
child: Column(
|
child: Column(children: [Text(licence, style: const TextStyle(fontSize: 15))]),
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
licence,
|
|
||||||
style: const TextStyle(fontSize: 15),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -115,11 +115,7 @@ class _MainScreenState extends State<MainScreen> {
|
||||||
|
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
debugSite = Row(
|
debugSite = Row(
|
||||||
children: [
|
children: [_debugSave(badDebugSave), _debugSave(goodDebugSave), _debugClearKeys()],
|
||||||
_debugSave(badDebugSave),
|
|
||||||
_debugSave(goodDebugSave),
|
|
||||||
_debugClearKeys(),
|
|
||||||
],
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -141,13 +137,15 @@ class _MainScreenState extends State<MainScreen> {
|
||||||
leadingAction: PlatformIconButton(
|
leadingAction: PlatformIconButton(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
icon: Icon(Icons.add, size: 28.0),
|
icon: Icon(Icons.add, size: 28.0),
|
||||||
onPressed: () => Utils.openPage(context, (context) {
|
onPressed:
|
||||||
return SiteConfigScreen(
|
() => Utils.openPage(context, (context) {
|
||||||
onSave: (_) {
|
return SiteConfigScreen(
|
||||||
_loadSites();
|
onSave: (_) {
|
||||||
},
|
_loadSites();
|
||||||
supportsQRScanning: supportsQRScanning);
|
},
|
||||||
}),
|
supportsQRScanning: supportsQRScanning,
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
refreshController: refreshController,
|
refreshController: refreshController,
|
||||||
onRefresh: () {
|
onRefresh: () {
|
||||||
|
@ -169,13 +167,15 @@ class _MainScreenState extends State<MainScreen> {
|
||||||
Widget _buildBody() {
|
Widget _buildBody() {
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: error!,
|
children: error!,
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10)));
|
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildSites();
|
return _buildSites();
|
||||||
|
@ -183,19 +183,22 @@ class _MainScreenState extends State<MainScreen> {
|
||||||
|
|
||||||
Widget _buildNoSites() {
|
Widget _buildNoSites() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(0, 8.0, 0, 8.0),
|
padding: const EdgeInsets.fromLTRB(0, 8.0, 0, 8.0),
|
||||||
child: Text('Welcome to Nebula!', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
child: Text('Welcome to Nebula!', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||||
),
|
),
|
||||||
Text('You don\'t have any site configurations installed yet. Hit the plus button above to get started.',
|
Text(
|
||||||
textAlign: TextAlign.center),
|
'You don\'t have any site configurations installed yet. Hit the plus button above to get started.',
|
||||||
],
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
));
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSites() {
|
Widget _buildSites() {
|
||||||
|
@ -205,7 +208,8 @@ class _MainScreenState extends State<MainScreen> {
|
||||||
|
|
||||||
List<Widget> items = [];
|
List<Widget> items = [];
|
||||||
sites!.forEach((site) {
|
sites!.forEach((site) {
|
||||||
items.add(SiteItem(
|
items.add(
|
||||||
|
SiteItem(
|
||||||
key: Key(site.id),
|
key: Key(site.id),
|
||||||
site: site,
|
site: site,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
@ -216,42 +220,45 @@ class _MainScreenState extends State<MainScreen> {
|
||||||
supportsQRScanning: supportsQRScanning,
|
supportsQRScanning: supportsQRScanning,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}));
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Widget child = ReorderableListView(
|
Widget child = ReorderableListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
padding: EdgeInsets.symmetric(vertical: 5),
|
padding: EdgeInsets.symmetric(vertical: 5),
|
||||||
children: items,
|
children: items,
|
||||||
onReorder: (oldI, newI) async {
|
onReorder: (oldI, newI) async {
|
||||||
if (oldI < newI) {
|
if (oldI < newI) {
|
||||||
// removing the item at oldIndex will shorten the list by 1.
|
// removing the item at oldIndex will shorten the list by 1.
|
||||||
newI -= 1;
|
newI -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
final Site moved = sites!.removeAt(oldI);
|
final Site moved = sites!.removeAt(oldI);
|
||||||
sites!.insert(newI, moved);
|
sites!.insert(newI, moved);
|
||||||
});
|
|
||||||
|
|
||||||
for (var i = 0; i < sites!.length; i++) {
|
|
||||||
if (sites![i].sortKey == i) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
sites![i].sortKey = i;
|
|
||||||
try {
|
|
||||||
await sites![i].save();
|
|
||||||
} catch (err) {
|
|
||||||
//TODO: display error at the end
|
|
||||||
print('ERR ${sites![i].name} - $err');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadSites();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (var i = 0; i < sites!.length; i++) {
|
||||||
|
if (sites![i].sortKey == i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sites![i].sortKey = i;
|
||||||
|
try {
|
||||||
|
await sites![i].save();
|
||||||
|
} catch (err) {
|
||||||
|
//TODO: display error at the end
|
||||||
|
print('ERR ${sites![i].name} - $err');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_loadSites();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
child = CupertinoTheme(child: child, data: CupertinoTheme.of(context));
|
child = CupertinoTheme(child: child, data: CupertinoTheme.of(context));
|
||||||
}
|
}
|
||||||
|
@ -267,16 +274,18 @@ class _MainScreenState extends State<MainScreen> {
|
||||||
var uuid = Uuid();
|
var uuid = Uuid();
|
||||||
|
|
||||||
var s = Site(
|
var s = Site(
|
||||||
name: siteConfig['name']!,
|
name: siteConfig['name']!,
|
||||||
id: uuid.v4(),
|
id: uuid.v4(),
|
||||||
staticHostmap: {
|
staticHostmap: {
|
||||||
"10.1.0.1": StaticHost(
|
"10.1.0.1": StaticHost(
|
||||||
lighthouse: true,
|
lighthouse: true,
|
||||||
destinations: [IPAndPort(ip: '10.1.1.53', port: 4242), IPAndPort(ip: '1::1', port: 4242)])
|
destinations: [IPAndPort(ip: '10.1.1.53', port: 4242), IPAndPort(ip: '1::1', port: 4242)],
|
||||||
},
|
),
|
||||||
ca: [CertificateInfo.debug(rawCert: siteConfig['ca'])],
|
},
|
||||||
certInfo: CertificateInfo.debug(rawCert: siteConfig['cert']),
|
ca: [CertificateInfo.debug(rawCert: siteConfig['ca'])],
|
||||||
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]);
|
certInfo: CertificateInfo.debug(rawCert: siteConfig['cert']),
|
||||||
|
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')],
|
||||||
|
);
|
||||||
|
|
||||||
s.key = siteConfig['key'];
|
s.key = siteConfig['key'];
|
||||||
|
|
||||||
|
@ -309,14 +318,17 @@ class _MainScreenState extends State<MainScreen> {
|
||||||
var site = Site.fromJson(rawSite);
|
var site = Site.fromJson(rawSite);
|
||||||
|
|
||||||
//TODO: we need to cancel change listeners when we rebuild
|
//TODO: we need to cancel change listeners when we rebuild
|
||||||
site.onChange().listen((_) {
|
site.onChange().listen(
|
||||||
setState(() {});
|
(_) {
|
||||||
}, onError: (err) {
|
setState(() {});
|
||||||
setState(() {});
|
},
|
||||||
if (ModalRoute.of(context)!.isCurrent) {
|
onError: (err) {
|
||||||
Utils.popError(context, "${site.name} Error", err);
|
setState(() {});
|
||||||
}
|
if (ModalRoute.of(context)!.isCurrent) {
|
||||||
});
|
Utils.popError(context, "${site.name} Error", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
sites!.add(site);
|
sites!.add(site);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -38,10 +38,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
List<Widget> colorSection = [];
|
List<Widget> colorSection = [];
|
||||||
|
|
||||||
colorSection.add(ConfigItem(
|
colorSection.add(
|
||||||
label: Text('Use system colors'),
|
ConfigItem(
|
||||||
labelWidth: 200,
|
label: Text('Use system colors'),
|
||||||
content: Align(
|
labelWidth: 200,
|
||||||
|
content: Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Switch.adaptive(
|
child: Switch.adaptive(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
@ -49,13 +50,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
settings.useSystemColors = value;
|
settings.useSystemColors = value;
|
||||||
},
|
},
|
||||||
value: settings.useSystemColors,
|
value: settings.useSystemColors,
|
||||||
)),
|
),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (!settings.useSystemColors) {
|
if (!settings.useSystemColors) {
|
||||||
colorSection.add(ConfigItem(
|
colorSection.add(
|
||||||
label: Text('Dark mode'),
|
ConfigItem(
|
||||||
content: Align(
|
label: Text('Dark mode'),
|
||||||
|
content: Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Switch.adaptive(
|
child: Switch.adaptive(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
@ -63,16 +67,19 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
settings.darkMode = value;
|
settings.darkMode = value;
|
||||||
},
|
},
|
||||||
value: settings.darkMode,
|
value: settings.darkMode,
|
||||||
)),
|
),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> items = [];
|
List<Widget> items = [];
|
||||||
items.add(ConfigSection(children: colorSection));
|
items.add(ConfigSection(children: colorSection));
|
||||||
items.add(ConfigItem(
|
items.add(
|
||||||
label: Text('Wrap log output'),
|
ConfigItem(
|
||||||
labelWidth: 200,
|
label: Text('Wrap log output'),
|
||||||
content: Align(
|
labelWidth: 200,
|
||||||
|
content: Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Switch.adaptive(
|
child: Switch.adaptive(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
@ -82,14 +89,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
settings.logWrap = value;
|
settings.logWrap = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)),
|
),
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
items.add(ConfigSection(children: [
|
items.add(
|
||||||
ConfigItem(
|
ConfigSection(
|
||||||
label: Text('Report errors automatically'),
|
children: [
|
||||||
labelWidth: 250,
|
ConfigItem(
|
||||||
content: Align(
|
label: Text('Report errors automatically'),
|
||||||
|
labelWidth: 250,
|
||||||
|
content: Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Switch.adaptive(
|
child: Switch.adaptive(
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
@ -99,27 +110,35 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||||
settings.trackErrors = value;
|
settings.trackErrors = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
))),
|
),
|
||||||
]));
|
),
|
||||||
|
),
|
||||||
items.add(ConfigSection(children: [
|
],
|
||||||
ConfigPageItem(
|
),
|
||||||
label: Text('Enroll with Managed Nebula'),
|
|
||||||
labelWidth: 250,
|
|
||||||
onPressed: () =>
|
|
||||||
Utils.openPage(context, (context) => EnrollmentScreen(stream: widget.stream, allowCodeEntry: true)))
|
|
||||||
]));
|
|
||||||
|
|
||||||
items.add(ConfigSection(children: [
|
|
||||||
ConfigPageItem(
|
|
||||||
label: Text('About'),
|
|
||||||
onPressed: () => Utils.openPage(context, (context) => AboutScreen()),
|
|
||||||
)
|
|
||||||
]));
|
|
||||||
|
|
||||||
return SimplePage(
|
|
||||||
title: Text('Settings'),
|
|
||||||
child: Column(children: items),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
ConfigSection(
|
||||||
|
children: [
|
||||||
|
ConfigPageItem(
|
||||||
|
label: Text('Enroll with Managed Nebula'),
|
||||||
|
labelWidth: 250,
|
||||||
|
onPressed:
|
||||||
|
() =>
|
||||||
|
Utils.openPage(context, (context) => EnrollmentScreen(stream: widget.stream, allowCodeEntry: true)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
items.add(
|
||||||
|
ConfigSection(
|
||||||
|
children: [
|
||||||
|
ConfigPageItem(label: Text('About'), onPressed: () => Utils.openPage(context, (context) => AboutScreen())),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return SimplePage(title: Text('Settings'), child: Column(children: items));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,12 +23,8 @@ import '../components/SiteTitle.dart';
|
||||||
//TODO: ios is now the problem with connecting screwing our ability to query the hostmap (its a race)
|
//TODO: ios is now the problem with connecting screwing our ability to query the hostmap (its a race)
|
||||||
|
|
||||||
class SiteDetailScreen extends StatefulWidget {
|
class SiteDetailScreen extends StatefulWidget {
|
||||||
const SiteDetailScreen({
|
const SiteDetailScreen({Key? key, required this.site, this.onChanged, required this.supportsQRScanning})
|
||||||
Key? key,
|
: super(key: key);
|
||||||
required this.site,
|
|
||||||
this.onChanged,
|
|
||||||
required this.supportsQRScanning,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Site site;
|
final Site site;
|
||||||
final Function? onChanged;
|
final Function? onChanged;
|
||||||
|
@ -54,21 +50,24 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
||||||
_listHostmap();
|
_listHostmap();
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange = site.onChange().listen((_) {
|
onChange = site.onChange().listen(
|
||||||
// TODO: Gross hack... we get site.connected = true to trigger the toggle before the VPN service has started.
|
(_) {
|
||||||
// If we fetch the hostmap now we'll never get a response. Wait until Nebula is running.
|
// TODO: Gross hack... we get site.connected = true to trigger the toggle before the VPN service has started.
|
||||||
if (site.status == 'Connected') {
|
// If we fetch the hostmap now we'll never get a response. Wait until Nebula is running.
|
||||||
_listHostmap();
|
if (site.status == 'Connected') {
|
||||||
} else {
|
_listHostmap();
|
||||||
activeHosts = null;
|
} else {
|
||||||
pendingHosts = null;
|
activeHosts = null;
|
||||||
}
|
pendingHosts = null;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}, onError: (err) {
|
},
|
||||||
setState(() {});
|
onError: (err) {
|
||||||
Utils.popError(context, "Error", err);
|
setState(() {});
|
||||||
});
|
Utils.popError(context, "Error", err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
@ -84,27 +83,33 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
||||||
final title = SiteTitle(site: widget.site);
|
final title = SiteTitle(site: widget.site);
|
||||||
|
|
||||||
return SimplePage(
|
return SimplePage(
|
||||||
title: title,
|
title: title,
|
||||||
leadingAction: Utils.leadingBackWidget(context, onPressed: () {
|
leadingAction: Utils.leadingBackWidget(
|
||||||
|
context,
|
||||||
|
onPressed: () {
|
||||||
if (changed && widget.onChanged != null) {
|
if (changed && widget.onChanged != null) {
|
||||||
widget.onChanged!();
|
widget.onChanged!();
|
||||||
}
|
}
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}),
|
|
||||||
refreshController: refreshController,
|
|
||||||
onRefresh: () async {
|
|
||||||
if (site.connected && site.status == "Connected") {
|
|
||||||
await _listHostmap();
|
|
||||||
}
|
|
||||||
refreshController.refreshCompleted();
|
|
||||||
},
|
},
|
||||||
child: Column(children: [
|
),
|
||||||
|
refreshController: refreshController,
|
||||||
|
onRefresh: () async {
|
||||||
|
if (site.connected && site.status == "Connected") {
|
||||||
|
await _listHostmap();
|
||||||
|
}
|
||||||
|
refreshController.refreshCompleted();
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
_buildErrors(),
|
_buildErrors(),
|
||||||
_buildConfig(),
|
_buildConfig(),
|
||||||
site.connected ? _buildHosts() : Container(),
|
site.connected ? _buildHosts() : Container(),
|
||||||
_buildSiteDetails(),
|
_buildSiteDetails(),
|
||||||
_buildDelete(),
|
_buildDelete(),
|
||||||
]));
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildErrors() {
|
Widget _buildErrors() {
|
||||||
|
@ -114,8 +119,12 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
||||||
|
|
||||||
List<Widget> items = [];
|
List<Widget> items = [];
|
||||||
site.errors.forEach((error) {
|
site.errors.forEach((error) {
|
||||||
items.add(ConfigItem(
|
items.add(
|
||||||
labelWidth: 0, content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error))));
|
ConfigItem(
|
||||||
|
labelWidth: 0,
|
||||||
|
content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error)),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return ConfigSection(
|
return ConfigSection(
|
||||||
|
@ -140,29 +149,38 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfigSection(children: <Widget>[
|
return ConfigSection(
|
||||||
ConfigItem(
|
children: <Widget>[
|
||||||
|
ConfigItem(
|
||||||
label: Text('Status'),
|
label: Text('Status'),
|
||||||
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[
|
content: Row(
|
||||||
Padding(
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
padding: EdgeInsets.only(right: 5),
|
padding: EdgeInsets.only(right: 5),
|
||||||
child: Text(widget.site.status,
|
child: Text(
|
||||||
style: TextStyle(color: CupertinoColors.secondaryLabel.resolveFrom(context)))),
|
widget.site.status,
|
||||||
Switch.adaptive(
|
style: TextStyle(color: CupertinoColors.secondaryLabel.resolveFrom(context)),
|
||||||
value: widget.site.connected,
|
),
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
),
|
||||||
onChanged: widget.site.errors.length > 0 && !widget.site.connected ? null : handleChange,
|
Switch.adaptive(
|
||||||
)
|
value: widget.site.connected,
|
||||||
])),
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
ConfigPageItem(
|
onChanged: widget.site.errors.length > 0 && !widget.site.connected ? null : handleChange,
|
||||||
label: Text('Logs'),
|
),
|
||||||
onPressed: () {
|
],
|
||||||
Utils.openPage(context, (context) {
|
),
|
||||||
return SiteLogsScreen(site: widget.site);
|
),
|
||||||
});
|
ConfigPageItem(
|
||||||
},
|
label: Text('Logs'),
|
||||||
),
|
onPressed: () {
|
||||||
]);
|
Utils.openPage(context, (context) {
|
||||||
|
return SiteLogsScreen(site: widget.site);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHosts() {
|
Widget _buildHosts() {
|
||||||
|
@ -184,83 +202,92 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
||||||
label: "TUNNELS",
|
label: "TUNNELS",
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConfigPageItem(
|
ConfigPageItem(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (activeHosts == null) return;
|
if (activeHosts == null) return;
|
||||||
|
|
||||||
Utils.openPage(
|
Utils.openPage(
|
||||||
context,
|
context,
|
||||||
(context) => SiteTunnelsScreen(
|
(context) => SiteTunnelsScreen(
|
||||||
pending: false,
|
pending: false,
|
||||||
tunnels: activeHosts!,
|
tunnels: activeHosts!,
|
||||||
site: site,
|
site: site,
|
||||||
onChanged: (hosts) {
|
onChanged: (hosts) {
|
||||||
setState(() {
|
setState(() {
|
||||||
activeHosts = hosts;
|
activeHosts = hosts;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
supportsQRScanning: widget.supportsQRScanning,
|
supportsQRScanning: widget.supportsQRScanning,
|
||||||
));
|
),
|
||||||
},
|
);
|
||||||
label: Text("Active"),
|
},
|
||||||
content: Container(alignment: Alignment.centerRight, child: active)),
|
label: Text("Active"),
|
||||||
|
content: Container(alignment: Alignment.centerRight, child: active),
|
||||||
|
),
|
||||||
ConfigPageItem(
|
ConfigPageItem(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (pendingHosts == null) return;
|
if (pendingHosts == null) return;
|
||||||
|
|
||||||
Utils.openPage(
|
Utils.openPage(
|
||||||
context,
|
context,
|
||||||
(context) => SiteTunnelsScreen(
|
(context) => SiteTunnelsScreen(
|
||||||
pending: true,
|
pending: true,
|
||||||
tunnels: pendingHosts!,
|
tunnels: pendingHosts!,
|
||||||
site: site,
|
site: site,
|
||||||
onChanged: (hosts) {
|
onChanged: (hosts) {
|
||||||
setState(() {
|
setState(() {
|
||||||
pendingHosts = hosts;
|
pendingHosts = hosts;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
supportsQRScanning: widget.supportsQRScanning,
|
supportsQRScanning: widget.supportsQRScanning,
|
||||||
));
|
),
|
||||||
},
|
);
|
||||||
label: Text("Pending"),
|
},
|
||||||
content: Container(alignment: Alignment.centerRight, child: pending))
|
label: Text("Pending"),
|
||||||
|
content: Container(alignment: Alignment.centerRight, child: pending),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSiteDetails() {
|
Widget _buildSiteDetails() {
|
||||||
return ConfigSection(children: <Widget>[
|
return ConfigSection(
|
||||||
ConfigPageItem(
|
children: <Widget>[
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
ConfigPageItem(
|
||||||
content: Text('Configuration'),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
onPressed: () {
|
content: Text('Configuration'),
|
||||||
Utils.openPage(context, (context) {
|
onPressed: () {
|
||||||
return SiteConfigScreen(
|
Utils.openPage(context, (context) {
|
||||||
site: widget.site,
|
return SiteConfigScreen(
|
||||||
onSave: (site) async {
|
site: widget.site,
|
||||||
changed = true;
|
onSave: (site) async {
|
||||||
setState(() {});
|
changed = true;
|
||||||
},
|
setState(() {});
|
||||||
supportsQRScanning: widget.supportsQRScanning,
|
},
|
||||||
);
|
supportsQRScanning: widget.supportsQRScanning,
|
||||||
});
|
);
|
||||||
},
|
});
|
||||||
),
|
},
|
||||||
]);
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDelete() {
|
Widget _buildDelete() {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: DangerButton(
|
child: DangerButton(
|
||||||
child: Text('Delete'),
|
child: Text('Delete'),
|
||||||
onPressed: () => Utils.confirmDelete(context, 'Delete Site?', () async {
|
onPressed:
|
||||||
|
() => Utils.confirmDelete(context, 'Delete Site?', () async {
|
||||||
if (await _deleteSite()) {
|
if (await _deleteSite()) {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_listHostmap() async {
|
_listHostmap() async {
|
||||||
|
|
|
@ -60,20 +60,21 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
|
||||||
},
|
},
|
||||||
refreshController: refreshController,
|
refreshController: refreshController,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(5),
|
padding: EdgeInsets.all(5),
|
||||||
constraints: logBoxConstraints(context),
|
constraints: logBoxConstraints(context),
|
||||||
child: ListenableBuilder(
|
child: ListenableBuilder(
|
||||||
listenable: logsNotifier,
|
listenable: logsNotifier,
|
||||||
builder: (context, child) => SelectableText(
|
builder:
|
||||||
switch (logsNotifier.logsResult) {
|
(context, child) => SelectableText(switch (logsNotifier.logsResult) {
|
||||||
Ok<String>(:var value) => value.trim(),
|
Ok<String>(:var value) => value.trim(),
|
||||||
Error<String>(:var error) => error is LogsNotFoundException
|
Error<String>(:var error) =>
|
||||||
|
error is LogsNotFoundException
|
||||||
? error.error()
|
? error.error()
|
||||||
: Utils.popError(context, "Error while reading logs.", error.toString()),
|
: Utils.popError(context, "Error while reading logs.", error.toString()),
|
||||||
null => "",
|
null => "",
|
||||||
},
|
}, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
),
|
||||||
)),
|
),
|
||||||
bottomBar: _buildBottomBar(),
|
bottomBar: _buildBottomBar(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -81,79 +82,80 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
|
||||||
Widget _buildTextWrapToggle() {
|
Widget _buildTextWrapToggle() {
|
||||||
return Platform.isIOS
|
return Platform.isIOS
|
||||||
? Tooltip(
|
? Tooltip(
|
||||||
message: "Turn ${settings.logWrap ? "off" : "on"} text wrapping",
|
message: "Turn ${settings.logWrap ? "off" : "on"} text wrapping",
|
||||||
child: CupertinoButton.tinted(
|
child: CupertinoButton.tinted(
|
||||||
// Use the default tint when enabled, match the background when not.
|
// Use the default tint when enabled, match the background when not.
|
||||||
color: settings.logWrap ? null : CupertinoColors.systemBackground,
|
color: settings.logWrap ? null : CupertinoColors.systemBackground,
|
||||||
sizeStyle: CupertinoButtonSize.small,
|
sizeStyle: CupertinoButtonSize.small,
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
child: const Icon(Icons.wrap_text),
|
child: const Icon(Icons.wrap_text),
|
||||||
onPressed: () => {
|
onPressed:
|
||||||
|
() => {
|
||||||
|
setState(() {
|
||||||
|
settings.logWrap = !settings.logWrap;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: IconButton.filledTonal(
|
||||||
|
isSelected: settings.logWrap,
|
||||||
|
tooltip: "Turn ${settings.logWrap ? "off" : "on"} text wrapping",
|
||||||
|
// The variants of wrap_text seem to be the same, but this seems most correct.
|
||||||
|
selectedIcon: const Icon(Icons.wrap_text_outlined),
|
||||||
|
icon: const Icon(Icons.wrap_text),
|
||||||
|
onPressed:
|
||||||
|
() => {
|
||||||
setState(() {
|
setState(() {
|
||||||
settings.logWrap = !settings.logWrap;
|
settings.logWrap = !settings.logWrap;
|
||||||
})
|
}),
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
)
|
|
||||||
: IconButton.filledTonal(
|
|
||||||
isSelected: settings.logWrap,
|
|
||||||
tooltip: "Turn ${settings.logWrap ? "off" : "on"} text wrapping",
|
|
||||||
// The variants of wrap_text seem to be the same, but this seems most correct.
|
|
||||||
selectedIcon: const Icon(Icons.wrap_text_outlined),
|
|
||||||
icon: const Icon(Icons.wrap_text),
|
|
||||||
onPressed: () => {
|
|
||||||
setState(() {
|
|
||||||
settings.logWrap = !settings.logWrap;
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBottomBar() {
|
Widget _buildBottomBar() {
|
||||||
var borderSide = BorderSide(
|
var borderSide = BorderSide(color: CupertinoColors.separator, style: BorderStyle.solid, width: 0.0);
|
||||||
color: CupertinoColors.separator,
|
|
||||||
style: BorderStyle.solid,
|
|
||||||
width: 0.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
var padding = Platform.isAndroid ? EdgeInsets.fromLTRB(0, 20, 0, 30) : EdgeInsets.all(10);
|
var padding = Platform.isAndroid ? EdgeInsets.fromLTRB(0, 20, 0, 30) : EdgeInsets.all(10);
|
||||||
|
|
||||||
return PlatformWidgetBuilder(
|
return PlatformWidgetBuilder(
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: "Share logs",
|
message: "Share logs",
|
||||||
child: PlatformIconButton(
|
child: PlatformIconButton(
|
||||||
icon: Icon(context.platformIcons.share),
|
icon: Icon(context.platformIcons.share),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Share.shareFile(context,
|
Share.shareFile(
|
||||||
title: '${widget.site.name} logs',
|
context,
|
||||||
filePath: widget.site.logFile,
|
title: '${widget.site.name} logs',
|
||||||
filename: '${widget.site.name}.log');
|
filePath: widget.site.logFile,
|
||||||
},
|
filename: '${widget.site.name}.log',
|
||||||
),
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Tooltip(
|
),
|
||||||
message: 'Go to latest',
|
Tooltip(
|
||||||
child: PlatformIconButton(
|
message: 'Go to latest',
|
||||||
icon: Icon(context.platformIcons.downArrow),
|
child: PlatformIconButton(
|
||||||
onPressed: () async {
|
icon: Icon(context.platformIcons.downArrow),
|
||||||
controller.animateTo(controller.position.maxScrollExtent,
|
onPressed: () async {
|
||||||
duration: const Duration(milliseconds: 500), curve: Curves.linearToEaseOut);
|
controller.animateTo(
|
||||||
},
|
controller.position.maxScrollExtent,
|
||||||
),
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.linearToEaseOut,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
cupertino: (context, child, platform) => Container(
|
),
|
||||||
decoration: BoxDecoration(
|
cupertino:
|
||||||
border: Border(top: borderSide),
|
(context, child, platform) =>
|
||||||
),
|
Container(decoration: BoxDecoration(border: Border(top: borderSide)), padding: padding, child: child),
|
||||||
padding: padding,
|
material: (context, child, platform) => BottomAppBar(child: child),
|
||||||
child: child),
|
);
|
||||||
material: (context, child, platform) => BottomAppBar(child: child));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logBoxConstraints(BuildContext context) {
|
logBoxConstraints(BuildContext context) {
|
||||||
|
|
|
@ -53,32 +53,36 @@ class _SiteTunnelsScreenState extends State<SiteTunnelsScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final double ipWidth = Utils.textSize("000.000.000.000", CupertinoTheme.of(context).textTheme.textStyle).width + 32;
|
final double ipWidth = Utils.textSize("000.000.000.000", CupertinoTheme.of(context).textTheme.textStyle).width + 32;
|
||||||
|
|
||||||
final List<ConfigPageItem> children = tunnels.map((hostInfo) {
|
final List<ConfigPageItem> children =
|
||||||
final isLh = site.staticHostmap[hostInfo.vpnIp]?.lighthouse ?? false;
|
tunnels.map((hostInfo) {
|
||||||
final icon = switch (isLh) {
|
final isLh = site.staticHostmap[hostInfo.vpnIp]?.lighthouse ?? false;
|
||||||
true => Icon(Icons.lightbulb_outline, color: CupertinoColors.placeholderText.resolveFrom(context)),
|
final icon = switch (isLh) {
|
||||||
false => Icon(Icons.computer, color: CupertinoColors.placeholderText.resolveFrom(context))
|
true => Icon(Icons.lightbulb_outline, color: CupertinoColors.placeholderText.resolveFrom(context)),
|
||||||
};
|
false => Icon(Icons.computer, color: CupertinoColors.placeholderText.resolveFrom(context)),
|
||||||
|
};
|
||||||
|
|
||||||
return (ConfigPageItem(
|
return (ConfigPageItem(
|
||||||
onPressed: () => Utils.openPage(
|
onPressed:
|
||||||
context,
|
() => Utils.openPage(
|
||||||
(context) => HostInfoScreen(
|
context,
|
||||||
isLighthouse: isLh,
|
(context) => HostInfoScreen(
|
||||||
hostInfo: hostInfo,
|
isLighthouse: isLh,
|
||||||
pending: widget.pending,
|
hostInfo: hostInfo,
|
||||||
site: widget.site,
|
pending: widget.pending,
|
||||||
onChanged: () {
|
site: widget.site,
|
||||||
_listHostmap();
|
onChanged: () {
|
||||||
},
|
_listHostmap();
|
||||||
supportsQRScanning: widget.supportsQRScanning,
|
},
|
||||||
),
|
supportsQRScanning: widget.supportsQRScanning,
|
||||||
),
|
),
|
||||||
label: Row(children: <Widget>[Padding(child: icon, padding: EdgeInsets.only(right: 10)), Text(hostInfo.vpnIp)]),
|
),
|
||||||
labelWidth: ipWidth,
|
label: Row(
|
||||||
content: Container(alignment: Alignment.centerRight, child: Text(hostInfo.cert?.details.name ?? "")),
|
children: <Widget>[Padding(child: icon, padding: EdgeInsets.only(right: 10)), Text(hostInfo.vpnIp)],
|
||||||
));
|
),
|
||||||
}).toList();
|
labelWidth: ipWidth,
|
||||||
|
content: Container(alignment: Alignment.centerRight, child: Text(hostInfo.cert?.details.name ?? "")),
|
||||||
|
));
|
||||||
|
}).toList();
|
||||||
|
|
||||||
final Widget child = switch (children.length) {
|
final Widget child = switch (children.length) {
|
||||||
0 => Center(child: Padding(child: Text('No tunnels to show'), padding: EdgeInsets.only(top: 30))),
|
0 => Center(child: Padding(child: Text('No tunnels to show'), padding: EdgeInsets.only(top: 30))),
|
||||||
|
@ -88,13 +92,14 @@ class _SiteTunnelsScreenState extends State<SiteTunnelsScreen> {
|
||||||
final title = widget.pending ? 'Pending' : 'Active';
|
final title = widget.pending ? 'Pending' : 'Active';
|
||||||
|
|
||||||
return SimplePage(
|
return SimplePage(
|
||||||
title: Text('$title Tunnels'),
|
title: Text('$title Tunnels'),
|
||||||
refreshController: refreshController,
|
refreshController: refreshController,
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await _listHostmap();
|
await _listHostmap();
|
||||||
refreshController.refreshCompleted();
|
refreshController.refreshCompleted();
|
||||||
},
|
},
|
||||||
child: child);
|
child: child,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_sortTunnels() {
|
_sortTunnels() {
|
||||||
|
|
|
@ -87,32 +87,34 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
List<Widget> _buildShare() {
|
List<Widget> _buildShare() {
|
||||||
return [
|
return [
|
||||||
ConfigSection(
|
ConfigSection(
|
||||||
label: 'Share your public key with a nebula CA so they can sign and return a certificate',
|
label: 'Share your public key with a nebula CA so they can sign and return a certificate',
|
||||||
children: [
|
children: [
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
labelWidth: 0,
|
labelWidth: 0,
|
||||||
content: SelectableText(pubKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
content: SelectableText(pubKey, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
),
|
),
|
||||||
Builder(
|
Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return ConfigButtonItem(
|
return ConfigButtonItem(
|
||||||
content: Text('Share Public Key'),
|
content: Text('Share Public Key'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Share.share(context,
|
await Share.share(
|
||||||
title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
|
context,
|
||||||
},
|
title: 'Please sign and return a certificate',
|
||||||
);
|
text: pubKey,
|
||||||
},
|
filename: 'device.pub',
|
||||||
),
|
);
|
||||||
])
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildLoadCert() {
|
List<Widget> _buildLoadCert() {
|
||||||
Map<String, Widget> children = {
|
Map<String, Widget> children = {'paste': Text('Copy/Paste'), 'file': Text('File')};
|
||||||
'paste': Text('Copy/Paste'),
|
|
||||||
'file': Text('File'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// not all devices have a camera for QR codes
|
// not all devices have a camera for QR codes
|
||||||
if (widget.supportsQRScanning) {
|
if (widget.supportsQRScanning) {
|
||||||
|
@ -121,18 +123,19 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
|
|
||||||
List<Widget> items = [
|
List<Widget> items = [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(10, 25, 10, 0),
|
padding: EdgeInsets.fromLTRB(10, 25, 10, 0),
|
||||||
child: CupertinoSlidingSegmentedControl(
|
child: CupertinoSlidingSegmentedControl(
|
||||||
groupValue: inputType,
|
groupValue: inputType,
|
||||||
onValueChanged: (v) {
|
onValueChanged: (v) {
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
inputType = v;
|
inputType = v;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
children: children,
|
children: children,
|
||||||
))
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (inputType == 'paste') {
|
if (inputType == 'paste') {
|
||||||
|
@ -149,23 +152,25 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
Widget _buildKey() {
|
Widget _buildKey() {
|
||||||
if (!showKey) {
|
if (!showKey) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(top: 20, bottom: 10, left: 10, right: 10),
|
padding: EdgeInsets.only(top: 20, bottom: 10, left: 10, right: 10),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
child: Text('Show/Import Private Key'),
|
child: Text('Show/Import Private Key'),
|
||||||
onPressed: () => Utils.confirmDelete(context, 'Show/Import Private Key?', () {
|
onPressed:
|
||||||
setState(() {
|
() => Utils.confirmDelete(context, 'Show/Import Private Key?', () {
|
||||||
showKey = true;
|
setState(() {
|
||||||
});
|
showKey = true;
|
||||||
}, deleteLabel: 'Yes'))));
|
});
|
||||||
|
}, deleteLabel: 'Yes'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ConfigSection(
|
return ConfigSection(
|
||||||
label: 'Import a private key generated on another device',
|
label: 'Import a private key generated on another device',
|
||||||
children: [
|
children: [ConfigTextItem(controller: keyController, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))],
|
||||||
ConfigTextItem(controller: keyController, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,17 +178,15 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
return [
|
return [
|
||||||
ConfigSection(
|
ConfigSection(
|
||||||
children: [
|
children: [
|
||||||
ConfigTextItem(
|
ConfigTextItem(placeholder: 'Certificate PEM Contents', controller: pasteController),
|
||||||
placeholder: 'Certificate PEM Contents',
|
|
||||||
controller: pasteController,
|
|
||||||
),
|
|
||||||
ConfigButtonItem(
|
ConfigButtonItem(
|
||||||
content: Center(child: Text('Load Certificate')),
|
content: Center(child: Text('Load Certificate')),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_addCertEntry(pasteController.text);
|
_addCertEntry(pasteController.text);
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,21 +195,22 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
ConfigSection(
|
ConfigSection(
|
||||||
children: [
|
children: [
|
||||||
ConfigButtonItem(
|
ConfigButtonItem(
|
||||||
content: Center(child: Text('Choose a file')),
|
content: Center(child: Text('Choose a file')),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
final content = await Utils.pickFile(context);
|
final content = await Utils.pickFile(context);
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
_addCertEntry(content);
|
|
||||||
} catch (err) {
|
|
||||||
return Utils.popError(context, 'Failed to load certificate file', err.toString());
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
_addCertEntry(content);
|
||||||
|
} catch (err) {
|
||||||
|
return Utils.popError(context, 'Failed to load certificate file', err.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,10 +223,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var result = await Navigator.push(
|
var result = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
platformPageRoute(
|
platformPageRoute(context: context, builder: (context) => new ScanQRScreen()),
|
||||||
context: context,
|
|
||||||
builder: (context) => new ScanQRScreen(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
_addCertEntry(result);
|
_addCertEntry(result);
|
||||||
|
@ -230,7 +231,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,18 +248,26 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
||||||
if (certs.length > 0) {
|
if (certs.length > 0) {
|
||||||
var tryCertInfo = CertificateInfo.fromJson(certs.first);
|
var tryCertInfo = CertificateInfo.fromJson(certs.first);
|
||||||
if (tryCertInfo.cert.details.isCa) {
|
if (tryCertInfo.cert.details.isCa) {
|
||||||
return Utils.popError(context, 'Error loading certificate content',
|
return Utils.popError(
|
||||||
'A certificate authority is not appropriate for a client certificate.');
|
context,
|
||||||
|
'Error loading certificate content',
|
||||||
|
'A certificate authority is not appropriate for a client certificate.',
|
||||||
|
);
|
||||||
} else if (!tryCertInfo.validity!.valid) {
|
} else if (!tryCertInfo.validity!.valid) {
|
||||||
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity!.reason);
|
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity!.reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
var certMatch = await platform
|
var certMatch = await platform.invokeMethod("nebula.verifyCertAndKey", <String, String>{
|
||||||
.invokeMethod("nebula.verifyCertAndKey", <String, String>{"cert": rawCert, "key": keyController.text});
|
"cert": rawCert,
|
||||||
|
"key": keyController.text,
|
||||||
|
});
|
||||||
if (!certMatch) {
|
if (!certMatch) {
|
||||||
// The method above will throw if there is a mismatch, this is just here in case we introduce a bug in the future
|
// The method above will throw if there is a mismatch, this is just here in case we introduce a bug in the future
|
||||||
return Utils.popError(context, 'Error loading certificate content',
|
return Utils.popError(
|
||||||
'The provided certificates public key is not compatible with the private key.');
|
context,
|
||||||
|
'Error loading certificate content',
|
||||||
|
'The provided certificates public key is not compatible with the private key.',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.onReplace != null) {
|
if (widget.onReplace != null) {
|
||||||
|
|
|
@ -40,11 +40,7 @@ class Advanced {
|
||||||
}
|
}
|
||||||
|
|
||||||
class AdvancedScreen extends StatefulWidget {
|
class AdvancedScreen extends StatefulWidget {
|
||||||
const AdvancedScreen({
|
const AdvancedScreen({Key? key, required this.site, required this.onSave}) : super(key: key);
|
||||||
Key? key,
|
|
||||||
required this.site,
|
|
||||||
required this.onSave,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Site site;
|
final Site site;
|
||||||
final ValueChanged<Advanced> onSave;
|
final ValueChanged<Advanced> onSave;
|
||||||
|
@ -73,22 +69,24 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormPage(
|
return FormPage(
|
||||||
title: 'Advanced Settings',
|
title: 'Advanced Settings',
|
||||||
changed: changed,
|
changed: changed,
|
||||||
onSave: () {
|
onSave: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
widget.onSave(settings);
|
widget.onSave(settings);
|
||||||
},
|
},
|
||||||
child: Column(children: [
|
child: Column(
|
||||||
|
children: [
|
||||||
ConfigSection(
|
ConfigSection(
|
||||||
children: [
|
children: [
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text("Lighthouse interval"),
|
label: Text("Lighthouse interval"),
|
||||||
labelWidth: 200,
|
labelWidth: 200,
|
||||||
//TODO: Auto select on focus?
|
//TODO: Auto select on focus?
|
||||||
content: widget.site.managed
|
content:
|
||||||
? Text(settings.lhDuration.toString() + " seconds", textAlign: TextAlign.right)
|
widget.site.managed
|
||||||
: PlatformTextFormField(
|
? Text(settings.lhDuration.toString() + " seconds", textAlign: TextAlign.right)
|
||||||
|
: PlatformTextFormField(
|
||||||
initialValue: settings.lhDuration.toString(),
|
initialValue: settings.lhDuration.toString(),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
suffix: Text("seconds"),
|
suffix: Text("seconds"),
|
||||||
|
@ -102,14 +100,16 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text("Listen port"),
|
label: Text("Listen port"),
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
//TODO: Auto select on focus?
|
//TODO: Auto select on focus?
|
||||||
content: widget.site.managed
|
content:
|
||||||
? Text(settings.port.toString(), textAlign: TextAlign.right)
|
widget.site.managed
|
||||||
: PlatformTextFormField(
|
? Text(settings.port.toString(), textAlign: TextAlign.right)
|
||||||
|
: PlatformTextFormField(
|
||||||
initialValue: settings.port.toString(),
|
initialValue: settings.port.toString(),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
|
@ -122,13 +122,15 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text("MTU"),
|
label: Text("MTU"),
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
content: widget.site.managed
|
content:
|
||||||
? Text(settings.mtu.toString(), textAlign: TextAlign.right)
|
widget.site.managed
|
||||||
: PlatformTextFormField(
|
? Text(settings.mtu.toString(), textAlign: TextAlign.right)
|
||||||
|
: PlatformTextFormField(
|
||||||
initialValue: settings.mtu.toString(),
|
initialValue: settings.mtu.toString(),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
|
@ -141,41 +143,46 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
ConfigPageItem(
|
ConfigPageItem(
|
||||||
disabled: widget.site.managed,
|
disabled: widget.site.managed,
|
||||||
label: Text('Cipher'),
|
label: Text('Cipher'),
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
content: Text(settings.cipher, textAlign: TextAlign.end),
|
content: Text(settings.cipher, textAlign: TextAlign.end),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
return CipherScreen(
|
return CipherScreen(
|
||||||
cipher: settings.cipher,
|
cipher: settings.cipher,
|
||||||
onSave: (cipher) {
|
onSave: (cipher) {
|
||||||
setState(() {
|
setState(() {
|
||||||
settings.cipher = cipher;
|
settings.cipher = cipher;
|
||||||
changed = true;
|
changed = true;
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
ConfigPageItem(
|
ConfigPageItem(
|
||||||
disabled: widget.site.managed,
|
disabled: widget.site.managed,
|
||||||
label: Text('Log verbosity'),
|
label: Text('Log verbosity'),
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
content: Text(settings.verbosity, textAlign: TextAlign.end),
|
content: Text(settings.verbosity, textAlign: TextAlign.end),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
return LogVerbosityScreen(
|
return LogVerbosityScreen(
|
||||||
verbosity: settings.verbosity,
|
verbosity: settings.verbosity,
|
||||||
onSave: (verbosity) {
|
onSave: (verbosity) {
|
||||||
setState(() {
|
setState(() {
|
||||||
settings.verbosity = verbosity;
|
settings.verbosity = verbosity;
|
||||||
changed = true;
|
changed = true;
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
ConfigPageItem(
|
ConfigPageItem(
|
||||||
label: Text('Unsafe routes'),
|
label: Text('Unsafe routes'),
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
|
@ -183,18 +190,20 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
return UnsafeRoutesScreen(
|
return UnsafeRoutesScreen(
|
||||||
unsafeRoutes: settings.unsafeRoutes,
|
unsafeRoutes: settings.unsafeRoutes,
|
||||||
onSave: widget.site.managed
|
onSave:
|
||||||
? null
|
widget.site.managed
|
||||||
: (routes) {
|
? null
|
||||||
|
: (routes) {
|
||||||
setState(() {
|
setState(() {
|
||||||
settings.unsafeRoutes = routes;
|
settings.unsafeRoutes = routes;
|
||||||
changed = true;
|
changed = true;
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
ConfigSection(
|
ConfigSection(
|
||||||
|
@ -211,9 +220,11 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
|
||||||
Utils.popError(context, 'Failed to render the site config', err.toString());
|
Utils.popError(context, 'Failed to render the site config', err.toString());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
]));
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,12 +18,7 @@ import 'package:mobile_nebula/services/utils.dart';
|
||||||
//TODO: In addition you will want to think about re-generation while the site is still active (This means storing multiple keys in secure storage)
|
//TODO: In addition you will want to think about re-generation while the site is still active (This means storing multiple keys in secure storage)
|
||||||
|
|
||||||
class CAListScreen extends StatefulWidget {
|
class CAListScreen extends StatefulWidget {
|
||||||
const CAListScreen({
|
const CAListScreen({Key? key, required this.cas, this.onSave, required this.supportsQRScanning}) : super(key: key);
|
||||||
Key? key,
|
|
||||||
required this.cas,
|
|
||||||
this.onSave,
|
|
||||||
required this.supportsQRScanning,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final List<CertificateInfo> cas;
|
final List<CertificateInfo> cas;
|
||||||
final ValueChanged<List<CertificateInfo>>? onSave;
|
final ValueChanged<List<CertificateInfo>>? onSave;
|
||||||
|
@ -65,41 +60,47 @@ class _CAListScreenState extends State<CAListScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return FormPage(
|
return FormPage(
|
||||||
title: 'Certificate Authorities',
|
title: 'Certificate Authorities',
|
||||||
changed: changed,
|
changed: changed,
|
||||||
onSave: () {
|
onSave: () {
|
||||||
if (widget.onSave != null) {
|
if (widget.onSave != null) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
widget.onSave!(cas.values.map((ca) {
|
widget.onSave!(
|
||||||
|
cas.values.map((ca) {
|
||||||
return ca;
|
return ca;
|
||||||
}).toList());
|
}).toList(),
|
||||||
}
|
);
|
||||||
},
|
}
|
||||||
child: Column(children: items));
|
},
|
||||||
|
child: Column(children: items),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildCAs() {
|
List<Widget> _buildCAs() {
|
||||||
List<Widget> items = [];
|
List<Widget> items = [];
|
||||||
cas.forEach((key, ca) {
|
cas.forEach((key, ca) {
|
||||||
items.add(ConfigPageItem(
|
items.add(
|
||||||
content: Text(ca.cert.details.name),
|
ConfigPageItem(
|
||||||
onPressed: () {
|
content: Text(ca.cert.details.name),
|
||||||
Utils.openPage(context, (context) {
|
onPressed: () {
|
||||||
return CertificateDetailsScreen(
|
Utils.openPage(context, (context) {
|
||||||
certInfo: ca,
|
return CertificateDetailsScreen(
|
||||||
onDelete: widget.onSave == null
|
certInfo: ca,
|
||||||
? null
|
onDelete:
|
||||||
: () {
|
widget.onSave == null
|
||||||
setState(() {
|
? null
|
||||||
changed = true;
|
: () {
|
||||||
cas.remove(key);
|
setState(() {
|
||||||
});
|
changed = true;
|
||||||
},
|
cas.remove(key);
|
||||||
supportsQRScanning: widget.supportsQRScanning,
|
});
|
||||||
);
|
},
|
||||||
});
|
supportsQRScanning: widget.supportsQRScanning,
|
||||||
},
|
);
|
||||||
));
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
@ -137,10 +138,7 @@ class _CAListScreenState extends State<CAListScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _addCA() {
|
List<Widget> _addCA() {
|
||||||
Map<String, Widget> children = {
|
Map<String, Widget> children = {'paste': Text('Copy/Paste'), 'file': Text('File')};
|
||||||
'paste': Text('Copy/Paste'),
|
|
||||||
'file': Text('File'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// not all devices have a camera for QR codes
|
// not all devices have a camera for QR codes
|
||||||
if (widget.supportsQRScanning) {
|
if (widget.supportsQRScanning) {
|
||||||
|
@ -149,18 +147,19 @@ class _CAListScreenState extends State<CAListScreen> {
|
||||||
|
|
||||||
List<Widget> items = [
|
List<Widget> items = [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(10, 25, 10, 0),
|
padding: EdgeInsets.fromLTRB(10, 25, 10, 0),
|
||||||
child: CupertinoSlidingSegmentedControl(
|
child: CupertinoSlidingSegmentedControl(
|
||||||
groupValue: inputType,
|
groupValue: inputType,
|
||||||
onValueChanged: (v) {
|
onValueChanged: (v) {
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
inputType = v;
|
inputType = v;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
children: children,
|
children: children,
|
||||||
))
|
),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (inputType == 'paste') {
|
if (inputType == 'paste') {
|
||||||
|
@ -178,25 +177,23 @@ class _CAListScreenState extends State<CAListScreen> {
|
||||||
return [
|
return [
|
||||||
ConfigSection(
|
ConfigSection(
|
||||||
children: [
|
children: [
|
||||||
ConfigTextItem(
|
ConfigTextItem(placeholder: 'CA PEM contents', controller: pasteController),
|
||||||
placeholder: 'CA PEM contents',
|
|
||||||
controller: pasteController,
|
|
||||||
),
|
|
||||||
ConfigButtonItem(
|
ConfigButtonItem(
|
||||||
content: Text('Load CA'),
|
content: Text('Load CA'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_addCAEntry(pasteController.text, (err) {
|
_addCAEntry(pasteController.text, (err) {
|
||||||
print(err);
|
print(err);
|
||||||
if (err != null) {
|
if (err != null) {
|
||||||
return Utils.popError(context, 'Failed to parse CA content', err);
|
return Utils.popError(context, 'Failed to parse CA content', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
pasteController.text = '';
|
pasteController.text = '';
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,27 +202,28 @@ class _CAListScreenState extends State<CAListScreen> {
|
||||||
ConfigSection(
|
ConfigSection(
|
||||||
children: [
|
children: [
|
||||||
ConfigButtonItem(
|
ConfigButtonItem(
|
||||||
content: Text('Choose a file'),
|
content: Text('Choose a file'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
try {
|
try {
|
||||||
final content = await Utils.pickFile(context);
|
final content = await Utils.pickFile(context);
|
||||||
if (content == null) {
|
if (content == null) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
_addCAEntry(content, (err) {
|
|
||||||
if (err != null) {
|
|
||||||
Utils.popError(context, 'Error loading CA file', err);
|
|
||||||
} else {
|
|
||||||
setState(() {});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
return Utils.popError(context, 'Failed to load CA file', err.toString());
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
_addCAEntry(content, (err) {
|
||||||
|
if (err != null) {
|
||||||
|
Utils.popError(context, 'Error loading CA file', err);
|
||||||
|
} else {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return Utils.popError(context, 'Failed to load CA file', err.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,10 +236,7 @@ class _CAListScreenState extends State<CAListScreen> {
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
var result = await Navigator.push(
|
var result = await Navigator.push(
|
||||||
context,
|
context,
|
||||||
platformPageRoute(
|
platformPageRoute(context: context, builder: (context) => new ScanQRScreen()),
|
||||||
context: context,
|
|
||||||
builder: (context) => new ScanQRScreen(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
_addCAEntry(result, (err) {
|
_addCAEntry(result, (err) {
|
||||||
|
@ -253,9 +248,9 @@ class _CAListScreenState extends State<CAListScreen> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
)
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,39 +76,44 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideSave: widget.onSave == null && widget.onReplace == null,
|
hideSave: widget.onSave == null && widget.onReplace == null,
|
||||||
child: Column(children: [
|
child: Column(
|
||||||
_buildID(),
|
children: [_buildID(), _buildFilters(), _buildValid(), _buildAdvanced(), _buildReplace(), _buildDelete()],
|
||||||
_buildFilters(),
|
),
|
||||||
_buildValid(),
|
|
||||||
_buildAdvanced(),
|
|
||||||
_buildReplace(),
|
|
||||||
_buildDelete(),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildID() {
|
Widget _buildID() {
|
||||||
return ConfigSection(children: <Widget>[
|
return ConfigSection(
|
||||||
ConfigItem(label: Text('Name'), content: SelectableText(certInfo.cert.details.name)),
|
children: <Widget>[
|
||||||
ConfigItem(
|
ConfigItem(label: Text('Name'), content: SelectableText(certInfo.cert.details.name)),
|
||||||
label: Text('Type'), content: Text(certInfo.cert.details.isCa ? 'CA certificate' : 'Client certificate')),
|
ConfigItem(
|
||||||
]);
|
label: Text('Type'),
|
||||||
|
content: Text(certInfo.cert.details.isCa ? 'CA certificate' : 'Client certificate'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildValid() {
|
Widget _buildValid() {
|
||||||
var valid = Text('yes');
|
var valid = Text('yes');
|
||||||
if (certInfo.validity != null && !certInfo.validity!.valid) {
|
if (certInfo.validity != null && !certInfo.validity!.valid) {
|
||||||
valid = Text(certInfo.validity!.valid ? 'yes' : certInfo.validity!.reason,
|
valid = Text(
|
||||||
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context)));
|
certInfo.validity!.valid ? 'yes' : certInfo.validity!.reason,
|
||||||
|
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ConfigSection(
|
return ConfigSection(
|
||||||
label: 'VALIDITY',
|
label: 'VALIDITY',
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConfigItem(label: Text('Valid?'), content: valid),
|
ConfigItem(label: Text('Valid?'), content: valid),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Created'), content: SelectableText(certInfo.cert.details.notBefore.toLocal().toString())),
|
label: Text('Created'),
|
||||||
|
content: SelectableText(certInfo.cert.details.notBefore.toLocal().toString()),
|
||||||
|
),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Expires'), content: SelectableText(certInfo.cert.details.notAfter.toLocal().toString())),
|
label: Text('Expires'),
|
||||||
|
content: SelectableText(certInfo.cert.details.notAfter.toLocal().toString()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -136,20 +141,24 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
return ConfigSection(
|
return ConfigSection(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Fingerprint'),
|
label: Text('Fingerprint'),
|
||||||
content:
|
content: SelectableText(certInfo.cert.fingerprint, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
SelectableText(certInfo.cert.fingerprint, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start),
|
),
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text('Public Key'),
|
label: Text('Public Key'),
|
||||||
content: SelectableText(certInfo.cert.details.publicKey,
|
content: SelectableText(
|
||||||
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
certInfo.cert.details.publicKey,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start),
|
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14),
|
||||||
|
),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
),
|
||||||
certInfo.rawCert != null
|
certInfo.rawCert != null
|
||||||
? ConfigItem(
|
? ConfigItem(
|
||||||
label: Text('PEM Format'),
|
label: Text('PEM Format'),
|
||||||
content: SelectableText(certInfo.rawCert!, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
content: SelectableText(certInfo.rawCert!, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -161,30 +170,32 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: DangerButton(
|
child: DangerButton(
|
||||||
child: Text('Replace certificate'),
|
child: Text('Replace certificate'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
return AddCertificateScreen(
|
return AddCertificateScreen(
|
||||||
onReplace: (result) {
|
onReplace: (result) {
|
||||||
setState(() {
|
setState(() {
|
||||||
changed = true;
|
changed = true;
|
||||||
certResult = result;
|
certResult = result;
|
||||||
certInfo = result.certInfo;
|
certInfo = result.certInfo;
|
||||||
});
|
|
||||||
// Slam the page back to the top
|
|
||||||
controller.animateTo(0,
|
|
||||||
duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut);
|
|
||||||
},
|
|
||||||
pubKey: widget.pubKey!,
|
|
||||||
privKey: widget.privKey!,
|
|
||||||
supportsQRScanning: widget.supportsQRScanning,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
})));
|
// Slam the page back to the top
|
||||||
|
controller.animateTo(0, duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut);
|
||||||
|
},
|
||||||
|
pubKey: widget.pubKey!,
|
||||||
|
privKey: widget.privKey!,
|
||||||
|
supportsQRScanning: widget.supportsQRScanning,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDelete() {
|
Widget _buildDelete() {
|
||||||
|
@ -195,14 +206,18 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
||||||
var title = certInfo.cert.details.isCa ? 'Delete CA?' : 'Delete cert?';
|
var title = certInfo.cert.details.isCa ? 'Delete CA?' : 'Delete cert?';
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: DangerButton(
|
child: DangerButton(
|
||||||
child: Text('Delete'),
|
child: Text('Delete'),
|
||||||
onPressed: () => Utils.confirmDelete(context, title, () async {
|
onPressed:
|
||||||
Navigator.pop(context);
|
() => Utils.confirmDelete(context, title, () async {
|
||||||
widget.onDelete!();
|
Navigator.pop(context);
|
||||||
}))));
|
widget.onDelete!();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,7 @@ import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
|
|
||||||
class CipherScreen extends StatefulWidget {
|
class CipherScreen extends StatefulWidget {
|
||||||
const CipherScreen({
|
const CipherScreen({Key? key, required this.cipher, required this.onSave}) : super(key: key);
|
||||||
Key? key,
|
|
||||||
required this.cipher,
|
|
||||||
required this.onSave,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final String cipher;
|
final String cipher;
|
||||||
final ValueChanged<String> onSave;
|
final ValueChanged<String> onSave;
|
||||||
|
@ -32,15 +28,16 @@ class _CipherScreenState extends State<CipherScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormPage(
|
return FormPage(
|
||||||
title: 'Cipher Selection',
|
title: 'Cipher Selection',
|
||||||
changed: changed,
|
changed: changed,
|
||||||
onSave: () {
|
onSave: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
widget.onSave(cipher);
|
widget.onSave(cipher);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConfigSection(children: [
|
ConfigSection(
|
||||||
|
children: [
|
||||||
ConfigCheckboxItem(
|
ConfigCheckboxItem(
|
||||||
label: Text("aes"),
|
label: Text("aes"),
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
|
@ -62,9 +59,11 @@ class _CipherScreenState extends State<CipherScreen> {
|
||||||
cipher = "chachapoly";
|
cipher = "chachapoly";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
])
|
],
|
||||||
],
|
),
|
||||||
));
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,7 @@ import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
|
||||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||||
|
|
||||||
class LogVerbosityScreen extends StatefulWidget {
|
class LogVerbosityScreen extends StatefulWidget {
|
||||||
const LogVerbosityScreen({
|
const LogVerbosityScreen({Key? key, required this.verbosity, required this.onSave}) : super(key: key);
|
||||||
Key? key,
|
|
||||||
required this.verbosity,
|
|
||||||
required this.onSave,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final String verbosity;
|
final String verbosity;
|
||||||
final ValueChanged<String> onSave;
|
final ValueChanged<String> onSave;
|
||||||
|
@ -32,24 +28,27 @@ class _LogVerbosityScreenState extends State<LogVerbosityScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormPage(
|
return FormPage(
|
||||||
title: 'Log Verbosity',
|
title: 'Log Verbosity',
|
||||||
changed: changed,
|
changed: changed,
|
||||||
onSave: () {
|
onSave: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
widget.onSave(verbosity);
|
widget.onSave(verbosity);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConfigSection(children: [
|
ConfigSection(
|
||||||
|
children: [
|
||||||
_buildEntry('debug'),
|
_buildEntry('debug'),
|
||||||
_buildEntry('info'),
|
_buildEntry('info'),
|
||||||
_buildEntry('warning'),
|
_buildEntry('warning'),
|
||||||
_buildEntry('error'),
|
_buildEntry('error'),
|
||||||
_buildEntry('fatal'),
|
_buildEntry('fatal'),
|
||||||
_buildEntry('panic'),
|
_buildEntry('panic'),
|
||||||
])
|
],
|
||||||
],
|
),
|
||||||
));
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEntry(String title) {
|
Widget _buildEntry(String title) {
|
||||||
|
|
|
@ -7,11 +7,7 @@ class RenderedConfigScreen extends StatelessWidget {
|
||||||
final String config;
|
final String config;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
RenderedConfigScreen({
|
RenderedConfigScreen({Key? key, required this.config, required this.name}) : super(key: key);
|
||||||
Key? key,
|
|
||||||
required this.config,
|
|
||||||
required this.name,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -19,18 +15,21 @@ class RenderedConfigScreen extends StatelessWidget {
|
||||||
title: Text('Rendered Site Config'),
|
title: Text('Rendered Site Config'),
|
||||||
scrollable: SimpleScrollable.both,
|
scrollable: SimpleScrollable.both,
|
||||||
trailingActions: <Widget>[
|
trailingActions: <Widget>[
|
||||||
Builder(builder: (BuildContext context) {
|
Builder(
|
||||||
return PlatformIconButton(
|
builder: (BuildContext context) {
|
||||||
padding: EdgeInsets.zero,
|
return PlatformIconButton(
|
||||||
icon: Icon(context.platformIcons.share, size: 28.0),
|
padding: EdgeInsets.zero,
|
||||||
onPressed: () => Share.share(context, title: '$name.yaml', text: config, filename: '$name.yaml'),
|
icon: Icon(context.platformIcons.share, size: 28.0),
|
||||||
);
|
onPressed: () => Share.share(context, title: '$name.yaml', text: config, filename: '$name.yaml'),
|
||||||
}),
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.all(5),
|
padding: EdgeInsets.all(5),
|
||||||
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||||
child: SelectableText(config, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))),
|
child: SelectableText(config, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,29 +14,28 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scanWindow = Rect.fromCenter(
|
final scanWindow = Rect.fromCenter(center: MediaQuery.sizeOf(context).center(Offset.zero), width: 250, height: 250);
|
||||||
center: MediaQuery.sizeOf(context).center(Offset.zero),
|
|
||||||
width: 250,
|
|
||||||
height: 250,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Scan QR')),
|
appBar: AppBar(title: const Text('Scan QR')),
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Stack(fit: StackFit.expand, children: [
|
body: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
Center(
|
Center(
|
||||||
child: MobileScanner(
|
child: MobileScanner(
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
controller: cameraController,
|
controller: cameraController,
|
||||||
scanWindow: scanWindow,
|
scanWindow: scanWindow,
|
||||||
onDetect: (BarcodeCapture barcodes) {
|
onDetect: (BarcodeCapture barcodes) {
|
||||||
var barcode = barcodes.barcodes.firstOrNull;
|
var barcode = barcodes.barcodes.firstOrNull;
|
||||||
if (barcode != null && mounted) {
|
if (barcode != null && mounted) {
|
||||||
cameraController.stop().then((_) {
|
cameraController.stop().then((_) {
|
||||||
Navigator.pop(context, barcode.rawValue);
|
Navigator.pop(context, barcode.rawValue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ValueListenableBuilder(
|
ValueListenableBuilder(
|
||||||
valueListenable: cameraController,
|
valueListenable: cameraController,
|
||||||
|
@ -45,9 +44,7 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomPaint(
|
return CustomPaint(painter: ScannerOverlay(scanWindow: scanWindow));
|
||||||
painter: ScannerOverlay(scanWindow: scanWindow),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
|
@ -63,15 +60,14 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]));
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScannerOverlay extends CustomPainter {
|
class ScannerOverlay extends CustomPainter {
|
||||||
const ScannerOverlay({
|
const ScannerOverlay({required this.scanWindow, this.borderRadius = 12.0});
|
||||||
required this.scanWindow,
|
|
||||||
this.borderRadius = 12.0,
|
|
||||||
});
|
|
||||||
|
|
||||||
final Rect scanWindow;
|
final Rect scanWindow;
|
||||||
final double borderRadius;
|
final double borderRadius;
|
||||||
|
@ -81,32 +77,30 @@ class ScannerOverlay extends CustomPainter {
|
||||||
// we need to pass the size to the custom paint widget
|
// we need to pass the size to the custom paint widget
|
||||||
final backgroundPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
final backgroundPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||||
|
|
||||||
final cutoutPath = Path()
|
final cutoutPath =
|
||||||
..addRRect(
|
Path()..addRRect(
|
||||||
RRect.fromRectAndCorners(
|
RRect.fromRectAndCorners(
|
||||||
scanWindow,
|
scanWindow,
|
||||||
topLeft: Radius.circular(borderRadius),
|
topLeft: Radius.circular(borderRadius),
|
||||||
topRight: Radius.circular(borderRadius),
|
topRight: Radius.circular(borderRadius),
|
||||||
bottomLeft: Radius.circular(borderRadius),
|
bottomLeft: Radius.circular(borderRadius),
|
||||||
bottomRight: Radius.circular(borderRadius),
|
bottomRight: Radius.circular(borderRadius),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final backgroundPaint = Paint()
|
final backgroundPaint =
|
||||||
..color = Colors.black.withValues(alpha: 0.5)
|
Paint()
|
||||||
..style = PaintingStyle.fill
|
..color = Colors.black.withValues(alpha: 0.5)
|
||||||
..blendMode = BlendMode.srcOver;
|
..style = PaintingStyle.fill
|
||||||
|
..blendMode = BlendMode.srcOver;
|
||||||
|
|
||||||
final backgroundWithCutout = Path.combine(
|
final backgroundWithCutout = Path.combine(PathOperation.difference, backgroundPath, cutoutPath);
|
||||||
PathOperation.difference,
|
|
||||||
backgroundPath,
|
|
||||||
cutoutPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
final borderPaint = Paint()
|
final borderPaint =
|
||||||
..color = Colors.white
|
Paint()
|
||||||
..style = PaintingStyle.stroke
|
..color = Colors.white
|
||||||
..strokeWidth = 4.0;
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 4.0;
|
||||||
|
|
||||||
final borderRect = RRect.fromRectAndCorners(
|
final borderRect = RRect.fromRectAndCorners(
|
||||||
scanWindow,
|
scanWindow,
|
||||||
|
@ -214,14 +208,7 @@ class ToggleFlashlightButton extends StatelessWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
case TorchState.unavailable:
|
case TorchState.unavailable:
|
||||||
return const SizedBox.square(
|
return const SizedBox.square(dimension: 48.0, child: Icon(Icons.no_flash, size: 32.0, color: Colors.grey));
|
||||||
dimension: 48.0,
|
|
||||||
child: Icon(
|
|
||||||
Icons.no_flash,
|
|
||||||
size: 32.0,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,12 +23,8 @@ import 'package:mobile_nebula/services/utils.dart';
|
||||||
//TODO: Enforce a name
|
//TODO: Enforce a name
|
||||||
|
|
||||||
class SiteConfigScreen extends StatefulWidget {
|
class SiteConfigScreen extends StatefulWidget {
|
||||||
const SiteConfigScreen({
|
const SiteConfigScreen({Key? key, this.site, required this.onSave, required this.supportsQRScanning})
|
||||||
Key? key,
|
: super(key: key);
|
||||||
this.site,
|
|
||||||
required this.onSave,
|
|
||||||
required this.supportsQRScanning,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Site? site;
|
final Site? site;
|
||||||
|
|
||||||
|
@ -71,36 +67,39 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (pubKey == null || privKey == null) {
|
if (pubKey == null || privKey == null) {
|
||||||
return Center(
|
return Center(
|
||||||
child: fpw.PlatformCircularProgressIndicator(cupertino: (_, __) {
|
child: fpw.PlatformCircularProgressIndicator(
|
||||||
return fpw.CupertinoProgressIndicatorData(radius: 50);
|
cupertino: (_, __) {
|
||||||
}),
|
return fpw.CupertinoProgressIndicatorData(radius: 50);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FormPage(
|
return FormPage(
|
||||||
title: newSite ? 'New Site' : 'Edit Site',
|
title: newSite ? 'New Site' : 'Edit Site',
|
||||||
changed: changed,
|
changed: changed,
|
||||||
onSave: () async {
|
onSave: () async {
|
||||||
site.name = nameController.text;
|
site.name = nameController.text;
|
||||||
try {
|
try {
|
||||||
await site.save();
|
await site.save();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Utils.popError(context, 'Failed to save the site configuration', error.toString());
|
return Utils.popError(context, 'Failed to save the site configuration', error.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
widget.onSave(site);
|
widget.onSave(site);
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
_main(),
|
_main(),
|
||||||
_keys(),
|
_keys(),
|
||||||
_hosts(),
|
_hosts(),
|
||||||
_advanced(),
|
_advanced(),
|
||||||
_managed(),
|
_managed(),
|
||||||
kDebugMode ? _debugConfig() : Container(height: 0),
|
kDebugMode ? _debugConfig() : Container(height: 0),
|
||||||
],
|
],
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _debugConfig() {
|
Widget _debugConfig() {
|
||||||
|
@ -116,8 +115,9 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _main() {
|
Widget _main() {
|
||||||
return ConfigSection(children: <Widget>[
|
return ConfigSection(
|
||||||
ConfigItem(
|
children: <Widget>[
|
||||||
|
ConfigItem(
|
||||||
label: Text("Name"),
|
label: Text("Name"),
|
||||||
content: PlatformTextFormField(
|
content: PlatformTextFormField(
|
||||||
placeholder: 'Required',
|
placeholder: 'Required',
|
||||||
|
@ -128,8 +128,10 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
))
|
),
|
||||||
]);
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _managed() {
|
Widget _managed() {
|
||||||
|
@ -140,15 +142,19 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return site.managed
|
return site.managed
|
||||||
? ConfigSection(label: "MANAGED CONFIG", children: <Widget>[
|
? ConfigSection(
|
||||||
|
label: "MANAGED CONFIG",
|
||||||
|
children: <Widget>[
|
||||||
ConfigItem(
|
ConfigItem(
|
||||||
label: Text("Last Update"),
|
label: Text("Last Update"),
|
||||||
content:
|
content: Wrap(
|
||||||
Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
|
alignment: WrapAlignment.end,
|
||||||
Text(lastUpdate),
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
]),
|
children: <Widget>[Text(lastUpdate)],
|
||||||
)
|
),
|
||||||
])
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
: Container();
|
: Container();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,14 +177,19 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
children: [
|
children: [
|
||||||
ConfigPageItem(
|
ConfigPageItem(
|
||||||
label: Text('Certificate'),
|
label: Text('Certificate'),
|
||||||
content: Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
|
content: Wrap(
|
||||||
certError
|
alignment: WrapAlignment.end,
|
||||||
? Padding(
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
certError
|
||||||
|
? Padding(
|
||||||
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
||||||
padding: EdgeInsets.only(right: 5))
|
padding: EdgeInsets.only(right: 5),
|
||||||
: Container(),
|
)
|
||||||
certError ? Text('Needs attention') : Text(site.certInfo?.cert.details.name ?? 'Unknown certificate')
|
: Container(),
|
||||||
]),
|
certError ? Text('Needs attention') : Text(site.certInfo?.cert.details.name ?? 'Unknown certificate'),
|
||||||
|
],
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
if (site.certInfo != null) {
|
if (site.certInfo != null) {
|
||||||
|
@ -186,15 +197,16 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
certInfo: site.certInfo!,
|
certInfo: site.certInfo!,
|
||||||
pubKey: pubKey,
|
pubKey: pubKey,
|
||||||
privKey: privKey,
|
privKey: privKey,
|
||||||
onReplace: site.managed
|
onReplace:
|
||||||
? null
|
site.managed
|
||||||
: (result) {
|
? null
|
||||||
setState(() {
|
: (result) {
|
||||||
changed = true;
|
setState(() {
|
||||||
site.certInfo = result.certInfo;
|
changed = true;
|
||||||
site.key = result.key;
|
site.certInfo = result.certInfo;
|
||||||
});
|
site.key = result.key;
|
||||||
},
|
});
|
||||||
|
},
|
||||||
supportsQRScanning: widget.supportsQRScanning,
|
supportsQRScanning: widget.supportsQRScanning,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -215,32 +227,38 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ConfigPageItem(
|
ConfigPageItem(
|
||||||
label: Text("CA"),
|
label: Text("CA"),
|
||||||
content:
|
content: Wrap(
|
||||||
Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
|
alignment: WrapAlignment.end,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
caError
|
caError
|
||||||
? Padding(
|
? Padding(
|
||||||
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
||||||
padding: EdgeInsets.only(right: 5))
|
padding: EdgeInsets.only(right: 5),
|
||||||
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
caError ? Text('Needs attention') : Text(Utils.itemCountFormat(site.ca.length))
|
caError ? Text('Needs attention') : Text(Utils.itemCountFormat(site.ca.length)),
|
||||||
]),
|
],
|
||||||
onPressed: () {
|
),
|
||||||
Utils.openPage(context, (context) {
|
onPressed: () {
|
||||||
return CAListScreen(
|
Utils.openPage(context, (context) {
|
||||||
cas: site.ca,
|
return CAListScreen(
|
||||||
onSave: site.managed
|
cas: site.ca,
|
||||||
? null
|
onSave:
|
||||||
: (ca) {
|
site.managed
|
||||||
|
? null
|
||||||
|
: (ca) {
|
||||||
setState(() {
|
setState(() {
|
||||||
changed = true;
|
changed = true;
|
||||||
site.ca = ca;
|
site.ca = ca;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
supportsQRScanning: widget.supportsQRScanning,
|
supportsQRScanning: widget.supportsQRScanning,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
})
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -251,28 +269,35 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConfigPageItem(
|
ConfigPageItem(
|
||||||
label: Text('Hosts'),
|
label: Text('Hosts'),
|
||||||
content: Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
|
content: Wrap(
|
||||||
site.staticHostmap.length == 0
|
alignment: WrapAlignment.end,
|
||||||
? Padding(
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
site.staticHostmap.length == 0
|
||||||
|
? Padding(
|
||||||
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
||||||
padding: EdgeInsets.only(right: 5))
|
padding: EdgeInsets.only(right: 5),
|
||||||
: Container(),
|
)
|
||||||
site.staticHostmap.length == 0
|
: Container(),
|
||||||
? Text('Needs attention')
|
site.staticHostmap.length == 0
|
||||||
: Text(Utils.itemCountFormat(site.staticHostmap.length))
|
? Text('Needs attention')
|
||||||
]),
|
: Text(Utils.itemCountFormat(site.staticHostmap.length)),
|
||||||
|
],
|
||||||
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
return StaticHostsScreen(
|
return StaticHostsScreen(
|
||||||
hostmap: site.staticHostmap,
|
hostmap: site.staticHostmap,
|
||||||
onSave: site.managed
|
onSave:
|
||||||
? null
|
site.managed
|
||||||
: (map) {
|
? null
|
||||||
|
: (map) {
|
||||||
setState(() {
|
setState(() {
|
||||||
changed = true;
|
changed = true;
|
||||||
site.staticHostmap = map;
|
site.staticHostmap = map;
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -285,24 +310,26 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
||||||
label: "ADVANCED",
|
label: "ADVANCED",
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ConfigPageItem(
|
ConfigPageItem(
|
||||||
label: Text('Advanced'),
|
label: Text('Advanced'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Utils.openPage(context, (context) {
|
Utils.openPage(context, (context) {
|
||||||
return AdvancedScreen(
|
return AdvancedScreen(
|
||||||
site: site,
|
site: site,
|
||||||
onSave: (settings) {
|
onSave: (settings) {
|
||||||
setState(() {
|
setState(() {
|
||||||
changed = true;
|
changed = true;
|
||||||
site.cipher = settings.cipher;
|
site.cipher = settings.cipher;
|
||||||
site.lhDuration = settings.lhDuration;
|
site.lhDuration = settings.lhDuration;
|
||||||
site.port = settings.port;
|
site.port = settings.port;
|
||||||
site.logVerbosity = settings.verbosity;
|
site.logVerbosity = settings.verbosity;
|
||||||
site.unsafeRoutes = settings.unsafeRoutes;
|
site.unsafeRoutes = settings.unsafeRoutes;
|
||||||
site.mtu = settings.mtu;
|
site.mtu = settings.mtu;
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
})
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ class StaticHostmapScreen extends StatefulWidget {
|
||||||
this.lighthouse = false,
|
this.lighthouse = false,
|
||||||
this.onDelete,
|
this.onDelete,
|
||||||
required this.onSave,
|
required this.onSave,
|
||||||
}) : this.destinations = destinations ?? [],
|
}) : this.destinations = destinations ?? [],
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
final List<IPAndPort> destinations;
|
final List<IPAndPort> destinations;
|
||||||
final String nebulaIp;
|
final String nebulaIp;
|
||||||
|
@ -67,67 +67,81 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormPage(
|
return FormPage(
|
||||||
title: widget.onDelete == null
|
title:
|
||||||
? widget.onSave == null
|
widget.onDelete == null
|
||||||
? 'View Static Host'
|
? widget.onSave == null
|
||||||
: 'New Static Host'
|
? 'View Static Host'
|
||||||
: 'Edit Static Host',
|
: 'New Static Host'
|
||||||
changed: changed,
|
: 'Edit Static Host',
|
||||||
onSave: _onSave,
|
changed: changed,
|
||||||
child: Column(children: [
|
onSave: _onSave,
|
||||||
ConfigSection(label: 'Maps a nebula ip address to multiple real world addresses', children: <Widget>[
|
child: Column(
|
||||||
ConfigItem(
|
children: [
|
||||||
|
ConfigSection(
|
||||||
|
label: 'Maps a nebula ip address to multiple real world addresses',
|
||||||
|
children: <Widget>[
|
||||||
|
ConfigItem(
|
||||||
label: Text('Nebula IP'),
|
label: Text('Nebula IP'),
|
||||||
labelWidth: 200,
|
labelWidth: 200,
|
||||||
content: widget.onSave == null
|
content:
|
||||||
? Text(_nebulaIp, textAlign: TextAlign.end)
|
widget.onSave == null
|
||||||
: IPFormField(
|
? Text(_nebulaIp, textAlign: TextAlign.end)
|
||||||
help: "Required",
|
: IPFormField(
|
||||||
initialValue: _nebulaIp,
|
help: "Required",
|
||||||
ipOnly: true,
|
initialValue: _nebulaIp,
|
||||||
textAlign: TextAlign.end,
|
ipOnly: true,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
textAlign: TextAlign.end,
|
||||||
textInputAction: TextInputAction.next,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
onSaved: (v) {
|
textInputAction: TextInputAction.next,
|
||||||
if (v != null) {
|
onSaved: (v) {
|
||||||
_nebulaIp = v;
|
if (v != null) {
|
||||||
}
|
_nebulaIp = v;
|
||||||
})),
|
}
|
||||||
ConfigItem(
|
},
|
||||||
label: Text('Lighthouse'),
|
),
|
||||||
labelWidth: 200,
|
),
|
||||||
content: Container(
|
ConfigItem(
|
||||||
|
label: Text('Lighthouse'),
|
||||||
|
labelWidth: 200,
|
||||||
|
content: Container(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Switch.adaptive(
|
child: Switch.adaptive(
|
||||||
value: _lighthouse,
|
value: _lighthouse,
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
onChanged: widget.onSave == null
|
onChanged:
|
||||||
? null
|
widget.onSave == null
|
||||||
: (v) {
|
? null
|
||||||
|
: (v) {
|
||||||
setState(() {
|
setState(() {
|
||||||
changed = true;
|
changed = true;
|
||||||
_lighthouse = v;
|
_lighthouse = v;
|
||||||
});
|
});
|
||||||
})),
|
},
|
||||||
),
|
),
|
||||||
]),
|
),
|
||||||
ConfigSection(
|
),
|
||||||
label: 'List of public ips or dns names where for this host',
|
],
|
||||||
children: _buildHosts(),
|
|
||||||
),
|
),
|
||||||
|
ConfigSection(label: 'List of public ips or dns names where for this host', children: _buildHosts()),
|
||||||
widget.onDelete != null
|
widget.onDelete != null
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: DangerButton(
|
child: DangerButton(
|
||||||
child: Text('Delete'),
|
child: Text('Delete'),
|
||||||
onPressed: () => Utils.confirmDelete(context, 'Delete host map?', () {
|
onPressed:
|
||||||
Navigator.of(context).pop();
|
() => Utils.confirmDelete(context, 'Delete host map?', () {
|
||||||
widget.onDelete!();
|
Navigator.of(context).pop();
|
||||||
}))))
|
widget.onDelete!();
|
||||||
: Container()
|
}),
|
||||||
]));
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSave() {
|
_onSave() {
|
||||||
|
@ -147,47 +161,61 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
|
||||||
List<Widget> items = [];
|
List<Widget> items = [];
|
||||||
|
|
||||||
_destinations.forEach((key, dest) {
|
_destinations.forEach((key, dest) {
|
||||||
items.add(ConfigItem(
|
items.add(
|
||||||
key: key,
|
ConfigItem(
|
||||||
label: Align(
|
key: key,
|
||||||
|
label: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: widget.onSave == null
|
child:
|
||||||
? Container()
|
widget.onSave == null
|
||||||
: PlatformIconButton(
|
? Container()
|
||||||
padding: EdgeInsets.zero,
|
: PlatformIconButton(
|
||||||
icon: Icon(Icons.remove_circle, color: CupertinoColors.systemRed.resolveFrom(context)),
|
padding: EdgeInsets.zero,
|
||||||
onPressed: () => setState(() {
|
icon: Icon(Icons.remove_circle, color: CupertinoColors.systemRed.resolveFrom(context)),
|
||||||
_removeDestination(key);
|
onPressed:
|
||||||
_dismissKeyboard();
|
() => setState(() {
|
||||||
}))),
|
_removeDestination(key);
|
||||||
labelWidth: 70,
|
_dismissKeyboard();
|
||||||
content: Row(children: <Widget>[
|
}),
|
||||||
Expanded(
|
),
|
||||||
child: widget.onSave == null
|
),
|
||||||
? Text(dest.destination.toString(), textAlign: TextAlign.end)
|
labelWidth: 70,
|
||||||
: IPAndPortFormField(
|
content: Row(
|
||||||
ipHelp: 'public ip or name',
|
children: <Widget>[
|
||||||
ipTextAlign: TextAlign.end,
|
Expanded(
|
||||||
enableIPV6: true,
|
child:
|
||||||
noBorder: true,
|
widget.onSave == null
|
||||||
initialValue: dest.destination,
|
? Text(dest.destination.toString(), textAlign: TextAlign.end)
|
||||||
onSaved: (v) {
|
: IPAndPortFormField(
|
||||||
if (v != null) {
|
ipHelp: 'public ip or name',
|
||||||
dest.destination = v;
|
ipTextAlign: TextAlign.end,
|
||||||
}
|
enableIPV6: true,
|
||||||
},
|
noBorder: true,
|
||||||
)),
|
initialValue: dest.destination,
|
||||||
]),
|
onSaved: (v) {
|
||||||
));
|
if (v != null) {
|
||||||
|
dest.destination = v;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (widget.onSave != null) {
|
if (widget.onSave != null) {
|
||||||
items.add(ConfigButtonItem(
|
items.add(
|
||||||
|
ConfigButtonItem(
|
||||||
content: Text('Add another'),
|
content: Text('Add another'),
|
||||||
onPressed: () => setState(() {
|
onPressed:
|
||||||
|
() => setState(() {
|
||||||
_addDestination();
|
_addDestination();
|
||||||
_dismissKeyboard();
|
_dismissKeyboard();
|
||||||
})));
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
|
|
@ -18,20 +18,11 @@ class _Hostmap {
|
||||||
List<IPAndPort> destinations;
|
List<IPAndPort> destinations;
|
||||||
bool lighthouse;
|
bool lighthouse;
|
||||||
|
|
||||||
_Hostmap({
|
_Hostmap({required this.focusNode, required this.nebulaIp, required this.destinations, required this.lighthouse});
|
||||||
required this.focusNode,
|
|
||||||
required this.nebulaIp,
|
|
||||||
required this.destinations,
|
|
||||||
required this.lighthouse,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class StaticHostsScreen extends StatefulWidget {
|
class StaticHostsScreen extends StatefulWidget {
|
||||||
const StaticHostsScreen({
|
const StaticHostsScreen({Key? key, required this.hostmap, required this.onSave}) : super(key: key);
|
||||||
Key? key,
|
|
||||||
required this.hostmap,
|
|
||||||
required this.onSave,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final Map<String, StaticHost> hostmap;
|
final Map<String, StaticHost> hostmap;
|
||||||
final ValueChanged<Map<String, StaticHost>>? onSave;
|
final ValueChanged<Map<String, StaticHost>>? onSave;
|
||||||
|
@ -47,8 +38,12 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
widget.hostmap.forEach((key, map) {
|
widget.hostmap.forEach((key, map) {
|
||||||
_hostmap[UniqueKey()] =
|
_hostmap[UniqueKey()] = _Hostmap(
|
||||||
_Hostmap(focusNode: FocusNode(), nebulaIp: key, destinations: map.destinations, lighthouse: map.lighthouse);
|
focusNode: FocusNode(),
|
||||||
|
nebulaIp: key,
|
||||||
|
destinations: map.destinations,
|
||||||
|
lighthouse: map.lighthouse,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -57,12 +52,11 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormPage(
|
return FormPage(
|
||||||
title: 'Static Hosts',
|
title: 'Static Hosts',
|
||||||
changed: changed,
|
changed: changed,
|
||||||
onSave: _onSave,
|
onSave: _onSave,
|
||||||
child: ConfigSection(
|
child: ConfigSection(children: _buildHosts()),
|
||||||
children: _buildHosts(),
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSave() {
|
_onSave() {
|
||||||
|
@ -81,59 +75,73 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
|
||||||
final double ipWidth = Utils.textSize("000.000.000.000", CupertinoTheme.of(context).textTheme.textStyle).width + 32;
|
final double ipWidth = Utils.textSize("000.000.000.000", CupertinoTheme.of(context).textTheme.textStyle).width + 32;
|
||||||
List<Widget> items = [];
|
List<Widget> items = [];
|
||||||
_hostmap.forEach((key, host) {
|
_hostmap.forEach((key, host) {
|
||||||
items.add(ConfigPageItem(
|
items.add(
|
||||||
label: Row(children: <Widget>[
|
ConfigPageItem(
|
||||||
Padding(
|
label: Row(
|
||||||
child: Icon(host.lighthouse ? Icons.lightbulb_outline : Icons.computer,
|
children: <Widget>[
|
||||||
color: CupertinoColors.placeholderText.resolveFrom(context)),
|
Padding(
|
||||||
padding: EdgeInsets.only(right: 10)),
|
child: Icon(
|
||||||
Text(host.nebulaIp),
|
host.lighthouse ? Icons.lightbulb_outline : Icons.computer,
|
||||||
]),
|
color: CupertinoColors.placeholderText.resolveFrom(context),
|
||||||
labelWidth: ipWidth,
|
),
|
||||||
content: Text(host.destinations.length.toString() + ' items', textAlign: TextAlign.end),
|
padding: EdgeInsets.only(right: 10),
|
||||||
onPressed: () {
|
),
|
||||||
Utils.openPage(context, (context) {
|
Text(host.nebulaIp),
|
||||||
return StaticHostmapScreen(
|
],
|
||||||
|
),
|
||||||
|
labelWidth: ipWidth,
|
||||||
|
content: Text(host.destinations.length.toString() + ' items', textAlign: TextAlign.end),
|
||||||
|
onPressed: () {
|
||||||
|
Utils.openPage(context, (context) {
|
||||||
|
return StaticHostmapScreen(
|
||||||
nebulaIp: host.nebulaIp,
|
nebulaIp: host.nebulaIp,
|
||||||
destinations: host.destinations,
|
destinations: host.destinations,
|
||||||
lighthouse: host.lighthouse,
|
lighthouse: host.lighthouse,
|
||||||
onSave: widget.onSave == null
|
onSave:
|
||||||
? null
|
widget.onSave == null
|
||||||
: (map) {
|
? null
|
||||||
setState(() {
|
: (map) {
|
||||||
changed = true;
|
setState(() {
|
||||||
host.nebulaIp = map.nebulaIp;
|
changed = true;
|
||||||
host.destinations = map.destinations;
|
host.nebulaIp = map.nebulaIp;
|
||||||
host.lighthouse = map.lighthouse;
|
host.destinations = map.destinations;
|
||||||
});
|
host.lighthouse = map.lighthouse;
|
||||||
},
|
});
|
||||||
onDelete: widget.onSave == null
|
},
|
||||||
? null
|
onDelete:
|
||||||
: () {
|
widget.onSave == null
|
||||||
setState(() {
|
? null
|
||||||
changed = true;
|
: () {
|
||||||
_hostmap.remove(key);
|
setState(() {
|
||||||
});
|
changed = true;
|
||||||
});
|
_hostmap.remove(key);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
));
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (widget.onSave != null) {
|
if (widget.onSave != null) {
|
||||||
items.add(ConfigButtonItem(
|
items.add(
|
||||||
content: Text('Add a new entry'),
|
ConfigButtonItem(
|
||||||
onPressed: () {
|
content: Text('Add a new entry'),
|
||||||
Utils.openPage(context, (context) {
|
onPressed: () {
|
||||||
return StaticHostmapScreen(onSave: (map) {
|
Utils.openPage(context, (context) {
|
||||||
setState(() {
|
return StaticHostmapScreen(
|
||||||
changed = true;
|
onSave: (map) {
|
||||||
_addHostmap(map);
|
setState(() {
|
||||||
});
|
changed = true;
|
||||||
|
_addHostmap(map);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
},
|
),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
@ -141,7 +149,11 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
|
||||||
|
|
||||||
_addHostmap(Hostmap map) {
|
_addHostmap(Hostmap map) {
|
||||||
_hostmap[UniqueKey()] = (_Hostmap(
|
_hostmap[UniqueKey()] = (_Hostmap(
|
||||||
focusNode: FocusNode(), nebulaIp: map.nebulaIp, destinations: map.destinations, lighthouse: map.lighthouse));
|
focusNode: FocusNode(),
|
||||||
|
nebulaIp: map.nebulaIp,
|
||||||
|
destinations: map.destinations,
|
||||||
|
lighthouse: map.lighthouse,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -10,12 +10,7 @@ import 'package:mobile_nebula/models/UnsafeRoute.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
class UnsafeRouteScreen extends StatefulWidget {
|
class UnsafeRouteScreen extends StatefulWidget {
|
||||||
const UnsafeRouteScreen({
|
const UnsafeRouteScreen({Key? key, required this.route, required this.onSave, this.onDelete}) : super(key: key);
|
||||||
Key? key,
|
|
||||||
required this.route,
|
|
||||||
required this.onSave,
|
|
||||||
this.onDelete,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final UnsafeRoute route;
|
final UnsafeRoute route;
|
||||||
final ValueChanged<UnsafeRoute> onSave;
|
final ValueChanged<UnsafeRoute> onSave;
|
||||||
|
@ -44,67 +39,79 @@ class _UnsafeRouteScreenState extends State<UnsafeRouteScreen> {
|
||||||
var routeCIDR = route.route == null ? CIDR() : CIDR.fromString(route.route!);
|
var routeCIDR = route.route == null ? CIDR() : CIDR.fromString(route.route!);
|
||||||
|
|
||||||
return FormPage(
|
return FormPage(
|
||||||
title: widget.onDelete == null ? 'New Unsafe Route' : 'Edit Unsafe Route',
|
title: widget.onDelete == null ? 'New Unsafe Route' : 'Edit Unsafe Route',
|
||||||
changed: changed,
|
changed: changed,
|
||||||
onSave: _onSave,
|
onSave: _onSave,
|
||||||
child: Column(children: [
|
child: Column(
|
||||||
ConfigSection(children: <Widget>[
|
children: [
|
||||||
ConfigItem(
|
ConfigSection(
|
||||||
|
children: <Widget>[
|
||||||
|
ConfigItem(
|
||||||
label: Text('Route'),
|
label: Text('Route'),
|
||||||
content: CIDRFormField(
|
content: CIDRFormField(
|
||||||
initialValue: routeCIDR,
|
initialValue: routeCIDR,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
focusNode: routeFocus,
|
focusNode: routeFocus,
|
||||||
nextFocusNode: viaFocus,
|
nextFocusNode: viaFocus,
|
||||||
onSaved: (v) {
|
onSaved: (v) {
|
||||||
route.route = v.toString();
|
route.route = v.toString();
|
||||||
})),
|
},
|
||||||
ConfigItem(
|
),
|
||||||
|
),
|
||||||
|
ConfigItem(
|
||||||
label: Text('Via'),
|
label: Text('Via'),
|
||||||
content: IPFormField(
|
content: IPFormField(
|
||||||
initialValue: route.via ?? '',
|
initialValue: route.via ?? '',
|
||||||
ipOnly: true,
|
ipOnly: true,
|
||||||
help: 'nebula ip',
|
help: 'nebula ip',
|
||||||
textAlign: TextAlign.end,
|
textAlign: TextAlign.end,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
focusNode: viaFocus,
|
focusNode: viaFocus,
|
||||||
nextFocusNode: mtuFocus,
|
nextFocusNode: mtuFocus,
|
||||||
onSaved: (v) {
|
onSaved: (v) {
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
route.via = v;
|
route.via = v;
|
||||||
}
|
}
|
||||||
})),
|
},
|
||||||
//TODO: Android doesn't appear to support route based MTU, figure this out
|
),
|
||||||
// ConfigItem(
|
),
|
||||||
// label: Text('MTU'),
|
//TODO: Android doesn't appear to support route based MTU, figure this out
|
||||||
// content: PlatformTextFormField(
|
// ConfigItem(
|
||||||
// placeholder: "",
|
// label: Text('MTU'),
|
||||||
// validator: mtuValidator(false),
|
// content: PlatformTextFormField(
|
||||||
// keyboardType: TextInputType.number,
|
// placeholder: "",
|
||||||
// inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
|
// validator: mtuValidator(false),
|
||||||
// initialValue: route?.mtu.toString(),
|
// keyboardType: TextInputType.number,
|
||||||
// textAlign: TextAlign.end,
|
// inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
|
||||||
// textInputAction: TextInputAction.done,
|
// initialValue: route?.mtu.toString(),
|
||||||
// focusNode: mtuFocus,
|
// textAlign: TextAlign.end,
|
||||||
// onSaved: (v) {
|
// textInputAction: TextInputAction.done,
|
||||||
// route.mtu = int.tryParse(v);
|
// focusNode: mtuFocus,
|
||||||
// })),
|
// onSaved: (v) {
|
||||||
]),
|
// route.mtu = int.tryParse(v);
|
||||||
|
// })),
|
||||||
|
],
|
||||||
|
),
|
||||||
widget.onDelete != null
|
widget.onDelete != null
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: DangerButton(
|
child: DangerButton(
|
||||||
child: Text('Delete'),
|
child: Text('Delete'),
|
||||||
onPressed: () => Utils.confirmDelete(context, 'Delete unsafe route?', () {
|
onPressed:
|
||||||
|
() => Utils.confirmDelete(context, 'Delete unsafe route?', () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
widget.onDelete!();
|
widget.onDelete!();
|
||||||
}),
|
}),
|
||||||
)))
|
),
|
||||||
: Container()
|
),
|
||||||
]));
|
)
|
||||||
|
: Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSave() {
|
_onSave() {
|
||||||
|
|
|
@ -8,11 +8,7 @@ import 'package:mobile_nebula/screens/siteConfig/UnsafeRouteScreen.dart';
|
||||||
import 'package:mobile_nebula/services/utils.dart';
|
import 'package:mobile_nebula/services/utils.dart';
|
||||||
|
|
||||||
class UnsafeRoutesScreen extends StatefulWidget {
|
class UnsafeRoutesScreen extends StatefulWidget {
|
||||||
const UnsafeRoutesScreen({
|
const UnsafeRoutesScreen({Key? key, required this.unsafeRoutes, required this.onSave}) : super(key: key);
|
||||||
Key? key,
|
|
||||||
required this.unsafeRoutes,
|
|
||||||
required this.onSave,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
final List<UnsafeRoute> unsafeRoutes;
|
final List<UnsafeRoute> unsafeRoutes;
|
||||||
final ValueChanged<List<UnsafeRoute>>? onSave;
|
final ValueChanged<List<UnsafeRoute>>? onSave;
|
||||||
|
@ -38,12 +34,11 @@ class _UnsafeRoutesScreenState extends State<UnsafeRoutesScreen> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FormPage(
|
return FormPage(
|
||||||
title: 'Unsafe Routes',
|
title: 'Unsafe Routes',
|
||||||
changed: changed,
|
changed: changed,
|
||||||
onSave: _onSave,
|
onSave: _onSave,
|
||||||
child: ConfigSection(
|
child: ConfigSection(children: _buildRoutes()),
|
||||||
children: _buildRoutes(),
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSave() {
|
_onSave() {
|
||||||
|
@ -57,14 +52,15 @@ class _UnsafeRoutesScreenState extends State<UnsafeRoutesScreen> {
|
||||||
final double ipWidth = Utils.textSize("000.000.000.000/00", CupertinoTheme.of(context).textTheme.textStyle).width;
|
final double ipWidth = Utils.textSize("000.000.000.000/00", CupertinoTheme.of(context).textTheme.textStyle).width;
|
||||||
List<Widget> items = [];
|
List<Widget> items = [];
|
||||||
unsafeRoutes.forEach((key, route) {
|
unsafeRoutes.forEach((key, route) {
|
||||||
items.add(ConfigPageItem(
|
items.add(
|
||||||
disabled: widget.onSave == null,
|
ConfigPageItem(
|
||||||
label: Text(route.route ?? ''),
|
disabled: widget.onSave == null,
|
||||||
labelWidth: ipWidth,
|
label: Text(route.route ?? ''),
|
||||||
content: Text('via ${route.via}', textAlign: TextAlign.end),
|
labelWidth: ipWidth,
|
||||||
onPressed: () {
|
content: Text('via ${route.via}', textAlign: TextAlign.end),
|
||||||
Utils.openPage(context, (context) {
|
onPressed: () {
|
||||||
return UnsafeRouteScreen(
|
Utils.openPage(context, (context) {
|
||||||
|
return UnsafeRouteScreen(
|
||||||
route: route,
|
route: route,
|
||||||
onSave: (route) {
|
onSave: (route) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -77,28 +73,33 @@ class _UnsafeRoutesScreenState extends State<UnsafeRoutesScreen> {
|
||||||
changed = true;
|
changed = true;
|
||||||
unsafeRoutes.remove(key);
|
unsafeRoutes.remove(key);
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
},
|
});
|
||||||
));
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (widget.onSave != null) {
|
if (widget.onSave != null) {
|
||||||
items.add(ConfigButtonItem(
|
items.add(
|
||||||
content: Text('Add a new route'),
|
ConfigButtonItem(
|
||||||
onPressed: () {
|
content: Text('Add a new route'),
|
||||||
Utils.openPage(context, (context) {
|
onPressed: () {
|
||||||
return UnsafeRouteScreen(
|
Utils.openPage(context, (context) {
|
||||||
|
return UnsafeRouteScreen(
|
||||||
route: UnsafeRoute(),
|
route: UnsafeRoute(),
|
||||||
onSave: (route) {
|
onSave: (route) {
|
||||||
setState(() {
|
setState(() {
|
||||||
changed = true;
|
changed = true;
|
||||||
unsafeRoutes[UniqueKey()] = route;
|
unsafeRoutes[UniqueKey()] = route;
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
},
|
});
|
||||||
));
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
|
|
|
@ -40,8 +40,12 @@ class Share {
|
||||||
/// - title: Title of message or subject if sending an email
|
/// - title: Title of message or subject if sending an email
|
||||||
/// - filePath: Path to the file to share
|
/// - filePath: Path to the file to share
|
||||||
/// - filename: An optional filename to override the existing file
|
/// - filename: An optional filename to override the existing file
|
||||||
static Future<bool> shareFile(BuildContext context,
|
static Future<bool> shareFile(
|
||||||
{required String title, required String filePath, String? filename}) async {
|
BuildContext context, {
|
||||||
|
required String title,
|
||||||
|
required String filePath,
|
||||||
|
String? filename,
|
||||||
|
}) async {
|
||||||
assert(title.isNotEmpty);
|
assert(title.isNotEmpty);
|
||||||
assert(filePath.isNotEmpty);
|
assert(filePath.isNotEmpty);
|
||||||
|
|
||||||
|
@ -51,8 +55,11 @@ class Share {
|
||||||
// If we want to support that again we will need to save the file to a temporary directory, share that,
|
// If we want to support that again we will need to save the file to a temporary directory, share that,
|
||||||
// and then delete it
|
// and then delete it
|
||||||
final xFile = sp.XFile(filePath, name: filename);
|
final xFile = sp.XFile(filePath, name: filename);
|
||||||
final result = await sp.Share.shareXFiles([xFile],
|
final result = await sp.Share.shareXFiles(
|
||||||
subject: title, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
|
[xFile],
|
||||||
|
subject: title,
|
||||||
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
|
);
|
||||||
return result.status == sp.ShareResultStatus.success;
|
return result.status == sp.ShareResultStatus.success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,14 @@ class Storage {
|
||||||
|
|
||||||
var completer = Completer<List<FileSystemEntity>>();
|
var completer = Completer<List<FileSystemEntity>>();
|
||||||
|
|
||||||
Directory(parent).list().listen((FileSystemEntity entity) {
|
Directory(parent)
|
||||||
list.add(entity);
|
.list()
|
||||||
}).onDone(() {
|
.listen((FileSystemEntity entity) {
|
||||||
completer.complete(list);
|
list.add(entity);
|
||||||
});
|
})
|
||||||
|
.onDone(() {
|
||||||
|
completer.complete(list);
|
||||||
|
});
|
||||||
|
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
|
@ -339,16 +339,13 @@ class MaterialTheme {
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeData theme(ColorScheme colorScheme) => ThemeData(
|
ThemeData theme(ColorScheme colorScheme) => ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
brightness: colorScheme.brightness,
|
brightness: colorScheme.brightness,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
textTheme: textTheme.apply(
|
textTheme: textTheme.apply(bodyColor: colorScheme.onSurface, displayColor: colorScheme.onSurface),
|
||||||
bodyColor: colorScheme.onSurface,
|
scaffoldBackgroundColor: colorScheme.surface,
|
||||||
displayColor: colorScheme.onSurface,
|
canvasColor: colorScheme.surface,
|
||||||
),
|
);
|
||||||
scaffoldBackgroundColor: colorScheme.surface,
|
|
||||||
canvasColor: colorScheme.surface,
|
|
||||||
);
|
|
||||||
|
|
||||||
List<ExtendedColor> get extendedColors => [];
|
List<ExtendedColor> get extendedColors => [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,20 +27,16 @@ class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Size textSize(String text, TextStyle style) {
|
static Size textSize(String text, TextStyle style) {
|
||||||
final TextPainter textPainter =
|
final TextPainter textPainter = TextPainter(
|
||||||
TextPainter(text: TextSpan(text: text, style: style), maxLines: 1, textDirection: TextDirection.ltr)
|
text: TextSpan(text: text, style: style),
|
||||||
..layout(minWidth: 0, maxWidth: double.infinity);
|
maxLines: 1,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
)..layout(minWidth: 0, maxWidth: double.infinity);
|
||||||
return textPainter.size;
|
return textPainter.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
static openPage(BuildContext context, WidgetBuilder pageToDisplayBuilder) {
|
static openPage(BuildContext context, WidgetBuilder pageToDisplayBuilder) {
|
||||||
Navigator.push(
|
Navigator.push(context, platformPageRoute(context: context, builder: pageToDisplayBuilder));
|
||||||
context,
|
|
||||||
platformPageRoute(
|
|
||||||
context: context,
|
|
||||||
builder: pageToDisplayBuilder,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static String itemCountFormat(int items, {singleSuffix = "item", multiSuffix = "items"}) {
|
static String itemCountFormat(int items, {singleSuffix = "item", multiSuffix = "items"}) {
|
||||||
|
@ -56,14 +52,15 @@ class Utils {
|
||||||
static Widget leadingBackWidget(BuildContext context, {label = 'Back', Function? onPressed}) {
|
static Widget leadingBackWidget(BuildContext context, {label = 'Back', Function? onPressed}) {
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
return CupertinoNavigationBarBackButton(
|
return CupertinoNavigationBarBackButton(
|
||||||
previousPageTitle: label,
|
previousPageTitle: label,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (onPressed == null) {
|
if (onPressed == null) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
} else {
|
} else {
|
||||||
onPressed();
|
onPressed();
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
|
@ -82,37 +79,47 @@ class Utils {
|
||||||
|
|
||||||
static Widget trailingSaveWidget(BuildContext context, Function onPressed) {
|
static Widget trailingSaveWidget(BuildContext context, Function onPressed) {
|
||||||
return PlatformTextButton(
|
return PlatformTextButton(
|
||||||
child: Text('Save'), padding: Platform.isAndroid ? null : EdgeInsets.zero, onPressed: () => onPressed());
|
child: Text('Save'),
|
||||||
|
padding: Platform.isAndroid ? null : EdgeInsets.zero,
|
||||||
|
onPressed: () => onPressed(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple cross platform delete confirmation dialog - can also be used to confirm throwing away a change by swapping the deleteLabel
|
/// Simple cross platform delete confirmation dialog - can also be used to confirm throwing away a change by swapping the deleteLabel
|
||||||
static confirmDelete(BuildContext context, String title, Function onConfirm,
|
static confirmDelete(
|
||||||
{String deleteLabel = 'Delete', String cancelLabel = 'Cancel'}) {
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
Function onConfirm, {
|
||||||
|
String deleteLabel = 'Delete',
|
||||||
|
String cancelLabel = 'Cancel',
|
||||||
|
}) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return PlatformAlertDialog(
|
return PlatformAlertDialog(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
PlatformDialogAction(
|
PlatformDialogAction(
|
||||||
child: Text(deleteLabel,
|
child: Text(
|
||||||
style:
|
deleteLabel,
|
||||||
TextStyle(fontWeight: FontWeight.bold, color: CupertinoColors.systemRed.resolveFrom(context))),
|
style: TextStyle(fontWeight: FontWeight.bold, color: CupertinoColors.systemRed.resolveFrom(context)),
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
onConfirm();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
PlatformDialogAction(
|
onPressed: () {
|
||||||
child: Text(cancelLabel),
|
Navigator.pop(context);
|
||||||
onPressed: () {
|
onConfirm();
|
||||||
Navigator.of(context).pop();
|
},
|
||||||
},
|
),
|
||||||
)
|
PlatformDialogAction(
|
||||||
],
|
child: Text(cancelLabel),
|
||||||
);
|
onPressed: () {
|
||||||
});
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static popError(BuildContext context, String title, String error, {StackTrace? stack}) {
|
static popError(BuildContext context, String title, String error, {StackTrace? stack}) {
|
||||||
|
@ -121,33 +128,38 @@ class Utils {
|
||||||
}
|
}
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
return AlertDialog(title: Text(title), content: Text(error), actions: <Widget>[
|
return AlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(error),
|
||||||
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text('Ok'),
|
child: Text('Ok'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CupertinoAlertDialog(
|
|
||||||
title: Text(title),
|
|
||||||
content: Text(error),
|
|
||||||
actions: <Widget>[
|
|
||||||
CupertinoDialogAction(
|
|
||||||
child: Text('Ok'),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
return CupertinoAlertDialog(
|
||||||
|
title: Text(title),
|
||||||
|
content: Text(error),
|
||||||
|
actions: <Widget>[
|
||||||
|
CupertinoDialogAction(
|
||||||
|
child: Text('Ok'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static launchUrl(String url, BuildContext context) async {
|
static launchUrl(String url, BuildContext context) async {
|
||||||
|
|
Loading…
Reference in a new issue