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:
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
# run: dart format -l120 lib/ --set-exit-if-changed --suppress-analytics --output none
- name: Check formating
run: dart format -l120 lib/ --set-exit-if-changed --suppress-analytics --output none

View file

@ -53,7 +53,8 @@ class _CIDRFieldState extends State<CIDRField> {
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
return Container(
child: Row(children: <Widget>[
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(6, 6, 2, 6),
@ -74,7 +75,9 @@ class _CIDRFieldState extends State<CIDRField> {
widget.onChanged!(cidr);
},
controller: widget.ipController,
))),
),
),
),
Text("/"),
Container(
width: Utils.textSize("bits", textStyle).width + 12,
@ -96,8 +99,11 @@ class _CIDRFieldState extends State<CIDRField> {
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textInputAction: widget.textInputAction ?? TextInputAction.done,
placeholder: 'bits',
))
]));
),
),
],
),
);
}
@override

View file

@ -46,7 +46,9 @@ class CIDRFormField extends FormField<CIDR> {
field.didChange(value);
}
return Column(crossAxisAlignment: CrossAxisAlignment.end, children: <Widget>[
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
CIDRField(
autoFocus: autoFocus,
focusNode: focusNode,
@ -57,12 +59,16 @@ class CIDRFormField extends FormField<CIDR> {
bitsController: state._effectiveBitsController,
),
field.hasError
? Text(field.errorText ?? "Unknown error",
? Text(
field.errorText ?? "Unknown error",
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
textAlign: TextAlign.end)
: Container(height: 0)
]);
});
textAlign: TextAlign.end,
)
: Container(height: 0),
],
);
},
);
final TextEditingController? ipController;
final TextEditingController? bitsController;

View file

@ -17,14 +17,20 @@ class DangerButton extends StatelessWidget {
child: child,
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError));
foregroundColor: Theme.of(context).colorScheme.onError,
),
);
} else {
// Workaround for https://github.com/flutter/flutter/issues/161590
final themeData = CupertinoTheme.of(context);
return CupertinoTheme(
data: themeData.copyWith(primaryColor: CupertinoColors.white),
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
class FormPage extends StatefulWidget {
const FormPage(
{Key? key,
const FormPage({
Key? key,
required this.title,
required this.child,
required this.onSave,
required this.changed,
this.hideSave = false,
this.scrollController})
: super(key: key);
this.scrollController,
}) : super(key: key);
final String title;
final Function onSave;
@ -46,9 +46,15 @@ class _FormPageState extends State<FormPage> {
}
final NavigatorState navigator = Navigator.of(context);
Utils.confirmDelete(context, 'Discard changes?', () {
Utils.confirmDelete(
context,
'Discard changes?',
() {
navigator.pop();
}, deleteLabel: 'Yes', cancelLabel: 'No');
},
deleteLabel: 'Yes',
cancelLabel: 'No',
);
},
child: SimplePage(
leadingAction: _buildLeader(context),
@ -57,24 +63,37 @@ class _FormPageState extends State<FormPage> {
title: Text(widget.title),
child: Form(
key: _formKey,
onChanged: () => setState(() {
onChanged:
() => setState(() {
changed = true;
}),
child: widget.child),
));
child: widget.child,
),
),
);
}
Widget _buildLeader(BuildContext context) {
return Utils.leadingBackWidget(context, label: changed ? 'Cancel' : 'Back', onPressed: () {
return Utils.leadingBackWidget(
context,
label: changed ? 'Cancel' : 'Back',
onPressed: () {
if (changed) {
Utils.confirmDelete(context, 'Discard changes?', () {
Utils.confirmDelete(
context,
'Discard changes?',
() {
changed = false;
Navigator.pop(context);
}, deleteLabel: 'Yes', cancelLabel: 'No');
},
deleteLabel: 'Yes',
cancelLabel: 'No',
);
} else {
Navigator.pop(context);
}
});
},
);
}
List<Widget> _buildTrailer(BuildContext context) {
@ -83,9 +102,7 @@ class _FormPageState extends State<FormPage> {
}
return [
Utils.trailingSaveWidget(
context,
() {
Utils.trailingSaveWidget(context, () {
if (_formKey.currentState == null) {
return;
}
@ -96,8 +113,7 @@ class _FormPageState extends State<FormPage> {
_formKey.currentState!.save();
widget.onSave();
},
)
}),
];
}
}

View file

@ -59,7 +59,8 @@ class _IPAndPortFieldState extends State<IPAndPortField> {
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
return Container(
child: Row(children: <Widget>[
child: Row(
children: <Widget>[
Expanded(
child: Padding(
padding: EdgeInsets.fromLTRB(6, 6, 2, 6),
@ -76,7 +77,9 @@ class _IPAndPortFieldState extends State<IPAndPortField> {
},
textAlign: widget.ipTextAlign,
controller: widget.ipController,
))),
),
),
),
Text(":"),
Container(
width: Utils.textSize("00000", textStyle).width + 12,
@ -94,8 +97,11 @@ class _IPAndPortFieldState extends State<IPAndPortField> {
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textInputAction: TextInputAction.done,
placeholder: 'port',
))
]));
),
),
],
),
);
}
@override

View file

@ -52,7 +52,8 @@ class IPAndPortFormField extends FormField<IPAndPort> {
field.didChange(value);
}
return Column(children: <Widget>[
return Column(
children: <Widget>[
IPAndPortField(
ipOnly: ipOnly,
ipHelp: ipHelp,
@ -67,11 +68,15 @@ class IPAndPortFormField extends FormField<IPAndPort> {
ipTextAlign: ipTextAlign,
),
field.hasError
? Text(field.errorText!,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13))
: Container(height: 0)
]);
});
? Text(
field.errorText!,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
)
: Container(height: 0),
],
);
},
);
final TextEditingController? ipController;
final TextEditingController? portController;
@ -109,8 +114,10 @@ class _IPAndPortFormField extends FormFieldState<IPAndPort> {
@override
void didUpdateWidget(IPAndPortFormField oldWidget) {
super.didUpdateWidget(oldWidget);
var update =
IPAndPort(ip: widget.ipController?.text, port: int.tryParse(widget.portController?.text ?? "") ?? null);
var update = IPAndPort(
ip: widget.ipController?.text,
port: int.tryParse(widget.portController?.text ?? "") ?? null,
);
bool shouldUpdate = false;
if (widget.ipController != oldWidget.ipController) {

View file

@ -16,8 +16,8 @@ class IPField extends StatelessWidget {
final controller;
final textAlign;
const IPField(
{Key? key,
const IPField({
Key? key,
this.ipOnly = false,
this.help = "ip address",
this.autoFocus = false,
@ -27,8 +27,8 @@ class IPField extends StatelessWidget {
this.textPadding = const EdgeInsets.all(6.0),
this.textInputAction,
this.controller,
this.textAlign = TextAlign.center})
: super(key: key);
this.textAlign = TextAlign.center,
}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -50,7 +50,8 @@ class IPField extends StatelessWidget {
inputFormatters: ipOnly ? [IPTextInputFormatter()] : [FilteringTextInputFormatter.allow(RegExp(r'[^\s]+'))],
textInputAction: this.textInputAction,
placeholder: help,
));
),
);
}
}
@ -59,16 +60,13 @@ class IPTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
return _selectionAwareTextManipulation(
newValue,
(String substring) {
return _selectionAwareTextManipulation(newValue, (String substring) {
return whitelistedPattern
.allMatches(substring)
.map<String>((Match match) => match.group(0)!)
.join()
.replaceAll(RegExp(r','), '.');
},
);
});
}
}

View file

@ -50,7 +50,9 @@ class IPFormField extends FormField<String> {
field.didChange(value);
}
return Column(crossAxisAlignment: crossAxisAlignment, children: <Widget>[
return Column(
crossAxisAlignment: crossAxisAlignment,
children: <Widget>[
IPField(
ipOnly: ipOnly,
help: help,
@ -61,16 +63,19 @@ class IPFormField extends FormField<String> {
textPadding: textPadding,
textInputAction: textInputAction,
controller: state._effectiveController,
textAlign: textAlign),
textAlign: textAlign,
),
field.hasError
? Text(
field.errorText!,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
textAlign: textAlign,
)
: Container(height: 0)
]);
});
: Container(height: 0),
],
);
},
);
final TextEditingController? controller;

View file

@ -5,8 +5,8 @@ import 'package:mobile_nebula/components/SpecialTextField.dart';
class PlatformTextFormField extends FormField<String> {
//TODO: auto-validate, enabled?
PlatformTextFormField(
{Key? key,
PlatformTextFormField({
Key? key,
widgetKey,
this.controller,
focusNode,
@ -28,8 +28,8 @@ class PlatformTextFormField extends FormField<String> {
String? initialValue,
String? placeholder,
FormFieldValidator<String>? validator,
ValueChanged<String?>? onSaved})
: super(
ValueChanged<String?>? onSaved,
}) : super(
key: key,
initialValue: controller != null ? controller.text : (initialValue ?? ''),
onSaved: onSaved,
@ -50,7 +50,9 @@ class PlatformTextFormField extends FormField<String> {
field.didChange(value);
}
return Column(crossAxisAlignment: CrossAxisAlignment.end, children: <Widget>[
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
SpecialTextField(
key: widgetKey,
controller: state._effectiveController,
@ -70,16 +72,19 @@ class PlatformTextFormField extends FormField<String> {
textAlignVertical: textAlignVertical,
placeholder: placeholder,
inputFormatters: inputFormatters,
suffix: suffix),
suffix: suffix,
),
field.hasError
? Text(
field.errorText!,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(field.context), fontSize: 13),
textAlign: textAlign,
)
: Container(height: 0)
]);
});
: Container(height: 0),
],
);
},
);
final TextEditingController? controller;

View file

