Add Flutter lint (#253)

* Enable `flutter_lints` linting

* Fix unmarked deps, we aren't on web so we don't need a URL strategy

* ` dart fix --apply --code=use_super_parameters`

* `dart fix --apply --code=use_key_in_widget_constructors`

* `dart fix --apply --code=use_function_type_syntax_for_parameters`

* Ignore code-generated `lib/services/theme.dart` file

* `dart fix --apply --code=unnecessary_this`

* `dart fix --apply --code=unnecessary_null_in_if_null_operators`

* `dart fix --apply --code=unnecessary_new`

* `dart fix --apply --code=sort_child_properties_last`

* `dart fix --apply --code=sized_box_for_whitespace`

* `dart fix --apply --code=prefer_typing_uninitialized_variables`

* `dart fix --apply --code=prefer_is_empty`

* `dart fix --apply --code=prefer_interpolation_to_compose_strings`

* `dart fix --apply --code=prefer_final_fields`

* `dart fix --apply --code=prefer_const_constructors_in_immutables`

* `dart fix --apply --code=prefer_collection_literals`

* `dart fix --apply --code=no_leading_underscores_for_local_identifiers`

* `dart fix --apply --code=curly_braces_in_flow_control_structures`

* `dart fix --apply --code=avoid_function_literals_in_foreach_calls`

* `dart fix --apply --code=annotate_overrides`

* Add CI for dart linting

* `dart format lib/`

* Re-enable the `usePathUrlStrategy` call, with proper deps

https://docs.flutter.dev/ui/navigation/url-strategies#configuring-the-url-strategy
This commit is contained in:
Caleb Jasik 2025-03-04 11:29:23 -06:00 committed by GitHub
parent bcfcadec8e
commit 2b844d27dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 297 additions and 233 deletions

View file

@ -1,12 +1,11 @@
name: Flutter format name: Flutter check
on: on:
push: push:
branches: branches:
- main - main
pull_request: pull_request:
paths: paths:
- '.github/workflows/flutterfmt.yml' - '.github/workflows/fluttercheck.yml'
- '.github/workflows/flutterfmt.sh'
- '**.dart' - '**.dart'
jobs: jobs:
flutterfmt: flutterfmt:
@ -25,3 +24,19 @@ jobs:
- name: Check formating - name: Check formating
run: dart format -l120 lib/ --set-exit-if-changed --suppress-analytics --output none run: dart format -l120 lib/ --set-exit-if-changed --suppress-analytics --output none
flutterlint:
name: Run flutter lint
runs-on: ubuntu-latest
steps:
- name: Install flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.29.0'
- name: Check out code
uses: actions/checkout@v4
with:
show-progress: false
- name: Check linting
run: dart fix --dry-run

View file

@ -1,3 +1,35 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/tools/linter-rules.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/tools/analysis # https://dart.dev/tools/analysis
formatter: formatter:
page_width: 120 page_width: 120
analyzer:
exclude:
# This is a generated file, let's ignore it.
- lib/services/theme.dart

View file

@ -8,7 +8,7 @@ import 'IPField.dart';
//TODO: Support initialValue //TODO: Support initialValue
class CIDRField extends StatefulWidget { class CIDRField extends StatefulWidget {
const CIDRField({ const CIDRField({
Key? key, super.key,
this.ipHelp = "ip address", this.ipHelp = "ip address",
this.autoFocus = false, this.autoFocus = false,
this.focusNode, this.focusNode,
@ -17,7 +17,7 @@ class CIDRField extends StatefulWidget {
this.textInputAction, this.textInputAction,
this.ipController, this.ipController,
this.bitsController, this.bitsController,
}) : super(key: key); });
final String ipHelp; final String ipHelp;
final bool autoFocus; final bool autoFocus;

View file

@ -6,21 +6,18 @@ import 'package:mobile_nebula/validators/ipValidator.dart';
class CIDRFormField extends FormField<CIDR> { class CIDRFormField extends FormField<CIDR> {
//TODO: onSaved, validator, auto-validate, enabled? //TODO: onSaved, validator, auto-validate, enabled?
CIDRFormField({ CIDRFormField({
Key? key, super.key,
autoFocus = false, autoFocus = false,
enableIPV6 = false, enableIPV6 = false,
focusNode, focusNode,
nextFocusNode, nextFocusNode,
ValueChanged<CIDR>? onChanged, ValueChanged<CIDR>? onChanged,
FormFieldSetter<CIDR>? onSaved, super.onSaved,
textInputAction, textInputAction,
CIDR? initialValue, super.initialValue,
this.ipController, this.ipController,
this.bitsController, this.bitsController,
}) : super( }) : super(
key: key,
initialValue: initialValue,
onSaved: onSaved,
validator: (cidr) { validator: (cidr) {
if (cidr == null) { if (cidr == null) {
return "Please fill out this field"; return "Please fill out this field";

View file

@ -4,7 +4,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class DangerButton extends StatelessWidget { class DangerButton extends StatelessWidget {
const DangerButton({Key? key, required this.child, this.onPressed}) : super(key: key); const DangerButton({super.key, required this.child, this.onPressed});
final Widget child; final Widget child;
final GestureTapCallback? onPressed; final GestureTapCallback? onPressed;
@ -14,11 +14,11 @@ class DangerButton extends StatelessWidget {
if (Platform.isAndroid) { if (Platform.isAndroid) {
return FilledButton( return FilledButton(
onPressed: onPressed, onPressed: onPressed,
child: child,
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error, backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError, foregroundColor: Theme.of(context).colorScheme.onError,
), ),
child: child,
); );
} else { } else {
// Workaround for https://github.com/flutter/flutter/issues/161590 // Workaround for https://github.com/flutter/flutter/issues/161590
@ -26,9 +26,9 @@ class DangerButton extends StatelessWidget {
return CupertinoTheme( return CupertinoTheme(
data: themeData.copyWith(primaryColor: CupertinoColors.white), data: themeData.copyWith(primaryColor: CupertinoColors.white),
child: CupertinoButton( child: CupertinoButton(
child: child,
onPressed: onPressed, onPressed: onPressed,
color: CupertinoColors.systemRed.resolveFrom(context), color: CupertinoColors.systemRed.resolveFrom(context),
child: child,
), ),
); );
} }

View file

@ -6,14 +6,14 @@ import 'package:mobile_nebula/services/utils.dart';
/// SimplePage with a form and built in validation and confirmation to discard changes if any are made /// SimplePage with a form and built in validation and confirmation to discard changes if any are made
class FormPage extends StatefulWidget { class FormPage extends StatefulWidget {
const FormPage({ const FormPage({
Key? key, super.key,
required this.title, required this.title,
required this.child, required this.child,
required this.onSave, required this.onSave,
required this.changed, required this.changed,
this.hideSave = false, this.hideSave = false,
this.scrollController, this.scrollController,
}) : super(key: key); });
final String title; final String title;
final Function onSave; final Function onSave;

View file

@ -8,7 +8,7 @@ import 'IPField.dart';
//TODO: Support initialValue //TODO: Support initialValue
class IPAndPortField extends StatefulWidget { class IPAndPortField extends StatefulWidget {
const IPAndPortField({ const IPAndPortField({
Key? key, super.key,
this.ipOnly = false, this.ipOnly = false,
this.ipHelp = "ip address", this.ipHelp = "ip address",
this.autoFocus = false, this.autoFocus = false,
@ -20,7 +20,7 @@ class IPAndPortField extends StatefulWidget {
this.ipTextAlign, this.ipTextAlign,
this.ipController, this.ipController,
this.portController, this.portController,
}) : super(key: key); });
final String ipHelp; final String ipHelp;
final bool ipOnly; final bool ipOnly;

View file

@ -8,7 +8,7 @@ import 'IPAndPortField.dart';
class IPAndPortFormField extends FormField<IPAndPort> { class IPAndPortFormField extends FormField<IPAndPort> {
//TODO: onSaved, validator, auto-validate, enabled? //TODO: onSaved, validator, auto-validate, enabled?
IPAndPortFormField({ IPAndPortFormField({
Key? key, super.key,
ipOnly = false, ipOnly = false,
enableIPV6 = false, enableIPV6 = false,
ipHelp = "ip address", ipHelp = "ip address",
@ -16,17 +16,14 @@ class IPAndPortFormField extends FormField<IPAndPort> {
focusNode, focusNode,
nextFocusNode, nextFocusNode,
ValueChanged<IPAndPort>? onChanged, ValueChanged<IPAndPort>? onChanged,
FormFieldSetter<IPAndPort>? onSaved, super.onSaved,
textInputAction, textInputAction,
IPAndPort? initialValue, super.initialValue,
noBorder, noBorder,
ipTextAlign = TextAlign.center, ipTextAlign = TextAlign.center,
this.ipController, this.ipController,
this.portController, this.portController,
}) : super( }) : super(
key: key,
initialValue: initialValue,
onSaved: onSaved,
validator: (ipAndPort) { validator: (ipAndPort) {
if (ipAndPort == null) { if (ipAndPort == null) {
return "Please fill out this field"; return "Please fill out this field";
@ -114,10 +111,7 @@ class _IPAndPortFormField extends FormFieldState<IPAndPort> {
@override @override
void didUpdateWidget(IPAndPortFormField oldWidget) { void didUpdateWidget(IPAndPortFormField oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
var update = IPAndPort( var update = IPAndPort(ip: widget.ipController?.text, port: int.tryParse(widget.portController?.text ?? ""));
ip: widget.ipController?.text,
port: int.tryParse(widget.portController?.text ?? "") ?? null,
);
bool shouldUpdate = false; bool shouldUpdate = false;
if (widget.ipController != oldWidget.ipController) { if (widget.ipController != oldWidget.ipController) {

View file

@ -17,7 +17,7 @@ class IPField extends StatelessWidget {
final textAlign; final textAlign;
const IPField({ const IPField({
Key? key, super.key,
this.ipOnly = false, this.ipOnly = false,
this.help = "ip address", this.help = "ip address",
this.autoFocus = false, this.autoFocus = false,
@ -28,7 +28,7 @@ class IPField extends StatelessWidget {
this.textInputAction, this.textInputAction,
this.controller, this.controller,
this.textAlign = TextAlign.center, this.textAlign = TextAlign.center,
}) : super(key: key); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -48,7 +48,7 @@ class IPField extends StatelessWidget {
maxLength: ipOnly ? 15 : null, maxLength: ipOnly ? 15 : null,
maxLengthEnforcement: ipOnly ? MaxLengthEnforcement.enforced : MaxLengthEnforcement.none, maxLengthEnforcement: ipOnly ? MaxLengthEnforcement.enforced : MaxLengthEnforcement.none,
inputFormatters: ipOnly ? [IPTextInputFormatter()] : [FilteringTextInputFormatter.allow(RegExp(r'[^\s]+'))], inputFormatters: ipOnly ? [IPTextInputFormatter()] : [FilteringTextInputFormatter.allow(RegExp(r'[^\s]+'))],
textInputAction: this.textInputAction, textInputAction: textInputAction,
placeholder: help, placeholder: help,
), ),
); );
@ -72,7 +72,7 @@ class IPTextInputFormatter extends TextInputFormatter {
TextEditingValue _selectionAwareTextManipulation( TextEditingValue _selectionAwareTextManipulation(
TextEditingValue value, TextEditingValue value,
String substringManipulation(String substring), String Function(String substring) substringManipulation,
) { ) {
final int selectionStartIndex = value.selection.start; final int selectionStartIndex = value.selection.start;
final int selectionEndIndex = value.selection.end; final int selectionEndIndex = value.selection.end;

View file

@ -9,7 +9,7 @@ import 'IPField.dart';
class IPFormField extends FormField<String> { class IPFormField extends FormField<String> {
//TODO: validator, auto-validate, enabled? //TODO: validator, auto-validate, enabled?
IPFormField({ IPFormField({
Key? key, super.key,
ipOnly = false, ipOnly = false,
enableIPV6 = false, enableIPV6 = false,
help = "ip address", help = "ip address",
@ -17,7 +17,7 @@ class IPFormField extends FormField<String> {
focusNode, focusNode,
nextFocusNode, nextFocusNode,
ValueChanged<String>? onChanged, ValueChanged<String>? onChanged,
FormFieldSetter<String>? onSaved, super.onSaved,
textPadding = const EdgeInsets.all(6.0), textPadding = const EdgeInsets.all(6.0),
textInputAction, textInputAction,
initialValue, initialValue,
@ -25,9 +25,7 @@ class IPFormField extends FormField<String> {
crossAxisAlignment = CrossAxisAlignment.center, crossAxisAlignment = CrossAxisAlignment.center,
textAlign = TextAlign.center, textAlign = TextAlign.center,
}) : super( }) : super(
key: key,
initialValue: initialValue, initialValue: initialValue,
onSaved: onSaved,
validator: (ip) { validator: (ip) {
if (ip == null || ip == "") { if (ip == null || ip == "") {
return "Please fill out this field"; return "Please fill out this field";
@ -108,8 +106,9 @@ class _IPFormField extends FormFieldState<String> {
oldWidget.controller?.removeListener(_handleControllerChanged); oldWidget.controller?.removeListener(_handleControllerChanged);
widget.controller?.addListener(_handleControllerChanged); widget.controller?.addListener(_handleControllerChanged);
if (oldWidget.controller != null && widget.controller == null) if (oldWidget.controller != null && widget.controller == null) {
_controller = TextEditingController.fromValue(oldWidget.controller!.value); _controller = TextEditingController.fromValue(oldWidget.controller!.value);
}
if (widget.controller != null) { if (widget.controller != null) {
setValue(widget.controller!.text); setValue(widget.controller!.text);
if (oldWidget.controller == null) _controller = null; if (oldWidget.controller == null) _controller = null;

View file

@ -6,7 +6,7 @@ import 'package:mobile_nebula/components/SpecialTextField.dart';
class PlatformTextFormField extends FormField<String> { class PlatformTextFormField extends FormField<String> {
//TODO: auto-validate, enabled? //TODO: auto-validate, enabled?
PlatformTextFormField({ PlatformTextFormField({
Key? key, super.key,
widgetKey, widgetKey,
this.controller, this.controller,
focusNode, focusNode,
@ -28,11 +28,9 @@ class PlatformTextFormField extends FormField<String> {
String? initialValue, String? initialValue,
String? placeholder, String? placeholder,
FormFieldValidator<String>? validator, FormFieldValidator<String>? validator,
ValueChanged<String?>? onSaved, super.onSaved,
}) : super( }) : super(
key: key,
initialValue: controller != null ? controller.text : (initialValue ?? ''), initialValue: controller != null ? controller.text : (initialValue ?? ''),
onSaved: onSaved,
validator: (str) { validator: (str) {
if (validator != null) { if (validator != null) {
return validator(str); return validator(str);
@ -117,8 +115,9 @@ class _PlatformTextFormFieldState extends FormFieldState<String> {
oldWidget.controller?.removeListener(_handleControllerChanged); oldWidget.controller?.removeListener(_handleControllerChanged);
widget.controller?.addListener(_handleControllerChanged); widget.controller?.addListener(_handleControllerChanged);
if (oldWidget.controller != null && widget.controller == null) if (oldWidget.controller != null && widget.controller == null) {
_controller = TextEditingController.fromValue(oldWidget.controller!.value); _controller = TextEditingController.fromValue(oldWidget.controller!.value);
}
if (widget.controller != null) { if (widget.controller != null) {
setValue(widget.controller!.text); setValue(widget.controller!.text);
if (oldWidget.controller == null) _controller = null; if (oldWidget.controller == null) _controller = null;

View file

@ -6,7 +6,7 @@ enum SimpleScrollable { none, vertical, horizontal, both }
class SimplePage extends StatelessWidget { class SimplePage extends StatelessWidget {
const SimplePage({ const SimplePage({
Key? key, super.key,
required this.title, required this.title,
required this.child, required this.child,
this.leadingAction, this.leadingAction,
@ -19,7 +19,7 @@ class SimplePage extends StatelessWidget {
this.onLoading, this.onLoading,
this.alignment, this.alignment,
this.refreshController, this.refreshController,
}) : super(key: key); });
final Widget title; final Widget title;
final Widget child; final Widget child;
@ -43,13 +43,13 @@ class SimplePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget realChild = child; Widget realChild = child;
var addScrollbar = this.scrollbar; var addScrollbar = scrollbar;
if (scrollable == SimpleScrollable.vertical || scrollable == SimpleScrollable.both) { if (scrollable == SimpleScrollable.vertical || scrollable == SimpleScrollable.both) {
realChild = SingleChildScrollView( realChild = SingleChildScrollView(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
child: realChild,
controller: refreshController == null ? scrollController : null, controller: refreshController == null ? scrollController : null,
child: realChild,
); );
addScrollbar = true; addScrollbar = true;
} }
@ -69,10 +69,10 @@ class SimplePage extends StatelessWidget {
onRefresh: onRefresh, onRefresh: onRefresh,
onLoading: onLoading, onLoading: onLoading,
controller: refreshController!, controller: refreshController!,
child: realChild,
enablePullUp: onLoading != null, enablePullUp: onLoading != null,
enablePullDown: onRefresh != null, enablePullDown: onRefresh != null,
footer: ClassicFooter(loadStyle: LoadStyle.ShowWhenLoading), footer: ClassicFooter(loadStyle: LoadStyle.ShowWhenLoading),
child: realChild,
), ),
); );
addScrollbar = true; addScrollbar = true;
@ -83,7 +83,7 @@ class SimplePage extends StatelessWidget {
} }
if (alignment != null) { if (alignment != null) {
realChild = Align(alignment: this.alignment!, child: realChild); realChild = Align(alignment: alignment!, child: realChild);
} }
if (bottomBar != null) { if (bottomBar != null) {

View file

@ -6,7 +6,7 @@ import 'package:mobile_nebula/models/Site.dart';
import 'package:mobile_nebula/services/utils.dart'; import 'package:mobile_nebula/services/utils.dart';
class SiteItem extends StatelessWidget { class SiteItem extends StatelessWidget {
const SiteItem({Key? key, required this.site, this.onPressed}) : super(key: key); const SiteItem({super.key, required this.site, this.onPressed});
final Site site; final Site site;
final onPressed; final onPressed;
@ -14,7 +14,7 @@ class SiteItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final borderColor = final borderColor =
site.errors.length > 0 site.errors.isNotEmpty
? CupertinoColors.systemRed.resolveFrom(context) ? CupertinoColors.systemRed.resolveFrom(context)
: site.connected : site.connected
? CupertinoColors.systemGreen.resolveFrom(context) ? CupertinoColors.systemGreen.resolveFrom(context)

View file

@ -4,7 +4,7 @@ import 'package:flutter_svg/svg.dart';
import '../models/Site.dart'; import '../models/Site.dart';
class SiteTitle extends StatelessWidget { class SiteTitle extends StatelessWidget {
const SiteTitle({Key? key, required this.site}) : super(key: key); const SiteTitle({super.key, required this.site});
final Site site; final Site site;

View file

@ -5,8 +5,14 @@ import 'package:flutter/material.dart';
// This is a button that pushes the bare minimum onto you, it doesn't even respect button themes - unless you tell it to // This is a button that pushes the bare minimum onto you, it doesn't even respect button themes - unless you tell it to
class SpecialButton extends StatefulWidget { class SpecialButton extends StatefulWidget {
const SpecialButton({Key? key, this.child, this.color, this.onPressed, this.useButtonTheme = false, this.decoration}) const SpecialButton({
: super(key: key); super.key,
this.child,
this.color,
this.onPressed,
this.useButtonTheme = false,
this.decoration,
});
final Widget? child; final Widget? child;
final Color? color; final Color? color;
@ -26,7 +32,7 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
} }
Widget _buildAndroid() { Widget _buildAndroid() {
var textStyle; TextStyle? textStyle;
if (widget.useButtonTheme) { if (widget.useButtonTheme) {
textStyle = Theme.of(context).textTheme.labelLarge; textStyle = Theme.of(context).textTheme.labelLarge;
} }
@ -36,7 +42,7 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
child: Ink( child: Ink(
decoration: widget.decoration, decoration: widget.decoration,
color: widget.color, color: widget.color,
child: InkWell(child: widget.child, onTap: widget.onPressed), child: InkWell(onTap: widget.onPressed, child: widget.child),
), ),
); );
} }
@ -59,7 +65,7 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
button: true, button: true,
child: FadeTransition( child: FadeTransition(
opacity: _opacityAnimation!, opacity: _opacityAnimation!,
child: DefaultTextStyle(style: textStyle, child: Container(child: widget.child, color: widget.color)), child: DefaultTextStyle(style: textStyle, child: Container(color: widget.color, child: widget.child)),
), ),
), ),
), ),

View file

@ -5,7 +5,7 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
/// A normal TextField or CupertinoTextField that looks the same on all platforms /// A normal TextField or CupertinoTextField that looks the same on all platforms
class SpecialTextField extends StatefulWidget { class SpecialTextField extends StatefulWidget {
const SpecialTextField({ const SpecialTextField({
Key? key, super.key,
this.placeholder, this.placeholder,
this.suffix, this.suffix,
this.controller, this.controller,
@ -28,7 +28,7 @@ class SpecialTextField extends StatefulWidget {
this.keyboardAppearance, this.keyboardAppearance,
this.textAlignVertical, this.textAlignVertical,
this.inputFormatters, this.inputFormatters,
}) : super(key: key); });
final String? placeholder; final String? placeholder;
final TextEditingController? controller; final TextEditingController? controller;
@ -64,7 +64,7 @@ class _SpecialTextFieldState extends State<SpecialTextField> {
@override @override
void initState() { void initState() {
if (widget.inputFormatters == null || formatters.length == 0) { if (widget.inputFormatters == null || formatters.isEmpty) {
formatters = [FilteringTextInputFormatter.allow(RegExp(r'[^\t]'))]; formatters = [FilteringTextInputFormatter.allow(RegExp(r'[^\t]'))];
} else { } else {
formatters = widget.inputFormatters!; formatters = widget.inputFormatters!;

View file

@ -4,7 +4,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PrimaryButton extends StatelessWidget { class PrimaryButton extends StatelessWidget {
const PrimaryButton({Key? key, required this.child, this.onPressed}) : super(key: key); const PrimaryButton({super.key, required this.child, this.onPressed});
final Widget child; final Widget child;
final GestureTapCallback? onPressed; final GestureTapCallback? onPressed;
@ -14,8 +14,8 @@ class PrimaryButton extends StatelessWidget {
if (Platform.isAndroid) { if (Platform.isAndroid) {
return FilledButton( return FilledButton(
onPressed: onPressed, onPressed: onPressed,
child: child,
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.primary), style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.primary),
child: child,
); );
} else { } else {
// Workaround for https://github.com/flutter/flutter/issues/161590 // Workaround for https://github.com/flutter/flutter/issues/161590
@ -23,9 +23,9 @@ class PrimaryButton extends StatelessWidget {
return CupertinoTheme( return CupertinoTheme(
data: themeData.copyWith(primaryColor: CupertinoColors.white), data: themeData.copyWith(primaryColor: CupertinoColors.white),
child: CupertinoButton( child: CupertinoButton(
child: child,
onPressed: onPressed, onPressed: onPressed,
color: CupertinoColors.secondaryLabel.resolveFrom(context), color: CupertinoColors.secondaryLabel.resolveFrom(context),
child: child,
), ),
); );
} }

View file

@ -5,7 +5,7 @@ import 'package:mobile_nebula/services/utils.dart';
// A config item that detects tapping and calls back on a tap // A config item that detects tapping and calls back on a tap
class ConfigButtonItem extends StatelessWidget { class ConfigButtonItem extends StatelessWidget {
const ConfigButtonItem({Key? key, this.content, this.onPressed}) : super(key: key); const ConfigButtonItem({super.key, this.content, this.onPressed});
final Widget? content; final Widget? content;
final onPressed; final onPressed;

View file

@ -4,13 +4,13 @@ import 'package:mobile_nebula/services/utils.dart';
class ConfigCheckboxItem extends StatelessWidget { class ConfigCheckboxItem extends StatelessWidget {
const ConfigCheckboxItem({ const ConfigCheckboxItem({
Key? key, super.key,
this.label, this.label,
this.content, this.content,
this.labelWidth = 100, this.labelWidth = 100,
this.onChanged, this.onChanged,
this.checked = false, this.checked = false,
}) : super(key: key); });
final Widget? label; final Widget? label;
final Widget? content; final Widget? content;
@ -26,8 +26,8 @@ class ConfigCheckboxItem extends StatelessWidget {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
label != null ? Container(width: labelWidth, child: label) : Container(), label != null ? SizedBox(width: labelWidth, child: label) : Container(),
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))), Expanded(child: Container(padding: EdgeInsets.only(right: 10), child: content)),
checked checked
? Icon(CupertinoIcons.check_mark, color: CupertinoColors.systemBlue.resolveFrom(context)) ? Icon(CupertinoIcons.check_mark, color: CupertinoColors.systemBlue.resolveFrom(context))
: Container(), : Container(),

View file

@ -9,7 +9,7 @@ TextStyle basicTextStyle(BuildContext context) =>
const double _headerFontSize = 13.0; const double _headerFontSize = 13.0;
class ConfigHeader extends StatelessWidget { class ConfigHeader extends StatelessWidget {
const ConfigHeader({Key? key, required this.label, this.color}) : super(key: key); const ConfigHeader({super.key, required this.label, this.color});
final String label; final String label;
final Color? color; final Color? color;

View file

@ -6,12 +6,12 @@ import 'package:mobile_nebula/services/utils.dart';
class ConfigItem extends StatelessWidget { class ConfigItem extends StatelessWidget {
const ConfigItem({ const ConfigItem({
Key? key, super.key,
this.label, this.label,
required this.content, required this.content,
this.labelWidth = 100, this.labelWidth = 100,
this.crossAxisAlignment = CrossAxisAlignment.center, this.crossAxisAlignment = CrossAxisAlignment.center,
}) : super(key: key); });
final Widget? label; final Widget? label;
final Widget content; final Widget content;
@ -20,7 +20,7 @@ class ConfigItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var textStyle; TextStyle textStyle;
if (Platform.isAndroid) { if (Platform.isAndroid) {
textStyle = Theme.of(context).textTheme.labelLarge!.copyWith(fontWeight: FontWeight.normal); textStyle = Theme.of(context).textTheme.labelLarge!.copyWith(fontWeight: FontWeight.normal);
} else { } else {
@ -34,7 +34,7 @@ class ConfigItem extends StatelessWidget {
child: Row( child: Row(
crossAxisAlignment: crossAxisAlignment, crossAxisAlignment: crossAxisAlignment,
children: <Widget>[ children: <Widget>[
Container(width: labelWidth, child: DefaultTextStyle(style: textStyle, child: Container(child: label))), SizedBox(width: labelWidth, child: DefaultTextStyle(style: textStyle, child: Container(child: label))),
Expanded(child: DefaultTextStyle(style: textStyle, child: Container(child: content))), Expanded(child: DefaultTextStyle(style: textStyle, child: Container(child: content))),
], ],
), ),

View file

@ -2,19 +2,20 @@ import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile_nebula/components/SpecialButton.dart'; import 'package:mobile_nebula/components/SpecialButton.dart';
import 'package:mobile_nebula/services/utils.dart'; import 'package:mobile_nebula/services/utils.dart';
class ConfigPageItem extends StatelessWidget { class ConfigPageItem extends StatelessWidget {
const ConfigPageItem({ const ConfigPageItem({
Key? key, super.key,
this.label, this.label,
this.content, this.content,
this.labelWidth = 100, this.labelWidth = 100,
this.onPressed, this.onPressed,
this.disabled = false, this.disabled = false,
this.crossAxisAlignment = CrossAxisAlignment.center, this.crossAxisAlignment = CrossAxisAlignment.center,
}) : super(key: key); });
final Widget? label; final Widget? label;
final Widget? content; final Widget? content;
@ -25,7 +26,7 @@ class ConfigPageItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme; dynamic theme;
if (Platform.isAndroid) { if (Platform.isAndroid) {
final origTheme = Theme.of(context); final origTheme = Theme.of(context);
@ -44,7 +45,7 @@ class ConfigPageItem extends StatelessWidget {
Widget _buildContent(BuildContext context) { Widget _buildContent(BuildContext context) {
return SpecialButton( return SpecialButton(
onPressed: this.disabled ? null : onPressed, onPressed: disabled ? null : onPressed,
color: Utils.configItemBackground(context), color: Utils.configItemBackground(context),
child: Container( child: Container(
padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15), padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15),
@ -52,9 +53,9 @@ class ConfigPageItem extends StatelessWidget {
child: Row( child: Row(
crossAxisAlignment: crossAxisAlignment, crossAxisAlignment: crossAxisAlignment,
children: <Widget>[ children: <Widget>[
label != null ? Container(width: labelWidth, child: label) : Container(), label != null ? SizedBox(width: labelWidth, child: label) : Container(),
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))), Expanded(child: Container(padding: EdgeInsets.only(right: 10), child: content)),
this.disabled disabled
? Container() ? Container()
: Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18), : Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18),
], ],

View file

@ -4,8 +4,7 @@ import 'package:mobile_nebula/services/utils.dart';
import 'ConfigHeader.dart'; import 'ConfigHeader.dart';
class ConfigSection extends StatelessWidget { class ConfigSection extends StatelessWidget {
const ConfigSection({Key? key, this.label, required this.children, this.borderColor, this.labelColor}) const ConfigSection({super.key, this.label, required this.children, this.borderColor, this.labelColor});
: super(key: key);
final List<Widget> children; final List<Widget> children;
final String? label; final String? label;
@ -16,21 +15,21 @@ class ConfigSection extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final border = BorderSide(color: borderColor ?? Utils.configSectionBorder(context)); final border = BorderSide(color: borderColor ?? Utils.configSectionBorder(context));
List<Widget> _children = []; List<Widget> mappedChildren = [];
final len = children.length; final len = children.length;
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
_children.add(children[i]); mappedChildren.add(children[i]);
if (i < len - 1) { if (i < len - 1) {
double pad = 15; double pad = 15;
if (children[i + 1].runtimeType.toString() == 'ConfigButtonItem') { if (children[i + 1].runtimeType.toString() == 'ConfigButtonItem') {
pad = 0; pad = 0;
} }
_children.add( mappedChildren.add(
Padding( Padding(
child: Divider(height: 1, color: Utils.configSectionBorder(context)),
padding: EdgeInsets.only(left: pad), padding: EdgeInsets.only(left: pad),
child: Divider(height: 1, color: Utils.configSectionBorder(context)),
), ),
); );
} }
@ -45,7 +44,7 @@ class ConfigSection extends StatelessWidget {
border: Border(top: border, bottom: border), border: Border(top: border, bottom: border),
color: Utils.configItemBackground(context), color: Utils.configItemBackground(context),
), ),
child: Column(children: _children), child: Column(children: mappedChildren),
), ),
], ],
); );

View file

@ -2,11 +2,11 @@ import 'package:flutter/cupertino.dart';
class ConfigTextItem extends StatelessWidget { class ConfigTextItem extends StatelessWidget {
const ConfigTextItem({ const ConfigTextItem({
Key? key, super.key,
this.placeholder, this.placeholder,
this.controller, this.controller,
this.style = const TextStyle(fontFamily: 'RobotoMono'), this.style = const TextStyle(fontFamily: 'RobotoMono'),
}) : super(key: key); });
final String? placeholder; final String? placeholder;
final TextEditingController? controller; final TextEditingController? controller;

View file

@ -9,14 +9,13 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:mobile_nebula/screens/MainScreen.dart'; import 'package:mobile_nebula/screens/MainScreen.dart';
import 'package:mobile_nebula/screens/EnrollmentScreen.dart'; import 'package:mobile_nebula/screens/EnrollmentScreen.dart';
import 'package:mobile_nebula/services/settings.dart'; import 'package:mobile_nebula/services/settings.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
import 'package:mobile_nebula/services/theme.dart'; import 'package:mobile_nebula/services/theme.dart';
import 'package:mobile_nebula/services/utils.dart'; import 'package:mobile_nebula/services/utils.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:flutter_web_plugins/url_strategy.dart';
Future<void> main() async { Future<void> main() async {
usePathUrlStrategy(); usePathUrlStrategy();
var settings = Settings(); var settings = Settings();
if (settings.trackErrors) { if (settings.trackErrors) {
await SentryFlutter.init((options) { await SentryFlutter.init((options) {
@ -34,12 +33,16 @@ Future<void> main() async {
//TODO: EventChannel might be better than the stream controller we are using now //TODO: EventChannel might be better than the stream controller we are using now
class Main extends StatelessWidget { class Main extends StatelessWidget {
const Main({super.key});
// This widget is the root of your application. // This widget is the root of your application.
@override @override
Widget build(BuildContext context) => App(); Widget build(BuildContext context) => App();
} }
class App extends StatefulWidget { class App extends StatefulWidget {
const App({super.key});
@override @override
_AppState createState() => _AppState(); _AppState createState() => _AppState();
} }
@ -98,15 +101,16 @@ class _AppState extends State<App> {
], ],
title: 'Nebula', title: 'Nebula',
material: (_, __) { material: (_, __) {
return new MaterialAppData( return MaterialAppData(
themeMode: brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark, themeMode: brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark,
theme: brightness == Brightness.light ? theme.light() : theme.dark(), theme: brightness == Brightness.light ? theme.light() : theme.dark(),
); );
}, },
cupertino: (_, __) => CupertinoAppData(theme: CupertinoThemeData(brightness: brightness)), cupertino: (_, __) => CupertinoAppData(theme: CupertinoThemeData(brightness: brightness)),
onGenerateRoute: (settings) { onGenerateRoute: (settings) {
print(settings);
if (settings.name == '/') { if (settings.name == '/') {
return platformPageRoute(context: context, builder: (context) => MainScreen(this.dnEnrolled)); return platformPageRoute(context: context, builder: (context) => MainScreen(dnEnrolled));
} }
final uri = Uri.parse(settings.name!); final uri = Uri.parse(settings.name!);
@ -116,7 +120,7 @@ class _AppState extends State<App> {
context: context, context: context,
builder: builder:
(context) => (context) =>
EnrollmentScreen(code: EnrollmentScreen.parseCode(settings.name!), stream: this.dnEnrolled), EnrollmentScreen(code: EnrollmentScreen.parseCode(settings.name!), stream: dnEnrolled),
); );
} }

View file

@ -3,9 +3,7 @@ class CertificateInfo {
String? rawCert; String? rawCert;
CertificateValidity? validity; CertificateValidity? validity;
CertificateInfo.debug({this.rawCert = ""}) CertificateInfo.debug({this.rawCert = ""}) : cert = Certificate.debug(), validity = CertificateValidity.debug();
: this.cert = Certificate.debug(),
this.validity = CertificateValidity.debug();
CertificateInfo.fromJson(Map<String, dynamic> json) CertificateInfo.fromJson(Map<String, dynamic> json)
: cert = Certificate.fromJson(json['Cert']), : cert = Certificate.fromJson(json['Cert']),
@ -24,7 +22,7 @@ class Certificate {
String fingerprint; String fingerprint;
String signature; String signature;
Certificate.debug() : this.details = CertificateDetails.debug(), this.fingerprint = "DEBUG", this.signature = "DEBUG"; Certificate.debug() : details = CertificateDetails.debug(), fingerprint = "DEBUG", signature = "DEBUG";
Certificate.fromJson(Map<String, dynamic> json) Certificate.fromJson(Map<String, dynamic> json)
: details = CertificateDetails.fromJson(json['details']), : details = CertificateDetails.fromJson(json['details']),
@ -44,7 +42,7 @@ class CertificateDetails {
String issuer; String issuer;
CertificateDetails.debug() CertificateDetails.debug()
: this.name = "DEBUG", : name = "DEBUG",
notBefore = DateTime.now(), notBefore = DateTime.now(),
notAfter = DateTime.now(), notAfter = DateTime.now(),
publicKey = "", publicKey = "",
@ -70,7 +68,7 @@ class CertificateValidity {
bool valid; bool valid;
String reason; String reason;
CertificateValidity.debug() : this.valid = true, this.reason = ""; CertificateValidity.debug() : valid = true, 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

@ -15,7 +15,7 @@ class Site {
late EventChannel _updates; late EventChannel _updates;
/// Signals that something about this site has changed. onError is called with an error string if there was an error /// Signals that something about this site has changed. onError is called with an error string if there was an error
StreamController _change = StreamController.broadcast(); final StreamController _change = StreamController.broadcast();
// Identifiers // Identifiers
late String name; late String name;
@ -139,25 +139,25 @@ class Site {
_updateFromJson(String json) { _updateFromJson(String json) {
var decoded = Site._fromJson(jsonDecode(json)); var decoded = Site._fromJson(jsonDecode(json));
this.name = decoded["name"]; name = decoded["name"];
this.id = decoded['id']; // TODO update EventChannel id = decoded['id']; // TODO update EventChannel
this.staticHostmap = decoded['staticHostmap']; staticHostmap = decoded['staticHostmap'];
this.ca = decoded['ca']; ca = decoded['ca'];
this.certInfo = decoded['certInfo']; certInfo = decoded['certInfo'];
this.lhDuration = decoded['lhDuration']; lhDuration = decoded['lhDuration'];
this.port = decoded['port']; port = decoded['port'];
this.cipher = decoded['cipher']; cipher = decoded['cipher'];
this.sortKey = decoded['sortKey']; sortKey = decoded['sortKey'];
this.mtu = decoded['mtu']; mtu = decoded['mtu'];
this.connected = decoded['connected']; connected = decoded['connected'];
this.status = decoded['status']; status = decoded['status'];
this.logFile = decoded['logFile']; logFile = decoded['logFile'];
this.logVerbosity = decoded['logVerbosity']; logVerbosity = decoded['logVerbosity'];
this.errors = decoded['errors']; errors = decoded['errors'];
this.unsafeRoutes = decoded['unsafeRoutes']; unsafeRoutes = decoded['unsafeRoutes'];
this.managed = decoded['managed']; managed = decoded['managed'];
this.rawConfig = decoded['rawConfig']; rawConfig = decoded['rawConfig'];
this.lastManagedUpdate = decoded['lastManagedUpdate']; lastManagedUpdate = decoded['lastManagedUpdate'];
} }
static _fromJson(Map<String, dynamic> json) { static _fromJson(Map<String, dynamic> json) {
@ -169,15 +169,15 @@ class Site {
List<dynamic> rawUnsafeRoutes = json['unsafeRoutes']; List<dynamic> rawUnsafeRoutes = json['unsafeRoutes'];
List<UnsafeRoute> unsafeRoutes = []; List<UnsafeRoute> unsafeRoutes = [];
rawUnsafeRoutes.forEach((val) { for (var val in rawUnsafeRoutes) {
unsafeRoutes.add(UnsafeRoute.fromJson(val)); unsafeRoutes.add(UnsafeRoute.fromJson(val));
}); }
List<dynamic> rawCA = json['ca']; List<dynamic> rawCA = json['ca'];
List<CertificateInfo> ca = []; List<CertificateInfo> ca = [];
rawCA.forEach((val) { for (var val in rawCA) {
ca.add(CertificateInfo.fromJson(val)); ca.add(CertificateInfo.fromJson(val));
}); }
CertificateInfo? certInfo; CertificateInfo? certInfo;
if (json['cert'] != null) { if (json['cert'] != null) {
@ -186,9 +186,9 @@ class Site {
List<dynamic> rawErrors = json["errors"]; List<dynamic> rawErrors = json["errors"];
List<String> errors = []; List<String> errors = [];
rawErrors.forEach((error) { for (var error in rawErrors) {
errors.add(error); errors.add(error);
}); }
return { return {
"name": json["name"], "name": json["name"],
@ -295,9 +295,9 @@ class Site {
List<dynamic> f = jsonDecode(ret); List<dynamic> f = jsonDecode(ret);
List<HostInfo> hosts = []; List<HostInfo> hosts = [];
f.forEach((v) { for (var v in f) {
hosts.add(HostInfo.fromJson(v)); hosts.add(HostInfo.fromJson(v));
}); }
return hosts; return hosts;
} on PlatformException catch (err) { } on PlatformException catch (err) {
@ -317,9 +317,9 @@ class Site {
List<dynamic> f = jsonDecode(ret); List<dynamic> f = jsonDecode(ret);
List<HostInfo> hosts = []; List<HostInfo> hosts = [];
f.forEach((v) { for (var v in f) {
hosts.add(HostInfo.fromJson(v)); hosts.add(HostInfo.fromJson(v));
}); }
return hosts; return hosts;
} on PlatformException catch (err) { } on PlatformException catch (err) {
@ -331,7 +331,7 @@ class Site {
Future<Map<String, List<HostInfo>>> listAllHostmaps() async { Future<Map<String, List<HostInfo>>> listAllHostmaps() async {
try { try {
var res = await Future.wait([this.listHostmap(), this.listPendingHostmap()]); var res = await Future.wait([listHostmap(), listPendingHostmap()]);
return {"active": res[0], "pending": res[1]}; return {"active": res[0], "pending": res[1]};
} on PlatformException catch (err) { } on PlatformException catch (err) {
throw err.details ?? err.message ?? err.toString(); throw err.details ?? err.message ?? err.toString();

View file

@ -10,9 +10,9 @@ class StaticHost {
var list = json['destinations'] as List<dynamic>; var list = json['destinations'] as List<dynamic>;
var result = <IPAndPort>[]; var result = <IPAndPort>[];
list.forEach((item) { for (var item in list) {
result.add(IPAndPort.fromString(item)); result.add(IPAndPort.fromString(item));
}); }
return StaticHost(lighthouse: json['lighthouse'], destinations: result); return StaticHost(lighthouse: json['lighthouse'], destinations: result);
} }

View file

@ -10,7 +10,7 @@ import 'package:mobile_nebula/services/utils.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
class AboutScreen extends StatefulWidget { class AboutScreen extends StatefulWidget {
const AboutScreen({Key? key}) : super(key: key); const AboutScreen({super.key});
@override @override
_AboutScreenState createState() => _AboutScreenState(); _AboutScreenState createState() => _AboutScreenState();

View file

@ -49,6 +49,7 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService'); static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
@override
void initState() { void initState() {
code = widget.code; code = widget.code;
super.initState(); super.initState();
@ -94,27 +95,30 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
} else { } else {
// No code, show the error // No code, show the error
child = Padding( child = Padding(
padding: EdgeInsets.only(top: 20),
child: Center( child: Center(
child: Text( child: Text(
'No valid enrollment code was found.\n\nContact your administrator to obtain a new enrollment code.', 'No valid enrollment code was found.\n\nContact your administrator to obtain a new enrollment code.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
padding: EdgeInsets.only(top: 20),
); );
} }
} else if (this.error != null) { } else if (error != null) {
// Error while enrolling, display it // Error while enrolling, display it
child = Center( child = Center(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20),
child: SelectableText( child: SelectableText(
'There was an issue while attempting to enroll this device. Contact your administrator to obtain a new enrollment code.', 'There was an issue while attempting to enroll this device. Contact your administrator to obtain a new enrollment code.',
), ),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20),
), ),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: SelectableText.rich( child: SelectableText.rich(
TextSpan( TextSpan(
children: [ children: [
@ -134,22 +138,19 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
], ],
), ),
), ),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
), ),
Container( Container(
child: Padding(child: SelectableText(this.error!), padding: EdgeInsets.all(16)),
color: Theme.of(context).colorScheme.errorContainer, color: Theme.of(context).colorScheme.errorContainer,
child: Padding(padding: EdgeInsets.all(16), child: SelectableText(error!)),
), ),
], ],
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
), ),
); );
} else if (this.enrolled) { } else if (enrolled) {
// Enrollment complete! // Enrollment complete!
child = Padding( child = Padding(
child: Center(child: Text('Enrollment complete! 🎉', textAlign: TextAlign.center)),
padding: EdgeInsets.only(top: 20), padding: EdgeInsets.only(top: 20),
child: Center(child: Text('Enrollment complete! 🎉', textAlign: TextAlign.center)),
); );
} else { } else {
// Have a code and actively enrolling // Have a code and actively enrolling
@ -157,7 +158,7 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
child = Center( child = Center(
child: Column( child: Column(
children: [ children: [
Padding(child: Text('Contacting DN for enrollment'), padding: EdgeInsets.only(bottom: 25)), Padding(padding: EdgeInsets.only(bottom: 25), child: Text('Contacting DN for enrollment')),
PlatformCircularProgressIndicator( PlatformCircularProgressIndicator(
cupertino: (_, __) { cupertino: (_, __) {
return CupertinoProgressIndicatorData(radius: 50); return CupertinoProgressIndicatorData(radius: 50);
@ -168,11 +169,11 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
); );
} }
return SimplePage(title: Text('Enroll with Managed Nebula'), child: child, alignment: alignment); return SimplePage(title: Text('Enroll with Managed Nebula'), alignment: alignment, child: child);
} }
Widget _codeEntry() { Widget _codeEntry() {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); final GlobalKey<FormState> formKey = GlobalKey<FormState>();
String? validator(String? value) { String? validator(String? value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
@ -182,7 +183,7 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
} }
Future<void> onSubmit() async { Future<void> onSubmit() async {
final bool isValid = _formKey.currentState?.validate() ?? false; final bool isValid = formKey.currentState?.validate() ?? false;
if (!isValid) { if (!isValid) {
return; return;
} }
@ -205,14 +206,14 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
), ),
); );
final form = Form(key: _formKey, child: Platform.isAndroid ? input : ConfigSection(children: [input])); final form = Form(key: formKey, child: Platform.isAndroid ? input : ConfigSection(children: [input]));
return Column( return Column(
children: [ children: [
Padding(padding: EdgeInsets.symmetric(vertical: 32), child: form), Padding(padding: EdgeInsets.symmetric(vertical: 32), child: form),
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16),
child: Row(children: [Expanded(child: PrimaryButton(child: Text('Submit'), onPressed: onSubmit))]), child: Row(children: [Expanded(child: PrimaryButton(onPressed: onSubmit, child: Text('Submit')))]),
), ),
], ],
); );

View file

@ -15,14 +15,14 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
class HostInfoScreen extends StatefulWidget { class HostInfoScreen extends StatefulWidget {
const HostInfoScreen({ const HostInfoScreen({
Key? key, super.key,
required this.hostInfo, required this.hostInfo,
required this.isLighthouse, required this.isLighthouse,
required this.pending, required this.pending,
this.onChanged, this.onChanged,
required this.site, required this.site,
required this.supportsQRScanning, required this.supportsQRScanning,
}) : super(key: key); });
final bool isLighthouse; final bool isLighthouse;
final bool pending; final bool pending;
@ -108,7 +108,7 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
} }
Widget _buildRemotes() { Widget _buildRemotes() {
if (hostInfo.remoteAddresses.length == 0) { if (hostInfo.remoteAddresses.isEmpty) {
return ConfigSection( return ConfigSection(
label: 'REMOTES', label: 'REMOTES',
children: [ConfigItem(content: Text('No remote addresses yet'), labelWidth: 0)], children: [ConfigItem(content: Text('No remote addresses yet'), labelWidth: 0)],
@ -124,7 +124,7 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
final double ipWidth = final double ipWidth =
Utils.textSize("000.000.000.000:000000", CupertinoTheme.of(context).textTheme.textStyle).width; Utils.textSize("000.000.000.000:000000", CupertinoTheme.of(context).textTheme.textStyle).width;
hostInfo.remoteAddresses.forEach((remoteObj) { for (var remoteObj in hostInfo.remoteAddresses) {
String remote = remoteObj.toString(); String remote = remoteObj.toString();
items.add( items.add(
ConfigCheckboxItem( ConfigCheckboxItem(
@ -148,9 +148,9 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
}, },
), ),
); );
}); }
return ConfigSection(label: items.length > 0 ? 'Tap to change the active address' : null, children: items); return ConfigSection(label: items.isNotEmpty ? 'Tap to change the active address' : null, children: items);
} }
Widget _buildStaticRemotes() { Widget _buildStaticRemotes() {
@ -159,7 +159,7 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
final double ipWidth = final double ipWidth =
Utils.textSize("000.000.000.000:000000", CupertinoTheme.of(context).textTheme.textStyle).width; Utils.textSize("000.000.000.000:000000", CupertinoTheme.of(context).textTheme.textStyle).width;
hostInfo.remoteAddresses.forEach((remoteObj) { for (var remoteObj in hostInfo.remoteAddresses) {
String remote = remoteObj.toString(); String remote = remoteObj.toString();
items.add( items.add(
ConfigCheckboxItem( ConfigCheckboxItem(
@ -169,9 +169,9 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
checked: currentRemote == remote, checked: currentRemote == remote,
), ),
); );
}); }
return ConfigSection(label: items.length > 0 ? 'REMOTES' : null, children: items); return ConfigSection(label: items.isNotEmpty ? 'REMOTES' : null, children: items);
} }
Widget _buildClose() { Widget _buildClose() {

View file

@ -60,7 +60,7 @@ MAIH7gzreMGgrH/yR6rZpIHR3DxJ3E0aHtEI
}; };
class MainScreen extends StatefulWidget { class MainScreen extends StatefulWidget {
const MainScreen(this.dnEnrollStream, {Key? key}) : super(key: key); const MainScreen(this.dnEnrollStream, {super.key});
final StreamController dnEnrollStream; final StreamController dnEnrollStream;
@ -115,8 +115,8 @@ class _MainScreenState extends State<MainScreen> {
if (kDebugMode) { if (kDebugMode) {
debugSite = Row( debugSite = Row(
children: [_debugSave(badDebugSave), _debugSave(goodDebugSave), _debugClearKeys()],
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [_debugSave(badDebugSave), _debugSave(goodDebugSave), _debugClearKeys()],
); );
} }
@ -168,12 +168,12 @@ class _MainScreenState extends State<MainScreen> {
if (error != null) { if (error != null) {
return Center( return Center(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: error!, children: error!,
), ),
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10),
), ),
); );
} }
@ -202,12 +202,12 @@ class _MainScreenState extends State<MainScreen> {
} }
Widget _buildSites() { Widget _buildSites() {
if (sites == null || sites!.length == 0) { if (sites == null || sites!.isEmpty) {
return _buildNoSites(); return _buildNoSites();
} }
List<Widget> items = []; List<Widget> items = [];
sites!.forEach((site) { for (var site in sites!) {
items.add( items.add(
SiteItem( SiteItem(
key: Key(site.id), key: Key(site.id),
@ -223,7 +223,7 @@ class _MainScreenState extends State<MainScreen> {
}, },
), ),
); );
}); }
Widget child = ReorderableListView( Widget child = ReorderableListView(
shrinkWrap: true, shrinkWrap: true,
@ -260,7 +260,7 @@ class _MainScreenState extends State<MainScreen> {
); );
if (Platform.isIOS) { if (Platform.isIOS) {
child = CupertinoTheme(child: child, data: CupertinoTheme.of(context)); child = CupertinoTheme(data: CupertinoTheme.of(context), child: child);
} }
// The theme here is to remove the hardcoded canvas border reordering forces on us // The theme here is to remove the hardcoded canvas border reordering forces on us

View file

@ -27,7 +27,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
void initState() { void initState() {
//TODO: we need to unregister on dispose? //TODO: we need to unregister on dispose?
settings.onChange().listen((_) { settings.onChange().listen((_) {
if (this.mounted) { if (mounted) {
setState(() {}); setState(() {});
} }
}); });

View file

@ -23,8 +23,7 @@ import '../components/SiteTitle.dart';
//TODO: ios is now the problem with connecting screwing our ability to query the hostmap (its a race) //TODO: ios is now the problem with connecting screwing our ability to query the hostmap (its a race)
class SiteDetailScreen extends StatefulWidget { class SiteDetailScreen extends StatefulWidget {
const SiteDetailScreen({Key? key, required this.site, this.onChanged, required this.supportsQRScanning}) const SiteDetailScreen({super.key, required this.site, this.onChanged, required this.supportsQRScanning});
: super(key: key);
final Site site; final Site site;
final Function? onChanged; final Function? onChanged;
@ -113,19 +112,19 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
} }
Widget _buildErrors() { Widget _buildErrors() {
if (site.errors.length == 0) { if (site.errors.isEmpty) {
return Container(); return Container();
} }
List<Widget> items = []; List<Widget> items = [];
site.errors.forEach((error) { for (var error in site.errors) {
items.add( items.add(
ConfigItem( ConfigItem(
labelWidth: 0, labelWidth: 0,
content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error)), content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error)),
), ),
); );
}); }
return ConfigSection( return ConfigSection(
label: 'ERRORS', label: 'ERRORS',
@ -166,7 +165,7 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
Switch.adaptive( Switch.adaptive(
value: widget.site.connected, value: widget.site.connected,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
onChanged: widget.site.errors.length > 0 && !widget.site.connected ? null : handleChange, onChanged: widget.site.errors.isNotEmpty && !widget.site.connected ? null : handleChange,
), ),
], ],
), ),

View file

@ -15,7 +15,7 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../components/SiteTitle.dart'; import '../components/SiteTitle.dart';
class SiteLogsScreen extends StatefulWidget { class SiteLogsScreen extends StatefulWidget {
const SiteLogsScreen({Key? key, required this.site}) : super(key: key); const SiteLogsScreen({super.key, required this.site});
final Site site; final Site site;
@ -59,6 +59,7 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
refreshController.loadComplete(); refreshController.loadComplete();
}, },
refreshController: refreshController, refreshController: refreshController,
bottomBar: _buildBottomBar(),
child: Container( child: Container(
padding: EdgeInsets.all(5), padding: EdgeInsets.all(5),
constraints: logBoxConstraints(context), constraints: logBoxConstraints(context),
@ -75,7 +76,6 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
}, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)), }, style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14)),
), ),
), ),
bottomBar: _buildBottomBar(),
); );
} }

View file

@ -11,13 +11,13 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
class SiteTunnelsScreen extends StatefulWidget { class SiteTunnelsScreen extends StatefulWidget {
const SiteTunnelsScreen({ const SiteTunnelsScreen({
Key? key, super.key,
required this.site, required this.site,
required this.tunnels, required this.tunnels,
required this.pending, required this.pending,
required this.onChanged, required this.onChanged,
required this.supportsQRScanning, required this.supportsQRScanning,
}) : super(key: key); });
final Site site; final Site site;
final List<HostInfo> tunnels; final List<HostInfo> tunnels;
@ -77,7 +77,7 @@ class _SiteTunnelsScreenState extends State<SiteTunnelsScreen> {
), ),
), ),
label: Row( label: Row(
children: <Widget>[Padding(child: icon, padding: EdgeInsets.only(right: 10)), Text(hostInfo.vpnIp)], children: <Widget>[Padding(padding: EdgeInsets.only(right: 10), child: icon), Text(hostInfo.vpnIp)],
), ),
labelWidth: ipWidth, labelWidth: ipWidth,
content: Container(alignment: Alignment.centerRight, child: Text(hostInfo.cert?.details.name ?? "")), content: Container(alignment: Alignment.centerRight, child: Text(hostInfo.cert?.details.name ?? "")),
@ -85,7 +85,7 @@ class _SiteTunnelsScreenState extends State<SiteTunnelsScreen> {
}).toList(); }).toList();
final Widget child = switch (children.length) { final Widget child = switch (children.length) {
0 => Center(child: Padding(child: Text('No tunnels to show'), padding: EdgeInsets.only(top: 30))), 0 => Center(child: Padding(padding: EdgeInsets.only(top: 30), child: Text('No tunnels to show'))),
_ => ConfigSection(children: children), _ => ConfigSection(children: children),
}; };

View file

@ -26,13 +26,13 @@ class CertificateResult {
class AddCertificateScreen extends StatefulWidget { class AddCertificateScreen extends StatefulWidget {
const AddCertificateScreen({ const AddCertificateScreen({
Key? key, super.key,
this.onSave, this.onSave,
this.onReplace, this.onReplace,
required this.pubKey, required this.pubKey,
required this.privKey, required this.privKey,
required this.supportsQRScanning, required this.supportsQRScanning,
}) : super(key: key); });
// onSave will pop a new CertificateDetailsScreen. // onSave will pop a new CertificateDetailsScreen.
// If onSave is null, onReplace must be set. // If onSave is null, onReplace must be set.
@ -223,7 +223,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
onPressed: () async { onPressed: () async {
var result = await Navigator.push( var result = await Navigator.push(
context, context,
platformPageRoute(context: context, builder: (context) => new ScanQRScreen()), platformPageRoute(context: context, builder: (context) => ScanQRScreen()),
); );
if (result != null) { if (result != null) {
_addCertEntry(result); _addCertEntry(result);
@ -245,7 +245,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert}); var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert});
List<dynamic> certs = jsonDecode(rawCerts); List<dynamic> certs = jsonDecode(rawCerts);
if (certs.length > 0) { if (certs.isNotEmpty) {
var tryCertInfo = CertificateInfo.fromJson(certs.first); var tryCertInfo = CertificateInfo.fromJson(certs.first);
if (tryCertInfo.cert.details.isCa) { if (tryCertInfo.cert.details.isCa) {
return Utils.popError( return Utils.popError(

View file

@ -40,7 +40,7 @@ class Advanced {
} }
class AdvancedScreen extends StatefulWidget { class AdvancedScreen extends StatefulWidget {
const AdvancedScreen({Key? key, required this.site, required this.onSave}) : super(key: key); const AdvancedScreen({super.key, required this.site, required this.onSave});
final Site site; final Site site;
final ValueChanged<Advanced> onSave; final ValueChanged<Advanced> onSave;
@ -85,7 +85,7 @@ class _AdvancedScreenState extends State<AdvancedScreen> {
//TODO: Auto select on focus? //TODO: Auto select on focus?
content: content:
widget.site.managed widget.site.managed
? Text(settings.lhDuration.toString() + " seconds", textAlign: TextAlign.right) ? Text("${settings.lhDuration} seconds", textAlign: TextAlign.right)
: PlatformTextFormField( : PlatformTextFormField(
initialValue: settings.lhDuration.toString(), initialValue: settings.lhDuration.toString(),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,

View file

@ -18,7 +18,7 @@ import 'package:mobile_nebula/services/utils.dart';
//TODO: In addition you will want to think about re-generation while the site is still active (This means storing multiple keys in secure storage) //TODO: In addition you will want to think about re-generation while the site is still active (This means storing multiple keys in secure storage)
class CAListScreen extends StatefulWidget { class CAListScreen extends StatefulWidget {
const CAListScreen({Key? key, required this.cas, this.onSave, required this.supportsQRScanning}) : super(key: key); const CAListScreen({super.key, required this.cas, this.onSave, required this.supportsQRScanning});
final List<CertificateInfo> cas; final List<CertificateInfo> cas;
final ValueChanged<List<CertificateInfo>>? onSave; final ValueChanged<List<CertificateInfo>>? onSave;
@ -39,9 +39,9 @@ class _CAListScreenState extends State<CAListScreen> {
@override @override
void initState() { void initState() {
widget.cas.forEach((ca) { for (var ca in widget.cas) {
cas[ca.cert.fingerprint] = ca; cas[ca.cert.fingerprint] = ca;
}); }
super.initState(); super.initState();
} }
@ -51,7 +51,7 @@ class _CAListScreenState extends State<CAListScreen> {
List<Widget> items = []; List<Widget> items = [];
final caItems = _buildCAs(); final caItems = _buildCAs();
if (caItems.length > 0) { if (caItems.isNotEmpty) {
items.add(ConfigSection(children: caItems)); items.add(ConfigSection(children: caItems));
} }
@ -115,14 +115,14 @@ class _CAListScreenState extends State<CAListScreen> {
var ignored = 0; var ignored = 0;
List<dynamic> certs = jsonDecode(rawCerts); List<dynamic> certs = jsonDecode(rawCerts);
certs.forEach((rawCert) { for (var rawCert in certs) {
final info = CertificateInfo.fromJson(rawCert); final info = CertificateInfo.fromJson(rawCert);
if (!info.cert.details.isCa) { if (!info.cert.details.isCa) {
ignored++; ignored++;
return; continue;
} }
cas[info.cert.fingerprint] = info; cas[info.cert.fingerprint] = info;
}); }
if (ignored > 0) { if (ignored > 0) {
error = 'One or more certificates were ignored because they were not certificate authorities.'; error = 'One or more certificates were ignored because they were not certificate authorities.';
@ -236,7 +236,7 @@ class _CAListScreenState extends State<CAListScreen> {
onPressed: () async { onPressed: () async {
var result = await Navigator.push( var result = await Navigator.push(
context, context,
platformPageRoute(context: context, builder: (context) => new ScanQRScreen()), platformPageRoute(context: context, builder: (context) => ScanQRScreen()),
); );
if (result != null) { if (result != null) {
_addCAEntry(result, (err) { _addCAEntry(result, (err) {

View file

@ -11,7 +11,7 @@ import 'package:mobile_nebula/services/utils.dart';
/// Displays the details of a CertificateInfo object. Respects incomplete objects (missing validity or rawCert) /// Displays the details of a CertificateInfo object. Respects incomplete objects (missing validity or rawCert)
class CertificateDetailsScreen extends StatefulWidget { class CertificateDetailsScreen extends StatefulWidget {
const CertificateDetailsScreen({ const CertificateDetailsScreen({
Key? key, super.key,
required this.certInfo, required this.certInfo,
this.onDelete, this.onDelete,
this.onSave, this.onSave,
@ -19,7 +19,7 @@ class CertificateDetailsScreen extends StatefulWidget {
this.pubKey, this.pubKey,
this.privKey, this.privKey,
required this.supportsQRScanning, required this.supportsQRScanning,
}) : super(key: key); });
final CertificateInfo certInfo; final CertificateInfo certInfo;
@ -120,19 +120,19 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
Widget _buildFilters() { Widget _buildFilters() {
List<Widget> items = []; List<Widget> items = [];
if (certInfo.cert.details.groups.length > 0) { if (certInfo.cert.details.groups.isNotEmpty) {
items.add(ConfigItem(label: Text('Groups'), content: SelectableText(certInfo.cert.details.groups.join(', ')))); items.add(ConfigItem(label: Text('Groups'), content: SelectableText(certInfo.cert.details.groups.join(', '))));
} }
if (certInfo.cert.details.ips.length > 0) { if (certInfo.cert.details.ips.isNotEmpty) {
items.add(ConfigItem(label: Text('IPs'), content: SelectableText(certInfo.cert.details.ips.join(', ')))); items.add(ConfigItem(label: Text('IPs'), content: SelectableText(certInfo.cert.details.ips.join(', '))));
} }
if (certInfo.cert.details.subnets.length > 0) { if (certInfo.cert.details.subnets.isNotEmpty) {
items.add(ConfigItem(label: Text('Subnets'), content: SelectableText(certInfo.cert.details.subnets.join(', ')))); items.add(ConfigItem(label: Text('Subnets'), content: SelectableText(certInfo.cert.details.subnets.join(', '))));
} }
return items.length > 0 return items.isNotEmpty
? ConfigSection(label: certInfo.cert.details.isCa ? 'FILTERS' : 'DETAILS', children: items) ? ConfigSection(label: certInfo.cert.details.isCa ? 'FILTERS' : 'DETAILS', children: items)
: Container(); : Container();
} }

View file

@ -6,7 +6,7 @@ import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart';
class CipherScreen extends StatefulWidget { class CipherScreen extends StatefulWidget {
const CipherScreen({Key? key, required this.cipher, required this.onSave}) : super(key: key); const CipherScreen({super.key, required this.cipher, required this.onSave});
final String cipher; final String cipher;
final ValueChanged<String> onSave; final ValueChanged<String> onSave;

View file

@ -6,7 +6,7 @@ import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
import 'package:mobile_nebula/components/config/ConfigSection.dart'; import 'package:mobile_nebula/components/config/ConfigSection.dart';
class LogVerbosityScreen extends StatefulWidget { class LogVerbosityScreen extends StatefulWidget {
const LogVerbosityScreen({Key? key, required this.verbosity, required this.onSave}) : super(key: key); const LogVerbosityScreen({super.key, required this.verbosity, required this.onSave});
final String verbosity; final String verbosity;
final ValueChanged<String> onSave; final ValueChanged<String> onSave;

View file

@ -7,7 +7,7 @@ class RenderedConfigScreen extends StatelessWidget {
final String config; final String config;
final String name; final String name;
RenderedConfigScreen({Key? key, required this.config, required this.name}) : super(key: key); const RenderedConfigScreen({super.key, required this.config, required this.name});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mobile_scanner/mobile_scanner.dart';
class ScanQRScreen extends StatefulWidget { class ScanQRScreen extends StatefulWidget {
const ScanQRScreen({super.key});
@override @override
State<ScanQRScreen> createState() => _ScanQRScreenState(); State<ScanQRScreen> createState() => _ScanQRScreenState();
} }

View file

@ -23,8 +23,7 @@ import 'package:mobile_nebula/services/utils.dart';
//TODO: Enforce a name //TODO: Enforce a name
class SiteConfigScreen extends StatefulWidget { class SiteConfigScreen extends StatefulWidget {
const SiteConfigScreen({Key? key, this.site, required this.onSave, required this.supportsQRScanning}) const SiteConfigScreen({super.key, this.site, required this.onSave, required this.supportsQRScanning});
: super(key: key);
final Site? site; final Site? site;
@ -105,7 +104,7 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
Widget _debugConfig() { Widget _debugConfig() {
var data = ""; var data = "";
try { try {
final encoder = new JsonEncoder.withIndent(' '); final encoder = JsonEncoder.withIndent(' ');
data = encoder.convert(site); data = encoder.convert(site);
} catch (err) { } catch (err) {
data = err.toString(); data = err.toString();
@ -162,13 +161,13 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
final certError = site.certInfo == null || site.certInfo!.validity == null || !site.certInfo!.validity!.valid; final certError = site.certInfo == null || site.certInfo!.validity == null || !site.certInfo!.validity!.valid;
var caError = false; var caError = false;
if (!site.managed) { if (!site.managed) {
caError = site.ca.length == 0; caError = site.ca.isEmpty;
if (!caError) { if (!caError) {
site.ca.forEach((ca) { for (var ca in site.ca) {
if (ca.validity == null || !ca.validity!.valid) { if (ca.validity == null || !ca.validity!.valid) {
caError = true; caError = true;
} }
}); }
} }
} }
@ -183,8 +182,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
children: <Widget>[ children: <Widget>[
certError certError
? Padding( ? Padding(
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
padding: EdgeInsets.only(right: 5), padding: EdgeInsets.only(right: 5),
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
) )
: Container(), : 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'),
@ -234,8 +233,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
children: <Widget>[ children: <Widget>[
caError caError
? Padding( ? Padding(
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
padding: EdgeInsets.only(right: 5), padding: EdgeInsets.only(right: 5),
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
) )
: Container(), : Container(),
caError ? Text('Needs attention') : Text(Utils.itemCountFormat(site.ca.length)), caError ? Text('Needs attention') : Text(Utils.itemCountFormat(site.ca.length)),
@ -273,13 +272,13 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
alignment: WrapAlignment.end, alignment: WrapAlignment.end,
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[ children: <Widget>[
site.staticHostmap.length == 0 site.staticHostmap.isEmpty
? Padding( ? Padding(
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
padding: EdgeInsets.only(right: 5), padding: EdgeInsets.only(right: 5),
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
) )
: Container(), : Container(),
site.staticHostmap.length == 0 site.staticHostmap.isEmpty
? Text('Needs attention') ? Text('Needs attention')
: Text(Utils.itemCountFormat(site.staticHostmap.length)), : Text(Utils.itemCountFormat(site.staticHostmap.length)),
], ],

View file

@ -21,14 +21,13 @@ class _IPAndPort {
class StaticHostmapScreen extends StatefulWidget { class StaticHostmapScreen extends StatefulWidget {
StaticHostmapScreen({ StaticHostmapScreen({
Key? key, super.key,
this.nebulaIp = '', this.nebulaIp = '',
destinations, destinations,
this.lighthouse = false, this.lighthouse = false,
this.onDelete, this.onDelete,
required this.onSave, required this.onSave,
}) : this.destinations = destinations ?? [], }) : destinations = destinations ?? [];
super(key: key);
final List<IPAndPort> destinations; final List<IPAndPort> destinations;
final String nebulaIp; final String nebulaIp;
@ -51,11 +50,11 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
_nebulaIp = widget.nebulaIp; _nebulaIp = widget.nebulaIp;
_lighthouse = widget.lighthouse; _lighthouse = widget.lighthouse;
_destinations = {}; _destinations = {};
widget.destinations.forEach((dest) { for (var dest in widget.destinations) {
_destinations[UniqueKey()] = _IPAndPort(focusNode: FocusNode(), destination: dest); _destinations[UniqueKey()] = _IPAndPort(focusNode: FocusNode(), destination: dest);
}); }
if (_destinations.length == 0) { if (_destinations.isEmpty) {
_addDestination(); _addDestination();
} }

View file

@ -22,7 +22,7 @@ class _Hostmap {
} }
class StaticHostsScreen extends StatefulWidget { class StaticHostsScreen extends StatefulWidget {
const StaticHostsScreen({Key? key, required this.hostmap, required this.onSave}) : super(key: key); const StaticHostsScreen({super.key, required this.hostmap, required this.onSave});
final Map<String, StaticHost> hostmap; final Map<String, StaticHost> hostmap;
final ValueChanged<Map<String, StaticHost>>? onSave; final ValueChanged<Map<String, StaticHost>>? onSave;
@ -32,7 +32,7 @@ class StaticHostsScreen extends StatefulWidget {
} }
class _StaticHostsScreenState extends State<StaticHostsScreen> { class _StaticHostsScreenState extends State<StaticHostsScreen> {
Map<Key, _Hostmap> _hostmap = {}; final Map<Key, _Hostmap> _hostmap = {};
bool changed = false; bool changed = false;
@override @override
@ -80,17 +80,17 @@ class _StaticHostsScreenState extends State<StaticHostsScreen> {
label: Row( label: Row(
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: EdgeInsets.only(right: 10),
child: Icon( child: Icon(
host.lighthouse ? Icons.lightbulb_outline : Icons.computer, host.lighthouse ? Icons.lightbulb_outline : Icons.computer,
color: CupertinoColors.placeholderText.resolveFrom(context), color: CupertinoColors.placeholderText.resolveFrom(context),
), ),
padding: EdgeInsets.only(right: 10),
), ),
Text(host.nebulaIp), Text(host.nebulaIp),
], ],
), ),
labelWidth: ipWidth, labelWidth: ipWidth,
content: Text(host.destinations.length.toString() + ' items', textAlign: TextAlign.end), content: Text('${host.destinations.length} items', textAlign: TextAlign.end),
onPressed: () { onPressed: () {
Utils.openPage(context, (context) { Utils.openPage(context, (context) {
return StaticHostmapScreen( return StaticHostmapScreen(

View file

@ -10,7 +10,7 @@ import 'package:mobile_nebula/models/UnsafeRoute.dart';
import 'package:mobile_nebula/services/utils.dart'; import 'package:mobile_nebula/services/utils.dart';
class UnsafeRouteScreen extends StatefulWidget { class UnsafeRouteScreen extends StatefulWidget {
const UnsafeRouteScreen({Key? key, required this.route, required this.onSave, this.onDelete}) : super(key: key); const UnsafeRouteScreen({super.key, required this.route, required this.onSave, this.onDelete});
final UnsafeRoute route; final UnsafeRoute route;
final ValueChanged<UnsafeRoute> onSave; final ValueChanged<UnsafeRoute> onSave;

View file

@ -8,7 +8,7 @@ import 'package:mobile_nebula/screens/siteConfig/UnsafeRouteScreen.dart';
import 'package:mobile_nebula/services/utils.dart'; import 'package:mobile_nebula/services/utils.dart';
class UnsafeRoutesScreen extends StatefulWidget { class UnsafeRoutesScreen extends StatefulWidget {
const UnsafeRoutesScreen({Key? key, required this.unsafeRoutes, required this.onSave}) : super(key: key); const UnsafeRoutesScreen({super.key, required this.unsafeRoutes, required this.onSave});
final List<UnsafeRoute> unsafeRoutes; final List<UnsafeRoute> unsafeRoutes;
final ValueChanged<List<UnsafeRoute>>? onSave; final ValueChanged<List<UnsafeRoute>>? onSave;
@ -24,9 +24,9 @@ class _UnsafeRoutesScreenState extends State<UnsafeRoutesScreen> {
@override @override
void initState() { void initState() {
unsafeRoutes = {}; unsafeRoutes = {};
widget.unsafeRoutes.forEach((route) { for (var route in widget.unsafeRoutes) {
unsafeRoutes[UniqueKey()] = route; unsafeRoutes[UniqueKey()] = route;
}); }
super.initState(); super.initState();
} }

View file

@ -10,8 +10,8 @@ bool DEFAULT_TRACK_ERRORS = true;
class Settings { class Settings {
final _storage = Storage(); final _storage = Storage();
StreamController _change = StreamController.broadcast(); final StreamController _change = StreamController.broadcast();
var _settings = Map<String, dynamic>(); var _settings = <String, dynamic>{};
bool get useSystemColors { bool get useSystemColors {
return _getBool('systemDarkMode', true); return _getBool('systemDarkMode', true);

View file

@ -39,12 +39,12 @@ class Utils {
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"}) { static String itemCountFormat(int items, {String singleSuffix = "item", String multiSuffix = "items"}) {
if (items == 1) { if (items == 1) {
return items.toString() + " " + singleSuffix; return "$items $singleSuffix";
} }
return items.toString() + " " + multiSuffix; return "$items $multiSuffix";
} }
/// Builds a simple leading widget that pops the current screen. /// Builds a simple leading widget that pops the current screen.
@ -79,9 +79,9 @@ class Utils {
static Widget trailingSaveWidget(BuildContext context, Function onPressed) { static Widget trailingSaveWidget(BuildContext context, Function onPressed) {
return PlatformTextButton( return PlatformTextButton(
child: Text('Save'),
padding: Platform.isAndroid ? null : EdgeInsets.zero, padding: Platform.isAndroid ? null : EdgeInsets.zero,
onPressed: () => onPressed(), onPressed: () => onPressed(),
child: Text('Save'),
); );
} }

View file

@ -142,6 +142,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_oss_licenses: flutter_oss_licenses:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -180,7 +188,7 @@ packages:
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: "direct main"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
@ -264,6 +272,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.1"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:
@ -321,7 +337,7 @@ packages:
source: hosted source: hosted
version: "3.0.2" version: "3.0.2"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"

View file

@ -19,6 +19,8 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_web_plugins:
sdk: flutter
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
@ -37,11 +39,13 @@ dependencies:
sentry_flutter: ^8.9.0 sentry_flutter: ^8.9.0
sentry_dart_plugin: ^2.0.0 sentry_dart_plugin: ^2.0.0
mobile_scanner: ^7.0.0-beta.3 mobile_scanner: ^7.0.0-beta.3
path: ^1.9.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_oss_licenses: ^3.0.4 flutter_oss_licenses: ^3.0.4
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec