Flutter formatting changes (#252)

* `flutter fmt lib/`

* Re-enable formatting in CI
This commit is contained in:
Caleb Jasik 2025-02-13 15:37:44 -06:00 committed by GitHub
parent b2ebe0289a
commit ed348ab126
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 2397 additions and 2125 deletions

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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),
),
);
} }
} }
} }

View file

@ -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();
}, }),
)
]; ];
} }
} }

View file

@ -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

View file

@ -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) {

View file

@ -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','), '.');
},
);
} }
} }

View file

@ -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;

View file

@ -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;

View file

@ -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),
);
} }
} }

View file

@ -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),
],
),
),
);
} }
} }

View file

@ -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, ),
)) ),
]))); );
} }
} }

View file

@ -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) {

View file

@ -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,
);
} }
} }

View file

@ -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),
),
);
} }
} }
} }

View file

@ -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),
)); ),
);
} }
} }

View file

@ -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(

View file

@ -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),
),
), ),
); );
} }

View file

@ -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))),
], ],
)); ),
);
} }
} }

View file

@ -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),
], ],
)), ),
),
); );
} }
} }

View file

@ -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),
),
],
);
} }
} }

View file

@ -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;

View file

@ -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;
}, },
), ),
); );
} }
} }

View file

@ -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]),
);
} }
} }

View file

@ -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'];
} }

View file

@ -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() {

View file

@ -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,
);
} }
} }

View file

@ -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;

View file

@ -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,
};
} }
} }

View file

@ -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,
};
} }
} }

View file

@ -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, ],
)), ),
]),
); );
} }

View file

@ -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))]),
]); ),
],
);
} }
} }

View file

@ -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 {

View file

@ -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),
),
],
),
), ),
), ),
), ),

View file

@ -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) {

View file

@ -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));
} }
} }

View file

@ -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 {

View file

@ -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) {

View file

@ -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() {

View file

@ -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) {

View file

@ -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());
} }
}, },
) ),
], ],
) ),
])); ],
),
);
} }
} }

View file

@ -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> {
}); });
} }
}, },
) ),
], ],
) ),
]; ];
} }
} }

View file

@ -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!();
}),
),
),
);
} }
} }

View file

@ -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";
}); });
}, },
) ),
]) ],
], ),
)); ],
),
);
} }
} }

View file

@ -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) {

View file

@ -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)),
),
); );
} }
} }

View file

@ -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,
),
);
} }
}, },
); );

View file

@ -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;
}); });
}); },
}); );
}) });
},
),
], ],
); );
} }

View file

@ -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;

View file

@ -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

View file

@ -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() {

View file

@ -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;

View file

@ -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;
} }
} }

View file

@ -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;
} }

View file

@ -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 => [];
} }

View file

@ -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 {