@ -2,16 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
enum SimpleScrollable {
none,
vertical,
horizontal,
both,
}
enum SimpleScrollable { none, vertical, horizontal, both }
class SimplePage extends StatelessWidget {
const SimplePage(
{Key? key,
const SimplePage({
Key? key,
required this.title,
required this.child,
this.leadingAction,
@ -23,8 +18,8 @@ class SimplePage extends StatelessWidget {
this.onRefresh,
this.onLoading,
this.alignment,
this.refreshController})
: super(key: key);
this.refreshController,
}) : super(key: key);
final Widget title;
final Widget child;
@ -54,7 +49,8 @@ class SimplePage extends StatelessWidget {
realChild = SingleChildScrollView(
scrollDirection: Axis.vertical,
child: realChild,
controller: refreshController == null ? scrollController : null);
controller: refreshController == null ? scrollController : null,
);
addScrollbar = true;
}
@ -77,7 +73,8 @@ class SimplePage extends StatelessWidget {
enablePullUp: onLoading != null,
enablePullDown: onRefresh != null,
footer: ClassicFooter(loadStyle: LoadStyle.ShowWhenLoading),
));
),
);
addScrollbar = true;
}
@ -90,10 +87,7 @@ class SimplePage extends StatelessWidget {
}
if (bottomBar != null) {
realChild = Column(children: [
Expanded(child: realChild),
bottomBar!,
]);
realChild = Column(children: [Expanded(child: realChild), bottomBar!]);
}
return PlatformScaffold(
@ -102,12 +96,15 @@ class SimplePage extends StatelessWidget {
title: title,
leading: leadingAction,
trailingActions: trailingActions,
cupertino: (_, __) => CupertinoNavigationBarData(
cupertino:
(_, __) => CupertinoNavigationBarData(
transitionBetweenRoutes: false,
// TODO: set title on route, show here instead of just "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,7 +13,8 @@ class SiteItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final borderColor = site.errors.length > 0
final borderColor =
site.errors.length > 0
? CupertinoColors.systemRed.resolveFrom(context)
: site.connected
? CupertinoColors.systemGreen.resolveFrom(context)
@ -23,7 +24,8 @@ class SiteItem extends StatelessWidget {
return Container(
margin: EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(border: Border(left: border)),
child: _buildContent(context));
child: _buildContent(context),
);
}
Widget _buildContent(BuildContext context) {
@ -32,8 +34,10 @@ class SiteItem extends StatelessWidget {
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
return SpecialButton(
decoration:
BoxDecoration(border: Border(top: border, bottom: border), color: Utils.configItemBackground(context)),
decoration: BoxDecoration(
border: Border(top: border, bottom: border),
color: Utils.configItemBackground(context),
),
onPressed: onPressed,
child: Padding(
padding: EdgeInsets.fromLTRB(10, 10, 5, 10),
@ -45,8 +49,10 @@ class SiteItem extends StatelessWidget {
: Container(),
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)
Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18),
],
)));
),
),
);
}
}

View file

@ -16,15 +16,15 @@ class SiteTitle extends StatelessWidget {
return IntrinsicWidth(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(children: [
child: Row(
children: [
site.managed
? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
: Container(),
Expanded(
child: Text(
site.name,
overflow: TextOverflow.ellipsis,
))
])));
Expanded(child: Text(site.name, overflow: TextOverflow.ellipsis)),
],
),
),
);
}
}

View file

@ -36,10 +36,9 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
child: Ink(
decoration: widget.decoration,
color: widget.color,
child: InkWell(
child: widget.child,
onTap: widget.onPressed,
)));
child: InkWell(child: widget.child, onTap: widget.onPressed),
),
);
}
Widget _buildGeneric() {
@ -63,7 +62,8 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
child: DefaultTextStyle(style: textStyle, child: Container(child: widget.child, color: widget.color)),
),
),
));
),
);
}
// Eyeballed values. Feel free to tweak.
@ -77,11 +77,7 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
value: 0.0,
vsync: this,
);
_animationController = AnimationController(duration: const Duration(milliseconds: 200), value: 0.0, vsync: this);
_opacityAnimation = _animationController!.drive(CurveTween(curve: Curves.decelerate)).drive(_opacityTween);
_setTween();
}
@ -131,7 +127,8 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
}
final bool wasHeldDown = _buttonHeldDown;
final TickerFuture ticker = _buttonHeldDown
final TickerFuture ticker =
_buttonHeldDown
? _animationController!.animateTo(1.0, duration: kFadeOutDuration)
: _animationController!.animateTo(0.0, duration: kFadeInDuration);

View file

@ -4,8 +4,8 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
/// A normal TextField or CupertinoTextField that looks the same on all platforms
class SpecialTextField extends StatefulWidget {
const SpecialTextField(
{Key? key,
const SpecialTextField({
Key? key,
this.placeholder,
this.suffix,
this.controller,
@ -27,8 +27,8 @@ class SpecialTextField extends StatefulWidget {
this.expands,
this.keyboardAppearance,
this.textAlignVertical,
this.inputFormatters})
: super(key: key);
this.inputFormatters,
}) : super(key: key);
final String? placeholder;
final TextEditingController? controller;
@ -98,20 +98,26 @@ class _SpecialTextFieldState extends State<SpecialTextField> {
},
expands: widget.expands,
inputFormatters: formatters,
material: (_, __) => MaterialTextFieldData(
material:
(_, __) => MaterialTextFieldData(
decoration: InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isDense: true,
hintText: widget.placeholder,
counterText: '',
suffix: widget.suffix)),
cupertino: (_, __) => CupertinoTextFieldData(
suffix: widget.suffix,
),
),
cupertino:
(_, __) => CupertinoTextFieldData(
decoration: BoxDecoration(),
padding: EdgeInsets.zero,
placeholder: widget.placeholder,
suffix: widget.suffix),
suffix: widget.suffix,
),
style: widget.style,
controller: widget.controller);
controller: widget.controller,
);
}
}

View file

@ -15,14 +15,19 @@ class PrimaryButton extends StatelessWidget {
return FilledButton(
onPressed: onPressed,
child: child,
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.primary));
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.primary),
);
} else {
// Workaround for https://github.com/flutter/flutter/issues/161590
final themeData = CupertinoTheme.of(context);
return CupertinoTheme(
data: themeData.copyWith(primaryColor: CupertinoColors.white),
child: CupertinoButton(
child: child, onPressed: onPressed, color: CupertinoColors.secondaryLabel.resolveFrom(context)));
child: child,
onPressed: onPressed,
color: CupertinoColors.secondaryLabel.resolveFrom(context),
),
);
}
}
}

View file

@ -19,6 +19,7 @@ class ConfigButtonItem extends StatelessWidget {
child: Container(
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
child: Center(child: content),
));
),
);
}
}

View file

@ -3,9 +3,14 @@ import 'package:mobile_nebula/components/SpecialButton.dart';
import 'package:mobile_nebula/services/utils.dart';
class ConfigCheckboxItem extends StatelessWidget {
const ConfigCheckboxItem(
{Key? key, this.label, this.content, this.labelWidth = 100, this.onChanged, this.checked = false})
: super(key: key);
const ConfigCheckboxItem({
Key? key,
this.label,
this.content,
this.labelWidth = 100,
this.onChanged,
this.checked = false,
}) : super(key: key);
final Widget? label;
final Widget? content;
@ -25,9 +30,10 @@ class ConfigCheckboxItem extends StatelessWidget {
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
checked
? Icon(CupertinoIcons.check_mark, color: CupertinoColors.systemBlue.resolveFrom(context))
: Container()
: Container(),
],
));
),
);
if (onChanged != null) {
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),
child: Text(
label,
style: basicTextStyle(context).copyWith(
color: color ?? CupertinoColors.secondaryLabel.resolveFrom(context),
fontSize: _headerFontSize,
),
style: basicTextStyle(
context,
).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';
class ConfigItem extends StatelessWidget {
const ConfigItem(
{Key? key,
const ConfigItem({
Key? key,
this.label,
required this.content,
this.labelWidth = 100,
this.crossAxisAlignment = CrossAxisAlignment.center})
: super(key: key);
this.crossAxisAlignment = CrossAxisAlignment.center,
}) : super(key: key);
final Widget? label;
final Widget content;
@ -37,6 +37,7 @@ class ConfigItem extends StatelessWidget {
Container(width: labelWidth, child: DefaultTextStyle(style: textStyle, child: Container(child: label))),
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';
class ConfigPageItem extends StatelessWidget {
const ConfigPageItem(
{Key? key,
const ConfigPageItem({
Key? key,
this.label,
this.content,
this.labelWidth = 100,
this.onPressed,
this.disabled = false,
this.crossAxisAlignment = CrossAxisAlignment.center})
: super(key: key);
this.crossAxisAlignment = CrossAxisAlignment.center,
}) : super(key: key);
final Widget? label;
final Widget? content;
@ -30,8 +30,10 @@ class ConfigPageItem extends StatelessWidget {
if (Platform.isAndroid) {
final origTheme = Theme.of(context);
theme = origTheme.copyWith(
textTheme: origTheme.textTheme
.copyWith(labelLarge: origTheme.textTheme.labelLarge!.copyWith(fontWeight: FontWeight.normal)));
textTheme: origTheme.textTheme.copyWith(
labelLarge: origTheme.textTheme.labelLarge!.copyWith(fontWeight: FontWeight.normal),
),
);
return Theme(data: theme, child: _buildContent(context));
} else {
final origTheme = CupertinoTheme.of(context);
@ -54,9 +56,10 @@ class ConfigPageItem extends StatelessWidget {
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
this.disabled
? Container()
: Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18)
: Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18),
],
)),
),
),
);
}
}

View file

