mirror of
https://github.com/DefinedNet/mobile_nebula.git
synced 2025-09-07 19:46:06 +00:00
Compare commits
39 commits
da68ac5a71
...
85349fa366
Author | SHA1 | Date | |
---|---|---|---|
|
85349fa366 | ||
|
d54007fcff | ||
|
f0b7e693f6 | ||
|
a8303a166d | ||
|
9a3d288a16 | ||
|
2dee5305db | ||
|
fd991b9a02 | ||
|
2b844d27dd | ||
|
bcfcadec8e | ||
|
330c8348fb | ||
|
69b0a4dafa | ||
|
f6016f5da8 | ||
|
4621cbc000 | ||
|
4d34083572 | ||
|
e128658599 | ||
|
ed348ab126 | ||
|
b2ebe0289a | ||
|
f2c4b07154 | ||
|
d3e5291944 | ||
|
961312a20a | ||
|
f69f7cc9a3 | ||
|
af1c582984 | ||
|
f3d22a83cf | ||
|
126ed2f4b0 | ||
|
5afc1ef692 | ||
|
21d8265f42 | ||
|
382e2bbbf7 | ||
|
b41054920a | ||
|
ad45cc1d78 | ||
|
ae412cc407 | ||
|
91e6a4f6a2 | ||
|
fc120053f2 | ||
|
5a641be96f | ||
|
f290a71b94 | ||
|
87c16ea95c | ||
|
6ed64b7349 | ||
|
301dc6c394 | ||
|
a28b922494 | ||
|
991837676a |
93 changed files with 5171 additions and 3919 deletions
|
@ -1,2 +1,8 @@
|
|||
# Big flutter format run
|
||||
9934f226e3e79c3567ce07dbab9e9f6443e7afc5
|
||||
|
||||
# Another big flutter format run
|
||||
ed348ab126160e64ba09899c946383ca9e54768c
|
||||
|
||||
# Start formatting with swift-format
|
||||
4621cbc0006b3c64c8948d920f69b0dc3f503565
|
42
.github/workflows/fluttercheck.yml
vendored
Normal file
42
.github/workflows/fluttercheck.yml
vendored
Normal file
|
@ -0,0 +1,42 @@
|
|||
name: Flutter check
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/fluttercheck.yml'
|
||||
- '**.dart'
|
||||
jobs:
|
||||
flutterfmt:
|
||||
name: Run flutter format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff #v2.18.0
|
||||
with:
|
||||
flutter-version: '3.29.2'
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check formating
|
||||
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@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff #v2.18.0
|
||||
with:
|
||||
flutter-version: '3.29.2'
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check linting
|
||||
run: dart fix --dry-run
|
29
.github/workflows/flutterfmt.yml
vendored
29
.github/workflows/flutterfmt.yml
vendored
|
@ -1,29 +0,0 @@
|
|||
name: Flutter format
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/flutterfmt.yml'
|
||||
- '.github/workflows/flutterfmt.sh'
|
||||
- '**.dart'
|
||||
jobs:
|
||||
|
||||
gofmt:
|
||||
name: Run flutter format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.24.1'
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check formating
|
||||
run: dart format -l120 lib/ --set-exit-if-changed --suppress-analytics --output none
|
6
.github/workflows/gofmt.yml
vendored
6
.github/workflows/gofmt.yml
vendored
|
@ -15,14 +15,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #5.3.0
|
||||
with:
|
||||
go-version: "1.22"
|
||||
go-version: '1.22'
|
||||
cache-dependency-path: nebula/go.sum
|
||||
|
||||
- name: Install goimports
|
||||
|
|
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
|
@ -8,30 +8,30 @@ on:
|
|||
jobs:
|
||||
build:
|
||||
name: Build ios and android package
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-15
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
|
||||
with:
|
||||
show-progress: false
|
||||
fetch-depth: 25 # For sentry releases
|
||||
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #5.3.0
|
||||
with:
|
||||
go-version: "1.22"
|
||||
go-version: '1.22'
|
||||
cache-dependency-path: nebula/go.sum
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
- uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 #v4.7.0
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff #v2.18.0
|
||||
with:
|
||||
flutter-version: '3.24.1'
|
||||
flutter-version: '3.29.2'
|
||||
|
||||
- name: Setup bundletool for APK generation
|
||||
uses: amyu/setup-bundletool@f7a6fdd8e04bb23d2fdf3c2f60c9257a6298a40a
|
||||
|
@ -60,8 +60,7 @@ jobs:
|
|||
- name: Place Github token for fastlane match
|
||||
env:
|
||||
TOKEN: ${{ secrets.MACHINE_USER_PAT }}
|
||||
run:
|
||||
echo "MATCH_GIT_BASIC_AUTHORIZATION=$(echo -n "defined-machine:${TOKEN}" | base64)" >> $GITHUB_ENV
|
||||
run: echo "MATCH_GIT_BASIC_AUTHORIZATION=$(echo -n "defined-machine:${TOKEN}" | base64)" >> $GITHUB_ENV
|
||||
|
||||
- name: Get build name and number, install dependencies
|
||||
env:
|
||||
|
@ -102,7 +101,7 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Collect iOS artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 #4.6.1
|
||||
with:
|
||||
name: MobileNebula.ipa
|
||||
path: ios/MobileNebula.ipa
|
||||
|
@ -140,7 +139,7 @@ jobs:
|
|||
unzip -p build/app/outputs/apk/release/MobileNebula.apks universal.apk > build/app/outputs/apk/release/MobileNebula.apk
|
||||
|
||||
- name: Collect Android artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 #4.6.1
|
||||
with:
|
||||
name: MobileNebula.aab
|
||||
path: build/app/outputs/bundle/release/app-release.aab
|
||||
|
|
28
.github/workflows/smoke.yml
vendored
28
.github/workflows/smoke.yml
vendored
|
@ -10,28 +10,28 @@ on:
|
|||
jobs:
|
||||
build-android:
|
||||
name: Android
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #5.3.0
|
||||
with:
|
||||
go-version: "1.22"
|
||||
go-version: '1.22'
|
||||
cache-dependency-path: nebula/go.sum
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
- uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 #v4.7.0
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff #v2.18.0
|
||||
with:
|
||||
flutter-version: '3.24.1'
|
||||
flutter-version: '3.29.2'
|
||||
|
||||
- name: install dependencies
|
||||
env:
|
||||
|
@ -77,7 +77,7 @@ jobs:
|
|||
unzip -p build/app/outputs/apk/debug/app-debug.apks universal.apk > build/app/outputs/apk/debug/app-debug.apk
|
||||
- name: Collect debug apk
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 #4.6.1
|
||||
with:
|
||||
name: MobileNebulaDebug.apk
|
||||
path: build/app/outputs/apk/debug/app-debug.apk
|
||||
|
@ -85,24 +85,24 @@ jobs:
|
|||
|
||||
build-ios:
|
||||
name: iOS
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-15
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Set up Go 1.22
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 #5.3.0
|
||||
with:
|
||||
go-version: "1.22"
|
||||
go-version: '1.22'
|
||||
cache-dependency-path: nebula/go.sum
|
||||
|
||||
- name: Install flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
uses: subosito/flutter-action@f2c4f6686ca8e8d6e6d0f28410eeef506ed66aff #v2.18.0
|
||||
with:
|
||||
flutter-version: '3.24.1'
|
||||
flutter-version: '3.29.2'
|
||||
|
||||
- name: install dependencies
|
||||
run: |
|
||||
|
|
21
.github/workflows/swiftfmt.yml
vendored
Normal file
21
.github/workflows/swiftfmt.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
name: Swift format
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/swiftfmt.yml"
|
||||
- "**.swift"
|
||||
jobs:
|
||||
swiftfmt:
|
||||
name: Run swift format
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #4.2.2
|
||||
with:
|
||||
show-progress: false
|
||||
|
||||
- name: Check formating
|
||||
run: ./swift-format.sh check
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -8,6 +8,7 @@
|
|||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
android/app/.cxx
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
|
@ -45,6 +46,7 @@ lib/generated_plugin_registrant.dart
|
|||
/env.sh
|
||||
/lib/gen.versions.dart
|
||||
/lib/.gen.versions.dart
|
||||
/lib/oss_licenses.dart
|
||||
/ios/Flutter/.last_build_id
|
||||
/local.properties
|
||||
/.gradle/
|
||||
|
|
1
.swiftformatignore
Normal file
1
.swiftformatignore
Normal file
|
@ -0,0 +1 @@
|
|||
ios/Pods/**
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
Install all of the following things:
|
||||
|
||||
- [`xcode`](https://apps.apple.com/us/app/xcode/)
|
||||
- [`xcode`](https://apps.apple.com/us/app/xcode/) - use the version specified by `xcode_select` in `/ios/fastlane/Fastfile`
|
||||
- [`android-studio`](https://developer.android.com/studio)
|
||||
- [`flutter` 3.27.0](https://docs.flutter.dev/get-started/install)
|
||||
- [`flutter` 3.29.2](https://docs.flutter.dev/get-started/install)
|
||||
- [`gomobile`](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile)
|
||||
- [Flutter Android Studio Extension](https://docs.flutter.dev/get-started/editor?tab=androidstudio)
|
||||
|
||||
|
@ -22,7 +22,7 @@ Run `flutter doctor` and fix everything it complains before proceeding
|
|||
|
||||
- Copy `env.sh.example` and set it up for your machine
|
||||
- Ensure you have run `gomobile init`
|
||||
- In Android Studio, make sure you have the current ndk installed by going to Tools -> SDK Manager, go to the SDK Tools tab, check the `Show package details` box, expand the NDK section and select `26.1.10909125` version.
|
||||
- In Android Studio, make sure you have the current ndk installed by going to Tools -> SDK Manager, go to the SDK Tools tab, check the `Show package details` box, expand the NDK section and select `27.0.12077973` version.
|
||||
- Ensure you have downloaded an ndk via android studio, this is likely not the default one and you need to check the
|
||||
`Show package details` box to select the correct version. The correct version comes from the error when you try and compile
|
||||
- Make sure you have `gem` installed with `sudo gem install`
|
||||
|
@ -41,6 +41,9 @@ dart format lib/ test/ -l 120
|
|||
|
||||
In Android Studio, set the line length using Preferences -> Editor -> Code Style -> Dart -> Line length, set it to 120. Enable auto-format with Preferences -> Languages & Frameworks -> Flutter -> Format code on save.
|
||||
|
||||
`./swift-format.sh` can be used to format Swift code in the repo.
|
||||
|
||||
Once `swift-format` supports ignoring directories (<https://github.com/swiftlang/swift-format/issues/870>), we can move to a method of running it more like what <https://calebhearth.com/swift-format-github-action> describes.
|
||||
|
||||
# Release
|
||||
|
||||
|
|
35
analysis_options.yaml
Normal file
35
analysis_options.yaml
Normal file
|
@ -0,0 +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
|
||||
formatter:
|
||||
page_width: 120
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
# This is a generated file, let's ignore it.
|
||||
- lib/services/theme.dart
|
|
@ -28,8 +28,8 @@ android {
|
|||
|
||||
compileSdkVersion 34
|
||||
|
||||
// default ndk version for AGP 8.5: https://developer.android.com/build/releases/past-releases/agp-8-5-0-release-notes
|
||||
ndkVersion "26.1.10909125"
|
||||
// default ndk version for AGP 8.7: https://developer.android.com/build/releases/past-releases/agp-8-7-0-release-notes
|
||||
ndkVersion "27.0.12077973"
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += 'src/main/kotlin'
|
||||
|
|
|
@ -36,6 +36,10 @@ class MainActivity: FlutterActivity() {
|
|||
private var apiClient: APIClient? = null
|
||||
private var sites: Sites? = null
|
||||
|
||||
// Don't attempt to unbind from the service unless the client has received some
|
||||
// information about the service's state.
|
||||
private var isServiceBound = false
|
||||
|
||||
// When starting a site we may need to request VPN permissions. These variables help us
|
||||
// maintain state while waiting for a permission result.
|
||||
private var startResult: MethodChannel.Result? = null
|
||||
|
@ -440,6 +444,7 @@ class MainActivity: FlutterActivity() {
|
|||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
outMessenger = Messenger(service)
|
||||
isServiceBound = true
|
||||
|
||||
// We want to monitor the service for as long as we are connected to it.
|
||||
try {
|
||||
|
@ -461,6 +466,7 @@ class MainActivity: FlutterActivity() {
|
|||
|
||||
override fun onServiceDisconnected(arg0: ComponentName) {
|
||||
outMessenger = null
|
||||
isServiceBound = false
|
||||
if (activeSiteId != null) {
|
||||
//TODO: this indicates the service died, notify that it is disconnected
|
||||
}
|
||||
|
@ -510,7 +516,14 @@ class MainActivity: FlutterActivity() {
|
|||
msg.replyTo = inMessenger
|
||||
outMessenger!!.send(msg)
|
||||
// Unbind
|
||||
if (isServiceBound) {
|
||||
try {
|
||||
unbindService(connection)
|
||||
isServiceBound = false
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e(TAG, e.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
outMessenger = null
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
|
|
|
@ -19,7 +19,7 @@ pluginManagement {
|
|||
plugins {
|
||||
id "org.gradle.toolchains.foojay-resolver-convention" version "0.8.0"
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version '8.6.1' apply false
|
||||
id "com.android.application" version '8.8.0' apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
|
||||
}
|
||||
|
||||
|
|
|
@ -50,3 +50,6 @@ cd ..
|
|||
# Try and avoid issues with building by moving into place after we are complete
|
||||
#TODO: this might be a parallel build of deps issue in kotlin, might need to solve there
|
||||
mv lib/.gen.versions.dart lib/gen.versions.dart
|
||||
|
||||
# Generate licenses library
|
||||
flutter pub run flutter_oss_licenses:generate.dart
|
||||
|
|
|
@ -5,13 +5,13 @@ let groupName = "group.net.defined.mobileNebula"
|
|||
class KeyChain {
|
||||
class func save(key: String, data: Data, managed: Bool) -> Bool {
|
||||
var query: [String: Any] = [
|
||||
kSecClass as String : kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String : key,
|
||||
kSecValueData as String : data,
|
||||
kSecClass as String: kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecValueData as String: data,
|
||||
kSecAttrAccessGroup as String: groupName,
|
||||
]
|
||||
|
||||
if (managed) {
|
||||
if managed {
|
||||
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||
}
|
||||
|
||||
|
@ -22,10 +22,10 @@ class KeyChain {
|
|||
|
||||
class func load(key: String) -> Data? {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String : kSecClassGenericPassword,
|
||||
kSecAttrAccount as String : key,
|
||||
kSecReturnData as String : kCFBooleanTrue!,
|
||||
kSecMatchLimit as String : kSecMatchLimitOne,
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecReturnData as String: kCFBooleanTrue!,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecAttrAccessGroup as String: groupName,
|
||||
]
|
||||
|
||||
|
@ -42,8 +42,8 @@ class KeyChain {
|
|||
|
||||
class func delete(key: String) -> Bool {
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String : kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String : key,
|
||||
kSecClass as String: kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String: key,
|
||||
kSecAttrAccessGroup as String: groupName,
|
||||
]
|
||||
|
||||
|
@ -56,7 +56,9 @@ extension Data {
|
|||
init<T>(from value: T) {
|
||||
var value = value
|
||||
var data = Data()
|
||||
withUnsafePointer(to: &value, { (ptr: UnsafePointer<T>) -> Void in
|
||||
withUnsafePointer(
|
||||
to: &value,
|
||||
{ (ptr: UnsafePointer<T>) -> Void in
|
||||
data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1))
|
||||
})
|
||||
self.init(data)
|
||||
|
@ -66,4 +68,3 @@ extension Data {
|
|||
return self.withUnsafeBytes { $0.load(as: T.self) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import NetworkExtension
|
||||
import MobileNebula
|
||||
import os.log
|
||||
import NetworkExtension
|
||||
import SwiftyJSON
|
||||
import os.log
|
||||
|
||||
enum VPNStartError: Error {
|
||||
case noManagers
|
||||
|
@ -23,7 +23,6 @@ extension AppMessageError: LocalizedError {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
private var networkMonitor: NWPathMonitor?
|
||||
|
||||
|
@ -34,7 +33,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
private var didSleep = false
|
||||
private var cachedRouteDescription: String?
|
||||
|
||||
override func startTunnel(options: [String : NSObject]? = nil) async throws {
|
||||
override func startTunnel(options: [String: NSObject]? = nil) async throws {
|
||||
// There is currently no way to get initialization errors back to the UI via completionHandler here
|
||||
// `expectStart` is sent only via the UI which means we should wait for the real start command which has another completion handler the UI can intercept
|
||||
if options?["expectStart"] != nil {
|
||||
|
@ -52,14 +51,22 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
var config: Data
|
||||
var key: String
|
||||
|
||||
do {
|
||||
// Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS
|
||||
// TODO: Remove else once we drop support for iOS 16
|
||||
if ProcessInfo().isOperatingSystemAtLeast(
|
||||
OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
|
||||
{
|
||||
manager = try await self.findManager()
|
||||
|
||||
guard let foundManager = manager else {
|
||||
throw VPNStartError.couldNotFindManager
|
||||
}
|
||||
|
||||
do {
|
||||
self.site = try Site(manager: foundManager)
|
||||
} else {
|
||||
// This does not save the manager with the site, which means we cannot update the
|
||||
// vpn profile name when updates happen (rare).
|
||||
self.site = try Site(proto: self.protocolConfiguration as! NETunnelProviderProtocol)
|
||||
}
|
||||
config = try self.site!.getConfig()
|
||||
} catch {
|
||||
//TODO: need a way to notify the app
|
||||
|
@ -81,16 +88,19 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
// Make sure our ip is routed to the tun device
|
||||
var err: NSError?
|
||||
let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err)
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
throw err!
|
||||
}
|
||||
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
|
||||
var routes: [NEIPv4Route] = [NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)]
|
||||
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(
|
||||
addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
|
||||
var routes: [NEIPv4Route] = [
|
||||
NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)
|
||||
]
|
||||
|
||||
// Add our unsafe routes
|
||||
try _site.unsafeRoutes.forEach { unsafeRoute in
|
||||
let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err)
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
throw err!
|
||||
}
|
||||
routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR))
|
||||
|
@ -101,7 +111,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
|
||||
try await self.setTunnelNetworkSettings(tunnelNetworkSettings)
|
||||
var nebulaErr: NSError?
|
||||
self.nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr)
|
||||
self.nebula = MobileNebulaNewNebula(
|
||||
String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr)
|
||||
self.startNetworkMonitor()
|
||||
|
||||
if nebulaErr != nil {
|
||||
|
@ -116,18 +127,20 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
private func handleDNUpdate(newSite: Site) {
|
||||
do {
|
||||
self.site = newSite
|
||||
try self.nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
|
||||
try self.nebula?.reload(
|
||||
String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
|
||||
|
||||
} catch {
|
||||
log.error("Got an error while updating nebula \(error.localizedDescription, privacy: .public)")
|
||||
log.error(
|
||||
"Got an error while updating nebula \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Sleep/wake get called aggressively and do nothing to help us here, we should locate why that is and make these work appropriately
|
||||
// override func sleep(completionHandler: @escaping () -> Void) {
|
||||
// nebula!.sleep()
|
||||
// completionHandler()
|
||||
// }
|
||||
//TODO: Sleep/wake get called aggressively and do nothing to help us here, we should locate why that is and make these work appropriately
|
||||
// override func sleep(completionHandler: @escaping () -> Void) {
|
||||
// nebula!.sleep()
|
||||
// completionHandler()
|
||||
// }
|
||||
|
||||
private func findManager() async throws -> NETunnelProviderManager {
|
||||
let targetProtoConfig = self.protocolConfiguration as? NETunnelProviderProtocol
|
||||
|
@ -144,7 +157,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
throw VPNStartError.noProviderConfig
|
||||
}
|
||||
let id = mgrProviderConfig["id"] as? String
|
||||
if (id == targetID) {
|
||||
if id == targetID {
|
||||
return manager
|
||||
}
|
||||
}
|
||||
|
@ -164,7 +177,9 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
networkMonitor = nil
|
||||
}
|
||||
|
||||
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||
override func stopTunnel(
|
||||
with reason: NEProviderStopReason, completionHandler: @escaping () -> Void
|
||||
) {
|
||||
nebula?.stop()
|
||||
stopNetworkMonitor()
|
||||
completionHandler()
|
||||
|
@ -175,7 +190,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
if routeDescription != cachedRouteDescription {
|
||||
// Don't bother to rebind if we don't have any gateways
|
||||
if routeDescription != "" {
|
||||
nebula?.rebind("network change to: \(routeDescription); from: \(cachedRouteDescription ?? "none")")
|
||||
nebula?.rebind(
|
||||
"network change to: \(routeDescription); from: \(cachedRouteDescription ?? "none")")
|
||||
}
|
||||
cachedRouteDescription = routeDescription
|
||||
}
|
||||
|
@ -183,7 +199,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
|
||||
private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String {
|
||||
var str: [String] = []
|
||||
endpoints.forEach{ endpoint in
|
||||
endpoints.forEach { endpoint in
|
||||
switch endpoint {
|
||||
case let .hostPort(.ipv6(host), port):
|
||||
str.append("[\(host)]:\(port)")
|
||||
|
@ -203,7 +219,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
return nil
|
||||
}
|
||||
|
||||
var error: Error?
|
||||
var error: (any Error)?
|
||||
var data: JSON?
|
||||
|
||||
// start command has special treatment due to needing to call two completers
|
||||
|
@ -216,7 +232,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
defer {
|
||||
self.cancelTunnelWithError(error)
|
||||
}
|
||||
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error.localizedDescription)))
|
||||
return try? JSONEncoder().encode(
|
||||
IPCResponse.init(type: .error, message: JSON(error.localizedDescription)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,32 +255,36 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
error = AppMessageError.unknownIPCType(command: call.command)
|
||||
}
|
||||
|
||||
if (error != nil) {
|
||||
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
|
||||
if error != nil {
|
||||
return try? JSONEncoder().encode(
|
||||
IPCResponse.init(
|
||||
type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
|
||||
} else {
|
||||
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data))
|
||||
}
|
||||
}
|
||||
|
||||
private func listHostmap(pending: Bool) -> (JSON?, Error?) {
|
||||
private func listHostmap(pending: Bool) -> (JSON?, (any Error)?) {
|
||||
var err: NSError?
|
||||
let res = nebula!.listHostmap(pending, error: &err)
|
||||
return (JSON(res), err)
|
||||
}
|
||||
|
||||
private func getHostInfo(args: JSON) -> (JSON?, Error?) {
|
||||
private func getHostInfo(args: JSON) -> (JSON?, (any Error)?) {
|
||||
var err: NSError?
|
||||
let res = nebula!.getHostInfo(byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err)
|
||||
let res = nebula!.getHostInfo(
|
||||
byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err)
|
||||
return (JSON(res), err)
|
||||
}
|
||||
|
||||
private func setRemoteForTunnel(args: JSON) -> (JSON?, Error?) {
|
||||
private func setRemoteForTunnel(args: JSON) -> (JSON?, (any Error)?) {
|
||||
var err: NSError?
|
||||
let res = nebula!.setRemoteForTunnel(args["vpnIp"].string, addr: args["addr"].string, error: &err)
|
||||
let res = nebula!.setRemoteForTunnel(
|
||||
args["vpnIp"].string, addr: args["addr"].string, error: &err)
|
||||
return (JSON(res), err)
|
||||
}
|
||||
|
||||
private func closeTunnel(args: JSON) -> (JSON?, Error?) {
|
||||
private func closeTunnel(args: JSON) -> (JSON?, (any Error)?) {
|
||||
let res = nebula!.closeTunnel(args["vpnIp"].string)
|
||||
return (JSON(res), nil)
|
||||
}
|
||||
|
@ -300,4 +321,3 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import NetworkExtension
|
||||
import MobileNebula
|
||||
import NetworkExtension
|
||||
import SwiftyJSON
|
||||
import os.log
|
||||
|
||||
let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site")
|
||||
|
||||
enum SiteError: Error {
|
||||
case nonConforming(site: [String : Any]?)
|
||||
case nonConforming(site: [String: Any]?)
|
||||
case noCertificate
|
||||
case keyLoad
|
||||
case keySave
|
||||
|
@ -131,7 +131,7 @@ struct CertificateValidity: Codable {
|
|||
}
|
||||
}
|
||||
|
||||
let statusMap: Dictionary<NEVPNStatus, Bool> = [
|
||||
let statusMap: [NEVPNStatus: Bool] = [
|
||||
NEVPNStatus.invalid: false,
|
||||
NEVPNStatus.disconnected: false,
|
||||
NEVPNStatus.connecting: false,
|
||||
|
@ -140,7 +140,7 @@ let statusMap: Dictionary<NEVPNStatus, Bool> = [
|
|||
NEVPNStatus.disconnecting: true,
|
||||
]
|
||||
|
||||
let statusString: Dictionary<NEVPNStatus, String> = [
|
||||
let statusString: [NEVPNStatus: String] = [
|
||||
NEVPNStatus.invalid: "Invalid configuration",
|
||||
NEVPNStatus.disconnected: "Disconnected",
|
||||
NEVPNStatus.connecting: "Connecting...",
|
||||
|
@ -156,7 +156,7 @@ class Site: Codable {
|
|||
var id: String
|
||||
|
||||
// Stored in proto
|
||||
var staticHostmap: Dictionary<String, StaticHosts>
|
||||
var staticHostmap: [String: StaticHosts]
|
||||
var unsafeRoutes: [UnsafeRoute]
|
||||
var cert: CertificateInfo?
|
||||
var ca: [CertificateInfo]
|
||||
|
@ -244,18 +244,18 @@ class Site: Codable {
|
|||
do {
|
||||
let rawCert = incoming.cert
|
||||
let rawDetails = MobileNebulaParseCerts(rawCert, &err)
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
throw err!
|
||||
}
|
||||
|
||||
var certs: [CertificateInfo]
|
||||
|
||||
certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!)
|
||||
if (certs.count == 0) {
|
||||
if certs.count == 0 {
|
||||
throw SiteError.noCertificate
|
||||
}
|
||||
cert = certs[0]
|
||||
if (!cert!.validity.valid) {
|
||||
if !cert!.validity.valid {
|
||||
errors.append("Certificate is invalid: \(cert!.validity.reason)")
|
||||
}
|
||||
|
||||
|
@ -266,19 +266,19 @@ class Site: Codable {
|
|||
do {
|
||||
let rawCa = incoming.ca
|
||||
let rawCaDetails = MobileNebulaParseCerts(rawCa, &err)
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
throw err!
|
||||
}
|
||||
ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!)
|
||||
|
||||
var hasErrors = false
|
||||
ca.forEach { cert in
|
||||
if (!cert.validity.valid) {
|
||||
if !cert.validity.valid {
|
||||
hasErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors && !managed) {
|
||||
if hasErrors && !managed {
|
||||
errors.append("There are issues with 1 or more ca certificates")
|
||||
}
|
||||
|
||||
|
@ -294,11 +294,11 @@ class Site: Codable {
|
|||
errors.append("Unable to create the site directory: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
if (managed && (try? getDNCredentials())?.invalid != false) {
|
||||
if managed && (try? getDNCredentials())?.invalid != false {
|
||||
errors.append("Unable to fetch managed updates - please re-enroll the device")
|
||||
}
|
||||
|
||||
if (errors.isEmpty) {
|
||||
if errors.isEmpty {
|
||||
do {
|
||||
let encoder = JSONEncoder()
|
||||
let rawConfig = try encoder.encode(incoming)
|
||||
|
@ -307,7 +307,7 @@ class Site: Codable {
|
|||
var err: NSError?
|
||||
|
||||
MobileNebulaTestConfig(strConfig, key, &err)
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
throw err!
|
||||
}
|
||||
} catch {
|
||||
|
@ -327,7 +327,7 @@ class Site: Codable {
|
|||
}
|
||||
|
||||
func getDNCredentials() throws -> DNCredentials {
|
||||
if (!managed) {
|
||||
if !managed {
|
||||
throw SiteError.unmanagedGetCredentials
|
||||
}
|
||||
|
||||
|
@ -344,7 +344,7 @@ class Site: Codable {
|
|||
let creds = try getDNCredentials()
|
||||
creds.invalid = true
|
||||
|
||||
if (!(try creds.save(siteID: self.id))) {
|
||||
if !(try creds.save(siteID: self.id)) {
|
||||
throw SiteError.dnCredentialLoad
|
||||
}
|
||||
}
|
||||
|
@ -353,7 +353,7 @@ class Site: Codable {
|
|||
let creds = try getDNCredentials()
|
||||
creds.invalid = false
|
||||
|
||||
if (!(try creds.save(siteID: self.id))) {
|
||||
if !(try creds.save(siteID: self.id)) {
|
||||
throw SiteError.dnCredentialSave
|
||||
}
|
||||
}
|
||||
|
@ -429,7 +429,7 @@ class DNCredentials: Codable {
|
|||
struct IncomingSite: Codable {
|
||||
var name: String
|
||||
var id: String
|
||||
var staticHostmap: Dictionary<String, StaticHosts>
|
||||
var staticHostmap: [String: StaticHosts]
|
||||
var unsafeRoutes: [UnsafeRoute]?
|
||||
var cert: String?
|
||||
var ca: String?
|
||||
|
@ -456,7 +456,10 @@ struct IncomingSite: Codable {
|
|||
return try encoder.encode(config)
|
||||
}
|
||||
|
||||
func save(manager: NETunnelProviderManager?, saveToManager: Bool = true, callback: @escaping (Error?) -> ()) {
|
||||
func save(
|
||||
manager: NETunnelProviderManager?, saveToManager: Bool = true,
|
||||
callback: @escaping ((any Error)?) -> Void
|
||||
) {
|
||||
let configPath: URL
|
||||
|
||||
do {
|
||||
|
@ -469,15 +472,15 @@ struct IncomingSite: Codable {
|
|||
|
||||
log.notice("Saving to \(configPath, privacy: .public)")
|
||||
do {
|
||||
if (self.key != nil) {
|
||||
if self.key != nil {
|
||||
let data = self.key!.data(using: .utf8)
|
||||
if (!KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false)) {
|
||||
if !KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false) {
|
||||
return callback(SiteError.keySave)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
if ((try self.dnCredentials?.save(siteID: self.id)) == false) {
|
||||
if (try self.dnCredentials?.save(siteID: self.id)) == false {
|
||||
return callback(SiteError.dnCredentialSave)
|
||||
}
|
||||
} catch {
|
||||
|
@ -490,24 +493,25 @@ struct IncomingSite: Codable {
|
|||
return callback(error)
|
||||
}
|
||||
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
#if targetEnvironment(simulator)
|
||||
// We are on a simulator and there is no NEVPNManager for us to interact with
|
||||
callback(nil)
|
||||
#else
|
||||
#else
|
||||
if saveToManager {
|
||||
self.saveToManager(manager: manager, callback: callback)
|
||||
} else {
|
||||
callback(nil)
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> ()) {
|
||||
if (manager != nil) {
|
||||
private func saveToManager(
|
||||
manager: NETunnelProviderManager?, callback: @escaping ((any Error)?) -> Void
|
||||
) {
|
||||
if manager != nil {
|
||||
// We need to refresh our settings to properly update config
|
||||
manager?.loadFromPreferences { error in
|
||||
if (error != nil) {
|
||||
if error != nil {
|
||||
return callback(error)
|
||||
}
|
||||
|
||||
|
@ -519,10 +523,13 @@ struct IncomingSite: Codable {
|
|||
return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback)
|
||||
}
|
||||
|
||||
private func finishSaveToManager(manager: NETunnelProviderManager, callback: @escaping (Error?) -> ()) {
|
||||
private func finishSaveToManager(
|
||||
manager: NETunnelProviderManager, callback: @escaping ((any Error)?) -> Void
|
||||
) {
|
||||
// Stuff our details in the protocol
|
||||
let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
|
||||
proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension";
|
||||
let proto =
|
||||
manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
|
||||
proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension"
|
||||
// WARN: If we stop setting providerConfiguration["id"] here, we'll need to use something else to match
|
||||
// managers in PacketTunnelProvider.findManager
|
||||
proto.providerConfiguration = ["id": self.id]
|
||||
|
@ -536,7 +543,7 @@ struct IncomingSite: Codable {
|
|||
manager.localizedDescription = self.name
|
||||
manager.isEnabled = true
|
||||
|
||||
manager.saveToPreferences{ error in
|
||||
manager.saveToPreferences { error in
|
||||
return callback(error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,10 @@ class SiteList {
|
|||
/// Gets the root directory that can be used to share files between the UI and VPN process. Does ensure the directory exists
|
||||
static func getRootDir() throws -> URL {
|
||||
let fileManager = FileManager.default
|
||||
let rootDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")!
|
||||
let rootDir = fileManager.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")!
|
||||
|
||||
if (!fileManager.fileExists(atPath: rootDir.absoluteString)) {
|
||||
if !fileManager.fileExists(atPath: rootDir.absoluteString) {
|
||||
try fileManager.createDirectory(at: rootDir, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
|
@ -19,7 +20,7 @@ class SiteList {
|
|||
static func getSitesDir() throws -> URL {
|
||||
let fileManager = FileManager.default
|
||||
let sitesDir = try getRootDir().appendingPathComponent("sites", isDirectory: true)
|
||||
if (!fileManager.fileExists(atPath: sitesDir.absoluteString)) {
|
||||
if !fileManager.fileExists(atPath: sitesDir.absoluteString) {
|
||||
try fileManager.createDirectory(at: sitesDir, withIntermediateDirectories: true)
|
||||
}
|
||||
return sitesDir
|
||||
|
@ -29,7 +30,7 @@ class SiteList {
|
|||
static func getSiteDir(id: String, create: Bool = false) throws -> URL {
|
||||
let fileManager = FileManager.default
|
||||
let siteDir = try getSitesDir().appendingPathComponent(id, isDirectory: true)
|
||||
if (create && !fileManager.fileExists(atPath: siteDir.absoluteString)) {
|
||||
if create && !fileManager.fileExists(atPath: siteDir.absoluteString) {
|
||||
try fileManager.createDirectory(at: siteDir, withIntermediateDirectories: true)
|
||||
}
|
||||
return siteDir
|
||||
|
@ -37,39 +38,43 @@ class SiteList {
|
|||
|
||||
/// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json
|
||||
static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL {
|
||||
return try getSiteDir(id: id, create: createDir).appendingPathComponent("config", isDirectory: false).appendingPathExtension("json")
|
||||
return try getSiteDir(id: id, create: createDir).appendingPathComponent(
|
||||
"config", isDirectory: false
|
||||
).appendingPathExtension("json")
|
||||
}
|
||||
|
||||
/// Gets the file that represents the site log output, $rootDir/sites/$siteID/log
|
||||
static func getSiteLogFile(id: String, createDir: Bool) throws -> URL {
|
||||
return try getSiteDir(id: id, create: createDir).appendingPathComponent("logs", isDirectory: false)
|
||||
return try getSiteDir(id: id, create: createDir).appendingPathComponent(
|
||||
"logs", isDirectory: false)
|
||||
}
|
||||
|
||||
init(completion: @escaping ([String: Site]?, Error?) -> ()) {
|
||||
#if targetEnvironment(simulator)
|
||||
init(completion: @escaping ([String: Site]?, (any Error)?) -> Void) {
|
||||
#if targetEnvironment(simulator)
|
||||
SiteList.loadAllFromFS { sites, err in
|
||||
if sites != nil {
|
||||
self.sites = sites!
|
||||
}
|
||||
completion(sites, err)
|
||||
}
|
||||
#else
|
||||
#else
|
||||
SiteList.loadAllFromNETPM { sites, err in
|
||||
if sites != nil {
|
||||
self.sites = sites!
|
||||
}
|
||||
completion(sites, err)
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
private static func loadAllFromFS(completion: @escaping ([String: Site]?, Error?) -> ()) {
|
||||
private static func loadAllFromFS(completion: @escaping ([String: Site]?, (any Error)?) -> Void) {
|
||||
let fileManager = FileManager.default
|
||||
var siteDirs: [URL]
|
||||
var sites = [String: Site]()
|
||||
|
||||
do {
|
||||
siteDirs = try fileManager.contentsOfDirectory(at: getSitesDir(), includingPropertiesForKeys: nil)
|
||||
siteDirs = try fileManager.contentsOfDirectory(
|
||||
at: getSitesDir(), includingPropertiesForKeys: nil)
|
||||
|
||||
} catch {
|
||||
completion(nil, error)
|
||||
|
@ -78,7 +83,8 @@ class SiteList {
|
|||
|
||||
siteDirs.forEach { path in
|
||||
do {
|
||||
let site = try Site(path: path.appendingPathComponent("config").appendingPathExtension("json"))
|
||||
let site = try Site(
|
||||
path: path.appendingPathComponent("config").appendingPathExtension("json"))
|
||||
sites[site.id] = site
|
||||
|
||||
} catch {
|
||||
|
@ -91,15 +97,17 @@ class SiteList {
|
|||
completion(sites, nil)
|
||||
}
|
||||
|
||||
private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, Error?) -> ()) {
|
||||
private static func loadAllFromNETPM(
|
||||
completion: @escaping ([String: Site]?, (any Error)?) -> Void
|
||||
) {
|
||||
var sites = [String: Site]()
|
||||
|
||||
// dispatchGroup is used to ensure we have migrated all sites before returning them
|
||||
// If there are no sites to migrate, there are never any entrants
|
||||
let dispatchGroup = DispatchGroup()
|
||||
|
||||
NETunnelProviderManager.loadAllFromPreferences() { newManagers, err in
|
||||
if (err != nil) {
|
||||
NETunnelProviderManager.loadAllFromPreferences { newManagers, err in
|
||||
if err != nil {
|
||||
return completion(nil, err)
|
||||
}
|
||||
|
||||
|
|
|
@ -56,17 +56,17 @@ EXTERNAL SOURCES:
|
|||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
file_picker: c79185e70b9b45728cde2a8d8da454e0cb43f287
|
||||
file_picker: 8272ff2f2365937598e2407f4f2ff55c723f084a
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
Sentry: 38ed8bf38eab5812787274bf591e528074c19e02
|
||||
sentry_flutter: 7d1f1df30f3768c411603ed449519bbb90a7d87b
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
sentry_flutter: a72ca0eb6e78335db7c4ddcddd1b9f6c8ed5b764
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
SwiftyJSON: f5b1bf1cd8dd53cd25887ac0eabcfd92301c6a5a
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
|
||||
|
||||
PODFILE CHECKSUM: b44d9de9944d89118a4ff4bfffe1c2dab91de156
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
|
|
|
@ -563,6 +563,18 @@
|
|||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||
SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
|
@ -772,6 +784,18 @@
|
|||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||
SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
|
@ -825,6 +849,18 @@
|
|||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
SWIFT_UPCOMING_FEATURE_CONCISE_MAGIC_FILE = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DEPRECATE_APPLICATION_MAIN = YES;
|
||||
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||
SWIFT_UPCOMING_FEATURE_EXISTENTIAL_ANY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_FORWARD_TRAILING_CLOSURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_GLOBAL_CONCURRENCY = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPLICIT_OPEN_EXISTENTIALS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_IMPORT_OBJC_FORWARD_DECLS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_INFER_SENDABLE_FROM_CAPTURES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_ISOLATED_DEFAULT_VALUES = YES;
|
||||
SWIFT_UPCOMING_FEATURE_REGION_BASED_ISOLATION = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
|
|
|
@ -10,7 +10,8 @@ class APIClient {
|
|||
|
||||
init() {
|
||||
let packageInfo = PackageInfo()
|
||||
apiClient = MobileNebulaNewAPIClient("MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
|
||||
apiClient = MobileNebulaNewAPIClient(
|
||||
"MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
|
||||
}
|
||||
|
||||
func enroll(code: String) throws -> IncomingSite {
|
||||
|
@ -18,7 +19,9 @@ class APIClient {
|
|||
return try decodeIncomingSite(jsonSite: res.site)
|
||||
}
|
||||
|
||||
func tryUpdate(siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String) throws -> IncomingSite? {
|
||||
func tryUpdate(
|
||||
siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String
|
||||
) throws -> IncomingSite? {
|
||||
let res: MobileNebulaTryUpdateResult
|
||||
do {
|
||||
res = try apiClient.tryUpdate(
|
||||
|
@ -29,14 +32,14 @@ class APIClient {
|
|||
trustedKeys: trustedKeys)
|
||||
} catch {
|
||||
// type information from Go is not available, use string matching instead
|
||||
if (error.localizedDescription == "invalid credentials") {
|
||||
if error.localizedDescription == "invalid credentials" {
|
||||
throw APIClientError.invalidCredentials
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
|
||||
if (res.fetchedUpdate) {
|
||||
if res.fetchedUpdate {
|
||||
return try decodeIncomingSite(jsonSite: res.site)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import UIKit
|
||||
import Flutter
|
||||
import MobileNebula
|
||||
import NetworkExtension
|
||||
import SwiftyJSON
|
||||
import UIKit
|
||||
|
||||
enum ChannelName {
|
||||
static let vpn = "net.defined.mobileNebula/NebulaVpnService"
|
||||
|
@ -19,18 +19,16 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
private var sites: Sites?
|
||||
private var ui: FlutterMethodChannel?
|
||||
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
|
||||
|
||||
dnUpdater.updateAllLoop { site in
|
||||
// Signal the site has changed in case the current site details screen is active
|
||||
let container = self.sites?.getContainer(id: site.id)
|
||||
if (container != nil) {
|
||||
if container != nil {
|
||||
// Update references to the site with the new site config
|
||||
container!.site = site
|
||||
container!.updater.update(connected: site.connected ?? false, replaceSite: site)
|
||||
|
@ -47,7 +45,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
sites = Sites(messenger: controller.binaryMessenger)
|
||||
ui = FlutterMethodChannel(name: ChannelName.vpn, binaryMessenger: controller.binaryMessenger)
|
||||
|
||||
ui!.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
|
||||
ui!.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
|
||||
switch call.method {
|
||||
case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result)
|
||||
case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result)
|
||||
|
@ -62,11 +60,16 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
case "startSite": return self.startSite(call: call, result: result)
|
||||
case "stopSite": return self.stopSite(call: call, result: result)
|
||||
|
||||
case "active.listHostmap": self.vpnRequest(command: "listHostmap", arguments: call.arguments, result: result)
|
||||
case "active.listPendingHostmap": self.vpnRequest(command: "listPendingHostmap", arguments: call.arguments, result: result)
|
||||
case "active.getHostInfo": self.vpnRequest(command: "getHostInfo", arguments: call.arguments, result: result)
|
||||
case "active.setRemoteForTunnel": self.vpnRequest(command: "setRemoteForTunnel", arguments: call.arguments, result: result)
|
||||
case "active.closeTunnel": self.vpnRequest(command: "closeTunnel", arguments: call.arguments, result: result)
|
||||
case "active.listHostmap":
|
||||
self.vpnRequest(command: "listHostmap", arguments: call.arguments, result: result)
|
||||
case "active.listPendingHostmap":
|
||||
self.vpnRequest(command: "listPendingHostmap", arguments: call.arguments, result: result)
|
||||
case "active.getHostInfo":
|
||||
self.vpnRequest(command: "getHostInfo", arguments: call.arguments, result: result)
|
||||
case "active.setRemoteForTunnel":
|
||||
self.vpnRequest(command: "setRemoteForTunnel", arguments: call.arguments, result: result)
|
||||
case "active.closeTunnel":
|
||||
self.vpnRequest(command: "closeTunnel", arguments: call.arguments, result: result)
|
||||
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
|
@ -77,28 +80,39 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
}
|
||||
|
||||
func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) {
|
||||
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
|
||||
guard let certs = args["certs"] else { return result(MissingArgumentError(message: "certs is a required argument")) }
|
||||
guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
|
||||
guard let certs = args["certs"] else {
|
||||
return result(MissingArgumentError(message: "certs is a required argument"))
|
||||
}
|
||||
|
||||
var err: NSError?
|
||||
let json = MobileNebulaParseCerts(certs, &err)
|
||||
if (err != nil) {
|
||||
return result(CallFailedError(message: "Error while parsing certificate(s)", details: err!.localizedDescription))
|
||||
if err != nil {
|
||||
return result(
|
||||
CallFailedError(
|
||||
message: "Error while parsing certificate(s)", details: err!.localizedDescription))
|
||||
}
|
||||
|
||||
return result(json)
|
||||
}
|
||||
|
||||
func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) {
|
||||
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
|
||||
guard let cert = args["cert"] else { return result(MissingArgumentError(message: "cert is a required argument")) }
|
||||
guard let key = args["key"] else { return result(MissingArgumentError(message: "key is a required argument")) }
|
||||
guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
|
||||
guard let cert = args["cert"] else {
|
||||
return result(MissingArgumentError(message: "cert is a required argument"))
|
||||
}
|
||||
guard let key = args["key"] else {
|
||||
return result(MissingArgumentError(message: "key is a required argument"))
|
||||
}
|
||||
|
||||
var err: NSError?
|
||||
var validd: ObjCBool = false
|
||||
let valid = MobileNebulaVerifyCertAndKey(cert, key, &validd, &err)
|
||||
if (err != nil) {
|
||||
return result(CallFailedError(message: "Error while verifying certificate and private key", details: err!.localizedDescription))
|
||||
if err != nil {
|
||||
return result(
|
||||
CallFailedError(
|
||||
message: "Error while verifying certificate and private key",
|
||||
details: err!.localizedDescription))
|
||||
}
|
||||
|
||||
return result(valid)
|
||||
|
@ -107,8 +121,10 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
func nebulaGenerateKeyPair(result: FlutterResult) {
|
||||
var err: NSError?
|
||||
let kp = MobileNebulaGenerateKeyPair(&err)
|
||||
if (err != nil) {
|
||||
return result(CallFailedError(message: "Error while generating key pairs", details: err!.localizedDescription))
|
||||
if err != nil {
|
||||
return result(
|
||||
CallFailedError(
|
||||
message: "Error while generating key pairs", details: err!.localizedDescription))
|
||||
}
|
||||
|
||||
return result(kp)
|
||||
|
@ -119,8 +135,10 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
|
||||
var err: NSError?
|
||||
let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err)
|
||||
if (err != nil) {
|
||||
return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription))
|
||||
if err != nil {
|
||||
return result(
|
||||
CallFailedError(message: "Error while rendering config", details: err!.localizedDescription)
|
||||
)
|
||||
}
|
||||
|
||||
return result(yaml)
|
||||
|
@ -134,21 +152,24 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
|
||||
let oldSite = self.sites?.getSite(id: site.id)
|
||||
site.save(manager: oldSite?.manager) { error in
|
||||
if (error != nil) {
|
||||
return result(CallFailedError(message: "Failed to enroll", details: error!.localizedDescription))
|
||||
if error != nil {
|
||||
return result(
|
||||
CallFailedError(message: "Failed to enroll", details: error!.localizedDescription))
|
||||
}
|
||||
|
||||
result(nil)
|
||||
}
|
||||
} catch {
|
||||
return result(CallFailedError(message: "Error from DN api", details: error.localizedDescription))
|
||||
return result(
|
||||
CallFailedError(message: "Error from DN api", details: error.localizedDescription))
|
||||
}
|
||||
}
|
||||
|
||||
func listSites(result: @escaping FlutterResult) {
|
||||
self.sites?.loadSites { (sites, err) -> () in
|
||||
if (err != nil) {
|
||||
return result(CallFailedError(message: "Failed to load site list", details: err!.localizedDescription))
|
||||
self.sites?.loadSites { (sites, err) -> Void in
|
||||
if err != nil {
|
||||
return result(
|
||||
CallFailedError(message: "Failed to load site list", details: err!.localizedDescription))
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
|
@ -162,8 +183,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
guard let id = call.arguments as? String else { return result(NoArgumentsError()) }
|
||||
//TODO: stop the site if its running currently
|
||||
self.sites?.deleteSite(id: id) { error in
|
||||
if (error != nil) {
|
||||
result(CallFailedError(message: "Failed to delete site", details: error!.localizedDescription))
|
||||
if error != nil {
|
||||
result(
|
||||
CallFailedError(message: "Failed to delete site", details: error!.localizedDescription))
|
||||
}
|
||||
|
||||
result(nil)
|
||||
|
@ -180,8 +202,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
|
||||
let oldSite = self.sites?.getSite(id: site.id)
|
||||
site.save(manager: oldSite?.manager) { error in
|
||||
if (error != nil) {
|
||||
return result(CallFailedError(message: "Failed to save site", details: error!.localizedDescription))
|
||||
if error != nil {
|
||||
return result(
|
||||
CallFailedError(message: "Failed to save site", details: error!.localizedDescription))
|
||||
}
|
||||
|
||||
self.sites?.loadSites { _, _ in
|
||||
|
@ -191,59 +214,67 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
}
|
||||
|
||||
func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
|
||||
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
|
||||
guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
|
||||
guard let id = args["id"] else {
|
||||
return result(MissingArgumentError(message: "id is a required argument"))
|
||||
}
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
#if targetEnvironment(simulator)
|
||||
let updater = self.sites?.getUpdater(id: id)
|
||||
updater?.update(connected: true)
|
||||
#else
|
||||
#else
|
||||
let container = self.sites?.getContainer(id: id)
|
||||
let manager = container?.site.manager
|
||||
|
||||
manager?.loadFromPreferences{ error in
|
||||
manager?.loadFromPreferences { error in
|
||||
//TODO: Handle load error
|
||||
// This is silly but we need to enable the site each time to avoid situations where folks have multiple sites
|
||||
manager?.isEnabled = true
|
||||
manager?.saveToPreferences{ error in
|
||||
manager?.saveToPreferences { error in
|
||||
//TODO: Handle load error
|
||||
manager?.loadFromPreferences{ error in
|
||||
manager?.loadFromPreferences { error in
|
||||
//TODO: Handle load error
|
||||
do {
|
||||
container?.updater.startFunc = {() -> Void in
|
||||
container?.updater.startFunc = { () -> Void in
|
||||
return self.vpnRequest(command: "start", arguments: args, result: result)
|
||||
}
|
||||
try manager?.connection.startVPNTunnel(options: ["expectStart": NSNumber(1)])
|
||||
} catch {
|
||||
return result(CallFailedError(message: "Could not start site", details: error.localizedDescription))
|
||||
return result(
|
||||
CallFailedError(
|
||||
message: "Could not start site", details: error.localizedDescription))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
func stopSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
|
||||
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
|
||||
#if targetEnvironment(simulator)
|
||||
guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
|
||||
guard let id = args["id"] else {
|
||||
return result(MissingArgumentError(message: "id is a required argument"))
|
||||
}
|
||||
#if targetEnvironment(simulator)
|
||||
let updater = self.sites?.getUpdater(id: id)
|
||||
updater?.update(connected: false)
|
||||
|
||||
#else
|
||||
#else
|
||||
let manager = self.sites?.getSite(id: id)?.manager
|
||||
manager?.loadFromPreferences{ error in
|
||||
manager?.loadFromPreferences { error in
|
||||
//TODO: Handle load error
|
||||
|
||||
manager?.connection.stopVPNTunnel()
|
||||
return result(nil)
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
func vpnRequest(command: String, arguments: Any?, result: @escaping FlutterResult) {
|
||||
guard let args = arguments as? Dictionary<String, Any> else { return result(NoArgumentsError()) }
|
||||
guard let id = args["id"] as? String else { return result(MissingArgumentError(message: "id is a required argument")) }
|
||||
guard let args = arguments as? [String: Any] else { return result(NoArgumentsError()) }
|
||||
guard let id = args["id"] as? String else {
|
||||
return result(MissingArgumentError(message: "id is a required argument"))
|
||||
}
|
||||
let container = sites?.getContainer(id: id)
|
||||
|
||||
if container == nil {
|
||||
|
@ -258,7 +289,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
|
||||
if let session = container!.site.manager?.connection as? NETunnelProviderSession {
|
||||
do {
|
||||
try session.sendProviderMessage(try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))) { data in
|
||||
try session.sendProviderMessage(
|
||||
try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))
|
||||
) { data in
|
||||
if data == nil {
|
||||
return result(nil)
|
||||
}
|
||||
|
@ -272,7 +305,8 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
return result(res.message?.object)
|
||||
}
|
||||
|
||||
return result(CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error"))
|
||||
return result(
|
||||
CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error"))
|
||||
}
|
||||
} catch {
|
||||
return result(CallFailedError(message: error.localizedDescription))
|
||||
|
@ -284,11 +318,14 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
|||
}
|
||||
}
|
||||
|
||||
func MissingArgumentError(message: String, details: Error? = nil) -> FlutterError {
|
||||
func MissingArgumentError(message: String, details: (any Error)? = nil) -> FlutterError {
|
||||
return FlutterError(code: "missingArgument", message: message, details: details)
|
||||
}
|
||||
|
||||
func NoArgumentsError(message: String? = "no arguments were provided or could not be deserialized", details: Error? = nil) -> FlutterError {
|
||||
func NoArgumentsError(
|
||||
message: String? = "no arguments were provided or could not be deserialized",
|
||||
details: (any Error)? = nil
|
||||
) -> FlutterError {
|
||||
return FlutterError(code: "noArguments", message: message, details: details)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@ class DNUpdater {
|
|||
private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes
|
||||
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater")
|
||||
|
||||
func updateAll(onUpdate: @escaping (Site) -> ()) {
|
||||
_ = SiteList{ (sites, _) -> () in
|
||||
func updateAll(onUpdate: @escaping (Site) -> Void) {
|
||||
_ = SiteList { (sites, _) -> Void in
|
||||
// NEVPN seems to force us onto the main thread and we are about to make network calls that
|
||||
// could block for a while. Push ourselves onto another thread to avoid blocking the UI.
|
||||
Task.detached(priority: .userInitiated) {
|
||||
sites?.values.forEach { site in
|
||||
if (site.connected == true) {
|
||||
if site.connected == true {
|
||||
// The vpn service is in charge of updating the currently connected site
|
||||
return
|
||||
}
|
||||
|
@ -23,23 +23,23 @@ class DNUpdater {
|
|||
}
|
||||
}
|
||||
|
||||
func updateAllLoop(onUpdate: @escaping (Site) -> ()) {
|
||||
func updateAllLoop(onUpdate: @escaping (Site) -> Void) {
|
||||
timer.eventHandler = {
|
||||
self.updateAll(onUpdate: onUpdate)
|
||||
}
|
||||
timer.resume()
|
||||
}
|
||||
|
||||
func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> ()) {
|
||||
func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> Void) {
|
||||
timer.eventHandler = {
|
||||
self.updateSite(site: site, onUpdate: onUpdate)
|
||||
}
|
||||
timer.resume()
|
||||
}
|
||||
|
||||
func updateSite(site: Site, onUpdate: @escaping (Site) -> ()) {
|
||||
func updateSite(site: Site, onUpdate: @escaping (Site) -> Void) {
|
||||
do {
|
||||
if (!site.managed) {
|
||||
if !site.managed {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ class DNUpdater {
|
|||
trustedKeys: credentials.trustedKeys
|
||||
)
|
||||
} catch (APIClientError.invalidCredentials) {
|
||||
if (!credentials.invalid) {
|
||||
if !credentials.invalid {
|
||||
try site.invalidateDNCredentials()
|
||||
log.notice("Invalidated credentials in site: \(site.name, privacy: .public)")
|
||||
}
|
||||
|
@ -63,8 +63,14 @@ class DNUpdater {
|
|||
return
|
||||
}
|
||||
|
||||
newSite?.save(manager: site.manager) { error in
|
||||
if (error != nil) {
|
||||
let siteManager = site.manager
|
||||
let shouldSaveToManager =
|
||||
siteManager != nil
|
||||
|| ProcessInfo().isOperatingSystemAtLeast(
|
||||
OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
|
||||
|
||||
newSite?.save(manager: site.manager, saveToManager: shouldSaveToManager) { error in
|
||||
if error != nil {
|
||||
self.log.error("failed to save update: \(error!.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
|
@ -72,13 +78,15 @@ class DNUpdater {
|
|||
onUpdate(Site(incoming: newSite!))
|
||||
}
|
||||
|
||||
if (credentials.invalid) {
|
||||
if credentials.invalid {
|
||||
try site.validateDNCredentials()
|
||||
log.notice("Revalidated credentials in site \(site.name, privacy: .public)")
|
||||
}
|
||||
|
||||
} catch {
|
||||
log.error("Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
log.error(
|
||||
"Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,7 +100,7 @@ class RepeatingTimer {
|
|||
self.timeInterval = timeInterval
|
||||
}
|
||||
|
||||
private lazy var timer: DispatchSourceTimer = {
|
||||
private lazy var timer: any DispatchSourceTimer = {
|
||||
let t = DispatchSource.makeTimerSource()
|
||||
t.schedule(deadline: .now(), repeating: self.timeInterval)
|
||||
t.setEventHandler(handler: { [weak self] in
|
||||
|
|
|
@ -2,11 +2,10 @@ import Foundation
|
|||
|
||||
class PackageInfo {
|
||||
func getVersion() -> String {
|
||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ??
|
||||
"unknown"
|
||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
|
||||
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
|
||||
|
||||
if (buildNumber == nil) {
|
||||
if buildNumber == nil {
|
||||
return version
|
||||
}
|
||||
|
||||
|
@ -14,9 +13,8 @@ class PackageInfo {
|
|||
}
|
||||
|
||||
func getName() -> String {
|
||||
return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ??
|
||||
Bundle.main.infoDictionary?["CFBundleName"] as? String ??
|
||||
"Nebula"
|
||||
return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? Bundle.main
|
||||
.infoDictionary?["CFBundleName"] as? String ?? "Nebula"
|
||||
}
|
||||
|
||||
func getSystemVersion() -> String {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import NetworkExtension
|
||||
import MobileNebula
|
||||
import NetworkExtension
|
||||
|
||||
class SiteContainer {
|
||||
var site: Site
|
||||
|
@ -13,21 +13,21 @@ class SiteContainer {
|
|||
|
||||
class Sites {
|
||||
private var containers = [String: SiteContainer]()
|
||||
private var messenger: FlutterBinaryMessenger?
|
||||
private var messenger: (any FlutterBinaryMessenger)?
|
||||
|
||||
init(messenger: FlutterBinaryMessenger?) {
|
||||
init(messenger: (any FlutterBinaryMessenger)?) {
|
||||
self.messenger = messenger
|
||||
}
|
||||
|
||||
func loadSites(completion: @escaping ([String: Site]?, Error?) -> ()) {
|
||||
func loadSites(completion: @escaping ([String: Site]?, (any Error)?) -> Void) {
|
||||
_ = SiteList { (sites, err) in
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
return completion(nil, err)
|
||||
}
|
||||
|
||||
sites?.values.forEach{ site in
|
||||
sites?.values.forEach { site in
|
||||
var updater = self.containers[site.id]?.updater
|
||||
if (updater != nil) {
|
||||
if updater != nil {
|
||||
updater!.setSite(site: site)
|
||||
} else {
|
||||
updater = SiteUpdater(messenger: self.messenger!, site: site)
|
||||
|
@ -42,7 +42,7 @@ class Sites {
|
|||
}
|
||||
}
|
||||
|
||||
func deleteSite(id: String, callback: @escaping (Error?) -> ()) {
|
||||
func deleteSite(id: String, callback: @escaping ((any Error)?) -> Void) {
|
||||
if let site = self.containers.removeValue(forKey: id) {
|
||||
_ = KeyChain.delete(key: "\(site.site.id).dnCredentials")
|
||||
_ = KeyChain.delete(key: "\(site.site.id).key")
|
||||
|
@ -55,10 +55,10 @@ class Sites {
|
|||
print("Failed to delete site from fs: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
#if !targetEnvironment(simulator)
|
||||
site.site.manager!.removeFromPreferences(completionHandler: callback)
|
||||
return
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
// Nothing to remove
|
||||
|
@ -79,15 +79,15 @@ class Sites {
|
|||
}
|
||||
|
||||
class SiteUpdater: NSObject, FlutterStreamHandler {
|
||||
private var eventSink: FlutterEventSink?;
|
||||
private var eventChannel: FlutterEventChannel;
|
||||
private var eventSink: FlutterEventSink?
|
||||
private var eventChannel: FlutterEventChannel
|
||||
private var site: Site
|
||||
private var notification: Any?
|
||||
public var startFunc: (() -> Void)?
|
||||
private var configFd: Int32? = nil
|
||||
private var configObserver: DispatchSourceFileSystemObject? = nil
|
||||
private var configObserver: (any DispatchSourceFileSystemObject)? = nil
|
||||
|
||||
init(messenger: FlutterBinaryMessenger, site: Site) {
|
||||
init(messenger: any FlutterBinaryMessenger, site: Site) {
|
||||
do {
|
||||
let configPath = try SiteList.getSiteConfigFile(id: site.id, createDir: false)
|
||||
self.configFd = open(configPath.path, O_EVTONLY)
|
||||
|
@ -101,7 +101,8 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
|
|||
self.configObserver = nil
|
||||
}
|
||||
|
||||
eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
|
||||
eventChannel = FlutterEventChannel(
|
||||
name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
|
||||
self.site = site
|
||||
super.init()
|
||||
|
||||
|
@ -123,17 +124,23 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
|
|||
}
|
||||
|
||||
/// onListen is called when flutter code attaches an event listener
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||
eventSink = events;
|
||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
|
||||
-> FlutterError?
|
||||
{
|
||||
eventSink = events
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
#if !targetEnvironment(simulator)
|
||||
if site.manager == nil {
|
||||
//TODO: The dn updater path seems to race to build a site that lacks a manager. The UI does not display this error
|
||||
// and a another listen should occur and succeed.
|
||||
return FlutterError(code: "Internal Error", message: "Flutter manager was not present", details: nil)
|
||||
return FlutterError(
|
||||
code: "Internal Error", message: "Flutter manager was not present", details: nil)
|
||||
}
|
||||
|
||||
self.notification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection , queue: nil) { n in
|
||||
self.notification = NotificationCenter.default.addObserver(
|
||||
forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection,
|
||||
queue: nil
|
||||
) { n in
|
||||
let oldConnected = self.site.connected
|
||||
self.site.status = statusString[self.site.manager!.connection.status]
|
||||
self.site.connected = statusMap[self.site.manager!.connection.status]
|
||||
|
@ -146,13 +153,13 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
|
|||
|
||||
self.update(connected: self.site.connected!)
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
|
||||
/// onCancel is called when the flutter listener stops listening
|
||||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||
if (self.notification != nil) {
|
||||
if self.notification != nil {
|
||||
NotificationCenter.default.removeObserver(self.notification!)
|
||||
}
|
||||
return nil
|
||||
|
@ -160,7 +167,7 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
|
|||
|
||||
/// update is a way to send information to the flutter listener and generally should not be used directly
|
||||
func update(connected: Bool, replaceSite: Site? = nil) {
|
||||
if (replaceSite != nil) {
|
||||
if replaceSite != nil {
|
||||
site = replaceSite!
|
||||
}
|
||||
site.connected = connected
|
||||
|
|
|
@ -18,6 +18,11 @@ default_platform(:ios)
|
|||
platform :ios do
|
||||
desc "Push a new beta build to TestFlight"
|
||||
|
||||
before_all do
|
||||
xcode_select("/Applications/Xcode_16.2.0.app")
|
||||
end
|
||||
|
||||
|
||||
lane :build do
|
||||
# Do some things like setting up a temporary keystore to host secrets in CI
|
||||
setup_ci
|
||||
|
|
|
@ -2,13 +2,14 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:mobile_nebula/components/SpecialTextField.dart';
|
||||
import 'package:mobile_nebula/models/CIDR.dart';
|
||||
|
||||
import '../services/utils.dart';
|
||||
import 'IPField.dart';
|
||||
|
||||
//TODO: Support initialValue
|
||||
class CIDRField extends StatefulWidget {
|
||||
const CIDRField({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.ipHelp = "ip address",
|
||||
this.autoFocus = false,
|
||||
this.focusNode,
|
||||
|
@ -17,7 +18,7 @@ class CIDRField extends StatefulWidget {
|
|||
this.textInputAction,
|
||||
this.ipController,
|
||||
this.bitsController,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final String ipHelp;
|
||||
final bool autoFocus;
|
||||
|
@ -52,8 +53,8 @@ class _CIDRFieldState extends State<CIDRField> {
|
|||
Widget build(BuildContext context) {
|
||||
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
|
||||
|
||||
return Container(
|
||||
child: Row(children: <Widget>[
|
||||
return 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,10 @@ class _CIDRFieldState extends State<CIDRField> {
|
|||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
textInputAction: widget.textInputAction ?? TextInputAction.done,
|
||||
placeholder: 'bits',
|
||||
))
|
||||
]));
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -6,21 +6,18 @@ import 'package:mobile_nebula/validators/ipValidator.dart';
|
|||
class CIDRFormField extends FormField<CIDR> {
|
||||
//TODO: onSaved, validator, auto-validate, enabled?
|
||||
CIDRFormField({
|
||||
Key? key,
|
||||
super.key,
|
||||
autoFocus = false,
|
||||
enableIPV6 = false,
|
||||
focusNode,
|
||||
nextFocusNode,
|
||||
ValueChanged<CIDR>? onChanged,
|
||||
FormFieldSetter<CIDR>? onSaved,
|
||||
super.onSaved,
|
||||
textInputAction,
|
||||
CIDR? initialValue,
|
||||
super.initialValue,
|
||||
this.ipController,
|
||||
this.bitsController,
|
||||
}) : super(
|
||||
key: key,
|
||||
initialValue: initialValue,
|
||||
onSaved: onSaved,
|
||||
validator: (cidr) {
|
||||
if (cidr == null) {
|
||||
return "Please fill out this field";
|
||||
|
@ -46,7 +43,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 +56,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;
|
||||
|
|
36
lib/components/DangerButton.dart
Normal file
36
lib/components/DangerButton.dart
Normal file
|
@ -0,0 +1,36 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DangerButton extends StatelessWidget {
|
||||
const DangerButton({super.key, required this.child, this.onPressed});
|
||||
|
||||
final Widget child;
|
||||
final GestureTapCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (Platform.isAndroid) {
|
||||
return FilledButton(
|
||||
onPressed: onPressed,
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
} 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(
|
||||
onPressed: onPressed,
|
||||
color: CupertinoColors.systemRed.resolveFrom(context),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.child,
|
||||
required this.onSave,
|
||||
required this.changed,
|
||||
this.hideSave = false,
|
||||
this.scrollController})
|
||||
: super(key: key);
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
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();
|
||||
},
|
||||
)
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:mobile_nebula/components/SpecialTextField.dart';
|
||||
import 'package:mobile_nebula/models/IPAndPort.dart';
|
||||
|
||||
import '../services/utils.dart';
|
||||
import 'IPField.dart';
|
||||
|
||||
//TODO: Support initialValue
|
||||
class IPAndPortField extends StatefulWidget {
|
||||
const IPAndPortField({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.ipOnly = false,
|
||||
this.ipHelp = "ip address",
|
||||
this.autoFocus = false,
|
||||
|
@ -20,7 +21,7 @@ class IPAndPortField extends StatefulWidget {
|
|||
this.ipTextAlign,
|
||||
this.ipController,
|
||||
this.portController,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final String ipHelp;
|
||||
final bool ipOnly;
|
||||
|
@ -58,8 +59,8 @@ class _IPAndPortFieldState extends State<IPAndPortField> {
|
|||
Widget build(BuildContext context) {
|
||||
var textStyle = CupertinoTheme.of(context).textTheme.textStyle;
|
||||
|
||||
return Container(
|
||||
child: Row(children: <Widget>[
|
||||
return 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,10 @@ class _IPAndPortFieldState extends State<IPAndPortField> {
|
|||
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
|
||||
textInputAction: TextInputAction.done,
|
||||
placeholder: 'port',
|
||||
))
|
||||
]));
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -8,7 +8,7 @@ import 'IPAndPortField.dart';
|
|||
class IPAndPortFormField extends FormField<IPAndPort> {
|
||||
//TODO: onSaved, validator, auto-validate, enabled?
|
||||
IPAndPortFormField({
|
||||
Key? key,
|
||||
super.key,
|
||||
ipOnly = false,
|
||||
enableIPV6 = false,
|
||||
ipHelp = "ip address",
|
||||
|
@ -16,17 +16,14 @@ class IPAndPortFormField extends FormField<IPAndPort> {
|
|||
focusNode,
|
||||
nextFocusNode,
|
||||
ValueChanged<IPAndPort>? onChanged,
|
||||
FormFieldSetter<IPAndPort>? onSaved,
|
||||
super.onSaved,
|
||||
textInputAction,
|
||||
IPAndPort? initialValue,
|
||||
super.initialValue,
|
||||
noBorder,
|
||||
ipTextAlign = TextAlign.center,
|
||||
this.ipController,
|
||||
this.portController,
|
||||
}) : super(
|
||||
key: key,
|
||||
initialValue: initialValue,
|
||||
onSaved: onSaved,
|
||||
validator: (ipAndPort) {
|
||||
if (ipAndPort == null) {
|
||||
return "Please fill out this field";
|
||||
|
@ -52,7 +49,8 @@ class IPAndPortFormField extends FormField<IPAndPort> {
|
|||
field.didChange(value);
|
||||
}
|
||||
|
||||
return Column(children: <Widget>[
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
IPAndPortField(
|
||||
ipOnly: ipOnly,
|
||||
ipHelp: ipHelp,
|
||||
|
@ -67,11 +65,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 +111,7 @@ 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 ?? ""));
|
||||
bool shouldUpdate = false;
|
||||
|
||||
if (widget.ipController != oldWidget.ipController) {
|
||||
|
|
|
@ -16,8 +16,8 @@ class IPField extends StatelessWidget {
|
|||
final controller;
|
||||
final textAlign;
|
||||
|
||||
const IPField(
|
||||
{Key? key,
|
||||
const IPField({
|
||||
super.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,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -48,9 +48,10 @@ class IPField extends StatelessWidget {
|
|||
maxLength: ipOnly ? 15 : null,
|
||||
maxLengthEnforcement: ipOnly ? MaxLengthEnforcement.enforced : MaxLengthEnforcement.none,
|
||||
inputFormatters: ipOnly ? [IPTextInputFormatter()] : [FilteringTextInputFormatter.allow(RegExp(r'[^\s]+'))],
|
||||
textInputAction: this.textInputAction,
|
||||
textInputAction: textInputAction,
|
||||
placeholder: help,
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,22 +60,19 @@ 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','), '.');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TextEditingValue _selectionAwareTextManipulation(
|
||||
TextEditingValue value,
|
||||
String substringManipulation(String substring),
|
||||
String Function(String substring) substringManipulation,
|
||||
) {
|
||||
final int selectionStartIndex = value.selection.start;
|
||||
final int selectionEndIndex = value.selection.end;
|
||||
|
|
|
@ -9,7 +9,7 @@ import 'IPField.dart';
|
|||
class IPFormField extends FormField<String> {
|
||||
//TODO: validator, auto-validate, enabled?
|
||||
IPFormField({
|
||||
Key? key,
|
||||
super.key,
|
||||
ipOnly = false,
|
||||
enableIPV6 = false,
|
||||
help = "ip address",
|
||||
|
@ -17,7 +17,7 @@ class IPFormField extends FormField<String> {
|
|||
focusNode,
|
||||
nextFocusNode,
|
||||
ValueChanged<String>? onChanged,
|
||||
FormFieldSetter<String>? onSaved,
|
||||
super.onSaved,
|
||||
textPadding = const EdgeInsets.all(6.0),
|
||||
textInputAction,
|
||||
initialValue,
|
||||
|
@ -25,9 +25,7 @@ class IPFormField extends FormField<String> {
|
|||
crossAxisAlignment = CrossAxisAlignment.center,
|
||||
textAlign = TextAlign.center,
|
||||
}) : super(
|
||||
key: key,
|
||||
initialValue: initialValue,
|
||||
onSaved: onSaved,
|
||||
validator: (ip) {
|
||||
if (ip == null || ip == "") {
|
||||
return "Please fill out this field";
|
||||
|
@ -50,7 +48,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 +61,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;
|
||||
|
||||
|
@ -103,8 +106,9 @@ class _IPFormField extends FormFieldState<String> {
|
|||
oldWidget.controller?.removeListener(_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);
|
||||
}
|
||||
if (widget.controller != null) {
|
||||
setValue(widget.controller!.text);
|
||||
if (oldWidget.controller == null) _controller = null;
|
||||
|
|
|
@ -5,8 +5,8 @@ import 'package:mobile_nebula/components/SpecialTextField.dart';
|
|||
|
||||
class PlatformTextFormField extends FormField<String> {
|
||||
//TODO: auto-validate, enabled?
|
||||
PlatformTextFormField(
|
||||
{Key? key,
|
||||
PlatformTextFormField({
|
||||
super.key,
|
||||
widgetKey,
|
||||
this.controller,
|
||||
focusNode,
|
||||
|
@ -28,11 +28,9 @@ class PlatformTextFormField extends FormField<String> {
|
|||
String? initialValue,
|
||||
String? placeholder,
|
||||
FormFieldValidator<String>? validator,
|
||||
ValueChanged<String?>? onSaved})
|
||||
: super(
|
||||
key: key,
|
||||
super.onSaved,
|
||||
}) : super(
|
||||
initialValue: controller != null ? controller.text : (initialValue ?? ''),
|
||||
onSaved: onSaved,
|
||||
validator: (str) {
|
||||
if (validator != null) {
|
||||
return validator(str);
|
||||
|
@ -50,7 +48,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 +70,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;
|
||||
|
||||
|
@ -112,8 +115,9 @@ class _PlatformTextFormFieldState extends FormFieldState<String> {
|
|||
oldWidget.controller?.removeListener(_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);
|
||||
}
|
||||
if (widget.controller != null) {
|
||||
setValue(widget.controller!.text);
|
||||
if (oldWidget.controller == null) _controller = null;
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
import 'package:flutter/cupertino.dart' as cupertino;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
import 'package:mobile_nebula/services/utils.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({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.child,
|
||||
this.leadingAction,
|
||||
|
@ -25,8 +18,8 @@ class SimplePage extends StatelessWidget {
|
|||
this.onRefresh,
|
||||
this.onLoading,
|
||||
this.alignment,
|
||||
this.refreshController})
|
||||
: super(key: key);
|
||||
this.refreshController,
|
||||
});
|
||||
|
||||
final Widget title;
|
||||
final Widget child;
|
||||
|
@ -50,13 +43,14 @@ class SimplePage extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget realChild = child;
|
||||
var addScrollbar = this.scrollbar;
|
||||
var addScrollbar = scrollbar;
|
||||
|
||||
if (scrollable == SimpleScrollable.vertical || scrollable == SimpleScrollable.both) {
|
||||
realChild = SingleChildScrollView(
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: refreshController == null ? scrollController : null,
|
||||
child: realChild,
|
||||
controller: refreshController == null ? scrollController : null);
|
||||
);
|
||||
addScrollbar = true;
|
||||
}
|
||||
|
||||
|
@ -75,11 +69,12 @@ class SimplePage extends StatelessWidget {
|
|||
onRefresh: onRefresh,
|
||||
onLoading: onLoading,
|
||||
controller: refreshController!,
|
||||
child: realChild,
|
||||
enablePullUp: onLoading != null,
|
||||
enablePullDown: onRefresh != null,
|
||||
footer: ClassicFooter(loadStyle: LoadStyle.ShowWhenLoading),
|
||||
));
|
||||
child: realChild,
|
||||
),
|
||||
);
|
||||
addScrollbar = true;
|
||||
}
|
||||
|
||||
|
@ -88,26 +83,28 @@ class SimplePage extends StatelessWidget {
|
|||
}
|
||||
|
||||
if (alignment != null) {
|
||||
realChild = Align(alignment: this.alignment!, child: realChild);
|
||||
realChild = Align(alignment: alignment!, child: realChild);
|
||||
}
|
||||
|
||||
if (bottomBar != null) {
|
||||
realChild = Column(children: [
|
||||
Expanded(child: realChild),
|
||||
bottomBar!,
|
||||
]);
|
||||
realChild = Column(children: [Expanded(child: realChild), bottomBar!]);
|
||||
}
|
||||
|
||||
return PlatformScaffold(
|
||||
backgroundColor: cupertino.CupertinoColors.systemGroupedBackground.resolveFrom(context),
|
||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
appBar: PlatformAppBar(
|
||||
title: title,
|
||||
leading: leadingAction != null ? leadingAction : Utils.leadingBackWidget(context),
|
||||
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),
|
||||
),
|
||||
),
|
||||
body: SafeArea(child: realChild));
|
||||
body: SafeArea(child: realChild),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,15 @@ import 'package:mobile_nebula/models/Site.dart';
|
|||
import 'package:mobile_nebula/services/utils.dart';
|
||||
|
||||
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 onPressed;
|
||||
final void Function()? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final borderColor = site.errors.length > 0
|
||||
final borderColor =
|
||||
site.errors.isNotEmpty
|
||||
? 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),
|
||||
],
|
||||
)));
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
30
lib/components/SiteTitle.dart
Normal file
30
lib/components/SiteTitle.dart
Normal file
|
@ -0,0 +1,30 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
import '../models/Site.dart';
|
||||
|
||||
class SiteTitle extends StatelessWidget {
|
||||
const SiteTitle({super.key, required this.site});
|
||||
|
||||
final Site site;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dnIcon =
|
||||
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
|
||||
|
||||
return IntrinsicWidth(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
class SpecialButton extends StatefulWidget {
|
||||
const SpecialButton({Key? key, this.child, this.color, this.onPressed, this.useButtonTheme = false, this.decoration})
|
||||
: super(key: key);
|
||||
const SpecialButton({
|
||||
super.key,
|
||||
this.child,
|
||||
this.color,
|
||||
this.onPressed,
|
||||
this.useButtonTheme = false,
|
||||
this.decoration,
|
||||
});
|
||||
|
||||
final Widget? child;
|
||||
final Color? color;
|
||||
|
@ -26,7 +32,7 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
|
|||
}
|
||||
|
||||
Widget _buildAndroid() {
|
||||
var textStyle;
|
||||
TextStyle? textStyle;
|
||||
if (widget.useButtonTheme) {
|
||||
textStyle = Theme.of(context).textTheme.labelLarge;
|
||||
}
|
||||
|
@ -36,10 +42,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(onTap: widget.onPressed, child: widget.child),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGeneric() {
|
||||
|
@ -60,10 +65,11 @@ class _SpecialButtonState extends State<SpecialButton> with SingleTickerProvider
|
|||
button: true,
|
||||
child: FadeTransition(
|
||||
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)),
|
||||
),
|
||||
),
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Eyeballed values. Feel free to tweak.
|
||||
|
@ -77,11 +83,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 +133,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);
|
||||
|
||||
|
|
|
@ -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({
|
||||
super.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,
|
||||
});
|
||||
|
||||
final String? placeholder;
|
||||
final TextEditingController? controller;
|
||||
|
@ -64,7 +64,7 @@ class _SpecialTextFieldState extends State<SpecialTextField> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
if (widget.inputFormatters == null || formatters.length == 0) {
|
||||
if (widget.inputFormatters == null || formatters.isEmpty) {
|
||||
formatters = [FilteringTextInputFormatter.allow(RegExp(r'[^\t]'))];
|
||||
} else {
|
||||
formatters = widget.inputFormatters!;
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
33
lib/components/buttons/PrimaryButton.dart
Normal file
33
lib/components/buttons/PrimaryButton.dart
Normal file
|
@ -0,0 +1,33 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PrimaryButton extends StatelessWidget {
|
||||
const PrimaryButton({super.key, required this.child, this.onPressed});
|
||||
|
||||
final Widget child;
|
||||
final GestureTapCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (Platform.isAndroid) {
|
||||
return FilledButton(
|
||||
onPressed: onPressed,
|
||||
style: FilledButton.styleFrom(backgroundColor: Theme.of(context).colorScheme.primary),
|
||||
child: child,
|
||||
);
|
||||
} 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(
|
||||
onPressed: onPressed,
|
||||
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,10 +5,10 @@ import 'package:mobile_nebula/services/utils.dart';
|
|||
|
||||
// A config item that detects tapping and calls back on a tap
|
||||
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 onPressed;
|
||||
final void Function()? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -19,6 +19,7 @@ class ConfigButtonItem extends StatelessWidget {
|
|||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
|
||||
child: Center(child: content),
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
super.key,
|
||||
this.label,
|
||||
this.content,
|
||||
this.labelWidth = 100,
|
||||
this.onChanged,
|
||||
this.checked = false,
|
||||
});
|
||||
|
||||
final Widget? label;
|
||||
final Widget? content;
|
||||
|
@ -16,18 +21,19 @@ class ConfigCheckboxItem extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget item = Container(
|
||||
padding: EdgeInsets.only(left: 15),
|
||||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||||
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
label != null ? Container(width: labelWidth, child: label) : Container(),
|
||||
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
|
||||
label != null ? SizedBox(width: labelWidth, child: label) : Container(),
|
||||
Expanded(child: Container(padding: EdgeInsets.only(right: 10), child: content)),
|
||||
checked
|
||||
? Icon(CupertinoIcons.check_mark, color: CupertinoColors.systemBlue.resolveFrom(context), size: 34)
|
||||
: Container()
|
||||
? Icon(CupertinoIcons.check_mark, color: CupertinoColors.systemBlue.resolveFrom(context))
|
||||
: Container(),
|
||||
],
|
||||
));
|
||||
),
|
||||
);
|
||||
|
||||
if (onChanged != null) {
|
||||
return SpecialButton(
|
||||
|
|
|
@ -9,7 +9,7 @@ TextStyle basicTextStyle(BuildContext context) =>
|
|||
const double _headerFontSize = 13.0;
|
||||
|
||||
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 Color? color;
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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({
|
||||
super.key,
|
||||
this.label,
|
||||
required this.content,
|
||||
this.labelWidth = 100,
|
||||
this.crossAxisAlignment = CrossAxisAlignment.center})
|
||||
: super(key: key);
|
||||
this.crossAxisAlignment = CrossAxisAlignment.center,
|
||||
});
|
||||
|
||||
final Widget? label;
|
||||
final Widget content;
|
||||
|
@ -20,7 +20,7 @@ class ConfigItem extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var textStyle;
|
||||
TextStyle textStyle;
|
||||
if (Platform.isAndroid) {
|
||||
textStyle = Theme.of(context).textTheme.labelLarge!.copyWith(fontWeight: FontWeight.normal);
|
||||
} else {
|
||||
|
@ -29,14 +29,15 @@ class ConfigItem extends StatelessWidget {
|
|||
|
||||
return Container(
|
||||
color: Utils.configItemBackground(context),
|
||||
padding: EdgeInsets.only(top: 2, bottom: 2, left: 15, right: 20),
|
||||
padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15),
|
||||
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize),
|
||||
child: Row(
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
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))),
|
||||
],
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,32 +6,34 @@ import 'package:mobile_nebula/components/SpecialButton.dart';
|
|||
import 'package:mobile_nebula/services/utils.dart';
|
||||
|
||||
class ConfigPageItem extends StatelessWidget {
|
||||
const ConfigPageItem(
|
||||
{Key? key,
|
||||
const ConfigPageItem({
|
||||
super.key,
|
||||
this.label,
|
||||
this.content,
|
||||
this.labelWidth = 100,
|
||||
this.onPressed,
|
||||
this.disabled = false,
|
||||
this.crossAxisAlignment = CrossAxisAlignment.center})
|
||||
: super(key: key);
|
||||
this.crossAxisAlignment = CrossAxisAlignment.center,
|
||||
});
|
||||
|
||||
final Widget? label;
|
||||
final Widget? content;
|
||||
final double labelWidth;
|
||||
final CrossAxisAlignment crossAxisAlignment;
|
||||
final onPressed;
|
||||
final void Function()? onPressed;
|
||||
final bool disabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme;
|
||||
dynamic theme;
|
||||
|
||||
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);
|
||||
|
@ -42,21 +44,22 @@ class ConfigPageItem extends StatelessWidget {
|
|||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
return SpecialButton(
|
||||
onPressed: this.disabled ? null : onPressed,
|
||||
onPressed: disabled ? null : onPressed,
|
||||
color: Utils.configItemBackground(context),
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(left: 15, right: 15),
|
||||
padding: EdgeInsets.symmetric(vertical: 6, horizontal: 15),
|
||||
constraints: BoxConstraints(minHeight: Utils.minInteractiveSize, minWidth: double.infinity),
|
||||
child: Row(
|
||||
crossAxisAlignment: crossAxisAlignment,
|
||||
children: <Widget>[
|
||||
label != null ? Container(width: labelWidth, child: label) : Container(),
|
||||
Expanded(child: Container(child: content, padding: EdgeInsets.only(right: 10))),
|
||||
this.disabled
|
||||
label != null ? SizedBox(width: labelWidth, child: label) : Container(),
|
||||
Expanded(child: Container(padding: EdgeInsets.only(right: 10), child: content)),
|
||||
disabled
|
||||
? Container()
|
||||
: Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18)
|
||||
: Icon(CupertinoIcons.forward, color: CupertinoColors.placeholderText.resolveFrom(context), size: 18),
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,7 @@ import 'package:mobile_nebula/services/utils.dart';
|
|||
import 'ConfigHeader.dart';
|
||||
|
||||
class ConfigSection extends StatelessWidget {
|
||||
const ConfigSection({Key? key, this.label, required this.children, this.borderColor, this.labelColor})
|
||||
: super(key: key);
|
||||
const ConfigSection({super.key, this.label, required this.children, this.borderColor, this.labelColor});
|
||||
|
||||
final List<Widget> children;
|
||||
final String? label;
|
||||
|
@ -16,30 +15,38 @@ class ConfigSection extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final border = BorderSide(color: borderColor ?? Utils.configSectionBorder(context));
|
||||
|
||||
List<Widget> _children = [];
|
||||
List<Widget> mappedChildren = [];
|
||||
final len = children.length;
|
||||
|
||||
for (var i = 0; i < len; i++) {
|
||||
_children.add(children[i]);
|
||||
mappedChildren.add(children[i]);
|
||||
|
||||
if (i < len - 1) {
|
||||
double pad = 15;
|
||||
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)));
|
||||
mappedChildren.add(
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: pad),
|
||||
child: Divider(height: 1, color: Utils.configSectionBorder(context)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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: mappedChildren),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_nebula/components/SpecialTextField.dart';
|
||||
|
||||
class ConfigTextItem extends StatelessWidget {
|
||||
const ConfigTextItem(
|
||||
{Key? key, this.placeholder, this.controller, this.style = const TextStyle(fontFamily: 'RobotoMono')})
|
||||
: super(key: key);
|
||||
const ConfigTextItem({
|
||||
super.key,
|
||||
this.placeholder,
|
||||
this.controller,
|
||||
this.style = const TextStyle(fontFamily: 'RobotoMono'),
|
||||
});
|
||||
|
||||
final String? placeholder;
|
||||
final TextEditingController? controller;
|
||||
|
@ -15,14 +14,13 @@ class ConfigTextItem extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: Platform.isAndroid ? EdgeInsets.all(5) : EdgeInsets.zero,
|
||||
child: SpecialTextField(
|
||||
return CupertinoTextFormFieldRow(
|
||||
autocorrect: false,
|
||||
minLines: 3,
|
||||
maxLines: 10,
|
||||
placeholder: placeholder,
|
||||
style: style,
|
||||
controller: controller));
|
||||
controller: controller,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/cupertino.dart' show CupertinoThemeData, DefaultCupertinoLocalizations;
|
||||
import 'package:flutter/material.dart'
|
||||
show BottomSheetThemeData, Colors, DefaultMaterialLocalizations, ThemeData, ThemeMode;
|
||||
import 'package:flutter/material.dart' show DefaultMaterialLocalizations, TextTheme, ThemeMode;
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
@ -10,24 +9,22 @@ import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
|||
import 'package:mobile_nebula/screens/MainScreen.dart';
|
||||
import 'package:mobile_nebula/screens/EnrollmentScreen.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/utils.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
usePathUrlStrategy();
|
||||
|
||||
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());
|
||||
}
|
||||
|
@ -36,12 +33,16 @@ Future<void> main() async {
|
|||
//TODO: EventChannel might be better than the stream controller we are using now
|
||||
|
||||
class Main extends StatelessWidget {
|
||||
const Main({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) => App();
|
||||
}
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
_AppState createState() => _AppState();
|
||||
}
|
||||
|
@ -85,34 +86,13 @@ class _AppState extends State<App> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData lightTheme = ThemeData(
|
||||
useMaterial3: false,
|
||||
brightness: Brightness.light,
|
||||
primarySwatch: Colors.blueGrey,
|
||||
primaryColor: Colors.blueGrey[900],
|
||||
fontFamily: 'PublicSans',
|
||||
//scaffoldBackgroundColor: Colors.grey[100],
|
||||
scaffoldBackgroundColor: Colors.white,
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: Colors.blueGrey[50],
|
||||
),
|
||||
);
|
||||
|
||||
final ThemeData darkTheme = ThemeData(
|
||||
useMaterial3: false,
|
||||
brightness: Brightness.dark,
|
||||
primarySwatch: Colors.grey,
|
||||
primaryColor: Colors.grey[900],
|
||||
fontFamily: 'PublicSans',
|
||||
scaffoldBackgroundColor: Colors.grey[800],
|
||||
bottomSheetTheme: BottomSheetThemeData(
|
||||
backgroundColor: Colors.grey[850],
|
||||
),
|
||||
);
|
||||
TextTheme textTheme = Utils.createTextTheme(context, "Public Sans", "Public Sans");
|
||||
MaterialTheme theme = MaterialTheme(textTheme);
|
||||
|
||||
return PlatformProvider(
|
||||
settings: PlatformSettingsData(iosUsesMaterialWidgets: true),
|
||||
builder: (context) => PlatformApp(
|
||||
builder:
|
||||
(context) => PlatformApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
localizationsDelegates: <LocalizationsDelegate<dynamic>>[
|
||||
DefaultMaterialLocalizations.delegate,
|
||||
|
@ -121,17 +101,16 @@ class _AppState extends State<App> {
|
|||
],
|
||||
title: 'Nebula',
|
||||
material: (_, __) {
|
||||
return new MaterialAppData(
|
||||
return MaterialAppData(
|
||||
themeMode: brightness == Brightness.light ? ThemeMode.light : ThemeMode.dark,
|
||||
theme: brightness == Brightness.light ? lightTheme : darkTheme,
|
||||
theme: brightness == Brightness.light ? theme.light() : theme.dark(),
|
||||
);
|
||||
},
|
||||
cupertino: (_, __) => CupertinoAppData(
|
||||
theme: CupertinoThemeData(brightness: brightness),
|
||||
),
|
||||
cupertino: (_, __) => CupertinoAppData(theme: CupertinoThemeData(brightness: brightness)),
|
||||
onGenerateRoute: (settings) {
|
||||
print(settings);
|
||||
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!);
|
||||
|
@ -139,8 +118,9 @@ 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) =>
|
||||
EnrollmentScreen(code: EnrollmentScreen.parseCode(settings.name!), stream: this.dnEnrolled),
|
||||
builder:
|
||||
(context) =>
|
||||
EnrollmentScreen(code: EnrollmentScreen.parseCode(settings.name!), stream: dnEnrolled),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,7 @@ class CertificateInfo {
|
|||
String? rawCert;
|
||||
CertificateValidity? validity;
|
||||
|
||||
CertificateInfo.debug({this.rawCert = ""})
|
||||
: this.cert = Certificate.debug(),
|
||||
this.validity = CertificateValidity.debug();
|
||||
CertificateInfo.debug({this.rawCert = ""}) : cert = Certificate.debug(), validity = CertificateValidity.debug();
|
||||
|
||||
CertificateInfo.fromJson(Map<String, dynamic> json)
|
||||
: cert = Certificate.fromJson(json['Cert']),
|
||||
|
@ -24,10 +22,7 @@ class Certificate {
|
|||
String fingerprint;
|
||||
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)
|
||||
: details = CertificateDetails.fromJson(json['details']),
|
||||
|
@ -47,7 +42,7 @@ class CertificateDetails {
|
|||
String issuer;
|
||||
|
||||
CertificateDetails.debug()
|
||||
: this.name = "DEBUG",
|
||||
: name = "DEBUG",
|
||||
notBefore = DateTime.now(),
|
||||
notAfter = DateTime.now(),
|
||||
publicKey = "",
|
||||
|
@ -73,11 +68,7 @@ class CertificateValidity {
|
|||
bool valid;
|
||||
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'];
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
|
|||
import 'package:mobile_nebula/models/HostInfo.dart';
|
||||
import 'package:mobile_nebula/models/UnsafeRoute.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'Certificate.dart';
|
||||
import 'StaticHosts.dart';
|
||||
|
||||
|
@ -15,7 +16,7 @@ class Site {
|
|||
late EventChannel _updates;
|
||||
|
||||
/// 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
|
||||
late String name;
|
||||
|
@ -53,48 +54,35 @@ class Site {
|
|||
late List<String> errors;
|
||||
|
||||
Site({
|
||||
String name = '',
|
||||
this.name = '',
|
||||
String? id,
|
||||
Map<String, StaticHost>? staticHostmap,
|
||||
List<CertificateInfo>? ca,
|
||||
CertificateInfo? certInfo,
|
||||
int lhDuration = 0,
|
||||
int port = 0,
|
||||
String cipher = "aes",
|
||||
int sortKey = 0,
|
||||
int mtu = 1300,
|
||||
bool connected = false,
|
||||
String status = '',
|
||||
String logFile = '',
|
||||
String logVerbosity = 'info',
|
||||
this.certInfo,
|
||||
this.lhDuration = 0,
|
||||
this.port = 0,
|
||||
this.cipher = "aes",
|
||||
this.sortKey = 0,
|
||||
this.mtu = 1300,
|
||||
this.connected = false,
|
||||
this.status = '',
|
||||
this.logFile = '',
|
||||
this.logVerbosity = 'info',
|
||||
List<String>? errors,
|
||||
List<UnsafeRoute>? unsafeRoutes,
|
||||
bool managed = false,
|
||||
String? rawConfig,
|
||||
DateTime? lastManagedUpdate,
|
||||
this.managed = false,
|
||||
this.rawConfig,
|
||||
this.lastManagedUpdate,
|
||||
}) {
|
||||
this.name = name;
|
||||
this.id = id ?? uuid.v4();
|
||||
this.staticHostmap = staticHostmap ?? {};
|
||||
this.ca = ca ?? [];
|
||||
this.certInfo = certInfo;
|
||||
this.lhDuration = lhDuration;
|
||||
this.port = port;
|
||||
this.cipher = cipher;
|
||||
this.sortKey = sortKey;
|
||||
this.mtu = mtu;
|
||||
this.connected = connected;
|
||||
this.status = status;
|
||||
this.logFile = logFile;
|
||||
this.logVerbosity = logVerbosity;
|
||||
this.errors = errors ?? [];
|
||||
this.unsafeRoutes = unsafeRoutes ?? [];
|
||||
this.managed = managed;
|
||||
this.rawConfig = rawConfig;
|
||||
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 +90,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) {
|
||||
|
@ -136,25 +126,25 @@ class Site {
|
|||
|
||||
_updateFromJson(String json) {
|
||||
var decoded = Site._fromJson(jsonDecode(json));
|
||||
this.name = decoded["name"];
|
||||
this.id = decoded['id']; // TODO update EventChannel
|
||||
this.staticHostmap = decoded['staticHostmap'];
|
||||
this.ca = decoded['ca'];
|
||||
this.certInfo = decoded['certInfo'];
|
||||
this.lhDuration = decoded['lhDuration'];
|
||||
this.port = decoded['port'];
|
||||
this.cipher = decoded['cipher'];
|
||||
this.sortKey = decoded['sortKey'];
|
||||
this.mtu = decoded['mtu'];
|
||||
this.connected = decoded['connected'];
|
||||
this.status = decoded['status'];
|
||||
this.logFile = decoded['logFile'];
|
||||
this.logVerbosity = decoded['logVerbosity'];
|
||||
this.errors = decoded['errors'];
|
||||
this.unsafeRoutes = decoded['unsafeRoutes'];
|
||||
this.managed = decoded['managed'];
|
||||
this.rawConfig = decoded['rawConfig'];
|
||||
this.lastManagedUpdate = decoded['lastManagedUpdate'];
|
||||
name = decoded["name"];
|
||||
id = decoded['id']; // TODO update EventChannel
|
||||
staticHostmap = decoded['staticHostmap'];
|
||||
ca = decoded['ca'];
|
||||
certInfo = decoded['certInfo'];
|
||||
lhDuration = decoded['lhDuration'];
|
||||
port = decoded['port'];
|
||||
cipher = decoded['cipher'];
|
||||
sortKey = decoded['sortKey'];
|
||||
mtu = decoded['mtu'];
|
||||
connected = decoded['connected'];
|
||||
status = decoded['status'];
|
||||
logFile = decoded['logFile'];
|
||||
logVerbosity = decoded['logVerbosity'];
|
||||
errors = decoded['errors'];
|
||||
unsafeRoutes = decoded['unsafeRoutes'];
|
||||
managed = decoded['managed'];
|
||||
rawConfig = decoded['rawConfig'];
|
||||
lastManagedUpdate = decoded['lastManagedUpdate'];
|
||||
}
|
||||
|
||||
static _fromJson(Map<String, dynamic> json) {
|
||||
|
@ -166,15 +156,15 @@ class Site {
|
|||
|
||||
List<dynamic> rawUnsafeRoutes = json['unsafeRoutes'];
|
||||
List<UnsafeRoute> unsafeRoutes = [];
|
||||
rawUnsafeRoutes.forEach((val) {
|
||||
for (var val in rawUnsafeRoutes) {
|
||||
unsafeRoutes.add(UnsafeRoute.fromJson(val));
|
||||
});
|
||||
}
|
||||
|
||||
List<dynamic> rawCA = json['ca'];
|
||||
List<CertificateInfo> ca = [];
|
||||
rawCA.forEach((val) {
|
||||
for (var val in rawCA) {
|
||||
ca.add(CertificateInfo.fromJson(val));
|
||||
});
|
||||
}
|
||||
|
||||
CertificateInfo? certInfo;
|
||||
if (json['cert'] != null) {
|
||||
|
@ -183,9 +173,9 @@ class Site {
|
|||
|
||||
List<dynamic> rawErrors = json["errors"];
|
||||
List<String> errors = [];
|
||||
rawErrors.forEach((error) {
|
||||
for (var error in rawErrors) {
|
||||
errors.add(error);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
"name": json["name"],
|
||||
|
@ -220,9 +210,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,
|
||||
|
@ -290,9 +282,9 @@ class Site {
|
|||
|
||||
List<dynamic> f = jsonDecode(ret);
|
||||
List<HostInfo> hosts = [];
|
||||
f.forEach((v) {
|
||||
for (var v in f) {
|
||||
hosts.add(HostInfo.fromJson(v));
|
||||
});
|
||||
}
|
||||
|
||||
return hosts;
|
||||
} on PlatformException catch (err) {
|
||||
|
@ -312,9 +304,9 @@ class Site {
|
|||
|
||||
List<dynamic> f = jsonDecode(ret);
|
||||
List<HostInfo> hosts = [];
|
||||
f.forEach((v) {
|
||||
for (var v in f) {
|
||||
hosts.add(HostInfo.fromJson(v));
|
||||
});
|
||||
}
|
||||
|
||||
return hosts;
|
||||
} on PlatformException catch (err) {
|
||||
|
@ -326,7 +318,7 @@ class Site {
|
|||
|
||||
Future<Map<String, List<HostInfo>>> listAllHostmaps() async {
|
||||
try {
|
||||
var res = await Future.wait([this.listHostmap(), this.listPendingHostmap()]);
|
||||
var res = await Future.wait([listHostmap(), listPendingHostmap()]);
|
||||
return {"active": res[0], "pending": res[1]};
|
||||
} on PlatformException catch (err) {
|
||||
throw err.details ?? err.message ?? err.toString();
|
||||
|
@ -341,8 +333,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 +353,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;
|
||||
|
|
|
@ -10,20 +10,14 @@ class StaticHost {
|
|||
var list = json['destinations'] as List<dynamic>;
|
||||
var result = <IPAndPort>[];
|
||||
|
||||
list.forEach((item) {
|
||||
for (var item in list) {
|
||||
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};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,12 @@ import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
|||
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||
import 'package:mobile_nebula/gen.versions.dart';
|
||||
import 'package:mobile_nebula/screens/LicensesScreen.dart';
|
||||
import 'package:mobile_nebula/services/utils.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class AboutScreen extends StatefulWidget {
|
||||
const AboutScreen({Key? key}) : super(key: key);
|
||||
const AboutScreen({super.key});
|
||||
|
||||
@override
|
||||
_AboutScreenState createState() => _AboutScreenState();
|
||||
|
@ -36,47 +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('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)),
|
||||
// ConfigPageItem(label: Text('Licenses'), labelWidth: 300, onPressed: () => Utils.launchUrl('https://defined.net/mobile/license', context)),
|
||||
]),
|
||||
onPressed: () => Utils.launchUrl('https://www.defined.net/privacy/', context),
|
||||
),
|
||||
ConfigPageItem(
|
||||
label: Text('Licenses'),
|
||||
labelWidth: 300,
|
||||
onPressed:
|
||||
() => Utils.openPage(context, (context) {
|
||||
return LicensesScreen();
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Text(
|
||||
'Copyright © 2024 Defined Networking, Inc',
|
||||
textAlign: TextAlign.center,
|
||||
)),
|
||||
]),
|
||||
child: Text('Copyright © 2024 Defined Networking, Inc', textAlign: TextAlign.center),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -6,8 +7,11 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
|
||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||
import 'package:mobile_nebula/components/buttons/PrimaryButton.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../components/config/ConfigSection.dart';
|
||||
|
||||
class EnrollmentScreen extends StatefulWidget {
|
||||
final String? code;
|
||||
final StreamController? stream;
|
||||
|
@ -45,6 +49,7 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
|
|||
|
||||
static const platform = MethodChannel('net.defined.mobileNebula/NebulaVpnService');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
code = widget.code;
|
||||
super.initState();
|
||||
|
@ -90,29 +95,39 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
|
|||
} else {
|
||||
// No code, show the error
|
||||
child = Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Center(
|
||||
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));
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (this.error != null) {
|
||||
} else if (error != null) {
|
||||
// Error while enrolling, display it
|
||||
child = Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20),
|
||||
child: SelectableText(
|
||||
'There was an issue while attempting to enroll this device. Contact your administrator to obtain a new enrollment code.'),
|
||||
padding: EdgeInsets.symmetric(vertical: 20)),
|
||||
'There was an issue while attempting to enroll this device. Contact your administrator to obtain a new enrollment code.',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
child: SelectableText.rich(TextSpan(children: [
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
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));
|
||||
|
@ -120,61 +135,87 @@ class _EnrollmentScreenState extends State<EnrollmentScreen> {
|
|||
},
|
||||
),
|
||||
TextSpan(text: ' and provide the following error:'),
|
||||
])),
|
||||
padding: EdgeInsets.only(bottom: 10)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
child: Padding(child: SelectableText(this.error!), padding: EdgeInsets.all(10)),
|
||||
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!
|
||||
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 {
|
||||
// Have a code and actively enrolling
|
||||
alignment = Alignment.center;
|
||||
child = Center(
|
||||
child: Column(children: [
|
||||
Padding(child: Text('Contacting DN for enrollment'), padding: EdgeInsets.only(bottom: 25)),
|
||||
PlatformCircularProgressIndicator(cupertino: (_, __) {
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(padding: EdgeInsets.only(bottom: 25), child: Text('Contacting DN for enrollment')),
|
||||
PlatformCircularProgressIndicator(
|
||||
cupertino: (_, __) {
|
||||
return CupertinoProgressIndicatorData(radius: 50);
|
||||
})
|
||||
]));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SimplePage(
|
||||
title: Text('Enroll with Managed Nebula', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
child: Padding(child: child, padding: EdgeInsets.symmetric(horizontal: 10)),
|
||||
alignment: alignment);
|
||||
return SimplePage(title: Text('Enroll with Managed Nebula'), alignment: alignment, child: child);
|
||||
}
|
||||
|
||||
Widget _codeEntry() {
|
||||
return Column(children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: PlatformTextField(
|
||||
hintText: 'defined.net enrollment code or link',
|
||||
controller: enrollInput,
|
||||
)),
|
||||
PlatformTextButton(
|
||||
child: Text('Submit'),
|
||||
onPressed: () {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
String? validator(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Code or link is required';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> onSubmit() async {
|
||||
final bool isValid = formKey.currentState?.validate() ?? false;
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
code = EnrollmentScreen.parseCode(enrollInput.text);
|
||||
error = null;
|
||||
_enroll();
|
||||
});
|
||||
},
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
final input = Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: PlatformTextFormField(
|
||||
controller: enrollInput,
|
||||
validator: validator,
|
||||
hintText: 'from admin.defined.net',
|
||||
cupertino: (_, __) => CupertinoTextFormFieldData(prefix: Text("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),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(children: [Expanded(child: PrimaryButton(onPressed: onSubmit, child: Text('Submit')))]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
import 'package:mobile_nebula/components/DangerButton.dart';
|
||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||
import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
|
||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||
|
@ -15,14 +15,14 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
|
|||
|
||||
class HostInfoScreen extends StatefulWidget {
|
||||
const HostInfoScreen({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.hostInfo,
|
||||
required this.isLighthouse,
|
||||
required this.pending,
|
||||
this.onChanged,
|
||||
required this.site,
|
||||
required this.supportsQRScanning,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final bool isLighthouse;
|
||||
final bool pending;
|
||||
|
@ -59,46 +59,60 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
|||
await _getHostInfo();
|
||||
refreshController.refreshCompleted();
|
||||
},
|
||||
leadingAction: Utils.leadingBackWidget(context, onPressed: () {
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
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) {
|
||||
if (hostInfo.remoteAddresses.isEmpty) {
|
||||
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();
|
||||
|
@ -110,9 +124,10 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
|||
final double ipWidth =
|
||||
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();
|
||||
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,
|
||||
|
@ -131,10 +146,11 @@ 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);
|
||||
return ConfigSection(label: items.isNotEmpty ? 'Tap to change the active address' : null, children: items);
|
||||
}
|
||||
|
||||
Widget _buildStaticRemotes() {
|
||||
|
@ -143,17 +159,19 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
|||
final double ipWidth =
|
||||
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();
|
||||
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);
|
||||
return ConfigSection(label: items.isNotEmpty ? 'REMOTES' : null, children: items);
|
||||
}
|
||||
|
||||
Widget _buildClose() {
|
||||
|
@ -161,10 +179,10 @@ class _HostInfoScreenState extends State<HostInfoScreen> {
|
|||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: PlatformElevatedButton(
|
||||
child: DangerButton(
|
||||
child: Text('Close Tunnel'),
|
||||
color: CupertinoColors.systemRed.resolveFrom(context),
|
||||
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) {
|
||||
|
@ -174,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 {
|
||||
|
|
64
lib/screens/LicensesScreen.dart
Normal file
64
lib/screens/LicensesScreen.dart
Normal file
|
@ -0,0 +1,64 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||
import 'package:mobile_nebula/services/utils.dart';
|
||||
import '../../oss_licenses.dart';
|
||||
|
||||
String capitalize(String input) {
|
||||
return input[0].toUpperCase() + input.substring(1);
|
||||
}
|
||||
|
||||
class LicensesScreen extends StatelessWidget {
|
||||
const LicensesScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SimplePage(
|
||||
title: const Text("Licences"),
|
||||
scrollable: SimpleScrollable.none,
|
||||
child: ListView.builder(
|
||||
itemCount: allDependencies.length,
|
||||
itemBuilder: (_, index) {
|
||||
var dep = allDependencies[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: PlatformListTile(
|
||||
onTap: () {
|
||||
Utils.openPage(context, (_) => LicenceDetailPage(title: capitalize(dep.name), licence: dep.license!));
|
||||
},
|
||||
title: Text(capitalize(dep.name)),
|
||||
subtitle: Text(dep.description),
|
||||
trailing: Icon(context.platformIcons.forward, size: 18),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//detail page for the licence
|
||||
class LicenceDetailPage extends StatelessWidget {
|
||||
final String title, licence;
|
||||
const LicenceDetailPage({super.key, required this.title, required this.licence});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SimplePage(
|
||||
title: Text(title),
|
||||
scrollable: SimpleScrollable.none,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)),
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
child: Column(children: [Text(licence, style: const TextStyle(fontSize: 15))]),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -60,7 +60,7 @@ MAIH7gzreMGgrH/yR6rZpIHR3DxJ3E0aHtEI
|
|||
};
|
||||
|
||||
class MainScreen extends StatefulWidget {
|
||||
const MainScreen(this.dnEnrollStream, {Key? key}) : super(key: key);
|
||||
const MainScreen(this.dnEnrollStream, {super.key});
|
||||
|
||||
final StreamController dnEnrollStream;
|
||||
|
||||
|
@ -115,12 +115,8 @@ class _MainScreenState extends State<MainScreen> {
|
|||
|
||||
if (kDebugMode) {
|
||||
debugSite = Row(
|
||||
children: [
|
||||
_debugSave(badDebugSave),
|
||||
_debugSave(goodDebugSave),
|
||||
_debugClearKeys(),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [_debugSave(badDebugSave), _debugSave(goodDebugSave), _debugClearKeys()],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -157,7 +155,7 @@ class _MainScreenState extends State<MainScreen> {
|
|||
trailingActions: <Widget>[
|
||||
PlatformIconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Icon(Icons.menu, size: 28.0),
|
||||
icon: Icon(Icons.adaptive.more, size: 28.0),
|
||||
onPressed: () => Utils.openPage(context, (_) => SettingsScreen(widget.dnEnrollStream)),
|
||||
),
|
||||
],
|
||||
|
@ -170,12 +168,14 @@ class _MainScreenState extends State<MainScreen> {
|
|||
if (error != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: error!,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(vertical: 0, horizontal: 10)));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return _buildSites();
|
||||
|
@ -191,21 +191,25 @@ 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() {
|
||||
if (sites == null || sites!.length == 0) {
|
||||
if (sites == null || sites!.isEmpty) {
|
||||
return _buildNoSites();
|
||||
}
|
||||
|
||||
List<Widget> items = [];
|
||||
sites!.forEach((site) {
|
||||
items.add(SiteItem(
|
||||
for (var site in sites!) {
|
||||
items.add(
|
||||
SiteItem(
|
||||
key: Key(site.id),
|
||||
site: site,
|
||||
onPressed: () {
|
||||
|
@ -216,8 +220,10 @@ class _MainScreenState extends State<MainScreen> {
|
|||
supportsQRScanning: supportsQRScanning,
|
||||
);
|
||||
});
|
||||
}));
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget child = ReorderableListView(
|
||||
shrinkWrap: true,
|
||||
|
@ -250,10 +256,11 @@ class _MainScreenState extends State<MainScreen> {
|
|||
}
|
||||
|
||||
_loadSites();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
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
|
||||
|
@ -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) {
|
||||
|
|
|
@ -27,7 +27,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||
void initState() {
|
||||
//TODO: we need to unregister on dispose?
|
||||
settings.onChange().listen((_) {
|
||||
if (this.mounted) {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||
import 'package:mobile_nebula/components/config/ConfigPageItem.dart';
|
||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||
|
@ -17,16 +16,14 @@ import 'package:mobile_nebula/screens/siteConfig/SiteConfigScreen.dart';
|
|||
import 'package:mobile_nebula/services/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../components/DangerButton.dart';
|
||||
import '../components/SiteTitle.dart';
|
||||
|
||||
//TODO: If the site isn't active, don't respond to reloads on hostmaps
|
||||
//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({super.key, required this.site, this.onChanged, required this.supportsQRScanning});
|
||||
|
||||
final Site site;
|
||||
final Function? onChanged;
|
||||
|
@ -52,7 +49,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') {
|
||||
|
@ -63,10 +61,12 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
|||
}
|
||||
|
||||
setState(() {});
|
||||
}, onError: (err) {
|
||||
},
|
||||
onError: (err) {
|
||||
setState(() {});
|
||||
Utils.popError(context, "Error", err);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
@ -79,23 +79,19 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dnIcon =
|
||||
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
|
||||
final title = Row(children: [
|
||||
site.managed
|
||||
? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
|
||||
: Container(),
|
||||
Expanded(child: Text(site.name, style: TextStyle(fontWeight: FontWeight.bold)))
|
||||
]);
|
||||
final title = SiteTitle(site: widget.site);
|
||||
|
||||
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") {
|
||||
|
@ -103,25 +99,32 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
|||
}
|
||||
refreshController.refreshCompleted();
|
||||
},
|
||||
child: Column(children: [
|
||||
child: Column(
|
||||
children: [
|
||||
_buildErrors(),
|
||||
_buildConfig(),
|
||||
site.connected ? _buildHosts() : Container(),
|
||||
_buildSiteDetails(),
|
||||
_buildDelete(),
|
||||
]));
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrors() {
|
||||
if (site.errors.length == 0) {
|
||||
if (site.errors.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
List<Widget> items = [];
|
||||
site.errors.forEach((error) {
|
||||
items.add(ConfigItem(
|
||||
labelWidth: 0, content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error))));
|
||||
});
|
||||
for (var error in site.errors) {
|
||||
items.add(
|
||||
ConfigItem(
|
||||
labelWidth: 0,
|
||||
content: Padding(padding: EdgeInsets.symmetric(vertical: 10), child: SelectableText(error)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ConfigSection(
|
||||
label: 'ERRORS',
|
||||
|
@ -132,18 +135,7 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
|||
}
|
||||
|
||||
Widget _buildConfig() {
|
||||
return ConfigSection(children: <Widget>[
|
||||
ConfigItem(
|
||||
label: Text('Status'),
|
||||
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)))),
|
||||
Switch.adaptive(
|
||||
value: widget.site.connected,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: (v) async {
|
||||
void handleChange(v) async {
|
||||
try {
|
||||
if (v) {
|
||||
await widget.site.start();
|
||||
|
@ -154,9 +146,30 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
|||
var action = v ? 'start' : 'stop';
|
||||
Utils.popError(context, 'Failed to $action the site', error.toString());
|
||||
}
|
||||
},
|
||||
)
|
||||
])),
|
||||
}
|
||||
|
||||
return ConfigSection(
|
||||
children: <Widget>[
|
||||
ConfigItem(
|
||||
label: Text('Status'),
|
||||
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)),
|
||||
),
|
||||
),
|
||||
Switch.adaptive(
|
||||
value: widget.site.connected,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
onChanged: widget.site.errors.isNotEmpty && !widget.site.connected ? null : handleChange,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ConfigPageItem(
|
||||
label: Text('Logs'),
|
||||
onPressed: () {
|
||||
|
@ -165,7 +178,8 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
|||
});
|
||||
},
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHosts() {
|
||||
|
@ -202,10 +216,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;
|
||||
|
@ -222,16 +238,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'),
|
||||
|
@ -248,7 +267,8 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
|||
});
|
||||
},
|
||||
),
|
||||
]);
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDelete() {
|
||||
|
@ -256,14 +276,17 @@ class _SiteDetailScreenState extends State<SiteDetailScreen> {
|
|||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: PlatformElevatedButton(
|
||||
child: DangerButton(
|
||||
child: Text('Delete'),
|
||||
color: CupertinoColors.systemRed.resolveFrom(context),
|
||||
onPressed: () => Utils.confirmDelete(context, 'Delete Site?', () async {
|
||||
onPressed:
|
||||
() => Utils.confirmDelete(context, 'Delete Site?', () async {
|
||||
if (await _deleteSite()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}))));
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_listHostmap() async {
|
||||
|
|
|
@ -3,16 +3,19 @@ import 'dart:io';
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:mobile_nebula/components/SimplePage.dart';
|
||||
import 'package:mobile_nebula/models/Site.dart';
|
||||
import 'package:mobile_nebula/services/logs.dart';
|
||||
import 'package:mobile_nebula/services/result.dart';
|
||||
import 'package:mobile_nebula/services/settings.dart';
|
||||
import 'package:mobile_nebula/services/share.dart';
|
||||
import 'package:mobile_nebula/services/utils.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../components/SiteTitle.dart';
|
||||
|
||||
class SiteLogsScreen extends StatefulWidget {
|
||||
const SiteLogsScreen({Key? key, required this.site}) : super(key: key);
|
||||
const SiteLogsScreen({super.key, required this.site});
|
||||
|
||||
final Site site;
|
||||
|
||||
|
@ -21,14 +24,14 @@ class SiteLogsScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _SiteLogsScreenState extends State<SiteLogsScreen> {
|
||||
String logs = '';
|
||||
ScrollController controller = ScrollController();
|
||||
RefreshController refreshController = RefreshController(initialRefresh: false);
|
||||
final ScrollController controller = ScrollController();
|
||||
final RefreshController refreshController = RefreshController(initialRefresh: false);
|
||||
final LogsNotifier logsNotifier = LogsNotifier();
|
||||
|
||||
var settings = Settings();
|
||||
@override
|
||||
void initState() {
|
||||
loadLogs();
|
||||
logsNotifier.loadLogs(logFile: widget.site.logFile);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
|
@ -40,93 +43,119 @@ class _SiteLogsScreenState extends State<SiteLogsScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dnIcon =
|
||||
Theme.of(context).brightness == Brightness.dark ? 'images/dn-logo-dark.svg' : 'images/dn-logo-light.svg';
|
||||
final title = Row(children: [
|
||||
widget.site.managed
|
||||
? Padding(padding: EdgeInsets.only(right: 10), child: SvgPicture.asset(dnIcon, width: 12))
|
||||
: Container(),
|
||||
Expanded(child: Text(widget.site.name, style: TextStyle(fontWeight: FontWeight.bold)))
|
||||
]);
|
||||
final title = SiteTitle(site: widget.site);
|
||||
|
||||
return SimplePage(
|
||||
title: title,
|
||||
trailingActions: [Padding(padding: const EdgeInsets.only(right: 8), child: _buildTextWrapToggle())],
|
||||
scrollable: SimpleScrollable.both,
|
||||
scrollController: controller,
|
||||
onRefresh: () async {
|
||||
await loadLogs();
|
||||
await logsNotifier.loadLogs(logFile: widget.site.logFile);
|
||||
refreshController.refreshCompleted();
|
||||
},
|
||||
onLoading: () async {
|
||||
await loadLogs();
|
||||
await logsNotifier.loadLogs(logFile: widget.site.logFile);
|
||||
refreshController.loadComplete();
|
||||
},
|
||||
refreshController: refreshController,
|
||||
bottomBar: _buildBottomBar(),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(5),
|
||||
constraints: logBoxConstraints(context),
|
||||
child: SelectableText(logs.trim(), style: TextStyle(fontFamily: 'RobotoMono', fontSize: 14))),
|
||||
bottomBar: _buildBottomBar(),
|
||||
child: ListenableBuilder(
|
||||
listenable: logsNotifier,
|
||||
builder:
|
||||
(context, child) => SelectableText(switch (logsNotifier.logsResult) {
|
||||
Ok<String>(:var value) => value.trim(),
|
||||
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)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextWrapToggle() {
|
||||
return Platform.isIOS
|
||||
? Tooltip(
|
||||
message: "Turn ${settings.logWrap ? "off" : "on"} text wrapping",
|
||||
child: CupertinoButton.tinted(
|
||||
// Use the default tint when enabled, match the background when not.
|
||||
color: settings.logWrap ? null : CupertinoColors.systemBackground,
|
||||
sizeStyle: CupertinoButtonSize.small,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: const Icon(Icons.wrap_text),
|
||||
onPressed:
|
||||
() => {
|
||||
setState(() {
|
||||
settings.logWrap = !settings.logWrap;
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
: IconButton.filledTonal(
|
||||
isSelected: settings.logWrap,
|
||||
tooltip: "Turn ${settings.logWrap ? "off" : "on"} text wrapping",
|
||||
// The variants of wrap_text seem to be the same, but this seems most correct.
|
||||
selectedIcon: const Icon(Icons.wrap_text_outlined),
|
||||
icon: const Icon(Icons.wrap_text),
|
||||
onPressed:
|
||||
() => {
|
||||
setState(() {
|
||||
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);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(top: borderSide),
|
||||
),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
|
||||
Expanded(child: Builder(builder: (BuildContext context) {
|
||||
return PlatformIconButton(
|
||||
padding: padding,
|
||||
icon: Icon(context.platformIcons.share, size: 30),
|
||||
return PlatformWidgetBuilder(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
spacing: 8,
|
||||
children: <Widget>[
|
||||
Tooltip(
|
||||
message: "Share logs",
|
||||
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',
|
||||
);
|
||||
})),
|
||||
Expanded(
|
||||
child: PlatformIconButton(
|
||||
padding: padding,
|
||||
icon: Icon(context.platformIcons.downArrow, size: 30),
|
||||
onPressed: () async {
|
||||
controller.animateTo(controller.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 500), curve: Curves.linearToEaseOut);
|
||||
},
|
||||
)),
|
||||
]));
|
||||
}
|
||||
|
||||
loadLogs() async {
|
||||
var file = File(widget.site.logFile);
|
||||
try {
|
||||
final v = await file.readAsString();
|
||||
|
||||
setState(() {
|
||||
logs = v;
|
||||
});
|
||||
} on FileSystemException {
|
||||
Utils.popError(context, 'Error while reading logs', 'No log file was present');
|
||||
} catch (err) {
|
||||
Utils.popError(context, 'Error while reading logs', err.toString());
|
||||
}
|
||||
}
|
||||
|
||||
deleteLogs() async {
|
||||
var file = File(widget.site.logFile);
|
||||
await file.writeAsBytes([]);
|
||||
await loadLogs();
|
||||
),
|
||||
),
|
||||
Tooltip(
|
||||
message: 'Go to latest',
|
||||
child: PlatformIconButton(
|
||||
icon: Icon(context.platformIcons.downArrow),
|
||||
onPressed: () async {
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
logBoxConstraints(BuildContext context) {
|
||||
|
|
|
@ -11,13 +11,13 @@ import 'package:pull_to_refresh/pull_to_refresh.dart';
|
|||
|
||||
class SiteTunnelsScreen extends StatefulWidget {
|
||||
const SiteTunnelsScreen({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.site,
|
||||
required this.tunnels,
|
||||
required this.pending,
|
||||
required this.onChanged,
|
||||
required this.supportsQRScanning,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final Site site;
|
||||
final List<HostInfo> tunnels;
|
||||
|
@ -53,19 +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;
|
||||
|
||||
List<Widget> children = [];
|
||||
tunnels.forEach((hostInfo) {
|
||||
Widget icon;
|
||||
|
||||
final List<ConfigPageItem> children =
|
||||
tunnels.map((hostInfo) {
|
||||
final isLh = site.staticHostmap[hostInfo.vpnIp]?.lighthouse ?? false;
|
||||
if (isLh) {
|
||||
icon = Icon(Icons.lightbulb_outline, color: CupertinoColors.placeholderText.resolveFrom(context));
|
||||
} else {
|
||||
icon = Icon(Icons.computer, color: CupertinoColors.placeholderText.resolveFrom(context));
|
||||
}
|
||||
final icon = switch (isLh) {
|
||||
true => Icon(Icons.lightbulb_outline, color: CupertinoColors.placeholderText.resolveFrom(context)),
|
||||
false => Icon(Icons.computer, color: CupertinoColors.placeholderText.resolveFrom(context)),
|
||||
};
|
||||
|
||||
children.add(ConfigPageItem(
|
||||
onPressed: () => Utils.openPage(
|
||||
return (ConfigPageItem(
|
||||
onPressed:
|
||||
() => Utils.openPage(
|
||||
context,
|
||||
(context) => HostInfoScreen(
|
||||
isLighthouse: isLh,
|
||||
|
@ -78,32 +76,30 @@ 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(padding: EdgeInsets.only(right: 10), child: icon), Text(hostInfo.vpnIp)],
|
||||
),
|
||||
labelWidth: ipWidth,
|
||||
content: Container(alignment: Alignment.centerRight, child: Text(hostInfo.cert?.details.name ?? "")),
|
||||
));
|
||||
});
|
||||
}).toList();
|
||||
|
||||
Widget child;
|
||||
if (children.length == 0) {
|
||||
child = Center(child: Padding(child: Text('No tunnels to show'), padding: EdgeInsets.only(top: 30)));
|
||||
} else {
|
||||
child = ConfigSection(children: children);
|
||||
}
|
||||
final Widget child = switch (children.length) {
|
||||
0 => Center(child: Padding(padding: EdgeInsets.only(top: 30), child: Text('No tunnels to show'))),
|
||||
_ => ConfigSection(children: children),
|
||||
};
|
||||
|
||||
final title = widget.pending ? 'Pending' : 'Active';
|
||||
|
||||
return SimplePage(
|
||||
title: Text('$title Tunnels'),
|
||||
leadingAction: Utils.leadingBackWidget(context, onPressed: () {
|
||||
Navigator.pop(context);
|
||||
}),
|
||||
refreshController: refreshController,
|
||||
onRefresh: () async {
|
||||
await _listHostmap();
|
||||
refreshController.refreshCompleted();
|
||||
},
|
||||
child: child);
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
_sortTunnels() {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
@ -15,6 +14,7 @@ import 'package:mobile_nebula/screens/siteConfig/ScanQRScreen.dart';
|
|||
import 'package:mobile_nebula/services/share.dart';
|
||||
import 'package:mobile_nebula/services/utils.dart';
|
||||
|
||||
import '../../components/buttons/PrimaryButton.dart';
|
||||
import 'CertificateDetailsScreen.dart';
|
||||
|
||||
class CertificateResult {
|
||||
|
@ -26,13 +26,13 @@ class CertificateResult {
|
|||
|
||||
class AddCertificateScreen extends StatefulWidget {
|
||||
const AddCertificateScreen({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.onSave,
|
||||
this.onReplace,
|
||||
required this.pubKey,
|
||||
required this.privKey,
|
||||
required this.supportsQRScanning,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
// onSave will pop a new CertificateDetailsScreen.
|
||||
// If onSave is null, onReplace must be set.
|
||||
|
@ -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') {
|
||||
|
@ -152,21 +155,22 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
|||
padding: EdgeInsets.only(top: 20, bottom: 10, left: 10, right: 10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: PlatformElevatedButton(
|
||||
child: PrimaryButton(
|
||||
child: Text('Show/Import Private Key'),
|
||||
color: CupertinoColors.secondaryLabel.resolveFrom(context),
|
||||
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))],
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -174,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);
|
||||
}),
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -205,9 +207,10 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
|||
} catch (err) {
|
||||
return Utils.popError(context, 'Failed to load certificate file', err.toString());
|
||||
}
|
||||
})
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -220,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) => ScanQRScreen()),
|
||||
);
|
||||
if (result != null) {
|
||||
_addCertEntry(result);
|
||||
|
@ -231,7 +231,7 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
|||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -245,21 +245,29 @@ class _AddCertificateScreenState extends State<AddCertificateScreen> {
|
|||
var rawCerts = await platform.invokeMethod("nebula.parseCerts", <String, String>{"certs": rawCert});
|
||||
|
||||
List<dynamic> certs = jsonDecode(rawCerts);
|
||||
if (certs.length > 0) {
|
||||
if (certs.isNotEmpty) {
|
||||
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) {
|
||||
|
|
|
@ -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({super.key, required this.site, required this.onSave});
|
||||
|
||||
final Site site;
|
||||
final ValueChanged<Advanced> onSave;
|
||||
|
@ -79,15 +75,17 @@ 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
|
||||
? Text(settings.lhDuration.toString() + " seconds", textAlign: TextAlign.right)
|
||||
content:
|
||||
widget.site.managed
|
||||
? Text("${settings.lhDuration} seconds", textAlign: TextAlign.right)
|
||||
: PlatformTextFormField(
|
||||
initialValue: settings.lhDuration.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
|
@ -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());
|
||||
}
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
]));
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({super.key, required this.cas, this.onSave, required this.supportsQRScanning});
|
||||
|
||||
final List<CertificateInfo> cas;
|
||||
final ValueChanged<List<CertificateInfo>>? onSave;
|
||||
|
@ -44,9 +39,9 @@ class _CAListScreenState extends State<CAListScreen> {
|
|||
|
||||
@override
|
||||
void initState() {
|
||||
widget.cas.forEach((ca) {
|
||||
for (var ca in widget.cas) {
|
||||
cas[ca.cert.fingerprint] = ca;
|
||||
});
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
@ -56,7 +51,7 @@ class _CAListScreenState extends State<CAListScreen> {
|
|||
List<Widget> items = [];
|
||||
final caItems = _buildCAs();
|
||||
|
||||
if (caItems.length > 0) {
|
||||
if (caItems.isNotEmpty) {
|
||||
items.add(ConfigSection(children: caItems));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -114,14 +115,14 @@ class _CAListScreenState extends State<CAListScreen> {
|
|||
var ignored = 0;
|
||||
|
||||
List<dynamic> certs = jsonDecode(rawCerts);
|
||||
certs.forEach((rawCert) {
|
||||
for (var rawCert in certs) {
|
||||
final info = CertificateInfo.fromJson(rawCert);
|
||||
if (!info.cert.details.isCa) {
|
||||
ignored++;
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
cas[info.cert.fingerprint] = info;
|
||||
});
|
||||
}
|
||||
|
||||
if (ignored > 0) {
|
||||
error = 'One or more certificates were ignored because they were not certificate authorities.';
|
||||
|
@ -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) => ScanQRScreen()),
|
||||
);
|
||||
if (result != null) {
|
||||
_addCAEntry(result, (err) {
|
||||
|
@ -253,9 +248,9 @@ class _CAListScreenState extends State<CAListScreen> {
|
|||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
)
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
import 'package:mobile_nebula/components/DangerButton.dart';
|
||||
import 'package:mobile_nebula/components/FormPage.dart';
|
||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||
|
@ -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)
|
||||
class CertificateDetailsScreen extends StatefulWidget {
|
||||
const CertificateDetailsScreen({
|
||||
Key? key,
|
||||
super.key,
|
||||
required this.certInfo,
|
||||
this.onDelete,
|
||||
this.onSave,
|
||||
|
@ -19,7 +19,7 @@ class CertificateDetailsScreen extends StatefulWidget {
|
|||
this.pubKey,
|
||||
this.privKey,
|
||||
required this.supportsQRScanning,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
final CertificateInfo certInfo;
|
||||
|
||||
|
@ -76,58 +76,63 @@ 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()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters() {
|
||||
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(', '))));
|
||||
}
|
||||
|
||||
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(', '))));
|
||||
}
|
||||
|
||||
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(', '))));
|
||||
}
|
||||
|
||||
return items.length > 0
|
||||
return items.isNotEmpty
|
||||
? ConfigSection(label: certInfo.cert.details.isCa ? 'FILTERS' : 'DETAILS', children: items)
|
||||
: Container();
|
||||
}
|
||||
|
@ -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(),
|
||||
],
|
||||
);
|
||||
|
@ -164,9 +173,8 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
|||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: PlatformElevatedButton(
|
||||
child: DangerButton(
|
||||
child: Text('Replace certificate'),
|
||||
color: CupertinoColors.systemRed.resolveFrom(context),
|
||||
onPressed: () {
|
||||
Utils.openPage(context, (context) {
|
||||
return AddCertificateScreen(
|
||||
|
@ -177,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() {
|
||||
|
@ -199,12 +209,15 @@ class _CertificateDetailsScreenState extends State<CertificateDetailsScreen> {
|
|||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: PlatformElevatedButton(
|
||||
child: DangerButton(
|
||||
child: Text('Delete'),
|
||||
color: CupertinoColors.systemRed.resolveFrom(context),
|
||||
onPressed: () => Utils.confirmDelete(context, title, () async {
|
||||
onPressed:
|
||||
() => Utils.confirmDelete(context, title, () async {
|
||||
Navigator.pop(context);
|
||||
widget.onDelete!();
|
||||
}))));
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,11 +6,7 @@ import 'package:mobile_nebula/components/config/ConfigCheckboxItem.dart';
|
|||
import 'package:mobile_nebula/components/config/ConfigSection.dart';
|
||||
|
||||
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 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";
|
||||
});
|
||||
},
|
||||
)
|
||||
])
|
||||
),
|
||||
],
|
||||
));
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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({super.key, required this.verbosity, required this.onSave});
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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);
|
||||
const RenderedConfigScreen({super.key, required this.config, required this.name});
|
||||
|
||||
@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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class ScanQRScreen extends StatefulWidget {
|
||||
const ScanQRScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ScanQRScreen> createState() => _ScanQRScreenState();
|
||||
}
|
||||
|
@ -14,16 +16,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 +36,8 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
|
|||
Navigator.pop(context, barcode.rawValue);
|
||||
});
|
||||
}
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: cameraController,
|
||||
|
@ -45,9 +46,7 @@ class _ScanQRScreenState extends State<ScanQRScreen> {
|
|||
return const SizedBox();
|
||||
}
|
||||
|
||||
return CustomPaint(
|
||||
painter: ScannerOverlay(scanWindow: scanWindow),
|
||||
);
|
||||
return CustomPaint(painter: ScannerOverlay(scanWindow: scanWindow));
|
||||
},
|
||||
),
|
||||
Align(
|
||||
|
@ -63,15 +62,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 +79,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 +90,16 @@ class ScannerOverlay extends CustomPainter {
|
|||
),
|
||||
);
|
||||
|
||||
final backgroundPaint = Paint()
|
||||
..color = Colors.black.withOpacity(0.5)
|
||||
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 +210,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));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -23,12 +23,7 @@ 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({super.key, this.site, required this.onSave, required this.supportsQRScanning});
|
||||
|
||||
final Site? site;
|
||||
|
||||
|
@ -71,9 +66,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,13 +97,14 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
_managed(),
|
||||
kDebugMode ? _debugConfig() : Container(height: 0),
|
||||
],
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _debugConfig() {
|
||||
var data = "";
|
||||
try {
|
||||
final encoder = new JsonEncoder.withIndent(' ');
|
||||
final encoder = JsonEncoder.withIndent(' ');
|
||||
data = encoder.convert(site);
|
||||
} catch (err) {
|
||||
data = err.toString();
|
||||
|
@ -116,7 +114,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
}
|
||||
|
||||
Widget _main() {
|
||||
return ConfigSection(children: <Widget>[
|
||||
return ConfigSection(
|
||||
children: <Widget>[
|
||||
ConfigItem(
|
||||
label: Text("Name"),
|
||||
content: PlatformTextFormField(
|
||||
|
@ -128,8 +127,10 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
}
|
||||
return null;
|
||||
},
|
||||
))
|
||||
]);
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _managed() {
|
||||
|
@ -140,15 +141,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();
|
||||
}
|
||||
|
||||
|
@ -156,13 +161,13 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
final certError = site.certInfo == null || site.certInfo!.validity == null || !site.certInfo!.validity!.valid;
|
||||
var caError = false;
|
||||
if (!site.managed) {
|
||||
caError = site.ca.length == 0;
|
||||
caError = site.ca.isEmpty;
|
||||
if (!caError) {
|
||||
site.ca.forEach((ca) {
|
||||
for (var ca in site.ca) {
|
||||
if (ca.validity == null || !ca.validity!.valid) {
|
||||
caError = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,14 +176,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(
|
||||
padding: EdgeInsets.only(right: 5),
|
||||
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
||||
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 +196,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
certInfo: site.certInfo!,
|
||||
pubKey: pubKey,
|
||||
privKey: privKey,
|
||||
onReplace: site.managed
|
||||
onReplace:
|
||||
site.managed
|
||||
? null
|
||||
: (result) {
|
||||
setState(() {
|
||||
|
@ -216,20 +227,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(
|
||||
padding: EdgeInsets.only(right: 5),
|
||||
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
||||
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 +256,8 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
supportsQRScanning: widget.supportsQRScanning,
|
||||
);
|
||||
});
|
||||
})
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -251,28 +268,35 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
children: <Widget>[
|
||||
ConfigPageItem(
|
||||
label: Text('Hosts'),
|
||||
content: Wrap(alignment: WrapAlignment.end, crossAxisAlignment: WrapCrossAlignment.center, children: <Widget>[
|
||||
site.staticHostmap.length == 0
|
||||
content: Wrap(
|
||||
alignment: WrapAlignment.end,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: <Widget>[
|
||||
site.staticHostmap.isEmpty
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(right: 5),
|
||||
child: Icon(Icons.error, color: CupertinoColors.systemRed.resolveFrom(context), size: 20),
|
||||
padding: EdgeInsets.only(right: 5))
|
||||
)
|
||||
: Container(),
|
||||
site.staticHostmap.length == 0
|
||||
site.staticHostmap.isEmpty
|
||||
? 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 +324,11 @@ class _SiteConfigScreenState extends State<SiteConfigScreen> {
|
|||
site.unsafeRoutes = settings.unsafeRoutes;
|
||||
site.mtu = settings.mtu;
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
})
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
import 'package:mobile_nebula/components/DangerButton.dart';
|
||||
import 'package:mobile_nebula/components/FormPage.dart';
|
||||
import 'package:mobile_nebula/components/IPAndPortFormField.dart';
|
||||
import 'package:mobile_nebula/components/IPFormField.dart';
|
||||
|
@ -20,14 +21,13 @@ class _IPAndPort {
|
|||
|
||||
class StaticHostmapScreen extends StatefulWidget {
|
||||
StaticHostmapScreen({
|
||||
Key? key,
|
||||
super.key,
|
||||
this.nebulaIp = '',
|
||||
destinations,
|
||||
this.lighthouse = false,
|
||||
this.onDelete,
|
||||
required this.onSave,
|
||||
}) : this.destinations = destinations ?? [],
|
||||
super(key: key);
|
||||
}) : destinations = destinations ?? [];
|
||||
|
||||
final List<IPAndPort> destinations;
|
||||
final String nebulaIp;
|
||||
|
@ -50,11 +50,11 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
|
|||
_nebulaIp = widget.nebulaIp;
|
||||
_lighthouse = widget.lighthouse;
|
||||
_destinations = {};
|
||||
widget.destinations.forEach((dest) {
|
||||
for (var dest in widget.destinations) {
|
||||
_destinations[UniqueKey()] = _IPAndPort(focusNode: FocusNode(), destination: dest);
|
||||
});
|
||||
}
|
||||
|
||||
if (_destinations.length == 0) {
|
||||
if (_destinations.isEmpty) {
|
||||
_addDestination();
|
||||
}
|
||||
|
||||
|
@ -66,19 +66,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",
|
||||
|
@ -91,7 +96,9 @@ class _StaticHostmapScreenState extends State<StaticHostmapScreen> {
|
|||
if (v != null) {
|
||||
_nebulaIp = v;
|
||||
}
|
||||
})),
|
||||
},
|
||||
),
|
||||
),
|
||||
ConfigItem(
|
||||
label: Text('Lighthouse'),
|
||||
labelWidth: 200,
|
||||
|
@ -100,35 +107,40 @@ 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),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: PlatformElevatedButton(
|
||||
child: DangerButton(
|
||||
child: Text('Delete'),
|
||||
color: CupertinoColors.systemRed.resolveFrom(context),
|
||||
onPressed: () => Utils.confirmDelete(context, 'Delete host map?', () {
|
||||
onPressed:
|
||||
() => Utils.confirmDelete(context, 'Delete host map?', () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onDelete!();
|
||||
}),
|
||||
)))
|
||||
: Container()
|
||||
]));
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_onSave() {
|
||||
|
@ -148,23 +160,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',
|
||||
|
@ -177,18 +196,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;
|
||||
|
|
|
@ -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({super.key, required this.hostmap, required this.onSave});
|
||||
|
||||
final Map<String, StaticHost> hostmap;
|
||||
final ValueChanged<Map<String, StaticHost>>? onSave;
|
||||
|
@ -41,14 +32,18 @@ class StaticHostsScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _StaticHostsScreenState extends State<StaticHostsScreen> {
|
||||
Map<Key, _Hostmap> _hostmap = {};
|
||||
final Map<Key, _Hostmap> _hostmap = {};
|
||||
bool changed = false;
|
||||
|
||||
@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,23 +75,30 @@ 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)),
|
||||
padding: EdgeInsets.only(right: 10),
|
||||
child: Icon(
|
||||
host.lighthouse ? Icons.lightbulb_outline : Icons.computer,
|
||||
color: CupertinoColors.placeholderText.resolveFrom(context),
|
||||
),
|
||||
),
|
||||
Text(host.nebulaIp),
|
||||
]),
|
||||
],
|
||||
),
|
||||
labelWidth: ipWidth,
|
||||
content: Text(host.destinations.length.toString() + ' items', textAlign: TextAlign.end),
|
||||
content: Text('${host.destinations.length} items', textAlign: TextAlign.end),
|
||||
onPressed: () {
|
||||
Utils.openPage(context, (context) {
|
||||
return StaticHostmapScreen(
|
||||
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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
import 'package:mobile_nebula/components/CIDRFormField.dart';
|
||||
import 'package:mobile_nebula/components/DangerButton.dart';
|
||||
import 'package:mobile_nebula/components/FormPage.dart';
|
||||
import 'package:mobile_nebula/components/IPFormField.dart';
|
||||
import 'package:mobile_nebula/components/config/ConfigItem.dart';
|
||||
|
@ -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({super.key, required this.route, required this.onSave, this.onDelete});
|
||||
|
||||
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,38 +73,45 @@ 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'),
|
||||
// content: PlatformTextFormField(
|
||||
// placeholder: "",
|
||||
// validator: mtuValidator(false),
|
||||
// keyboardType: TextInputType.number,
|
||||
// inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
|
||||
// initialValue: route?.mtu.toString(),
|
||||
// textAlign: TextAlign.end,
|
||||
// textInputAction: TextInputAction.done,
|
||||
// focusNode: mtuFocus,
|
||||
// onSaved: (v) {
|
||||
// route.mtu = int.tryParse(v);
|
||||
// })),
|
||||
]),
|
||||
},
|
||||
),
|
||||
),
|
||||
//TODO: Android doesn't appear to support route based MTU, figure this out
|
||||
// ConfigItem(
|
||||
// label: Text('MTU'),
|
||||
// content: PlatformTextFormField(
|
||||
// placeholder: "",
|
||||
// validator: mtuValidator(false),
|
||||
// keyboardType: TextInputType.number,
|
||||
// inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
|
||||
// initialValue: route?.mtu.toString(),
|
||||
// textAlign: TextAlign.end,
|
||||
// textInputAction: TextInputAction.done,
|
||||
// focusNode: mtuFocus,
|
||||
// onSaved: (v) {
|
||||
// route.mtu = int.tryParse(v);
|
||||
// })),
|
||||
],
|
||||
),
|
||||
widget.onDelete != null
|
||||
? Padding(
|
||||
padding: EdgeInsets.only(top: 50, bottom: 10, left: 10, right: 10),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: PlatformElevatedButton(
|
||||
child: DangerButton(
|
||||
child: Text('Delete'),
|
||||
color: CupertinoColors.systemRed.resolveFrom(context),
|
||||
onPressed: () => Utils.confirmDelete(context, 'Delete unsafe route?', () {
|
||||
onPressed:
|
||||
() => Utils.confirmDelete(context, 'Delete unsafe route?', () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onDelete!();
|
||||
}),
|
||||
)))
|
||||
: Container()
|
||||
]));
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_onSave() {
|
||||
|
|
|
@ -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({super.key, required this.unsafeRoutes, required this.onSave});
|
||||
|
||||
final List<UnsafeRoute> unsafeRoutes;
|
||||
final ValueChanged<List<UnsafeRoute>>? onSave;
|
||||
|
@ -28,9 +24,9 @@ class _UnsafeRoutesScreenState extends State<UnsafeRoutesScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
unsafeRoutes = {};
|
||||
widget.unsafeRoutes.forEach((route) {
|
||||
for (var route in widget.unsafeRoutes) {
|
||||
unsafeRoutes[UniqueKey()] = route;
|
||||
});
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
@ -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;
|
||||
|
|
31
lib/services/logs.dart
Normal file
31
lib/services/logs.dart
Normal file
|
@ -0,0 +1,31 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:mobile_nebula/services/result.dart';
|
||||
|
||||
class LogsNotFoundException implements Exception {
|
||||
String error() => 'No logs found. Logs will be available after starting the site for the first time.';
|
||||
}
|
||||
|
||||
class LogsNotifier extends ChangeNotifier {
|
||||
Result<String>? logsResult;
|
||||
|
||||
LogsNotifier();
|
||||
|
||||
loadLogs({required String logFile}) async {
|
||||
final file = File(logFile);
|
||||
try {
|
||||
logsResult = Result.ok(await file.readAsString());
|
||||
notifyListeners();
|
||||
} on FileSystemException {
|
||||
logsResult = Result.error(LogsNotFoundException());
|
||||
notifyListeners();
|
||||
} on Exception catch (err) {
|
||||
logsResult = Result.error(err);
|
||||
notifyListeners();
|
||||
} catch (err) {
|
||||
logsResult = Result.error(Exception(err));
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
}
|
52
lib/services/result.dart
Normal file
52
lib/services/result.dart
Normal file
|
@ -0,0 +1,52 @@
|
|||
/// Utility class that simplifies handling errors.
|
||||
///
|
||||
/// Return a [Result] from a function to indicate success or failure.
|
||||
///
|
||||
/// A [Result] is either an [Ok] with a value of type [T]
|
||||
/// or an [Error] with an [Exception].
|
||||
///
|
||||
/// Use [Result.ok] to create a successful result with a value of type [T].
|
||||
/// Use [Result.error] to create an error result with an [Exception].
|
||||
///
|
||||
/// Evaluate the result using a switch statement:
|
||||
/// ```dart
|
||||
/// switch (result) {
|
||||
/// case Ok(): {
|
||||
/// print(result.value);
|
||||
/// }
|
||||
/// case Error(): {
|
||||
/// print(result.error);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
sealed class Result<T> {
|
||||
const Result();
|
||||
|
||||
/// Creates a successful [Result], completed with the specified [value].
|
||||
const factory Result.ok(T value) = Ok._;
|
||||
|
||||
/// Creates an error [Result], completed with the specified [error].
|
||||
const factory Result.error(Exception error) = Error._;
|
||||
}
|
||||
|
||||
/// A successful [Result] with a returned [value].
|
||||
final class Ok<T> extends Result<T> {
|
||||
const Ok._(this.value);
|
||||
|
||||
/// The returned value of this result.
|
||||
final T value;
|
||||
|
||||
@override
|
||||
String toString() => 'Result<$T>.ok($value)';
|
||||
}
|
||||
|
||||
/// An error [Result] with a resulting [error].
|
||||
final class Error<T> extends Result<T> {
|
||||
const Error._(this.error);
|
||||
|
||||
/// The resulting error of this result.
|
||||
final Exception error;
|
||||
|
||||
@override
|
||||
String toString() => 'Result<$T>.error($error)';
|
||||
}
|
|
@ -10,8 +10,8 @@ bool DEFAULT_TRACK_ERRORS = true;
|
|||
|
||||
class Settings {
|
||||
final _storage = Storage();
|
||||
StreamController _change = StreamController.broadcast();
|
||||
var _settings = Map<String, dynamic>();
|
||||
final StreamController _change = StreamController.broadcast();
|
||||
var _settings = <String, dynamic>{};
|
||||
|
||||
bool get useSystemColors {
|
||||
return _getBool('systemDarkMode', true);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
392
lib/services/theme.dart
Normal file
392
lib/services/theme.dart
Normal file
|
@ -0,0 +1,392 @@
|
|||
import "package:flutter/material.dart";
|
||||
|
||||
// Originally generated by https://material-foundation.github.io/material-theme-builder/
|
||||
// from a source color of #5D23DD
|
||||
|
||||
class MaterialTheme {
|
||||
final TextTheme textTheme;
|
||||
|
||||
const MaterialTheme(this.textTheme);
|
||||
|
||||
static ColorScheme lightScheme() {
|
||||
return const ColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primary: Color(4284700303),
|
||||
surfaceTint: Color(4284700303),
|
||||
onPrimary: Color(4294967295),
|
||||
primaryContainer: Color(4293451519),
|
||||
onPrimaryContainer: Color(4283121270),
|
||||
secondary: Color(4284570481),
|
||||
onSecondary: Color(4294967295),
|
||||
secondaryContainer: Color(4293385976),
|
||||
onSecondaryContainer: Color(4282991704),
|
||||
tertiary: Color(4286403169),
|
||||
onTertiary: Color(4294967295),
|
||||
tertiaryContainer: Color(4294957540),
|
||||
onTertiaryContainer: Color(4284693322),
|
||||
error: Color(4290386458),
|
||||
onError: Color(4294967295),
|
||||
errorContainer: Color(4294957782),
|
||||
onErrorContainer: Color(4287823882),
|
||||
surface: Color(4294834175),
|
||||
onSurface: Color(4280032032),
|
||||
onSurfaceVariant: Color(4282926414),
|
||||
outline: Color(4286150015),
|
||||
outlineVariant: Color(4291478735),
|
||||
shadow: Color(4278190080),
|
||||
scrim: Color(4278190080),
|
||||
inverseSurface: Color(4281478965),
|
||||
inversePrimary: Color(4291673599),
|
||||
primaryFixed: Color(4293451519),
|
||||
onPrimaryFixed: Color(4280225864),
|
||||
primaryFixedDim: Color(4291673599),
|
||||
onPrimaryFixedVariant: Color(4283121270),
|
||||
secondaryFixed: Color(4293385976),
|
||||
onSecondaryFixed: Color(4280097067),
|
||||
secondaryFixedDim: Color(4291544028),
|
||||
onSecondaryFixedVariant: Color(4282991704),
|
||||
tertiaryFixed: Color(4294957540),
|
||||
onTertiaryFixed: Color(4281405726),
|
||||
tertiaryFixedDim: Color(4293834953),
|
||||
onTertiaryFixedVariant: Color(4284693322),
|
||||
surfaceDim: Color(4292729056),
|
||||
surfaceBright: Color(4294834175),
|
||||
surfaceContainerLowest: Color(4294967295),
|
||||
surfaceContainerLow: Color(4294439674),
|
||||
surfaceContainer: Color(4294110452),
|
||||
surfaceContainerHigh: Color(4293715694),
|
||||
surfaceContainerHighest: Color(4293321193),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData light() {
|
||||
return theme(lightScheme());
|
||||
}
|
||||
|
||||
static ColorScheme lightMediumContrastScheme() {
|
||||
return const ColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primary: Color(4282002788),
|
||||
surfaceTint: Color(4284700303),
|
||||
onPrimary: Color(4294967295),
|
||||
primaryContainer: Color(4285686943),
|
||||
onPrimaryContainer: Color(4294967295),
|
||||
secondary: Color(4281873223),
|
||||
onSecondary: Color(4294967295),
|
||||
secondaryContainer: Color(4285557376),
|
||||
onSecondaryContainer: Color(4294967295),
|
||||
tertiary: Color(4283444025),
|
||||
onTertiary: Color(4294967295),
|
||||
tertiaryContainer: Color(4287455344),
|
||||
onTertiaryContainer: Color(4294967295),
|
||||
error: Color(4285792262),
|
||||
onError: Color(4294967295),
|
||||
errorContainer: Color(4291767335),
|
||||
onErrorContainer: Color(4294967295),
|
||||
surface: Color(4294834175),
|
||||
onSurface: Color(4279373846),
|
||||
onSurfaceVariant: Color(4281873725),
|
||||
outline: Color(4283715930),
|
||||
outlineVariant: Color(4285492085),
|
||||
shadow: Color(4278190080),
|
||||
scrim: Color(4278190080),
|
||||
inverseSurface: Color(4281478965),
|
||||
inversePrimary: Color(4291673599),
|
||||
primaryFixed: Color(4285686943),
|
||||
onPrimaryFixed: Color(4294967295),
|
||||
primaryFixedDim: Color(4284042373),
|
||||
onPrimaryFixedVariant: Color(4294967295),
|
||||
secondaryFixed: Color(4285557376),
|
||||
onSecondaryFixed: Color(4294967295),
|
||||
secondaryFixedDim: Color(4283912807),
|
||||
onSecondaryFixedVariant: Color(4294967295),
|
||||
tertiaryFixed: Color(4287455344),
|
||||
onTertiaryFixed: Color(4294967295),
|
||||
tertiaryFixedDim: Color(4285679960),
|
||||
onTertiaryFixedVariant: Color(4294967295),
|
||||
surfaceDim: Color(4291478989),
|
||||
surfaceBright: Color(4294834175),
|
||||
surfaceContainerLowest: Color(4294967295),
|
||||
surfaceContainerLow: Color(4294439674),
|
||||
surfaceContainer: Color(4293715694),
|
||||
surfaceContainerHigh: Color(4292926435),
|
||||
surfaceContainerHighest: Color(4292202712),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData lightMediumContrast() {
|
||||
return theme(lightMediumContrastScheme());
|
||||
}
|
||||
|
||||
static ColorScheme lightHighContrastScheme() {
|
||||
return const ColorScheme(
|
||||
brightness: Brightness.light,
|
||||
primary: Color(4281344857),
|
||||
surfaceTint: Color(4284700303),
|
||||
onPrimary: Color(4294967295),
|
||||
primaryContainer: Color(4283252856),
|
||||
onPrimaryContainer: Color(4294967295),
|
||||
secondary: Color(4281215292),
|
||||
onSecondary: Color(4294967295),
|
||||
secondaryContainer: Color(4283123291),
|
||||
onSecondaryContainer: Color(4294967295),
|
||||
tertiary: Color(4282655023),
|
||||
onTertiary: Color(4294967295),
|
||||
tertiaryContainer: Color(4284824908),
|
||||
onTertiaryContainer: Color(4294967295),
|
||||
error: Color(4284481540),
|
||||
onError: Color(4294967295),
|
||||
errorContainer: Color(4288151562),
|
||||
onErrorContainer: Color(4294967295),
|
||||
surface: Color(4294834175),
|
||||
onSurface: Color(4278190080),
|
||||
onSurfaceVariant: Color(4278190080),
|
||||
outline: Color(4281150259),
|
||||
outlineVariant: Color(4283123793),
|
||||
shadow: Color(4278190080),
|
||||
scrim: Color(4278190080),
|
||||
inverseSurface: Color(4281478965),
|
||||
inversePrimary: Color(4291673599),
|
||||
primaryFixed: Color(4283252856),
|
||||
onPrimaryFixed: Color(4294967295),
|
||||
primaryFixedDim: Color(4281739616),
|
||||
onPrimaryFixedVariant: Color(4294967295),
|
||||
secondaryFixed: Color(4283123291),
|
||||
onSecondaryFixed: Color(4294967295),
|
||||
secondaryFixedDim: Color(4281675843),
|
||||
onSecondaryFixedVariant: Color(4294967295),
|
||||
tertiaryFixed: Color(4284824908),
|
||||
onTertiaryFixed: Color(4294967295),
|
||||
tertiaryFixedDim: Color(4283180853),
|
||||
onTertiaryFixedVariant: Color(4294967295),
|
||||
surfaceDim: Color(4290557887),
|
||||
surfaceBright: Color(4294834175),
|
||||
surfaceContainerLowest: Color(4294967295),
|
||||
surfaceContainerLow: Color(4294242295),
|
||||
surfaceContainer: Color(4293321193),
|
||||
surfaceContainerHigh: Color(4292400090),
|
||||
surfaceContainerHighest: Color(4291478989),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData lightHighContrast() {
|
||||
return theme(lightHighContrastScheme());
|
||||
}
|
||||
|
||||
static ColorScheme darkScheme() {
|
||||
return const ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primary: Color(4291673599),
|
||||
surfaceTint: Color(4291673599),
|
||||
onPrimary: Color(4281608030),
|
||||
primaryContainer: Color(4283121270),
|
||||
onPrimaryContainer: Color(4293451519),
|
||||
secondary: Color(4291544028),
|
||||
onSecondary: Color(4281478721),
|
||||
secondaryContainer: Color(4282991704),
|
||||
onSecondaryContainer: Color(4293385976),
|
||||
tertiary: Color(4293834953),
|
||||
onTertiary: Color(4282983731),
|
||||
tertiaryContainer: Color(4284693322),
|
||||
onTertiaryContainer: Color(4294957540),
|
||||
error: Color(4294948011),
|
||||
onError: Color(4285071365),
|
||||
errorContainer: Color(4287823882),
|
||||
onErrorContainer: Color(4294957782),
|
||||
surface: Color(4279505688),
|
||||
onSurface: Color(4293321193),
|
||||
onSurfaceVariant: Color(4291478735),
|
||||
outline: Color(4287860633),
|
||||
outlineVariant: Color(4282926414),
|
||||
shadow: Color(4278190080),
|
||||
scrim: Color(4278190080),
|
||||
inverseSurface: Color(4293321193),
|
||||
inversePrimary: Color(4284700303),
|
||||
primaryFixed: Color(4293451519),
|
||||
onPrimaryFixed: Color(4280225864),
|
||||
primaryFixedDim: Color(4291673599),
|
||||
onPrimaryFixedVariant: Color(4283121270),
|
||||
secondaryFixed: Color(4293385976),
|
||||
onSecondaryFixed: Color(4280097067),
|
||||
secondaryFixedDim: Color(4291544028),
|
||||
onSecondaryFixedVariant: Color(4282991704),
|
||||
tertiaryFixed: Color(4294957540),
|
||||
onTertiaryFixed: Color(4281405726),
|
||||
tertiaryFixedDim: Color(4293834953),
|
||||
onTertiaryFixedVariant: Color(4284693322),
|
||||
surfaceDim: Color(4279505688),
|
||||
surfaceBright: Color(4282005566),
|
||||
surfaceContainerLowest: Color(4279176467),
|
||||
surfaceContainerLow: Color(4280032032),
|
||||
surfaceContainer: Color(4280295204),
|
||||
surfaceContainerHigh: Color(4281018671),
|
||||
surfaceContainerHighest: Color(4281742394),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData dark() {
|
||||
return theme(darkScheme());
|
||||
}
|
||||
|
||||
static ColorScheme darkMediumContrastScheme() {
|
||||
return const ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primary: Color(4293056255),
|
||||
surfaceTint: Color(4291673599),
|
||||
onPrimary: Color(4280884306),
|
||||
primaryContainer: Color(4288055493),
|
||||
onPrimaryContainer: Color(4278190080),
|
||||
secondary: Color(4292991218),
|
||||
onSecondary: Color(4280754998),
|
||||
secondaryContainer: Color(4287925669),
|
||||
onSecondaryContainer: Color(4278190080),
|
||||
tertiary: Color(4294955230),
|
||||
onTertiary: Color(4282194472),
|
||||
tertiaryContainer: Color(4290020244),
|
||||
onTertiaryContainer: Color(4278190080),
|
||||
error: Color(4294955724),
|
||||
onError: Color(4283695107),
|
||||
errorContainer: Color(4294923337),
|
||||
onErrorContainer: Color(4278190080),
|
||||
surface: Color(4279505688),
|
||||
onSurface: Color(4294967295),
|
||||
onSurfaceVariant: Color(4292926181),
|
||||
outline: Color(4290097339),
|
||||
outlineVariant: Color(4287860377),
|
||||
shadow: Color(4278190080),
|
||||
scrim: Color(4278190080),
|
||||
inverseSurface: Color(4293321193),
|
||||
inversePrimary: Color(4283187063),
|
||||
primaryFixed: Color(4293451519),
|
||||
onPrimaryFixed: Color(4279501629),
|
||||
primaryFixedDim: Color(4291673599),
|
||||
onPrimaryFixedVariant: Color(4282002788),
|
||||
secondaryFixed: Color(4293385976),
|
||||
onSecondaryFixed: Color(4279438880),
|
||||
secondaryFixedDim: Color(4291544028),
|
||||
onSecondaryFixedVariant: Color(4281873223),
|
||||
tertiaryFixed: Color(4294957540),
|
||||
onTertiaryFixed: Color(4280550932),
|
||||
tertiaryFixedDim: Color(4293834953),
|
||||
onTertiaryFixedVariant: Color(4283444025),
|
||||
surfaceDim: Color(4279505688),
|
||||
surfaceBright: Color(4282794826),
|
||||
surfaceContainerLowest: Color(4278716172),
|
||||
surfaceContainerLow: Color(4280163618),
|
||||
surfaceContainer: Color(4280887085),
|
||||
surfaceContainerHigh: Color(4281610808),
|
||||
surfaceContainerHighest: Color(4282334531),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData darkMediumContrast() {
|
||||
return theme(darkMediumContrastScheme());
|
||||
}
|
||||
|
||||
static ColorScheme darkHighContrastScheme() {
|
||||
return const ColorScheme(
|
||||
brightness: Brightness.dark,
|
||||
primary: Color(4294241791),
|
||||
surfaceTint: Color(4291673599),
|
||||
onPrimary: Color(4278190080),
|
||||
primaryContainer: Color(4291410427),
|
||||
onPrimaryContainer: Color(4279107636),
|
||||
secondary: Color(4294241791),
|
||||
onSecondary: Color(4278190080),
|
||||
secondaryContainer: Color(4291280856),
|
||||
onSecondaryContainer: Color(4279044122),
|
||||
tertiary: Color(4294962160),
|
||||
onTertiary: Color(4278190080),
|
||||
tertiaryContainer: Color(4293571782),
|
||||
onTertiaryContainer: Color(4280091150),
|
||||
error: Color(4294962409),
|
||||
onError: Color(4278190080),
|
||||
errorContainer: Color(4294946468),
|
||||
onErrorContainer: Color(4280418305),
|
||||
surface: Color(4279505688),
|
||||
onSurface: Color(4294967295),
|
||||
onSurfaceVariant: Color(4294967295),
|
||||
outline: Color(4294242041),
|
||||
outlineVariant: Color(4291215563),
|
||||
shadow: Color(4278190080),
|
||||
scrim: Color(4278190080),
|
||||
inverseSurface: Color(4293321193),
|
||||
inversePrimary: Color(4283187063),
|
||||
primaryFixed: Color(4293451519),
|
||||
onPrimaryFixed: Color(4278190080),
|
||||
primaryFixedDim: Color(4291673599),
|
||||
onPrimaryFixedVariant: Color(4279501629),
|
||||
secondaryFixed: Color(4293385976),
|
||||
onSecondaryFixed: Color(4278190080),
|
||||
secondaryFixedDim: Color(4291544028),
|
||||
onSecondaryFixedVariant: Color(4279438880),
|
||||
tertiaryFixed: Color(4294957540),
|
||||
onTertiaryFixed: Color(4278190080),
|
||||
tertiaryFixedDim: Color(4293834953),
|
||||
onTertiaryFixedVariant: Color(4280550932),
|
||||
surfaceDim: Color(4279505688),
|
||||
surfaceBright: Color(4283584341),
|
||||
surfaceContainerLowest: Color(4278190080),
|
||||
surfaceContainerLow: Color(4280295204),
|
||||
surfaceContainer: Color(4281478965),
|
||||
surfaceContainerHigh: Color(4282202689),
|
||||
surfaceContainerHighest: Color(4282926668),
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData darkHighContrast() {
|
||||
return theme(darkHighContrastScheme());
|
||||
}
|
||||
|
||||
ThemeData theme(ColorScheme colorScheme) => ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: colorScheme.brightness,
|
||||
colorScheme: colorScheme,
|
||||
textTheme: textTheme.apply(bodyColor: colorScheme.onSurface, displayColor: colorScheme.onSurface),
|
||||
scaffoldBackgroundColor: colorScheme.surface,
|
||||
canvasColor: colorScheme.surface,
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: Map<TargetPlatform, PageTransitionsBuilder>.fromIterable(
|
||||
TargetPlatform.values,
|
||||
value: (_) => const FadeForwardsPageTransitionsBuilder(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
List<ExtendedColor> get extendedColors => [];
|
||||
}
|
||||
|
||||
class ExtendedColor {
|
||||
final Color seed, value;
|
||||
final ColorFamily light;
|
||||
final ColorFamily lightHighContrast;
|
||||
final ColorFamily lightMediumContrast;
|
||||
final ColorFamily dark;
|
||||
final ColorFamily darkHighContrast;
|
||||
final ColorFamily darkMediumContrast;
|
||||
|
||||
const ExtendedColor({
|
||||
required this.seed,
|
||||
required this.value,
|
||||
required this.light,
|
||||
required this.lightHighContrast,
|
||||
required this.lightMediumContrast,
|
||||
required this.dark,
|
||||
required this.darkHighContrast,
|
||||
required this.darkMediumContrast,
|
||||
});
|
||||
}
|
||||
|
||||
class ColorFamily {
|
||||
const ColorFamily({
|
||||
required this.color,
|
||||
required this.onColor,
|
||||
required this.colorContainer,
|
||||
required this.onColorContainer,
|
||||
});
|
||||
|
||||
final Color color;
|
||||
final Color onColor;
|
||||
final Color colorContainer;
|
||||
final Color onColorContainer;
|
||||
}
|
|
@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class Utils {
|
||||
/// Minimum size (width or height) of a interactive component
|
||||
|
@ -26,37 +27,32 @@ 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"}) {
|
||||
static String itemCountFormat(int items, {String singleSuffix = "item", String multiSuffix = "items"}) {
|
||||
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.
|
||||
/// Provide your own onPressed to override that behavior, just remember you have to pop
|
||||
static Widget leadingBackWidget(BuildContext context, {label = 'Back', Function? onPressed}) {
|
||||
if (Platform.isIOS) {
|
||||
return CupertinoButton(
|
||||
child: Row(children: <Widget>[Icon(context.platformIcons.back), Text(label)]),
|
||||
padding: EdgeInsets.zero,
|
||||
return CupertinoNavigationBarBackButton(
|
||||
previousPageTitle: label,
|
||||
onPressed: () {
|
||||
if (onPressed == null) {
|
||||
Navigator.pop(context);
|
||||
|
@ -82,19 +78,21 @@ class Utils {
|
|||
}
|
||||
|
||||
static Widget trailingSaveWidget(BuildContext context, Function onPressed) {
|
||||
return CupertinoButton(
|
||||
child: Text('Save',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
//TODO: For some reason on android if inherit is the default of true the text color here turns to the background color
|
||||
inherit: Platform.isIOS ? true : false)),
|
||||
return PlatformTextButton(
|
||||
padding: Platform.isAndroid ? null : EdgeInsets.zero,
|
||||
onPressed: () => onPressed());
|
||||
onPressed: () => onPressed(),
|
||||
child: Text('Save'),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
|
@ -103,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();
|
||||
|
@ -116,10 +115,11 @@ class Utils {
|
|||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static popError(BuildContext context, String title, String error, {StackTrace? stack}) {
|
||||
|
@ -132,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(
|
||||
|
@ -151,10 +155,11 @@ class Utils {
|
|||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static launchUrl(String url, BuildContext context) async {
|
||||
|
@ -180,4 +185,19 @@ class Utils {
|
|||
final file = File(result.files.first.path!);
|
||||
return file.readAsString();
|
||||
}
|
||||
|
||||
static TextTheme createTextTheme(BuildContext context, String bodyFontString, String displayFontString) {
|
||||
TextTheme baseTextTheme = Theme.of(context).textTheme;
|
||||
TextTheme bodyTextTheme = GoogleFonts.getTextTheme(bodyFontString, baseTextTheme);
|
||||
TextTheme displayTextTheme = GoogleFonts.getTextTheme(displayFontString, baseTextTheme);
|
||||
TextTheme textTheme = displayTextTheme.copyWith(
|
||||
bodyLarge: bodyTextTheme.bodyLarge,
|
||||
bodyMedium: bodyTextTheme.bodyMedium,
|
||||
bodySmall: bodyTextTheme.bodySmall,
|
||||
labelLarge: bodyTextTheme.labelLarge,
|
||||
labelMedium: bodyTextTheme.labelMedium,
|
||||
labelSmall: bodyTextTheme.labelSmall,
|
||||
);
|
||||
return textTheme;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ bool dnsValidator(str, {requireTld = true, allowUnderscore = false}) {
|
|||
return false;
|
||||
}
|
||||
|
||||
List parts = str.split('.');
|
||||
List<String> parts = str.split('.');
|
||||
if (requireTld) {
|
||||
var tld = parts.removeLast();
|
||||
if (parts.isEmpty || !RegExp(r'^[a-z]{2,}$').hasMatch(tld)) {
|
||||
|
|
|
@ -10,7 +10,7 @@ require (
|
|||
github.com/DefinedNet/dnapi v0.0.0-20241212205635-1d1f0084d118
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/slackhq/nebula v1.9.5
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/crypto v0.32.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
|
@ -39,15 +39,16 @@ require (
|
|||
github.com/vishvananda/netlink v1.3.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/mobile v0.0.0-20241213221354-a87c1cf6cf46 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.32.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/slackhq/nebula => github.com/DefinedNet/nebula v1.7.0-pre5.0.20250507182031-48fbd1290b23
|
||||
|
|
|
@ -3,6 +3,8 @@ dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
|||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/DefinedNet/dnapi v0.0.0-20241212205635-1d1f0084d118 h1:UvJ/1ox4SC2iVeARN1p6ZfzyqMIHMDcXyHG6VP+Amo8=
|
||||
github.com/DefinedNet/dnapi v0.0.0-20241212205635-1d1f0084d118/go.mod h1:6LGHsBXaix2kyOPPrmGbzarKSedzR36P941h04+mkkM=
|
||||
github.com/DefinedNet/nebula v1.7.0-pre5.0.20250507182031-48fbd1290b23 h1:KKTX303FpfcECshrkTVZbN5PpuvwE1zz2p3mEM7+fUo=
|
||||
github.com/DefinedNet/nebula v1.7.0-pre5.0.20250507182031-48fbd1290b23/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
|
@ -131,8 +133,6 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
|
|||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/slackhq/nebula v1.9.5 h1:ZrxcvP/lxwFglaijmiwXLuCSkybZMJnqSYI1S8DtGnY=
|
||||
github.com/slackhq/nebula v1.9.5/go.mod h1:1+4q4wd3dDAjO8rKCttSb9JIVbklQhuJiBp5I0lbIsQ=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
@ -154,13 +154,11 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20241213221354-a87c1cf6cf46 h1:E+R1qmJL8cmWTyWXBHVtmqRxr7FdiTwntffsba1F1Tg=
|
||||
golang.org/x/mobile v0.0.0-20241213221354-a87c1cf6cf46/go.mod h1:Sf9LBimL0mWKEdgAjRmJ6iu7Z34osHQTK/devqFbM2I=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
|
@ -176,8 +174,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -203,11 +201,11 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -216,8 +214,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
136
pubspec.lock
136
pubspec.lock
|
@ -21,42 +21,42 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
|
||||
sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.0"
|
||||
version: "2.12.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: boolean_selector
|
||||
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
|
||||
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: clock
|
||||
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
|
||||
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -89,14 +89,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dart_pubspec_licenses:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_pubspec_licenses
|
||||
sha256: "23ddb78ff9204d08e3109ced67cd3c6c6a066f581b0edf5ee092fc3e1127f4ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fake_async
|
||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||
sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.3.2"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -134,6 +142,22 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_oss_licenses
|
||||
sha256: e4bbaeb00bc768e8430ee0c95ad304d2f256fb15194d30b912cea269871c8885
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
flutter_platform_widgets:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -164,7 +188,7 @@ packages:
|
|||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_web_plugins:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
|
@ -176,6 +200,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.1"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -208,22 +240,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
|
||||
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.7"
|
||||
version: "10.0.8"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
|
||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.8"
|
||||
version: "3.0.9"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -232,14 +272,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16+1"
|
||||
version: "0.12.17"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -252,10 +300,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
version: "1.16.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -289,13 +337,13 @@ packages:
|
|||
source: hosted
|
||||
version: "3.0.2"
|
||||
path:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -449,10 +497,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
version: "1.10.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -465,26 +513,26 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
|
||||
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
version: "1.12.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
version: "2.1.4"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_scanner
|
||||
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
|
||||
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.1"
|
||||
system_info2:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -497,18 +545,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: term_glyph
|
||||
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
|
||||
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
version: "1.2.2"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
|
||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.4"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -569,18 +617,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
|
||||
sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.4.0"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
|
||||
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "3.1.4"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -625,10 +673,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
|
||||
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.3.0"
|
||||
version: "14.3.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -670,5 +718,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.5.1 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
dart: ">=3.7.0 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
|
10
pubspec.yaml
10
pubspec.yaml
|
@ -14,11 +14,13 @@ description: Mobile Nebula Client
|
|||
version: 0.1.0+54
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.1
|
||||
sdk: ^3.7.0
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_web_plugins:
|
||||
sdk: flutter
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
|
@ -26,6 +28,7 @@ dependencies:
|
|||
flutter_platform_widgets: ^7.0.1
|
||||
path_provider: ^2.0.11
|
||||
file_picker: ^8.1.2
|
||||
google_fonts: ^6.2.1
|
||||
uuid: ^4.4.2
|
||||
package_info_plus: ^8.0.2
|
||||
url_launcher: ^6.1.6
|
||||
|
@ -36,18 +39,19 @@ dependencies:
|
|||
sentry_flutter: ^8.9.0
|
||||
sentry_dart_plugin: ^2.0.0
|
||||
mobile_scanner: ^7.0.0-beta.3
|
||||
path: ^1.9.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter_oss_licenses: ^3.0.4
|
||||
flutter_lints: ^5.0.0
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
# The following section is specific to Flutter.
|
||||
flutter:
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
# the material Icons class.
|
||||
|
|
50
swift-format.sh
Executable file
50
swift-format.sh
Executable file
|
@ -0,0 +1,50 @@
|
|||
#!/bin/bash
|
||||
##===----------------------------------------------------------------------===##
|
||||
##
|
||||
## This source file is part of the Swift.org open source project
|
||||
##
|
||||
## Copyright (c) 2024 Apple Inc. and the Swift project authors
|
||||
## Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
##
|
||||
## See https://swift.org/LICENSE.txt for license information
|
||||
## See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
##
|
||||
##===----------------------------------------------------------------------===##
|
||||
|
||||
# Vendored from <https://github.com/swiftlang/github-workflows/blob/main/.github/workflows/scripts/check-swift-format.sh> while <https://github.com/swiftlang/swift-format/issues/870> is open.
|
||||
|
||||
# This file has been modified to only check formatting, with no linting, and to require a `check` command flag to fail when formatting was performed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
log() { printf -- "** %s\n" "$*" >&2; }
|
||||
error() { printf -- "** ERROR: %s\n" "$*" >&2; }
|
||||
fatal() { error "$@"; exit 1; }
|
||||
|
||||
|
||||
if [[ -f .swiftformatignore ]]; then
|
||||
log "Found swiftformatignore file..."
|
||||
|
||||
log "Running swift format format..."
|
||||
tr '\n' '\0' < .swiftformatignore| xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 xcrun swift-format --parallel --recursive --in-place
|
||||
|
||||
# log "Running swift format lint..."
|
||||
|
||||
# tr '\n' '\0' < .swiftformatignore | xargs -0 -I% printf '":(exclude)%" '| xargs git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel
|
||||
else
|
||||
log "Running swift format format..."
|
||||
git ls-files -z '*.swift' | xargs -0 xcrun swift-format --parallel --recursive --in-place
|
||||
|
||||
# log "Running swift format lint..."
|
||||
|
||||
# git ls-files -z '*.swift' | xargs -0 swift format lint --strict --parallel
|
||||
fi
|
||||
|
||||
|
||||
if [ "${1-default}" = "check" ]; then
|
||||
log "Checking for modified files..."
|
||||
|
||||
GIT_PAGER='' git diff --exit-code '*.swift'
|
||||
|
||||
log "✅ Found no formatting issues."
|
||||
fi
|
Loading…
Add table
Reference in a new issue