@ -27,19 +27,27 @@ class ConfigSection extends StatelessWidget {
if (children[i + 1].runtimeType.toString() == 'ConfigButtonItem') {
pad = 0;
}
_children.add(Padding(
child: Divider(height: 1, color: Utils.configSectionBorder(context)), padding: EdgeInsets.only(left: pad)));
_children.add(
Padding(
child: Divider(height: 1, color: Utils.configSectionBorder(context)),
padding: EdgeInsets.only(left: pad),
),
);
}
}
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
label != null ? ConfigHeader(label: label!, color: labelColor) : Container(height: 20),
Container(
decoration:
BoxDecoration(border: Border(top: border, bottom: border), color: Utils.configItemBackground(context)),
child: Column(
children: _children,
))
]);
decoration: BoxDecoration(
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';
class ConfigTextItem extends StatelessWidget {
const ConfigTextItem(
{Key? key, this.placeholder, this.controller, this.style = const TextStyle(fontFamily: 'RobotoMono')})
: super(key: key);
const ConfigTextItem({
Key? key,
this.placeholder,
this.controller,
this.style = const TextStyle(fontFamily: 'RobotoMono'),
}) : super(key: key);
final String? placeholder;
final TextEditingController? controller;

View file

@ -19,16 +19,13 @@ Future<void> main() async {
var settings = Settings();
if (settings.trackErrors) {
await SentryFlutter.init(
(options) {
await SentryFlutter.init((options) {
options.dsn = 'https://96106df405ade3f013187dfc8e4200e7@o920269.ingest.us.sentry.io/4508132321001472';
// Capture all traces. May need to adjust if overwhelming
options.tracesSampleRate = 1.0;
// For each trace, capture all profiles
options.profilesSampleRate = 1.0;
},
appRunner: () => runApp(Main()),
);
}, appRunner: () => runApp(Main()));
} else {
runApp(Main());
}
@ -91,7 +88,8 @@ class _AppState extends State<App> {
return PlatformProvider(
settings: PlatformSettingsData(iosUsesMaterialWidgets: true),
builder: (context) => PlatformApp(
builder:
(context) => PlatformApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
@ -105,9 +103,7 @@ class _AppState extends State<App> {
theme: brightness == Brightness.light ? theme.light() : theme.dark(),
);
},
cupertino: (_, __) => CupertinoAppData(
theme: CupertinoThemeData(brightness: brightness),
),
cupertino: (_, __) => CupertinoAppData(theme: CupertinoThemeData(brightness: brightness)),
onGenerateRoute: (settings) {
if (settings.name == '/') {
return platformPageRoute(context: context, builder: (context) => MainScreen(this.dnEnrolled));
@ -118,7 +114,8 @@ class _AppState extends State<App> {
// TODO: maybe implement this as a dialog instead of a page, you can stack multiple enrollment screens which is annoying in dev
return platformPageRoute(
context: context,
builder: (context) =>
builder:
(context) =>
EnrollmentScreen(code: EnrollmentScreen.parseCode(settings.name!), stream: this.dnEnrolled),
);
}

View file

@ -19,9 +19,6 @@ class CIDR {
throw 'Invalid CIDR string';
}
return CIDR(
ip: parts[0],
bits: int.parse(parts[1]),
);
return CIDR(ip: parts[0], bits: int.parse(parts[1]));
}
}

View file

@ -24,10 +24,7 @@ class Certificate {
String fingerprint;
String signature;
Certificate.debug()
: this.details = CertificateDetails.debug(),
this.fingerprint = "DEBUG",
this.signature = "DEBUG";
Certificate.debug() : this.details = CertificateDetails.debug(), this.fingerprint = "DEBUG", this.signature = "DEBUG";
Certificate.fromJson(Map<String, dynamic> json)
: details = CertificateDetails.fromJson(json['details']),
@ -73,11 +70,7 @@ class CertificateValidity {
bool valid;
String reason;
CertificateValidity.debug()
: this.valid = true,
this.reason = "";
CertificateValidity.debug() : this.valid = true, this.reason = "";
CertificateValidity.fromJson(Map<String, dynamic> json)
: valid = json['Valid'],
reason = json['Reason'];
CertificateValidity.fromJson(Map<String, dynamic> json) : valid = json['Valid'], reason = json['Reason'];
}

View file

@ -52,10 +52,7 @@ class UDPAddress {
String ip;
int port;
UDPAddress({
required this.ip,
required this.port,
});
UDPAddress({required this.ip, required this.port});
@override
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
final uri = Uri.parse("ugh://$val");
return IPAndPort(
ip: uri.host,
port: uri.port,
);
return IPAndPort(ip: uri.host, port: uri.port);
}
}

View file

@ -94,7 +94,8 @@ class Site {
this.lastManagedUpdate = lastManagedUpdate;
_updates = EventChannel('net.defined.nebula/${this.id}');
_updates.receiveBroadcastStream().listen((d) {
_updates.receiveBroadcastStream().listen(
(d) {
try {
_updateFromJson(d);
_change.add(null);
@ -102,11 +103,13 @@ class Site {
//TODO: handle the error
print(err);
}
}, onError: (err) {
},
onError: (err) {
_updateFromJson(err.details);
var error = err as PlatformException;
_change.addError(error.message ?? 'An unexpected error occurred');
});
},
);
}
factory Site.fromJson(Map<String, dynamic> json) {
@ -220,9 +223,11 @@ class Site {
'id': id,
'staticHostmap': staticHostmap,
'unsafeRoutes': unsafeRoutes,
'ca': ca.map((cert) {
'ca': ca
.map((cert) {
return cert.rawCert;
}).join('\n'),
})
.join('\n'),
'cert': certInfo?.rawCert,
'key': key,
'lhDuration': lhDuration,
@ -341,8 +346,11 @@ class Site {
Future<HostInfo?> getHostInfo(String vpnIp, bool pending) async {
try {
var ret = await platform
.invokeMethod("active.getHostInfo", <String, dynamic>{"id": id, "vpnIp": vpnIp, "pending": pending});
var ret = await platform.invokeMethod("active.getHostInfo", <String, dynamic>{
"id": id,
"vpnIp": vpnIp,
"pending": pending,
});
final h = jsonDecode(ret);
if (h == null) {
return null;
@ -358,8 +366,11 @@ class Site {
Future<HostInfo?> setRemoteForTunnel(String vpnIp, String addr) async {
try {
var ret = await platform
.invokeMethod("active.setRemoteForTunnel", <String, dynamic>{"id": id, "vpnIp": vpnIp, "addr": addr});
var ret = await platform.invokeMethod("active.setRemoteForTunnel", <String, dynamic>{
"id": id,
"vpnIp": vpnIp,
"addr": addr,
});
final h = jsonDecode(ret);
if (h == null) {
return null;

View file

@ -14,16 +14,10 @@ class StaticHost {
result.add(IPAndPort.fromString(item));
});
return StaticHost(
lighthouse: json['lighthouse'],
destinations: result,
);
return StaticHost(lighthouse: json['lighthouse'], destinations: result);
}
Map<String, dynamic> toJson() {
return {
'lighthouse': lighthouse,
'destinations': destinations,
};
return {'lighthouse': lighthouse, 'destinations': destinations};
}
}

View file

@ -5,16 +5,10 @@ class UnsafeRoute {
UnsafeRoute({this.route, this.via});
factory UnsafeRoute.fromJson(Map<String, dynamic> json) {
return UnsafeRoute(
route: json['route'],
via: json['via'],
);
return UnsafeRoute(route: json['route'], via: json['via']);
}
Map<String, dynamic> toJson() {
return {
'route': route,
'via': via,
};
return {'route': route, 'via': via};
}
}

View file

@ -37,52 +37,67 @@ class _AboutScreenState extends State<AboutScreen> {
// packageInfo is null until ready is true
if (!ready) {
return Center(
child: PlatformCircularProgressIndicator(cupertino: (_, __) {
child: PlatformCircularProgressIndicator(
cupertino: (_, __) {
return CupertinoProgressIndicatorData(radius: 50);
}),
},
),
);
}
return SimplePage(
title: Text('About'),
child: Column(children: [
ConfigSection(children: <Widget>[
child: Column(
children: [
ConfigSection(
children: <Widget>[
ConfigItem(
label: Text('App version'),
labelWidth: 150,
content: _buildText('${packageInfo!.version}-${packageInfo!.buildNumber} (sha: $gitSha)')),
content: _buildText('${packageInfo!.version}-${packageInfo!.buildNumber} (sha: $gitSha)'),
),
ConfigItem(
label: Text('Nebula version'), labelWidth: 150, content: _buildText('$nebulaVersion ($goVersion)')),
label: Text('Nebula version'),
labelWidth: 150,
content: _buildText('$nebulaVersion ($goVersion)'),
),
ConfigItem(
label: Text('Flutter version'),
labelWidth: 150,
content: _buildText(flutterVersion['frameworkVersion'] ?? 'Unknown')),
content: _buildText(flutterVersion['frameworkVersion'] ?? 'Unknown'),
),
ConfigItem(
label: Text('Dart version'),
labelWidth: 150,
content: _buildText(flutterVersion['dartSdkVersion'] ?? 'Unknown')),
]),
ConfigSection(children: <Widget>[
content: _buildText(flutterVersion['dartSdkVersion'] ?? 'Unknown'),
),
],
),
ConfigSection(
children: <Widget>[
//TODO: wire up these other pages
// ConfigPageItem(label: Text('Changelog'), labelWidth: 300, onPressed: () => Utils.launchUrl('https://defined.net/mobile/changelog', context)),
ConfigPageItem(
label: Text('Privacy policy'),
labelWidth: 300,
onPressed: () => Utils.launchUrl('https://www.defined.net/privacy/', context)),
onPressed: () => Utils.launchUrl('https://www.defined.net/privacy/', context),
),
ConfigPageItem(
label: Text('Licenses'),
labelWidth: 300,
onPressed: () => Utils.openPage(context, (context) {
onPressed:
() => Utils.openPage(context, (context) {
return LicensesScreen();
})),
]),
}),
),
],
),
Padding(
padding: EdgeInsets.only(top: 20),
child: Text(
'Copyright © 2024 Defined Networking, Inc',
textAlign: TextAlign.center,
)),
]),
child: Text('Copyright © 2024 Defined Networking, Inc', textAlign: TextAlign.center),
),
],
),
);
}

View file

@ -98,8 +98,10 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
child: Text(
'No valid enrollment code was found.\n\nContact your administrator to obtain a new enrollment code.',
textAlign: TextAlign.center,
)),
padding: EdgeInsets.only(top: 20));
),
),
padding: EdgeInsets.only(top: 20),
);
}
} else if (this.error != null) {
// Error while enrolling, display it
@ -108,15 +110,20 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
children: [
Padding(
child: SelectableText(
'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)),
'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(
child: SelectableText.rich(TextSpan(children: [
child: SelectableText.rich(
TextSpan(
children: [
TextSpan(text: 'If the problem persists, please let us know at '),
TextSpan(
text: 'support@defined.net',
style: bodyTextStyle.apply(color: colorScheme.primary),
recognizer: TapGestureRecognizer()
recognizer:
TapGestureRecognizer()
..onTap = () async {
if (await canLaunchUrl(contactUri)) {
print(await launchUrl(contactUri));
@ -124,8 +131,11 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
},
),
TextSpan(text: ' and provide the following error:'),
])),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10)),
],
),
),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
),
Container(
child: Padding(child: SelectableText(this.error!), padding: EdgeInsets.all(16)),
color: Theme.of(context).colorScheme.errorContainer,
@ -133,26 +143,29 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
],
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
));
),
);
} else if (this.enrolled) {
// Enrollment complete!
child = Padding(
child: Center(
child: Text(
'Enrollment complete! 🎉',
textAlign: TextAlign.center,
)),
padding: EdgeInsets.only(top: 20));
child: Center(child: Text('Enrollment complete! 🎉', textAlign: TextAlign.center)),
padding: EdgeInsets.only(top: 20),
);
} else {
// Have a code and actively enrolling
alignment = Alignment.center;
child = Center(
child: Column(children: [
child: Column(
children: [
Padding(child: Text('Contacting DN for enrollment'), padding: EdgeInsets.only(bottom: 25)),
PlatformCircularProgressIndicator(cupertino: (_, __) {
PlatformCircularProgressIndicator(
cupertino: (_, __) {
return CupertinoProgressIndicatorData(radius: 50);
})
]));
},
),
],
),
);
}
return SimplePage(title: Text('Enroll with Managed Nebula'), child: child, alignment: alignment);
@ -187,27 +200,21 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
controller: enrollInput,
validator: validator,
hintText: 'from admin.defined.net',
cupertino: (_, __) => CupertinoTextFormFieldData(
prefix: Text("Code or link"),
cupertino: (_, __) => CupertinoTextFormFieldData(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: [
Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: form,
),
final form = Form(key: _formKey, child: Platform.isAndroid ? input : ConfigSection(children: [input]));
return Column(
children: [
Padding(padding: EdgeInsets.symmetric(vertical: 32), child: form),
Padding(
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

@ -60,42 +60,59 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
refreshController.refreshCompleted();
},
child: Column(
children: [_buildMain(), _buildDetails(), _buildRemotes(), !widget.pending ? _buildClose() : Container()]));
children: [_buildMain(), _buildDetails(), _buildRemotes(), !widget.pending ? _buildClose() : Container()],
),
);
}
Widget _buildMain() {
return ConfigSection(children: [
return ConfigSection(
children: [
ConfigItem(label: Text('VPN IP'), labelWidth: 150, content: SelectableText(hostInfo.vpnIp)),
hostInfo.cert != null
? ConfigPageItem(
label: Text('Certificate'),
labelWidth: 150,
content: Text(hostInfo.cert!.details.name),
onPressed: () => Utils.openPage(
onPressed:
() => Utils.openPage(
context,
(context) => CertificateDetailsScreen(
certInfo: CertificateInfo(cert: hostInfo.cert!),
supportsQRScanning: widget.supportsQRScanning,
)))
),
),
)
: Container(),
]);
],
);
}
Widget _buildDetails() {
return ConfigSection(children: <Widget>[
return ConfigSection(
children: <Widget>[
ConfigItem(
label: Text('Lighthouse'), labelWidth: 150, content: SelectableText(widget.isLighthouse ? 'Yes' : 'No')),
label: Text('Lighthouse'),
labelWidth: 150,
content: SelectableText(widget.isLighthouse ? 'Yes' : 'No'),
),
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}')),
]);
label: Text('Message Counter'),
labelWidth: 150,
content: SelectableText('${hostInfo.messageCounter}'),
),
],
);
}
Widget _buildRemotes() {
if (hostInfo.remoteAddresses.length == 0) {
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();
@ -109,7 +126,8 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
hostInfo.remoteAddresses.forEach((remoteObj) {
String remote = remoteObj.toString();
items.add(ConfigCheckboxItem(
items.add(
ConfigCheckboxItem(
key: Key(remote),
label: Text(remote), //TODO: need to do something to adjust the font size in the event we have an ipv6 address
labelWidth: ipWidth,
@ -128,7 +146,8 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
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);
@ -142,12 +161,14 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
hostInfo.remoteAddresses.forEach((remoteObj) {
String remote = remoteObj.toString();
items.add(ConfigCheckboxItem(
items.add(
ConfigCheckboxItem(
key: Key(remote),
label: Text(remote), //TODO: need to do something to adjust the font size in the event we have an ipv6 address
labelWidth: ipWidth,
checked: currentRemote == remote,
));
),
);
});
return ConfigSection(label: items.length > 0 ? 'REMOTES' : null, children: items);
@ -160,7 +181,8 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
width: double.infinity,
child: DangerButton(
child: Text('Close Tunnel'),
onPressed: () => Utils.confirmDelete(context, 'Close Tunnel?', () async {
onPressed:
() => Utils.confirmDelete(context, 'Close Tunnel?', () async {
try {
await widget.site.closeTunnel(hostInfo.vpnIp);
if (widget.onChanged != null) {
@ -170,7 +192,10 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
} catch (err) {
Utils.popError(context, 'Error while trying to close the tunnel', err.toString());
}
}, deleteLabel: 'Close'))));
}, deleteLabel: 'Close'),
),
),
);
}
_getHostInfo() async {

View file

@ -25,19 +25,12 @@ class LicensesScreen extends StatelessWidget {
padding: const EdgeInsets.all(8),
child: PlatformListTile(
onTap: () {
Utils.openPage(
context,
(_) => LicenceDetailPage(
title: capitalize(dep.name),
licence: dep.license!,
),
);
Utils.openPage(context, (_) => LicenceDetailPage(title: capitalize(dep.name), licence: dep.license!));
},
title: Text(
capitalize(dep.name),
),
title: Text(capitalize(dep.name)),
subtitle: Text(dep.description),
trailing: Icon(context.platformIcons.forward, size: 18)),
trailing: Icon(context.platformIcons.forward, size: 18),
),
);
},
),
@ -62,14 +55,7 @@ class LicenceDetailPage extends StatelessWidget {
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Column(
children: [
Text(
licence,
style: const TextStyle(fontSize: 15),
),
],
),
child: Column(children: [Text(licence, style: const TextStyle(fontSize: 15))]),
),
),
),

View file

@ -115,11 +115,7 @@ class _MainScreenState extends State<MainScreen> {
if (kDebugMode) {
debugSite = Row(
children: [
_debugSave(badDebugSave),
_debugSave(goodDebugSave),
_debugClearKeys(),
],
children: [_debugSave(badDebugSave), _debugSave(goodDebugSave), _debugClearKeys()],
mainAxisAlignment: MainAxisAlignment.center,
);
}
@ -141,12 +137,14 @@ class _MainScreenState extends State<MainScreen> {
leadingAction: PlatformIconButton(
padding: EdgeInsets.zero,
icon: Icon(Icons.add, size: 28.0),
onPressed: () => Utils.openPage(context, (context) {
onPressed:
() => Utils.openPage(context, (context) {
return SiteConfigScreen(
onSave: (_) {
_loadSites();
},
supportsQRScanning: supportsQRScanning);
supportsQRScanning: supportsQRScanning,
);
}),
),
refreshController: refreshController,
@ -175,7 +173,9 @@ class _MainScreenState extends State<MainScreen> {
crossAxisAlignment: CrossAxisAlignment.center,
children: error!,
),
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10)));
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10),
),
);
}
return _buildSites();
@ -191,11 +191,14 @@ class _MainScreenState extends State<MainScreen> {
padding: const EdgeInsets.fromLTRB(0, 8.0, 0, 8.0),
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.',
textAlign: TextAlign.center),
Text(
'You don\'t have any site configurations installed yet. Hit the plus button above to get started.',
textAlign: TextAlign.center,
),
],
),
));
),
);
}
Widget _buildSites() {
@ -205,7 +208,8 @@ class _MainScreenState extends State<MainScreen> {
List<Widget> items = [];
sites!.forEach((site) {
items.add(SiteItem(
items.add(
SiteItem(
key: Key(site.id),
site: site,
onPressed: () {
@ -216,7 +220,9 @@ class _MainScreenState extends State<MainScreen> {
supportsQRScanning: supportsQRScanning,
);
});
}));
},
),
);
});
Widget child = ReorderableListView(
@ -250,7 +256,8 @@ class _MainScreenState extends State<MainScreen> {
}
_loadSites();
});
},
);
if (Platform.isIOS) {
child = CupertinoTheme(child: child, data: CupertinoTheme.of(context));
@ -272,11 +279,13 @@ class _MainScreenState extends State<MainScreen> {
staticHostmap: {
"10.1.0.1": StaticHost(
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']),
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')]);
unsafeRoutes: [UnsafeRoute(route: '10.3.3.3/32', via: '10.1.0.1')],
);
s.key = siteConfig['key'];
@ -309,14 +318,17 @@ class _MainScreenState extends State<MainScreen> {
var site = Site.fromJson(rawSite);
//TODO: we need to cancel change listeners when we rebuild
site.onChange().listen((_) {
site.onChange().listen(
(_) {
setState(() {});
}, onError: (err) {
},
onError: (err) {
setState(() {});
if (ModalRoute.of(context)!.isCurrent) {
Utils.popError(context, "${site.name} Error", err);
}
});
},
);
sites!.add(site);
} catch (err) {

View file

@ -38,7 +38,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) {
List<Widget> colorSection = [];
colorSection.add(ConfigItem(
colorSection.add(
ConfigItem(
label: Text('Use system colors'),
labelWidth: 200,
content: Align(
@ -49,11 +50,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
settings.useSystemColors = value;
},
value: settings.useSystemColors,
)),
));
),
),
),
);
if (!settings.useSystemColors) {
colorSection.add(ConfigItem(
colorSection.add(
ConfigItem(
label: Text('Dark mode'),
content: Align(
alignment: Alignment.centerRight,
@ -63,13 +67,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
settings.darkMode = value;
},
value: settings.darkMode,
)),
));
),
),
),
);
}
List<Widget> items = [];
items.add(ConfigSection(children: colorSection));
items.add(ConfigItem(
items.add(
ConfigItem(
label: Text('Wrap log output'),
labelWidth: 200,
content: Align(
@ -82,10 +89,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
settings.logWrap = value;
});
},
)),
));
),
),
),
);
items.add(ConfigSection(children: [
items.add(
ConfigSection(
children: [
ConfigItem(
label: Text('Report errors automatically'),
labelWidth: 250,
@ -99,27 +110,35 @@ class _SettingsScreenState extends State<SettingsScreen> {
settings.trackErrors = value;
});
},
))),
]));
),
),
),
],
),
);
items.add(ConfigSection(children: [
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),
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)
class SiteDetailScreen extends StatefulWidget {
const SiteDetailScreen({
Key? key,
required this.site,
this.onChanged,
required this.supportsQRScanning,
}) : super(key: key);
const SiteDetailScreen({Key? key, required this.site, this.onChanged, required this.supportsQRScanning})
: super(key: key);
final Site site;
final Function? onChanged;
@ -54,7 +50,8 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
_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.
if (site.status == 'Connected') {
@ -65,10 +62,12 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
}
setState(() {});
}, onError: (err) {
},
onError: (err) {
setState(() {});
Utils.popError(context, "Error", err);
});
},
);
super.initState();
}
@ -85,12 +84,15 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
return SimplePage(
title: title,
leadingAction: Utils.leadingBackWidget(context, onPressed: () {
leadingAction: Utils.leadingBackWidget(
context,
onPressed: () {
if (changed && widget.onChanged != null) {
widget.onChanged!();
}
Navigator.pop(context);
}),
},
),
refreshController: refreshController,
onRefresh: () async {
if (site.connected && site.status == "Connected") {
@ -98,13 +100,16 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
}
refreshController.refreshCompleted();
},
child: Column(children: [
child: Column(
children: [
_buildErrors(),
_buildConfig(),
site.connected ? _buildHosts() : Container(),
_buildSiteDetails(),
_buildDelete(),
]));
],
),
);
}
Widget _buildErrors() {
@ -114,8 +119,12 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
List<Widget> items = [];
site.errors.forEach((error) {
items.add(ConfigItem(
labelWidth: 0, content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error))));
items.add(
ConfigItem(
labelWidth: 0,
content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error)),
),
);
});
return ConfigSection(
@ -140,20 +149,28 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
}
}
return ConfigSection(children: <Widget>[
return ConfigSection(
children: <Widget>[
ConfigItem(
label: Text('Status'),
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[
content: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.only(right: 5),
child: Text(widget.site.status,
style: TextStyle(color: CupertinoColors.secondaryLabel.resolveFrom(context)))),
child: Text(
widget.site.status,
style: TextStyle(color: CupertinoColors.secondaryLabel.resolveFrom(context)),
),
),
Switch.adaptive(
value: widget.site.connected,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: widget.site.errors.length > 0 && !widget.site.connected ? null : handleChange,
)
])),
),
],
),
),
ConfigPageItem(
label: Text('Logs'),
onPressed: () {
@ -162,7 +179,8 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
});
},
),
]);
],
);
}
Widget _buildHosts() {
@ -199,10 +217,12 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
});
},
supportsQRScanning: widget.supportsQRScanning,
));
),
);
},
label: Text("Active"),
content: Container(alignment: Alignment.centerRight, child: active)),
content: Container(alignment: Alignment.centerRight, child: active),
),
ConfigPageItem(
onPressed: () {
if (pendingHosts == null) return;
@ -219,16 +239,19 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
});
},
supportsQRScanning: widget.supportsQRScanning,
));
),
);
},
label: Text("Pending"),
content: Container(alignment: Alignment.centerRight, child: pending))
content: Container(alignment: Alignment.centerRight, child: pending),
),
],
);
}
Widget _buildSiteDetails() {
return ConfigSection(children: <Widget>[
return ConfigSection(
children: <Widget>[
ConfigPageItem(
crossAxisAlignment: CrossAxisAlignment.center,
content: Text('Configuration'),
@ -245,7 +268,8 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
});
},
),
]);
],
);
}
Widget _buildDelete() {
@ -255,12 +279,15 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
width: double.infinity,
child: DangerButton(
child: Text('Delete'),
onPressed: () => Utils.confirmDelete(context, 'Delete Site?', () async {
onPressed:
() => Utils.confirmDelete(context, 'Delete Site?', () async {
if (await _deleteSite()) {
Navigator.of(context).pop();
}
}),
)));
),
),
);
}
_listHostmap() async {

View file

@ -64,16 +64,17 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
constraints: logBoxConstraints(context),
child: ListenableBuilder(
listenable: logsNotifier,
builder: (context, child) => SelectableText(
switch (logsNotifier.logsResult) {
builder:
(context, child) => SelectableText(switch (logsNotifier.logsResult) {
Ok<String>(:var value) => value.trim(),
Error<String>(:var error) => error is LogsNotFoundException
Error<String>(:var error) =>
error is LogsNotFoundException
? error.error()
: Utils.popError(context, "Error while reading logs.", error.toString()),
null => "",
},
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
)),
}, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
),
),
bottomBar: _buildBottomBar(),
);
}
@ -88,10 +89,11 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
sizeStyle: CupertinoButtonSize.small,
borderRadius: const BorderRadius.all(Radius.circular(8)),
child: const Icon(Icons.wrap_text),
onPressed: () => {
onPressed:
() => {
setState(() {
settings.logWrap = !settings.logWrap;
})
}),
},
),
)
@ -101,20 +103,17 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
// 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: () => {
onPressed:
() => {
setState(() {
settings.logWrap = !settings.logWrap;
})
}),
},
);
}
Widget _buildBottomBar() {
var borderSide = BorderSide(
color: CupertinoColors.separator,
style: BorderStyle.solid,
width: 0.0,
);
var borderSide = BorderSide(color: CupertinoColors.separator, style: BorderStyle.solid, width: 0.0);
var padding = Platform.isAndroid ? EdgeInsets.fromLTRB(0, 20, 0, 30) : EdgeInsets.all(10);
@ -128,10 +127,12 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
child: PlatformIconButton(
icon: Icon(context.platformIcons.share),
onPressed: () {
Share.shareFile(context,
Share.shareFile(
context,
title: '${widget.site.name} logs',
filePath: widget.site.logFile,
filename: '${widget.site.name}.log');
filename: '${widget.site.name}.log',
);
},
),
),
@ -140,20 +141,21 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
child: PlatformIconButton(
icon: Icon(context.platformIcons.downArrow),
onPressed: () async {
controller.animateTo(controller.position.maxScrollExtent,
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(
border: Border(top: borderSide),
),
padding: padding,
child: child),
material: (context, child, platform) => BottomAppBar(child: child));
cupertino:
(context, child, platform) =>
Container(decoration: BoxDecoration(border: Border(top: borderSide)), padding: padding, child: child),
material: (context, child, platform) => BottomAppBar(child: child),
);
}
logBoxConstraints(BuildContext context) {

View file

@ -53,15 +53,17 @@ class _SiteTunnelsScreenState extends State<SiteTunnelsScreen> {
Widget build(BuildContext context) {
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 =
tunnels.map((hostInfo) {
final isLh = site.staticHostmap[hostInfo.vpnIp]?.lighthouse ?? false;
final icon = switch (isLh) {
true => Icon(Icons.lightbulb_outline, color: CupertinoColors.placeholderText.resolveFrom(context)),
false => Icon(Icons.computer, color: CupertinoColors.placeholderText.resolveFrom(context))
false => Icon(Icons.computer, color: CupertinoColors.placeholderText.resolveFrom(context)),
};
return (ConfigPageItem(
onPressed: () => Utils.openPage(
onPressed:
() => Utils.openPage(
context,
(context) => HostInfoScreen(
isLighthouse: isLh,
@ -74,7 +76,9 @@ class _SiteTunnelsScreenState extends State<SiteTunnelsScreen> {
supportsQRScanning: widget.supportsQRScanning,
),
),
label: Row(children: <Widget>[Padding(child: icon, padding: EdgeInsets.only(right: 10)), Text(hostInfo.vpnIp)]),
label: Row(
children: <Widget>[Padding(child: icon, padding: EdgeInsets.only(right: 10)), Text(hostInfo.vpnIp)],
),
labelWidth: ipWidth,
content: Container(alignment: Alignment.centerRight, child: Text(hostInfo.cert?.details.name ?? "")),
));
@ -94,7 +98,8 @@ class _SiteTunnelsScreenState extends State<SiteTunnelsScreen> {
await _listHostmap();
refreshController.refreshCompleted();
},
child: child);
child: child,
);
}
_sortTunnels() {

View file

@ -98,21 +98,23 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
return ConfigButtonItem(
content: Text('Share Public Key'),
onPressed: () async {
await Share.share(context,
title: 'Please sign and return a certificate', text: pubKey, filename: 'device.pub');
await Share.share(
context,
title: 'Please sign and return a certificate',
text: pubKey,
filename: 'device.pub',
);
},
);
},
),
])
],
),
];
}
List<Widget> _buildLoadCert() {
Map<String, Widget> children = {
'paste': Text('Copy/Paste'),
'file': Text('File'),
};
Map<String, Widget> children = {'paste': Text('Copy/Paste'), 'file': Text('File')};
// not all devices have a camera for QR codes
if (widget.supportsQRScanning) {
@ -132,7 +134,8 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
}
},
children: children,
))
),
),
];
if (inputType == 'paste') {
@ -154,18 +157,20 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
width: double.infinity,
child: PrimaryButton(
child: Text('Show/Import Private Key'),
onPressed: () => Utils.confirmDelete(context, 'Show/Import Private Key?', () {
onPressed:
() => Utils.confirmDelete(context, 'Show/Import Private Key?', () {
setState(() {
showKey = true;
});
}, deleteLabel: 'Yes'))));
}, deleteLabel: 'Yes'),
),
),
);
}
return ConfigSection(
label: 'Import a private key generated on another device',
children: [
ConfigTextItem(controller: keyController, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
],
children: [ConfigTextItem(controller: keyController, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))],
);
}
@ -173,17 +178,15 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
return [
ConfigSection(
children: [
ConfigTextItem(
placeholder: 'Certificate PEM Contents',
controller: pasteController,
),
ConfigTextItem(placeholder: 'Certificate PEM Contents', controller: pasteController),
ConfigButtonItem(
content: Center(child: Text('Load Certificate')),
onPressed: () {
_addCertEntry(pasteController.text);
}),
},
),
],
)
),
];
}
@ -204,9 +207,10 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
} catch (err) {
return Utils.popError(context, 'Failed to load certificate file', err.toString());
}
})
},
),
],
)
),
];
}
@ -219,10 +223,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
onPressed: () async {
var result = await Navigator.push(
context,
platformPageRoute(
context: context,
builder: (context) => new ScanQRScreen(),
),
platformPageRoute(context: context, builder: (context) => new ScanQRScreen()),
);
if (result != null) {
_addCertEntry(result);
@ -230,7 +231,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
},
),
],
)
),
];
}
@ -247,18 +248,26 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
if (certs.length > 0) {
var tryCertInfo = CertificateInfo.fromJson(certs.first);
if (tryCertInfo.cert.details.isCa) {
return Utils.popError(context, 'Error loading certificate content',
'A certificate authority is not appropriate for a client certificate.');
return Utils.popError(
context,
'Error loading certificate content',
'A certificate authority is not appropriate for a client certificate.',
);
} else if (!tryCertInfo.validity!.valid) {
return Utils.popError(context, 'Certificate was invalid', tryCertInfo.validity!.reason);
}
var certMatch = await platform
.invokeMethod("nebula.verifyCertAndKey", <String, String>{"cert": rawCert, "key": keyController.text});
var certMatch = await platform.invokeMethod("nebula.verifyCertAndKey", <String, String>{
"cert": rawCert,
"key": keyController.text,
});
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
return Utils.popError(context, 'Error loading certificate content',
'The provided certificates public key is not compatible with the private key.');
return Utils.popError(
context,
'Error loading certificate content',
'The provided certificates public key is not compatible with the private key.',
);
}
if (widget.onReplace != null) {

View file

@ -40,11 +40,7 @@ class Advanced {
}
class AdvancedScreen extends StatefulWidget {
const AdvancedScreen({
Key? key,
required this.site,
required this.onSave,
}) : super(key: key);
const AdvancedScreen({Key? key, required this.site, required this.onSave}) : super(key: key);
final Site site;
final ValueChanged<Advanced> onSave;
@ -79,14 +75,16 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
Navigator.pop(context);
widget.onSave(settings);
},
child: Column(children: [
child: Column(
children: [
ConfigSection(
children: [
ConfigItem(
label: Text("Lighthouse interval"),
labelWidth: 200,
//TODO: Auto select on focus?
content: widget.site.managed
content:
widget.site.managed
? Text(settings.lhDuration.toString() + " seconds", textAlign: TextAlign.right)
: PlatformTextFormField(
initialValue: settings.lhDuration.toString(),
@ -102,12 +100,14 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
}
});
},
)),
),
),
ConfigItem(
label: Text("Listen port"),
labelWidth: 150,
//TODO: Auto select on focus?
content: widget.site.managed
content:
widget.site.managed
? Text(settings.port.toString(), textAlign: TextAlign.right)
: PlatformTextFormField(
initialValue: settings.port.toString(),
@ -122,11 +122,13 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
}
});
},
)),
),
),
ConfigItem(
label: Text("MTU"),
labelWidth: 150,
content: widget.site.managed
content:
widget.site.managed
? Text(settings.mtu.toString(), textAlign: TextAlign.right)
: PlatformTextFormField(
initialValue: settings.mtu.toString(),
@ -141,7 +143,8 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
}
});
},
)),
),
),
ConfigPageItem(
disabled: widget.site.managed,
label: Text('Cipher'),
@ -156,9 +159,11 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
settings.cipher = cipher;
changed = true;
});
},
);
});
});
}),
},
),
ConfigPageItem(
disabled: widget.site.managed,
label: Text('Log verbosity'),
@ -173,9 +178,11 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
settings.verbosity = verbosity;
changed = true;
});
},
);
});
});
}),
},
),
ConfigPageItem(
label: Text('Unsafe routes'),
labelWidth: 150,
@ -184,17 +191,19 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
Utils.openPage(context, (context) {
return UnsafeRoutesScreen(
unsafeRoutes: settings.unsafeRoutes,
onSave: widget.site.managed
onSave:
widget.site.managed
? null
: (routes) {
setState(() {
settings.unsafeRoutes = routes;
changed = true;
});
});
},
);
});
},
)
),
],
),
ConfigSection(
@ -211,9 +220,11 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
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)
class CAListScreen extends StatefulWidget {
const CAListScreen({
Key? key,
required this.cas,
this.onSave,
required this.supportsQRScanning,
}) : super(key: key);
const CAListScreen({Key? key, required this.cas, this.onSave, required this.supportsQRScanning}) : super(key: key);
final List<CertificateInfo> cas;
final ValueChanged<List<CertificateInfo>>? onSave;
@ -70,24 +65,29 @@ class _CAListScreenState extends State<CAListScreen> {
onSave: () {
if (widget.onSave != null) {
Navigator.pop(context);
widget.onSave!(cas.values.map((ca) {
widget.onSave!(
cas.values.map((ca) {
return ca;
}).toList());
}).toList(),
);
}
},
child: Column(children: items));
child: Column(children: items),
);
}
List<Widget> _buildCAs() {
List<Widget> items = [];
cas.forEach((key, ca) {
items.add(ConfigPageItem(
items.add(
ConfigPageItem(
content: Text(ca.cert.details.name),
onPressed: () {
Utils.openPage(context, (context) {
return CertificateDetailsScreen(
certInfo: ca,
onDelete: widget.onSave == null
onDelete:
widget.onSave == null
? null
: () {
setState(() {
@ -99,7 +99,8 @@ class _CAListScreenState extends State<CAListScreen> {
);
});
},
));
),
);
});
return items;
@ -137,10 +138,7 @@ class _CAListScreenState extends State<CAListScreen> {
}
List<Widget> _addCA() {
Map<String, Widget> children = {
'paste': Text('Copy/Paste'),
'file': Text('File'),
};
Map<String, Widget> children = {'paste': Text('Copy/Paste'), 'file': Text('File')};
// not all devices have a camera for QR codes
if (widget.supportsQRScanning) {
@ -160,7 +158,8 @@ class _CAListScreenState extends State<CAListScreen> {
}
},
children: children,
))
),
),
];
if (inputType == 'paste') {
@ -178,10 +177,7 @@ class _CAListScreenState extends State<CAListScreen> {
return [
ConfigSection(
children: [
ConfigTextItem(
placeholder: 'CA PEM contents',
controller: pasteController,
),
ConfigTextItem(placeholder: 'CA PEM contents', controller: pasteController),
ConfigButtonItem(
content: Text('Load CA'),
onPressed: () {
@ -194,9 +190,10 @@ class _CAListScreenState extends State<CAListScreen> {
pasteController.text = '';
setState(() {});
});
}),
},
),
],
)
),
];
}
@ -223,9 +220,10 @@ class _CAListScreenState extends State<CAListScreen> {
} catch (err) {
return Utils.popError(context, 'Failed to load CA file', err.toString());
}
})
},
),
],
)
),
];
}
@ -238,10 +236,7 @@ class _CAListScreenState extends State<CAListScreen> {
onPressed: () async {
var result = await Navigator.push(
context,
platformPageRoute(
context: context,
builder: (context) => new ScanQRScreen(),
),
platformPageRoute(context: context, builder: (context) => new ScanQRScreen()),
);
if (result != null) {
_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,
child: Column(children: [
_buildID(),
_buildFilters(),
_buildValid(),
_buildAdvanced(),
_buildReplace(),
_buildDelete(),
]),
child: Column(
children: [_buildID(), _buildFilters(), _buildValid(), _buildAdvanced(), _buildReplace(), _buildDelete()],
),
);
}
Widget _buildID() {
return ConfigSection(children: <Widget>[
return ConfigSection(
children: <Widget>[
ConfigItem(label: Text('Name'), content: SelectableText(certInfo.cert.details.name)),
ConfigItem(
label: Text('Type'), content: Text(certInfo.cert.details.isCa ? 'CA certificate' : 'Client certificate')),
]);
label: Text('Type'),
content: Text(certInfo.cert.details.isCa ? 'CA certificate' : 'Client certificate'),
),
],
);
}
Widget _buildValid() {
var valid = Text('yes');
if (certInfo.validity != null && !certInfo.validity!.valid) {
valid = Text(certInfo.validity!.valid ? 'yes' : certInfo.validity!.reason,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context)));
valid = Text(
certInfo.validity!.valid ? 'yes' : certInfo.validity!.reason,
style: TextStyle(color: CupertinoColors.systemRed.resolveFrom(context)),
);
}
return ConfigSection(
label: 'VALIDITY',
children: <Widget>[
ConfigItem(label: Text('Valid?'), content: valid),
ConfigItem(
label: Text('Created'), content: SelectableText(certInfo.cert.details.notBefore.toLocal().toString())),
label: Text('Created'),
content: SelectableText(certInfo.cert.details.notBefore.toLocal().toString()),
),
ConfigItem(
label: Text('Expires'), content: SelectableText(certInfo.cert.details.notAfter.toLocal().toString())),
label: Text('Expires'),
content: SelectableText(certInfo.cert.details.notAfter.toLocal().toString()),
),
],
);
}
@ -137,19 +142,23 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
children: <Widget>[
ConfigItem(
label: Text('Fingerprint'),
content:
SelectableText(certInfo.cert.fingerprint, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start),
content: SelectableText(certInfo.cert.fingerprint, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start,
),
ConfigItem(
label: Text('Public Key'),
content: SelectableText(certInfo.cert.details.publicKey,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start),
content: SelectableText(
certInfo.cert.details.publicKey,
style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14),
),
crossAxisAlignment: CrossAxisAlignment.start,
),
certInfo.rawCert != null
? ConfigItem(
label: Text('PEM Format'),
content: SelectableText(certInfo.rawCert!, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
crossAxisAlignment: CrossAxisAlignment.start)
crossAxisAlignment: CrossAxisAlignment.start,
)
: Container(),
],
);
@ -176,15 +185,17 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
certInfo = result.certInfo;
});
// Slam the page back to the top
controller.animateTo(0,
duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut);
controller.animateTo(0, duration: const Duration(milliseconds: 10), curve: Curves.linearToEaseOut);
},
pubKey: widget.pubKey!,
privKey: widget.privKey!,
supportsQRScanning: widget.supportsQRScanning,
);
});
})));
},
),
),
);
}
Widget _buildDelete() {
@ -200,9 +211,13 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
width: double.infinity,
child: DangerButton(
child: Text('Delete'),
onPressed: () => Utils.confirmDelete(context, title, () async {
onPressed:
() => Utils.confirmDelete(context, title, () async {
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';
class CipherScreen extends StatefulWidget {
const CipherScreen({
Key? key,
required this.cipher,
required this.onSave,
}) : super(key: key);
const CipherScreen({Key? key, required this.cipher, required this.onSave}) : super(key: key);
final String cipher;
final ValueChanged<String> onSave;
@ -40,7 +36,8 @@ class _CipherScreenState extends State<CipherScreen> {
},
child: Column(
children: <Widget>[
ConfigSection(children: [
ConfigSection(
children: [
ConfigCheckboxItem(
label: Text("aes"),
labelWidth: 150,
@ -62,9 +59,11 @@ class _CipherScreenState extends State<CipherScreen> {
cipher = "chachapoly";
});
},
)
])
),
],
));
),
],
),
);
}
}

View file

@ -6,11 +6,7 @@ import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart';
class LogVerbosityScreen extends StatefulWidget {
const LogVerbosityScreen({
Key? key,
required this.verbosity,
required this.onSave,
}) : super(key: key);
const LogVerbosityScreen({Key? key, required this.verbosity, required this.onSave}) : super(key: key);
final String verbosity;
final ValueChanged<String> onSave;
@ -40,16 +36,19 @@ class _LogVerbosityScreenState extends State<LogVerbosityScreen> {
},
child: Column(
children: <Widget>[
ConfigSection(children: [
ConfigSection(
children: [
_buildEntry('debug'),
_buildEntry('info'),
_buildEntry('warning'),
_buildEntry('error'),
_buildEntry('fatal'),
_buildEntry('panic'),
])
],
));
),
],
),
);
}
Widget _buildEntry(String title) {

View file

@ -7,11 +7,7 @@ class RenderedConfigScreen extends StatelessWidget {
final String config;
final String name;
RenderedConfigScreen({
Key? key,
required this.config,
required this.name,
}) : super(key: key);
RenderedConfigScreen({Key? key, required this.config, required this.name}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -19,18 +15,21 @@ class RenderedConfigScreen extends StatelessWidget {
title: Text('Rendered Site Config'),
scrollable: SimpleScrollable.both,
trailingActions: <Widget>[
Builder(builder: (BuildContext context) {
Builder(
builder: (BuildContext context) {
return PlatformIconButton(
padding: EdgeInsets.zero,
icon: Icon(context.platformIcons.share, size: 28.0),
onPressed: () => Share.share(context, title: '$name.yaml', text: config, filename: '$name.yaml'),
);
}),
},
),
],
child: Container(
padding: EdgeInsets.all(5),
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,16 +14,14 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
@override
Widget build(BuildContext context) {
final scanWindow = Rect.fromCenter(
center: MediaQuery.sizeOf(context).center(Offset.zero),
width: 250,
height: 250,
);
final scanWindow = Rect.fromCenter(center: MediaQuery.sizeOf(context).center(Offset.zero), width: 250, height: 250);
return Scaffold(
appBar: AppBar(title: const Text('Scan QR')),
backgroundColor: Colors.black,
body: Stack(fit: StackFit.expand, children: [
body: Stack(
fit: StackFit.expand,
children: [
Center(
child: MobileScanner(
fit: BoxFit.contain,
@ -36,7 +34,8 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
Navigator.pop(context, barcode.rawValue);
});
}
}),
},
),
),
ValueListenableBuilder(
valueListenable: cameraController,
@ -45,9 +44,7 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
return const SizedBox();
}
return CustomPaint(
painter: ScannerOverlay(scanWindow: scanWindow),
);
return CustomPaint(painter: ScannerOverlay(scanWindow: scanWindow));
},
),
Align(
@ -63,15 +60,14 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
),
),
),
]));
],
),
);
}
}
class ScannerOverlay extends CustomPainter {
const ScannerOverlay({
required this.scanWindow,
this.borderRadius = 12.0,
});
const ScannerOverlay({required this.scanWindow, this.borderRadius = 12.0});
final Rect scanWindow;
final double borderRadius;
@ -81,8 +77,8 @@ class ScannerOverlay extends CustomPainter {
// we need to pass the size to the custom paint widget
final backgroundPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
final cutoutPath = Path()
..addRRect(
final cutoutPath =
Path()..addRRect(
RRect.fromRectAndCorners(
scanWindow,
topLeft: Radius.circular(borderRadius),
@ -92,18 +88,16 @@ class ScannerOverlay extends CustomPainter {
),
);
final backgroundPaint = Paint()
final backgroundPaint =
Paint()
..color = Colors.black.withValues(alpha: 0.5)
..style = PaintingStyle.fill
..blendMode = BlendMode.srcOver;
final backgroundWithCutout = Path.combine(
PathOperation.difference,
backgroundPath,
cutoutPath,
);
final backgroundWithCutout = Path.combine(PathOperation.difference, backgroundPath, cutoutPath);
final borderPaint = Paint()
final borderPaint =
Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 4.0;
@ -214,14 +208,7 @@ class ToggleFlashlightButton extends StatelessWidget {
},
);
case TorchState.unavailable:
return const SizedBox.square(
dimension: 48.0,
child: Icon(
Icons.no_flash,
size: 32.0,
color: Colors.grey,
),
);
return const SizedBox.square(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
class SiteConfigScreen extends StatefulWidget {
const SiteConfigScreen({
Key? key,
this.site,
required this.onSave,
required this.supportsQRScanning,
}) : super(key: key);
const SiteConfigScreen({Key? key, this.site, required this.onSave, required this.supportsQRScanning})
: super(key: key);
final Site? site;
@ -71,9 +67,11 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
Widget build(BuildContext context) {
if (pubKey == null || privKey == null) {
return Center(
child: fpw.PlatformCircularProgressIndicator(cupertino: (_, __) {
child: fpw.PlatformCircularProgressIndicator(
cupertino: (_, __) {
return fpw.CupertinoProgressIndicatorData(radius: 50);
}),
},
),
);
}
@ -100,7 +98,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
_managed(),
kDebugMode ? _debugConfig() : Container(height: 0),
],
));
),
);
}
Widget _debugConfig() {
@ -116,7 +115,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
}
Widget _main() {
return ConfigSection(children: <Widget>[
return ConfigSection(
children: <Widget>[
ConfigItem(
label: Text("Name"),
content: PlatformTextFormField(
@ -128,8 +128,10 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
}
return null;
},
))
]);
),
),
],
);
}
Widget _managed() {
@ -140,15 +142,19 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
}
return site.managed
? ConfigSection(label: "MANAGED CONFIG", children: <Widget>[
? ConfigSection(
label: "MANAGED CONFIG",
children: <Widget>[
ConfigItem(
label: Text("Last Update"),
content:
Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
Text(lastUpdate),
]),
content: Wrap(
alignment: WrapAlignment.end,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[Text(lastUpdate)],
),
),
],
)
])
: Container();
}
@ -171,14 +177,19 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
children: [
ConfigPageItem(
label: Text('Certificate'),
content: Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
content: Wrap(
alignment: WrapAlignment.end,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
certError
? Padding(
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')
]),
certError ? Text('Needs attention') : Text(site.certInfo?.cert.details.name ?? 'Unknown certificate'),
],
),
onPressed: () {
Utils.openPage(context, (context) {
if (site.certInfo != null) {
@ -186,7 +197,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
certInfo: site.certInfo!,
pubKey: pubKey,
privKey: privKey,
onReplace: site.managed
onReplace:
site.managed
? null
: (result) {
setState(() {
@ -216,20 +228,25 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
),
ConfigPageItem(
label: Text("CA"),
content:
Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
content: Wrap(
alignment: WrapAlignment.end,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
caError
? Padding(
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
padding: EdgeInsets.only(right: 5))
padding: EdgeInsets.only(right: 5),
)
: 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) {
return CAListScreen(
cas: site.ca,
onSave: site.managed
onSave:
site.managed
? null
: (ca) {
setState(() {
@ -240,7 +257,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
supportsQRScanning: widget.supportsQRScanning,
);
});
})
},
),
],
);
}
@ -251,28 +269,35 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
children: <Widget>[
ConfigPageItem(
label: Text('Hosts'),
content: Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
content: Wrap(
alignment: WrapAlignment.end,
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
site.staticHostmap.length == 0
? Padding(
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
? Text('Needs attention')
: Text(Utils.itemCountFormat(site.staticHostmap.length))
]),
: Text(Utils.itemCountFormat(site.staticHostmap.length)),
],
),
onPressed: () {
Utils.openPage(context, (context) {
return StaticHostsScreen(
hostmap: site.staticHostmap,
onSave: site.managed
onSave:
site.managed
? null
: (map) {
setState(() {
changed = true;
site.staticHostmap = map;
});
});
},
);
});
},
),
@ -300,9 +325,11 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
site.unsafeRoutes = settings.unsafeRoutes;
site.mtu = settings.mtu;
});
},
);
});
});
})
},
),
],
);
}

View file

@ -67,19 +67,24 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
@override
Widget build(BuildContext context) {
return FormPage(
title: widget.onDelete == null
title:
widget.onDelete == null
? widget.onSave == null
? 'View Static Host'
: 'New Static Host'
: 'Edit Static Host',
changed: changed,
onSave: _onSave,
child: Column(children: [
ConfigSection(label: 'Maps a nebula ip address to multiple real world addresses', children: <Widget>[
child: Column(
children: [
ConfigSection(
label: 'Maps a nebula ip address to multiple real world addresses',
children: <Widget>[
ConfigItem(
label: Text('Nebula IP'),
labelWidth: 200,
content: widget.onSave == null
content:
widget.onSave == null
? Text(_nebulaIp, textAlign: TextAlign.end)
: IPFormField(
help: "Required",
@ -92,7 +97,9 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
if (v != null) {
_nebulaIp = v;
}
})),
},
),
),
ConfigItem(
label: Text('Lighthouse'),
labelWidth: 200,
@ -101,20 +108,21 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
child: Switch.adaptive(
value: _lighthouse,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: widget.onSave == null
onChanged:
widget.onSave == null
? null
: (v) {
setState(() {
changed = true;
_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
? Padding(
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
@ -122,12 +130,18 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
width: double.infinity,
child: DangerButton(
child: Text('Delete'),
onPressed: () => Utils.confirmDelete(context, 'Delete host map?', () {
onPressed:
() => Utils.confirmDelete(context, 'Delete host map?', () {
Navigator.of(context).pop();
widget.onDelete!();
}))))
: Container()
]));
}),
),
),
)
: Container(),
],
),
);
}
_onSave() {
@ -147,23 +161,30 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
List<Widget> items = [];
_destinations.forEach((key, dest) {
items.add(ConfigItem(
items.add(
ConfigItem(
key: key,
label: Align(
alignment: Alignment.centerLeft,
child: widget.onSave == null
child:
widget.onSave == null
? Container()
: PlatformIconButton(
padding: EdgeInsets.zero,
icon: Icon(Icons.remove_circle, color: CupertinoColors.systemRed.resolveFrom(context)),
onPressed: () => setState(() {
onPressed:
() => setState(() {
_removeDestination(key);
_dismissKeyboard();
}))),
}),
),
),
labelWidth: 70,
content: Row(children: <Widget>[
content: Row(
children: <Widget>[
Expanded(
child: widget.onSave == null
child:
widget.onSave == null
? Text(dest.destination.toString(), textAlign: TextAlign.end)
: IPAndPortFormField(
ipHelp: 'public ip or name',
@ -176,18 +197,25 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
dest.destination = v;
}
},
)),
]),
));
),
),
],
),
),
);
});
if (widget.onSave != null) {
items.add(ConfigButtonItem(
items.add(
ConfigButtonItem(
content: Text('Add another'),
onPressed: () => setState(() {
onPressed:
() => setState(() {
_addDestination();
_dismissKeyboard();
})));
}),
),
);
}
return items;

View file

@ -18,20 +18,11 @@ class _Hostmap {
List<IPAndPort> destinations;
bool lighthouse;
_Hostmap({
required this.focusNode,
required this.nebulaIp,
required this.destinations,
required this.lighthouse,
});
_Hostmap({required this.focusNode, required this.nebulaIp, required this.destinations, required this.lighthouse});
}
class StaticHostsScreen extends StatefulWidget {
const StaticHostsScreen({
Key? key,
required this.hostmap,
required this.onSave,
}) : super(key: key);
const StaticHostsScreen({Key? key, required this.hostmap, required this.onSave}) : super(key: key);
final Map<String, StaticHost> hostmap;
final ValueChanged<Map<String, StaticHost>>? onSave;
@ -47,8 +38,12 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
@override
void initState() {
widget.hostmap.forEach((key, map) {
_hostmap[UniqueKey()] =
_Hostmap(focusNode: FocusNode(), nebulaIp: key, destinations: map.destinations, lighthouse: map.lighthouse);
_hostmap[UniqueKey()] = _Hostmap(
focusNode: FocusNode(),
nebulaIp: key,
destinations: map.destinations,
lighthouse: map.lighthouse,
);
});
super.initState();
@ -60,9 +55,8 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
title: 'Static Hosts',
changed: changed,
onSave: _onSave,
child: ConfigSection(
children: _buildHosts(),
));
child: ConfigSection(children: _buildHosts()),
);
}
_onSave() {
@ -81,14 +75,20 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
final double ipWidth = Utils.textSize("000.000.000.000", CupertinoTheme.of(context).textTheme.textStyle).width + 32;
List<Widget> items = [];
_hostmap.forEach((key, host) {
items.add(ConfigPageItem(
label: Row(children: <Widget>[
items.add(
ConfigPageItem(
label: Row(
children: <Widget>[
Padding(
child: Icon(host.lighthouse ? Icons.lightbulb_outline : Icons.computer,
color: CupertinoColors.placeholderText.resolveFrom(context)),
padding: EdgeInsets.only(right: 10)),
child: Icon(
host.lighthouse ? Icons.lightbulb_outline : Icons.computer,
color: CupertinoColors.placeholderText.resolveFrom(context),
),
padding: EdgeInsets.only(right: 10),
),
Text(host.nebulaIp),
]),
],
),
labelWidth: ipWidth,
content: Text(host.destinations.length.toString() + ' items', textAlign: TextAlign.end),
onPressed: () {
@ -97,7 +97,8 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
nebulaIp: host.nebulaIp,
destinations: host.destinations,
lighthouse: host.lighthouse,
onSave: widget.onSave == null
onSave:
widget.onSave == null
? null
: (map) {
setState(() {
@ -107,33 +108,40 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
host.lighthouse = map.lighthouse;
});
},
onDelete: widget.onSave == null
onDelete:
widget.onSave == null
? null
: () {
setState(() {
changed = true;
_hostmap.remove(key);
});
});
},
);
});
},
));
),
);
});
if (widget.onSave != null) {
items.add(ConfigButtonItem(
items.add(
ConfigButtonItem(
content: Text('Add a new entry'),
onPressed: () {
Utils.openPage(context, (context) {
return StaticHostmapScreen(onSave: (map) {
return StaticHostmapScreen(
onSave: (map) {
setState(() {
changed = true;
_addHostmap(map);
});
});
},
);
});
},
));
),
);
}
return items;
@ -141,7 +149,11 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
_addHostmap(Hostmap map) {
_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

View file

@ -10,12 +10,7 @@ import 'package:mobile_nebula/models/UnsafeRoute.dart';
import 'package:mobile_nebula/services/utils.dart';
class UnsafeRouteScreen extends StatefulWidget {
const UnsafeRouteScreen({
Key? key,
required this.route,
required this.onSave,
this.onDelete,
}) : super(key: key);
const UnsafeRouteScreen({Key? key, required this.route, required this.onSave, this.onDelete}) : super(key: key);
final UnsafeRoute route;
final ValueChanged<UnsafeRoute> onSave;
@ -47,8 +42,10 @@ class _UnsafeRouteScreenState extends State<UnsafeRouteScreen> {
title: widget.onDelete == null ? 'New Unsafe Route' : 'Edit Unsafe Route',
changed: changed,
onSave: _onSave,
child: Column(children: [
ConfigSection(children: <Widget>[
child: Column(
children: [
ConfigSection(
children: <Widget>[
ConfigItem(
label: Text('Route'),
content: CIDRFormField(
@ -58,7 +55,9 @@ class _UnsafeRouteScreenState extends State<UnsafeRouteScreen> {
nextFocusNode: viaFocus,
onSaved: (v) {
route.route = v.toString();
})),
},
),
),
ConfigItem(
label: Text('Via'),
content: IPFormField(
@ -74,7 +73,9 @@ class _UnsafeRouteScreenState extends State<UnsafeRouteScreen> {
if (v != null) {
route.via = v;
}
})),
},
),
),
//TODO: Android doesn't appear to support route based MTU, figure this out
// ConfigItem(
// label: Text('MTU'),
@ -90,7 +91,8 @@ class _UnsafeRouteScreenState extends State<UnsafeRouteScreen> {
// onSaved: (v) {
// route.mtu = int.tryParse(v);
// })),
]),
],
),
widget.onDelete != null
? Padding(
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
@ -98,13 +100,18 @@ class _UnsafeRouteScreenState extends State<UnsafeRouteScreen> {
width: double.infinity,
child: DangerButton(
child: Text('Delete'),
onPressed: () => Utils.confirmDelete(context, 'Delete unsafe route?', () {
onPressed:
() => Utils.confirmDelete(context, 'Delete unsafe route?', () {
Navigator.of(context).pop();
widget.onDelete!();
}),
)))
: Container()
]));
),
),
)
: Container(),
],
),
);
}
_onSave() {

View file

@ -8,11 +8,7 @@ import 'package:mobile_nebula/screens/siteConfig/UnsafeRouteScreen.dart';
import 'package:mobile_nebula/services/utils.dart';
class UnsafeRoutesScreen extends StatefulWidget {
const UnsafeRoutesScreen({
Key? key,
required this.unsafeRoutes,
required this.onSave,
}) : super(key: key);
const UnsafeRoutesScreen({Key? key, required this.unsafeRoutes, required this.onSave}) : super(key: key);
final List<UnsafeRoute> unsafeRoutes;
final ValueChanged<List<UnsafeRoute>>? onSave;
@ -41,9 +37,8 @@ class _UnsafeRoutesScreenState extends State<UnsafeRoutesScreen> {
title: 'Unsafe Routes',
changed: changed,
onSave: _onSave,
child: ConfigSection(
children: _buildRoutes(),
));
child: ConfigSection(children: _buildRoutes()),
);
}
_onSave() {
@ -57,7 +52,8 @@ class _UnsafeRoutesScreenState extends State<UnsafeRoutesScreen> {
final double ipWidth = Utils.textSize("000.000.000.000/00", CupertinoTheme.of(context).textTheme.textStyle).width;
List<Widget> items = [];
unsafeRoutes.forEach((key, route) {
items.add(ConfigPageItem(
items.add(
ConfigPageItem(
disabled: widget.onSave == null,
label: Text(route.route ?? ''),
labelWidth: ipWidth,
@ -77,14 +73,17 @@ class _UnsafeRoutesScreenState extends State<UnsafeRoutesScreen> {
changed = true;
unsafeRoutes.remove(key);
});
});
},
);
});
},
));
),
);
});
if (widget.onSave != null) {
items.add(ConfigButtonItem(
items.add(
ConfigButtonItem(
content: Text('Add a new route'),
onPressed: () {
Utils.openPage(context, (context) {
@ -95,10 +94,12 @@ class _UnsafeRoutesScreenState extends State<UnsafeRoutesScreen> {
changed = true;
unsafeRoutes[UniqueKey()] = route;
});
});
},
);
});
},
));
),
);
}
return items;

View file

@ -40,8 +40,12 @@ class Share {
/// - title: Title of message or subject if sending an email
/// - filePath: Path to the file to share
/// - filename: An optional filename to override the existing file
static Future<bool> shareFile(BuildContext context,
{required String title, required String filePath, String? filename}) async {
static Future<bool> shareFile(
BuildContext context, {
required String title,
required String filePath,
String? filename,
}) async {
assert(title.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,
// and then delete it
final xFile = sp.XFile(filePath, name: filename);
final result = await sp.Share.shareXFiles([xFile],
subject: title, sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size);
final result = await sp.Share.shareXFiles(
[xFile],
subject: title,
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
return result.status == sp.ShareResultStatus.success;
}
}

View file

@ -20,9 +20,12 @@ class Storage {
var completer = Completer<List<FileSystemEntity>>();
Directory(parent).list().listen((FileSystemEntity entity) {
Directory(parent)
.list()
.listen((FileSystemEntity entity) {
list.add(entity);
}).onDone(() {
})
.onDone(() {
completer.complete(list);
});

View file

@ -342,10 +342,7 @@ class MaterialTheme {
useMaterial3: true,
brightness: colorScheme.brightness,
colorScheme: colorScheme,
textTheme: textTheme.apply(
bodyColor: colorScheme.onSurface,
displayColor: colorScheme.onSurface,
),
textTheme: textTheme.apply(bodyColor: colorScheme.onSurface, displayColor: colorScheme.onSurface),
scaffoldBackgroundColor: colorScheme.surface,
canvasColor: colorScheme.surface,
);

View file

@ -27,20 +27,16 @@ class Utils {
}
static Size textSize(String text, TextStyle style) {
final TextPainter textPainter =
TextPainter(text: TextSpan(text: text, style: style), maxLines: 1, textDirection: TextDirection.ltr)
..layout(minWidth: 0, maxWidth: double.infinity);
final TextPainter textPainter = TextPainter(
text: TextSpan(text: text, style: style),
maxLines: 1,
textDirection: TextDirection.ltr,
)..layout(minWidth: 0, maxWidth: double.infinity);
return textPainter.size;
}
static openPage(BuildContext context, WidgetBuilder pageToDisplayBuilder) {
Navigator.push(
context,
platformPageRoute(
context: context,
builder: pageToDisplayBuilder,
),
);
Navigator.push(context, platformPageRoute(context: context, builder: pageToDisplayBuilder));
}
static String itemCountFormat(int items, {singleSuffix = "item", multiSuffix = "items"}) {
@ -63,7 +59,8 @@ class Utils {
} else {
onPressed();
}
});
},
);
}
return IconButton(
@ -82,12 +79,20 @@ class Utils {
static Widget trailingSaveWidget(BuildContext context, Function onPressed) {
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
static confirmDelete(BuildContext context, String title, Function onConfirm,
{String deleteLabel = 'Delete', String cancelLabel = 'Cancel'}) {
static confirmDelete(
BuildContext context,
String title,
Function onConfirm, {
String deleteLabel = 'Delete',
String cancelLabel = 'Cancel',
}) {
showDialog<void>(
context: context,
barrierDismissible: false,
@ -96,9 +101,10 @@ class Utils {
title: Text(title),
actions: <Widget>[
PlatformDialogAction(
child: Text(deleteLabel,
style:
TextStyle(fontWeight: FontWeight.bold, color: CupertinoColors.systemRed.resolveFrom(context))),
child: Text(
deleteLabel,
style: TextStyle(fontWeight: FontWeight.bold, color: CupertinoColors.systemRed.resolveFrom(context)),
),
onPressed: () {
Navigator.pop(context);
onConfirm();
@ -109,10 +115,11 @@ class Utils {
onPressed: () {
Navigator.of(context).pop();
},
)
),
],
);
});
},
);
}
static popError(BuildContext context, String title, String error, {StackTrace? stack}) {
@ -125,14 +132,18 @@ class Utils {
barrierDismissible: false,
builder: (context) {
if (Platform.isAndroid) {
return AlertDialog(title: Text(title), content: Text(error), actions: <Widget>[
return AlertDialog(
title: Text(title),
content: Text(error),
actions: <Widget>[
TextButton(
child: Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
)
]);
),
],
);
}
return CupertinoAlertDialog(
@ -144,10 +155,11 @@ class Utils {
onPressed: () {
Navigator.of(context).pop();
},
)
),
],
);
});
},
);
}
static launchUrl(String url, BuildContext context) async {