diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..3031ac4 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,37 @@ +name: Lint & Test + +on: + push: + branches: [ '**' ] + pull_request: + branches: [ '**' ] + +jobs: + dart-lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: subosito/flutter-action@v2 + with: + flutter-version: '3.7.5' + channel: 'stable' + cache: true + - run: flutter --version + + - name: Install dependencies + run: dart pub get + + - name: Generate App Localizations + run: | + flutter clean + flutter gen-l10n + + - name: Get dependencies again after code-gen + run: flutter pub get + + - name: Verify formatting + run: dart format --output=none --set-exit-if-changed . + + - name: Analyze project source + run: dart analyze --fatal-infos diff --git a/.github/workflows/release-android.yml b/.github/workflows/release-android.yml new file mode 100644 index 0000000..1792863 --- /dev/null +++ b/.github/workflows/release-android.yml @@ -0,0 +1,47 @@ +name: Build Android +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + + workflow_dispatch: + +jobs: + release-android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: "12.x" + cache: 'gradle' + - uses: subosito/flutter-action@v2 + with: + flutter-version: "3.7.5" + channel: 'stable' + cache: true + - name: Decode Keystore + env: + KEY_JKS: ${{ secrets.KEY_JKS }} + run: echo $KEY_JKS | base64 -di > android/key.jks + - name: Get dependencies + run: flutter pub get + - name: Build Flutter (App Bundle) + env: + KEY_PASSWORD: ${{ secrets.ALIAS_PASSWORD }} + ALIAS_PASSWORD: ${{ secrets.KEY_PASSWORD }} + run: flutter build appbundle --release + - name: Build Flutter (APK) + env: + KEY_PASSWORD: ${{ secrets.ALIAS_PASSWORD }} + ALIAS_PASSWORD: ${{ secrets.KEY_PASSWORD }} + run: flutter build apk --release + - uses: actions/upload-artifact@v3 + with: + name: Android App Bundle + path: build/app/outputs/bundle/release + - uses: actions/upload-artifact@v3 + with: + name: Android APK + path: build/app/outputs/flutter-apk diff --git a/.github/workflows/release-ios.yml b/.github/workflows/release-ios.yml new file mode 100644 index 0000000..54ba130 --- /dev/null +++ b/.github/workflows/release-ios.yml @@ -0,0 +1,71 @@ +name: Build iOS +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + + workflow_dispatch: + +jobs: + release-ios: + runs-on: macos-latest + steps: + # Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it + - name: Checkout repository + uses: actions/checkout@v3 + + # Install the Apple certificate and provisioning profile + - name: Install Apple certificate and provisioning profile + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.APPSTORE_CERT_BASE64 }} + P12_PASSWORD: ${{ secrets.APPSTORE_CERT_PASSWORD }} + BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.MOBILEPROVISION_BASE64 }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + # import certificate and provisioning profile from secrets + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode --output $CERTIFICATE_PATH + echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode --output $PP_PATH + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + # apply provisioning profile + mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles + cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles + + # Install flutter + - name: Get Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.7.5" + channel: 'stable' + cache: true + - name: Get dependencies + run: flutter pub get + # Build and sign the ipa using a single flutter command + - name: Building IPA + run: flutter build ipa --release --export-options-plist=ios/ExportOptions.plist +# - name: Build Flutter +# run: flutter build ios --release --no-codesign + # Collect the file and upload as artifact + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: ios +# path: "build/ios/iphoneos/*.app" + # Path to the release files + path: build/ios/ipa + + # Important! Cleanup: remove the certificate and provisioning profile from the runner! + - name: Clean up keychain and provisioning profile + if: ${{ always() }} + run: | + security delete-keychain $RUNNER_TEMP/app-signing.keychain-db + rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24476c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..9637226 --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: c07f7888888435fd9df505aa2efc38d3cf65681b + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c07f7888888435fd9df505aa2efc38d3cf65681b + base_revision: c07f7888888435fd9df505aa2efc38d3cf65681b + - platform: android + create_revision: c07f7888888435fd9df505aa2efc38d3cf65681b + base_revision: c07f7888888435fd9df505aa2efc38d3cf65681b + - platform: ios + create_revision: c07f7888888435fd9df505aa2efc38d3cf65681b + base_revision: c07f7888888435fd9df505aa2efc38d3cf65681b + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..137d314 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 Henrik Herzig + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..385ea26 --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ + + +# SimpleWoL - Simple Wake on Lan + + + +

+ MIT License + Flutter + Dart + Google Play + App Store + Workflow Lint + GitHub Release +

+ +Simple Wake on Lan is a simple cross-platform flutter application for Android and iOS to send Wake On Lan packets to a +device. + +

+Get it on Google Play +    +Download on the App Store +

+ +## Usage +Wake on LAN (WoL) is a network protocol that allows a device to be turned on or awakened remotely +over a network while it is sleeping. This project aims to make the process of waking devices easy with a mobile application. + + + +## Screenshots + + +| | | +|:----------------------------------------:|:-----------------------------------:| +| ![play_integrity](docs/screenshot-1.png) | ![dark_mode](docs/screenshot-2.png) | + +| | | +|:----------------------------------:|:-------------------------------:| +| ![settings](docs/screenshot-3.png) | ![about](docs/screenshot-4.png) | + +## Features + +- Automatic device discovery +- Simple interface to send Wake On Lan packets +- Export and import user data as a `json` file (see below) + + +The app stores the added devices in a `json` file which can be exported and imported within the app UI. An example of the file structure is shown below: +```json +[ + { + "id": "6b353440-d183-11ed-964b-69a9facd6cfd", + "hostName": "Raspberry Pi", + "ipAddress": "192.168.1.9", + "macAddress": "12:12:12:12:12:12", + "wolPort": 9, + "deviceType": "computer", + "modified": "2023-04-14T14:17:45.974511" + }, + { + "id": "87c87ab0-d184-13ed-9d56-a5f550305985", + "hostName": "Printer", + "ipAddress": "192.168.1.10", + "macAddress": "f0:f0:f0:f0:f0:f0", + "wolPort": 9, + "deviceType": "printer", + "modified": "2023-04-14T14:18:14.997081" + } +] +``` + +## Download + +- You can download the latest version of the app from [GitHub Releases]() +- Download from the [PlayStore](https://play.google.com/store/apps/details?) +- Download from the [App Store](https://apps.apple.com/de/app/) + +## Architecture +The app is built using the [Flutter](https://flutter.dev/) framework. It uses the [Material 3](https://m3.material.io) design system from Google. + +## Build +To build the app yourself, you need to have the Flutter SDK installed. You can find the installation instructions [here](https://flutter.dev/docs/get-started/install). + +## License +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details + +```License +Copyright (c) 2023 Henrik Herzig + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` \ No newline at end of file diff --git a/TODOS.md b/TODOS.md new file mode 100644 index 0000000..db8bfb6 --- /dev/null +++ b/TODOS.md @@ -0,0 +1,5 @@ +- [ ] Appearance + - [x] Theme (System, Light, Dark) + - [ ] Color Palette (Auto (Material You on Android) or different color palettes) +- [ ] Version Notes +- [ ] Contact / About Developer Info \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,29 @@ +# 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-lang.github.io/linter/lints/index.html. + # + # 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/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..37c49de --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,78 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.henrikherzig.simplewol" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + signingConfigs { + release { + storeFile file("../key.jks") + storePassword = "$System.env.KEY_PASSWORD" + keyAlias = "release" + keyPassword = "$System.env.ALIAS_PASSWORD" + } + } + + buildTypes { + release { + signingConfig signingConfigs.release + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..1fd43a6 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..dfa6e7a --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/henrikherzig/simplewol/MainActivity.kt b/android/app/src/main/kotlin/com/henrikherzig/simplewol/MainActivity.kt new file mode 100644 index 0000000..af60621 --- /dev/null +++ b/android/app/src/main/kotlin/com/henrikherzig/simplewol/MainActivity.kt @@ -0,0 +1,6 @@ +package com.henrikherzig.simplewol + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..2b81f40 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..033e0c9 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..3f4c3d5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..55fd756 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..4dc927c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..1fd43a6 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..58a8c74 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.2.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3c472b9 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/assets/Icon.png b/assets/Icon.png new file mode 100644 index 0000000..5aa9855 Binary files /dev/null and b/assets/Icon.png differ diff --git a/assets/sample.json b/assets/sample.json new file mode 100644 index 0000000..ae90e4e --- /dev/null +++ b/assets/sample.json @@ -0,0 +1,20 @@ +[ + { + "id": "6b353440-d183-11ed-964b-69a9facd6cfd", + "hostName": "Raspberry Pi", + "ipAddress": "192.168.1.9", + "macAddress": "12:12:12:12:12:12", + "wolPort": 9, + "deviceType": "computer", + "modified": "2023-04-14T14:17:45.974511" + }, + { + "id": "87c87ab0-d184-13ed-9d56-a5f550305985", + "hostName": "Printer", + "ipAddress": "192.168.1.10", + "macAddress": "f0:f0:f0:f0:f0:f0", + "wolPort": 9, + "deviceType": "printer", + "modified": "2023-04-14T14:18:14.997081" + } +] \ No newline at end of file diff --git a/docs/appStore.svg b/docs/appStore.svg new file mode 100755 index 0000000..072b425 --- /dev/null +++ b/docs/appStore.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/googlePlay.png b/docs/googlePlay.png new file mode 100644 index 0000000..522dcb8 Binary files /dev/null and b/docs/googlePlay.png differ diff --git a/docs/icon.png b/docs/icon.png new file mode 100644 index 0000000..b791455 Binary files /dev/null and b/docs/icon.png differ diff --git a/docs/screenshot-1.png b/docs/screenshot-1.png new file mode 100644 index 0000000..8780cb4 Binary files /dev/null and b/docs/screenshot-1.png differ diff --git a/docs/screenshot-2.png b/docs/screenshot-2.png new file mode 100644 index 0000000..21eb466 Binary files /dev/null and b/docs/screenshot-2.png differ diff --git a/docs/screenshot-3.png b/docs/screenshot-3.png new file mode 100644 index 0000000..2bb7f5e Binary files /dev/null and b/docs/screenshot-3.png differ diff --git a/docs/screenshot-4.png b/docs/screenshot-4.png new file mode 100644 index 0000000..6bea6ae Binary files /dev/null and b/docs/screenshot-4.png differ diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/ExportOptions.plist b/ios/ExportOptions.plist new file mode 100644 index 0000000..c32cceb --- /dev/null +++ b/ios/ExportOptions.plist @@ -0,0 +1,15 @@ + + + + + method + app-store + teamID + 74U65NZ28B + provisioningProfiles + + com.henrikherzig.simplewol + Simple Wake On Lan + + + \ No newline at end of file diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..9625e10 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 11.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Network Extension/AppProxyProvider.swift b/ios/Network Extension/AppProxyProvider.swift new file mode 100644 index 0000000..120debc --- /dev/null +++ b/ios/Network Extension/AppProxyProvider.swift @@ -0,0 +1,41 @@ +// +// AppProxyProvider.swift +// Network Extension +// +// Created by Henrik Herzig on 01.04.23. +// + +import NetworkExtension + +class AppProxyProvider: NEAppProxyProvider { + + override func startProxy(options: [String : Any]? = nil, completionHandler: @escaping (Error?) -> Void) { + // Add code here to start the process of connecting the tunnel. + } + + override func stopProxy(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + // Add code here to start the process of stopping the tunnel. + completionHandler() + } + + override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { + // Add code here to handle the message. + if let handler = completionHandler { + handler(messageData) + } + } + + override func sleep(completionHandler: @escaping() -> Void) { + // Add code here to get ready to sleep. + completionHandler() + } + + override func wake() { + // Add code here to wake up. + } + + override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { + // Add code here to handle the incoming flow. + return false + } +} diff --git a/ios/Network Extension/Info.plist b/ios/Network Extension/Info.plist new file mode 100644 index 0000000..dd575d7 --- /dev/null +++ b/ios/Network Extension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.app-proxy + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).AppProxyProvider + + + diff --git a/ios/Network Extension/Network_Extension.entitlements b/ios/Network Extension/Network_Extension.entitlements new file mode 100644 index 0000000..430a36d --- /dev/null +++ b/ios/Network Extension/Network_Extension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.henrikherzig.simpleWakeOnLan + + + diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..88359b2 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..04035b9 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,107 @@ +PODS: + - DKImagePickerController/Core (4.3.4): + - DKImagePickerController/ImageDataManager + - DKImagePickerController/Resource + - DKImagePickerController/ImageDataManager (4.3.4) + - DKImagePickerController/PhotoGallery (4.3.4): + - DKImagePickerController/Core + - DKPhotoGallery + - DKImagePickerController/Resource (4.3.4) + - DKPhotoGallery (0.0.17): + - DKPhotoGallery/Core (= 0.0.17) + - DKPhotoGallery/Model (= 0.0.17) + - DKPhotoGallery/Preview (= 0.0.17) + - DKPhotoGallery/Resource (= 0.0.17) + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Core (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Preview + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Model (0.0.17): + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Preview (0.0.17): + - DKPhotoGallery/Model + - DKPhotoGallery/Resource + - SDWebImage + - SwiftyGif + - DKPhotoGallery/Resource (0.0.17): + - SDWebImage + - SwiftyGif + - file_picker (0.0.1): + - DKImagePickerController/PhotoGallery + - Flutter + - Flutter (1.0.0) + - flutter_icmp_ping (0.0.1): + - Flutter + - network_info_plus (0.0.1): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - SDWebImage (5.15.5): + - SDWebImage/Core (= 5.15.5) + - SDWebImage/Core (5.15.5) + - share_plus (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - SwiftyGif (5.4.4) + - url_launcher_ios (0.0.1): + - Flutter + +DEPENDENCIES: + - file_picker (from `.symlinks/plugins/file_picker/ios`) + - Flutter (from `Flutter`) + - flutter_icmp_ping (from `.symlinks/plugins/flutter_icmp_ping/ios`) + - network_info_plus (from `.symlinks/plugins/network_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - DKImagePickerController + - DKPhotoGallery + - SDWebImage + - SwiftyGif + +EXTERNAL SOURCES: + file_picker: + :path: ".symlinks/plugins/file_picker/ios" + Flutter: + :path: Flutter + flutter_icmp_ping: + :path: ".symlinks/plugins/flutter_icmp_ping/ios" + network_info_plus: + :path: ".symlinks/plugins/network_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac + DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_icmp_ping: 2b159955eee0c487c766ad83fec224ae35e7c935 + network_info_plus: b78876159360f5580608c2cea620d6ceffabd0ad + path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 + shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472 + SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + +PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3 + +COCOAPODS: 1.12.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..df422c9 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,608 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 008A58623530BEB33F01A6A7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A0E6F181C3D5658DF38F28D1 /* Pods_Runner.framework */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + AFC76A9929D873870052F12A /* ExternalAccessory.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFC76A9829D873870052F12A /* ExternalAccessory.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + AFC76AAD29D875ED0052F12A /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2697348831012BEA9624FE51 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4252D71ADFC4FB400F372E79 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 63BBEF64BD378CD68F4411C2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A0E6F181C3D5658DF38F28D1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AFC76A9729D873870052F12A /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + AFC76A9829D873870052F12A /* ExternalAccessory.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExternalAccessory.framework; path = System/Library/Frameworks/ExternalAccessory.framework; sourceTree = SDKROOT; }; + AFC76A9F29D875ED0052F12A /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; + AFC76AA229D875ED0052F12A /* AppProxyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppProxyProvider.swift; sourceTree = ""; }; + AFC76AA429D875ED0052F12A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AFC76AA529D875ED0052F12A /* Network_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Network_Extension.entitlements; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + AFC76A9929D873870052F12A /* ExternalAccessory.framework in Frameworks */, + 008A58623530BEB33F01A6A7 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + AFC76AA129D875ED0052F12A /* Network Extension */, + 97C146EF1CF9000F007C117D /* Products */, + B9B39F19858114B40F26BB7C /* Pods */, + D85299C5713ABFCD207DCB2E /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + AFC76A9729D873870052F12A /* Runner.entitlements */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + AFC76AA129D875ED0052F12A /* Network Extension */ = { + isa = PBXGroup; + children = ( + AFC76AA229D875ED0052F12A /* AppProxyProvider.swift */, + AFC76AA429D875ED0052F12A /* Info.plist */, + AFC76AA529D875ED0052F12A /* Network_Extension.entitlements */, + ); + path = "Network Extension"; + sourceTree = ""; + }; + B9B39F19858114B40F26BB7C /* Pods */ = { + isa = PBXGroup; + children = ( + 63BBEF64BD378CD68F4411C2 /* Pods-Runner.debug.xcconfig */, + 2697348831012BEA9624FE51 /* Pods-Runner.release.xcconfig */, + 4252D71ADFC4FB400F372E79 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + D85299C5713ABFCD207DCB2E /* Frameworks */ = { + isa = PBXGroup; + children = ( + AFC76A9829D873870052F12A /* ExternalAccessory.framework */, + A0E6F181C3D5658DF38F28D1 /* Pods_Runner.framework */, + AFC76A9F29D875ED0052F12A /* NetworkExtension.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 88134F77B7366D928C272ADB /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 38D12028D1F5EA61F9E75E1F /* [CP] Embed Pods Frameworks */, + AFC76AAD29D875ED0052F12A /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1340; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 38D12028D1F5EA61F9E75E1F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 88134F77B7366D928C272ADB /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 74U65NZ28B; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 74U65NZ28B; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.henrikherzig.simplewol; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Simple Wake On Lan"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Simple Wake On Lan"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 74U65NZ28B; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 74U65NZ28B; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.henrikherzig.simplewol; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Simple Wake On Lan"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Simple Wake On Lan"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 74U65NZ28B; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = 74U65NZ28B; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.henrikherzig.simplewol; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "Simple Wake On Lan"; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Simple Wake On Lan"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..1dd6241 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..628760a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..4b5bbcd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..d06086e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..e716ee6 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..5be6d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..26f8eeb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..4b5bbcd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..a591a2a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..437f784 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..07186f3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..a3f3ded Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..def281f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..115c821 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..437f784 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e016ece Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..2b81f40 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..55fd756 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..b1a3b0d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..aba96f9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..c3f63bb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..406c51a --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,53 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Simple Wake On Lan + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + simple_wake_on_lan + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..4e6692e --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart \ No newline at end of file diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..51e928f --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,153 @@ +import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:simple_wake_on_lan/widgets/chip_cards.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class AppConstants { + /// Navigation Bar Icons + static const homeIcon = Icons.home; + static const settingsIcon = Icons.settings; + static const aboutIcon = Icons.info; + + /// HomePage Elements + static const wakeUp = Icons.power_settings_new_outlined; + static const edit = Icons.mode_edit_outline_outlined; + static const macText = 'MAC'; + static const ipText = 'IP'; + static const add = Icon(Icons.add); + static const sort = Icon(Icons.sort); + + // Wake Up Dialog Elements + static const errorMessageColor = Colors.red; + static const successMessageColor = Colors.green; + static const infoMessageColor = Colors.black; + + // Discover Page Elements + static const addCustomDeviceType = 'desktop'; + + // Form Elements + static const formIcon = Icons.done_rounded; + static const nameValidationRegex = r'^.{1,200}$'; + static const ipValidationRegex = + r'\b((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}\b'; + static const macValidationRegex = + r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$'; + static const portValidationRegex = + r'^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'; + static const formWrongFormatIcon = Icons.assignment_outlined; + static const formInvalidArgument = Icons.cancel_outlined; + + // WOL Port Chips + List> getChipsWolPorts({BuildContext? context}) { + final List> chipsWolPorts = >[ + const CustomChoiceChip(value: 7), + const CustomChoiceChip(value: 9), + ]; + if (context != null) { + return chipsWolPorts + .map((e) => CustomChoiceChip( + label: "${AppLocalizations.of(context)!.formPort} ${e.value}", + value: e.value)) + .toList(); + } else { + return chipsWolPorts; + } + } + + // Icon chips + List> getChipsDeviceTypes({BuildContext? context}) { + return >[ + CustomChoiceChip( + label: context != null + ? AppLocalizations.of(context)!.deviceChoiceServer + : null, + icon: Icons.storage_rounded, + value: 'server'), + CustomChoiceChip( + label: context != null + ? AppLocalizations.of(context)!.deviceChoiceDesktop + : null, + icon: Icons.desktop_mac_rounded, + value: 'desktop'), + CustomChoiceChip( + label: context != null + ? AppLocalizations.of(context)!.deviceChoiceLaptop + : null, + icon: Icons.laptop_mac, + value: 'laptop'), + CustomChoiceChip( + label: context != null + ? AppLocalizations.of(context)!.deviceChoicePrinter + : null, + icon: Icons.print_rounded, + value: 'printer'), + CustomChoiceChip( + label: context != null + ? AppLocalizations.of(context)!.deviceChoiceNetwork + : null, + icon: Icons.lan_rounded, + value: 'network'), + CustomChoiceChip( + label: context != null + ? AppLocalizations.of(context)!.deviceChoiceIOT + : null, + icon: Icons.smart_toy, + value: 'iot'), + CustomChoiceChip( + label: context != null + ? AppLocalizations.of(context)!.deviceChoiceTv + : null, + icon: Icons.tv_rounded, + value: 'tv'), + CustomChoiceChip( + label: context != null + ? AppLocalizations.of(context)!.deviceChoiceMobile + : null, + icon: Icons.phone_iphone, + value: 'mobile'), + CustomChoiceChip( + label: context != null + ? AppLocalizations.of(context)!.deviceChoiceOther + : null, + icon: Icons.tune_rounded, + value: 'other'), + ]; + } + + // Theme Chips + List> getChipsTheme( + {required BuildContext context}) { + return >[ + CustomChoiceChip( + label: AppLocalizations.of(context)!.settingsThemeSelectorSystem, + icon: Icons.brightness_4_rounded, + value: AdaptiveThemeMode.system), + CustomChoiceChip( + label: AppLocalizations.of(context)!.settingsThemeSelectorLight, + icon: Icons.brightness_5_rounded, + value: AdaptiveThemeMode.light), + CustomChoiceChip( + label: AppLocalizations.of(context)!.settingsThemeSelectorDark, + icon: Icons.brightness_2_rounded, + value: AdaptiveThemeMode.dark) + ]; + } + + /// SettingsPage Elements + static const warningIcon = Icons.warning; + static const checkIcon = Icons.check; + static const denyIcon = Icons.close; + + /// AboutPage Elements + static const sourceCodeIcon = Icons.code; + static const licenseIcon = Icons.article; + static const sourceCodeLink = + 'https://github.com/herzhenr/simple-wake-on-lan'; + + /// Other + static const screenPadding = + EdgeInsets.only(left: 20, right: 20, top: 0, bottom: 0); + static const screenPaddingScrollView = + EdgeInsets.only(left: 20, right: 20, top: 0, bottom: 80); + static BorderRadius borderRadius = BorderRadius.circular(10); +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..5c0ee80 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,102 @@ +{ + "@PAGES": {}, + "appTitle": "Simple WOL", + "homePageTitle": "Simple WOL", + "homePageLabel": "Home", + "settingsPageTitle": "Settings", + "settingsPageLabel": "Settings", + "aboutPageTitle": "About", + "aboutPageLabel": "About", + "@GENERAL": {}, + "cancel": "Cancel", + "confirm": "Confirm", + "done": "Done", + "ok": "OK", + "back": "Back", + "genericWarning": "Warning", + "@HOME": {}, + "homeAddDeviceButton": "Add Devices", + "homeNoDevices": "No devices yet. Add devices by clicking the + button below", + "homeFilterDevicesTitle": "Filter Devices", + "homeDeviceListTitle": "Devices", + "homeDeviceCardWakeButton": "Wake Up", + "homeDeviceCardEditButton": "Edit", + "homeWolCardTitle": "Waking up...", + "@DISCOVER": {}, + "discoverTitle": "Discover Devices", + "discoverAddCustomAlertTitle": "Add Custom Device", + "discoverAddDeviceAlertTitle": "Add New Device", + "discoverAddCustomDeviceButton": "Add Custom", + "discoverAddCustomDeviceCard": "Custom Configuration", + "discoverNetworkDevicesTitle": "Network Devices", + "saveWithError": "Save Anyways", + "@FORM": {}, + "formApplyButtonText": "Apply", + "formNameHint": "Name", + "formNameError": "name should not be empty", + "formIpHint": "IP Address", + "formIpError": "invalid IP Address", + "formMacHint": "MAC Address", + "formMacError": "invalid MAC Address", + "formPortLabel": "Wake On Lan Port", + "formPortHint": "Port", + "formPortError": "invalid", + "formIconLabel": "Icon", + "formIconError": "no device type selected", + "formIconErrorTitle": "Wrong Format", + "formPort": "Port", + "formErrorMessageName": "device has no name", + "formErrorMessageIp": "invalid IP Address", + "formErrorMessageMac": "invalid MAC Address", + "formErrorMessagePort": "invalid port", + "formErrorMessageType": "no device type selected", + "formDeleteAlertTitle": "Delete Device", + "formDeleteAlertText": "Are you sure you want to delete the device", + "@formDeleteAlertText": { + "description": "After the text, the device name is inserted in bold text followed by a question mark." + }, + "formDeleteAlertDelete": "Delete", + "deviceChoiceServer": "Server", + "deviceChoiceDesktop": "Desktop", + "deviceChoiceLaptop": "Laptop", + "deviceChoicePrinter": "Printer", + "deviceChoiceNetwork": "Network", + "deviceChoiceIOT": "Smart Device", + "deviceChoiceTv": "TV", + "deviceChoiceMobile": "Mobile", + "deviceChoiceOther": "Other", + "@SETTINGS": {}, + "settingsAppearanceTitle": "Appearance", + "settingsThemeSelectorTitle": "Theme", + "settingsThemeSelectorLight": "Light", + "settingsThemeSelectorDark": "Dark", + "settingsThemeSelectorSystem": "Auto", + "settingsSystemColorsText": "Use system colors", + "settingsAppDataTitle": "App Data", + "settingsExport": "Export", + "settingsImport": "Import", + "settingsReset": "Reset App Data", + "settingsResetDialogText": "Are you sure you want to reset all saved devices? It is recommended to export your data before doing this.", + "settingsResetDialogButton": "Reset", + "settingsResetDialogWrongFormatTitle": "Wrong File Format", + "settingsResetDialogWrongFormatText": "Only .json files are supported for importing devices. You provided a file ending with .{fileExt} which is not supported.", + "@settingsResetDialogWrongFormatText": { + "description": "The message shown when the user tries to import a file with a wrong file extension. The file extension is provided as {fileExt}.", + "placeholders": { + "fileExt": { + "type": "String", + "example": "xml" + } + } + }, + "settingsResetDialogWrongJsonFormatTitle": "Wrong Json format", + "settingsResetDialogWrongJsonFormatText": "The file you provided is not in the correct format. Please make sure it is a json file which got exported by this app before.", + "settingsResetDialogConfirmText": "Are you sure you want to import these devices? This will delete all current data and replace it with the devices in the file.", + "settingsResetDialogConfirmButton": "Import", + "@ABOUT": {}, + "aboutInfoTitle": "About this App", + "aboutInfoText": "A simple tool to wake devices in the local network remotely if they are turned off. This app tries to make this process easy for the user.", + "aboutOpenSourceTitle": "Open Source", + "aboutOpenSourceCodeButton": "Source Code", + "aboutOpenSourceLicenseButton": "Licenses" +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..9d9fefe --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,120 @@ +import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:dart_ping_ios/dart_ping_ios.dart'; +import 'package:flutter/material.dart'; +import 'package:simple_wake_on_lan/constants.dart'; +import 'package:simple_wake_on_lan/screens/about/about.dart'; +import 'package:simple_wake_on_lan/screens/home/home.dart'; +import 'package:simple_wake_on_lan/screens/settings/settings.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +void main() async { + // Register dart_ping_ios with dart_ping + DartPingIOS.register(); + WidgetsFlutterBinding.ensureInitialized(); + final savedThemeMode = await AdaptiveTheme.getThemeMode(); + runApp(MyApp(savedThemeMode: savedThemeMode)); +} + +class MyApp extends StatelessWidget { + final AdaptiveThemeMode? savedThemeMode; + + const MyApp({super.key, required this.savedThemeMode}); + + @override + Widget build(BuildContext context) { + return AdaptiveTheme( + light: ThemeData( + brightness: Brightness.light, + useMaterial3: true, + ), + dark: ThemeData( + brightness: Brightness.dark, + useMaterial3: true, + ), + initial: savedThemeMode ?? AdaptiveThemeMode.system, + builder: (ThemeData light, ThemeData dark) => MaterialApp( + onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en'), // English + ], + theme: light, + darkTheme: dark, + home: const MyHomePage(), + ), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + // current selected navigation index + int selectedNavigationIndex = 0; + + // values from homePage which should be stored in memory while the app is running + SortingOrder selectedMenu = SortingOrder.alphabetical; + late List deviceTypesValues = List.filled( + AppConstants().getChipsDeviceTypes().length, true, + growable: false); + + @override + Widget build(BuildContext context) { + final screens = [ + HomePage( + title: AppLocalizations.of(context)!.homePageTitle, + onSelectedMenuChange: (SortingOrder order) { + setState(() { + selectedMenu = order; + }); + }, + selectedMenu: selectedMenu, + onSelectedDeviceTypesChange: (List values) { + setState(() { + deviceTypesValues = values; + }); + }, + deviceTypesValues: deviceTypesValues), + SettingsPage(title: AppLocalizations.of(context)!.settingsPageTitle), + AboutPage(title: AppLocalizations.of(context)!.aboutPageTitle), + ]; + return Scaffold( + body: screens[selectedNavigationIndex], + bottomNavigationBar: NavigationBar( + onDestinationSelected: (int index) { + setState(() { + selectedNavigationIndex = index; + }); + }, + selectedIndex: selectedNavigationIndex, + labelBehavior: NavigationDestinationLabelBehavior.onlyShowSelected, + destinations: [ + NavigationDestination( + icon: const Icon(AppConstants.homeIcon), + label: AppLocalizations.of(context)!.homePageLabel), + NavigationDestination( + icon: const Icon( + AppConstants.settingsIcon, + ), + label: AppLocalizations.of(context)!.settingsPageTitle), + NavigationDestination( + icon: const Icon( + AppConstants.aboutIcon, + ), + label: AppLocalizations.of(context)!.aboutPageTitle), + ], + ), + ); + } +} diff --git a/lib/screens/about/about.dart b/lib/screens/about/about.dart new file mode 100644 index 0000000..e7bb5f0 --- /dev/null +++ b/lib/screens/about/about.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'dart:developer'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../../constants.dart'; +import '../../widgets/layout_elements.dart'; + +class AboutPage extends StatefulWidget { + const AboutPage({super.key, required this.title}); + + final String title; + + @override + State createState() => _AboutPageState(); +} + +class _AboutPageState extends State { + final Uri _url = Uri.parse(AppConstants.sourceCodeLink); + + Future _launchUrl(Uri url) async { + if (!await launchUrl(url)) { + log('Could not launch $url'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: ListView( + padding: AppConstants.screenPadding, + children: [ + TextTitle( + title: AppLocalizations.of(context)!.aboutInfoTitle, + children: [ + TextBox(text: AppLocalizations.of(context)!.aboutInfoText), + ], + ), + TextTitle( + title: AppLocalizations.of(context)!.aboutOpenSourceTitle, + children: [ + SpacedRow( + children: [ + IconTextButton( + text: + AppLocalizations.of(context)!.aboutOpenSourceCodeButton, + icon: AppConstants.sourceCodeIcon, + onPressed: () async { + await _launchUrl(_url); + }, + ), + IconTextButton( + text: AppLocalizations.of(context)! + .aboutOpenSourceLicenseButton, + icon: AppConstants.licenseIcon, + onPressed: () => {showLicensePage(context: context)}, + ), + ], + ) + ], + ), + ], + ), + // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} diff --git a/lib/screens/history/history.dart b/lib/screens/history/history.dart new file mode 100644 index 0000000..6a01195 --- /dev/null +++ b/lib/screens/history/history.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class HistoryPage extends StatefulWidget { + const HistoryPage({super.key, required this.title}); + final String title; + + @override + State createState() => _HistoryPageState(); +} + +class _HistoryPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: const Text('Hallo'), + // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} diff --git a/lib/screens/home/bottom_sheet_form.dart b/lib/screens/home/bottom_sheet_form.dart new file mode 100644 index 0000000..8ec3554 --- /dev/null +++ b/lib/screens/home/bottom_sheet_form.dart @@ -0,0 +1,576 @@ +import 'package:flutter/material.dart'; +import 'package:simple_wake_on_lan/constants.dart'; +import 'package:simple_wake_on_lan/screens/home/discover.dart'; +import 'package:simple_wake_on_lan/screens/home/home.dart'; +import 'package:simple_wake_on_lan/services/data.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:simple_wake_on_lan/widgets/layout_elements.dart'; +import '../../services/database.dart'; +import '../../widgets/chip_cards.dart'; +import '../../widgets/universal_ui_components.dart'; + +abstract class ModularBottomFormPage extends StatefulWidget { + final String title; + final Device device; + final Function(List) onSubmitDeviceCallback; + final bool deleteButton; + + ModularBottomFormPage( + {Key? key, + required this.device, + required this.title, + required this.onSubmitDeviceCallback, + this.deleteButton = false}) + : super(key: key); + + // text controllers for the text input fields + final TextEditingController controllerName = TextEditingController(); + final TextEditingController controllerIp = TextEditingController(); + final TextEditingController controllerMac = TextEditingController(); + final TextEditingController controllerPort = TextEditingController(); + final TextEditingController controllerIcon = TextEditingController(); + + final formKeyIp = GlobalKey(); + final formKeyMac = GlobalKey(); + final formKeyName = GlobalKey(); + final formKeyPort = GlobalKey(); + + final chipDeviceTypes = AppConstants().getChipsDeviceTypes(); + final chipWolPorts = AppConstants().getChipsWolPorts(); + + /// creates a [NetworkDevice] or [StorageDevice] class out of the currently stored inputs in the TextEditingControllers + Device get getDevice { + final wolPort = + controllerPort.text.isEmpty ? null : int.parse(controllerPort.text); + final deviceType = controllerIcon.text.isEmpty ? null : controllerIcon.text; + if (device is StorageDevice) { + final storageDevice = device as StorageDevice; + return StorageDevice( + id: storageDevice.id, + hostName: controllerName.text, + ipAddress: controllerIp.text, + macAddress: controllerMac.text, + modified: DateTime.now(), + wolPort: wolPort, + deviceType: deviceType); + } else { + return NetworkDevice( + hostName: controllerName.text, + ipAddress: controllerIp.text, + macAddress: controllerMac.text, + wolPort: wolPort, + deviceType: deviceType, + ); + } + } + + // DeviceStorage object to save the device to the json file + final deviceStorage = DeviceStorage(); + + /// dataOperationOnSave() is an abstract method that is implemented in the child classes and is called when the submitButton is pressed + /// it saves the device to the json file and returns the updated [StorageDevice] list + Future> dataOperationOnSave(); + + /// dataOperationOnDelete() is triggered when the delete button is pressed and delete a device from the json file and returns the updated [StorageDevice] list + Future> dataOperationOnDelete() async { + StorageDevice device = getDevice as StorageDevice; + List devices = await deviceStorage.deleteDevice( + device.id, + ); + return devices; + } + + // Future> dataOperationOnDelete(); + + @override + State createState() => _ModularBottomFormPageState(); +} + +class _ModularBottomFormPageState extends State { + // set label of chipsWolPorts to the translated string + late List> chipsWolPorts = + AppConstants().getChipsWolPorts(context: context); + + // variables for the chip selectors and initial port value + int? indexWolSelector; + int? indexIconSelector; + + @override + void initState() { + super.initState(); + // initialize the text controllers + widget.controllerName.text = widget.device.hostName; + widget.controllerIp.text = widget.device.ipAddress; + widget.controllerMac.text = widget.device.macAddress; + + // initialize the port text controller and the chip selector. AppConstants().chipsWolPorts + final wolElement = widget.chipWolPorts + .where((element) => element.value == widget.device.wolPort); + if (wolElement.isNotEmpty) { + indexWolSelector = widget.chipWolPorts.indexOf(wolElement.first); + } + if (widget.device.wolPort != null) { + widget.controllerPort.text = widget.device.wolPort.toString(); + } + + // initialize the icon selector + final deviceType = widget.chipDeviceTypes + .where((element) => element.value == widget.device.deviceType); + if (deviceType.isNotEmpty) { + indexIconSelector = widget.chipDeviceTypes.indexOf(deviceType.first); + } + if (widget.device.deviceType != null) { + widget.controllerIcon.text = widget.device.deviceType.toString(); + } + } + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + + // GestureDetector is required to close the keyboard when the user taps outside of the text input fields + return GestureDetector( + onTap: () { + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + }, + child: Padding( + padding: EdgeInsets.fromLTRB( + 20, 15, 20, MediaQuery.of(context).viewInsets.bottom), + child: ListView( + primary: true, + shrinkWrap: true, + children: [ + dragIndicator(), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.headlineSmall, + ), + buildSaveButton(context), + ], + ), + getCustomTextFormField( + label: AppLocalizations.of(context)!.formNameHint, + formKey: widget.formKeyName, + controller: widget.controllerName, + validator: createValidator(AppConstants.nameValidationRegex, + AppLocalizations.of(context)!.formNameError), + ), + getCustomTextFormField( + label: AppLocalizations.of(context)!.formIpHint, + formKey: widget.formKeyIp, + controller: widget.controllerIp, + validator: createValidator(AppConstants.ipValidationRegex, + AppLocalizations.of(context)!.formIpError), + ), + getCustomTextFormField( + label: AppLocalizations.of(context)!.formMacHint, + formKey: widget.formKeyMac, + controller: widget.controllerMac, + validator: createValidator(AppConstants.macValidationRegex, + AppLocalizations.of(context)!.formMacError), + ), + const SizedBox( + height: 20, + ), + buildPortSelector(textTheme), + buildIconSelector(textTheme), + if (widget.deleteButton) buildDeleteButton(), + ], + ), + ), + ); + } + + /// return a button for saving the user input on the form to the device storage + Padding buildSaveButton(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(right: 5), + child: Transform.translate( + offset: const Offset(0, 0), + child: ActionButton( + onPressed: () => { + validateFormFields(onSubmitDeviceCallback: () async { + Navigator.popUntil(context, (route) => route.isFirst); + List device = + await widget.dataOperationOnSave(); + // sent device to callback function in order to update the UI + widget.onSubmitDeviceCallback(device); + }) + }, + text: AppLocalizations.of(context)!.formApplyButtonText, + icon: const Icon(AppConstants.formIcon)), + ), + ); + } + + /// validates the user input on the form and calls the [onSubmitDeviceCallback] if the input is valid + /// otherwise it shows an error dialog which lists the invalid fields + /// the user has the option to save the device anyway and the [onSubmitDeviceCallback] is called again or to cancel the operation + /// [onSubmitDeviceCallback] the callback function that is called when the user decides to save the device + void validateFormFields({Function()? onSubmitDeviceCallback}) { + List errorMessage = []; + + if (!widget.formKeyName.currentState!.validate()) { + errorMessage.add(AppLocalizations.of(context)!.formErrorMessageName); + } + if (!widget.formKeyIp.currentState!.validate()) { + errorMessage.add(AppLocalizations.of(context)!.formErrorMessageIp); + } + if (!widget.formKeyMac.currentState!.validate()) { + errorMessage.add(AppLocalizations.of(context)!.formErrorMessageMac); + } + if (!widget.formKeyPort.currentState!.validate()) { + errorMessage.add(AppLocalizations.of(context)!.formErrorMessagePort); + } + if (indexIconSelector == null) { + errorMessage.add(AppLocalizations.of(context)!.formErrorMessageType); + } + + if (errorMessage.isNotEmpty) { + showDialog( + context: context, + builder: (BuildContext context) { + return customDualChoiceAlertdialog( + title: AppLocalizations.of(context)!.formIconErrorTitle, + icon: AppConstants.formWrongFormatIcon, + iconColor: Theme.of(context).colorScheme.error, + child: Column( + children: errorMessage + .map((error) => Row(children: [ + Icon( + AppConstants.formInvalidArgument, + size: 15, + color: Theme.of(context).colorScheme.error, + ), + const SizedBox(width: 5), + Text(error) + ])) + .toList(), + ), + leftText: AppLocalizations.of(context)!.back, + leftOnPressed: () { + Navigator.of(context).pop(); + }, + rightText: AppLocalizations.of(context)!.saveWithError, + rightColor: Theme.of(context).colorScheme.error, + rightOnPressed: onSubmitDeviceCallback, + ); + }, + ); + } else if (onSubmitDeviceCallback != null) { + onSubmitDeviceCallback(); + } + } + + /// Pill shaped container which tries to indicate that the bottom sheet can be dragged + Center dragIndicator() { + return Center( + child: Container( + height: 5.0, + width: 40.0, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: const BorderRadius.all(Radius.circular(8.0)))), + ); + } + + /// returns a custom text form field + /// * [label] the label of the text form field + /// * [controller] the TextEditingController of the text form field + /// * [validator] a validator function which can be created with [createValidator] + /// * [onSaved] the onSaved function called when the form is saved + Widget getCustomTextFormField( + {String? label, + required TextEditingController controller, + required GlobalKey formKey, + String? Function(String?)? validator, + String? Function(String?)? onSaved}) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: Form( + key: formKey, + child: TextFormField( + autovalidateMode: AutovalidateMode.always, + validator: validator, + controller: controller, + onSaved: onSaved, + cursorColor: Theme.of(context).colorScheme.primaryContainer, + decoration: InputDecoration( + isDense: true, + labelText: label, + errorStyle: const TextStyle(height: 0.1), + border: const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(12.0))), + ), + ), + ), + ); + } + + /// returns a custom selector and text input field for the port + /// * [textTheme] the text theme of the current context + Row buildPortSelector(TextTheme textTheme) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(AppLocalizations.of(context)!.formPortLabel, + style: textTheme.labelLarge), + SizedBox( + height: 45, + child: Wrap( + spacing: 5.0, + children: + List.generate(chipsWolPorts.length, (index) { + String? label = chipsWolPorts[index].label; + IconData? icon = chipsWolPorts[index].icon; + return ChoiceChip( + label: IntrinsicWidth( + child: Row( + children: [ + if (label != null) Text(label), + if (icon != null) const SizedBox(width: 10.0), + if (icon != null) Icon(icon), + ], + ), + ), + side: widget.formKeyPort.currentState?.validate() == false + ? BorderSide( + color: Theme.of(context).colorScheme.error, + ) + : null, + selected: indexWolSelector == index, + onSelected: (bool selected) { + setState(() { + indexWolSelector = selected ? index : null; + (selected) + ? widget.controllerPort.text = + chipsWolPorts[index].value.toString() + : widget.controllerPort.text = ''; + }); + }, + ); + })), + ), + ], + ), + SizedBox( + width: 90, + child: getCustomTextFormField( + label: AppLocalizations.of(context)!.formPortHint, + formKey: widget.formKeyPort, + controller: widget.controllerPort, + validator: createValidator(AppConstants.portValidationRegex, + AppLocalizations.of(context)!.formPortError), + onSaved: (String? value) { + // TODO ugly + setState(() { + if (value == '9') { + indexWolSelector = 1; + } else if (value == '7') { + indexWolSelector = 0; + } else { + indexWolSelector = null; + } + }); + return null; + }, + ), + ), + ], + ); + } + + /// returns a custom selector for the icon of the device + /// * [textTheme] the text theme of the current context + Column buildIconSelector(TextTheme textTheme) { + List> chipsDeviceTypes = + AppConstants().getChipsDeviceTypes(context: context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(AppLocalizations.of(context)!.formIconLabel, + style: textTheme.labelLarge), + const SizedBox(height: 3.0), + SizedBox( + height: 45, + child: ListView( + primary: true, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: [ + Wrap( + spacing: 5.0, + runSpacing: 0.0, + children: + List.generate(chipsDeviceTypes.length, (index) { + String? label = chipsDeviceTypes[index].label; + IconData? icon = chipsDeviceTypes[index].icon; + return ChoiceChip( + label: IntrinsicWidth( + child: Row( + children: [ + if (label != null) Text(label), + if (icon != null) const SizedBox(width: 10.0), + if (icon != null) Icon(icon), + ], + ), + ), + side: indexIconSelector == null + ? BorderSide( + color: Theme.of(context).colorScheme.error, + ) + : null, + selected: indexIconSelector == index, + onSelected: (bool selected) { + setState(() { + indexIconSelector = selected ? index : null; + if (selected) { + widget.controllerIcon.text = + chipsDeviceTypes[index].value.toString(); + } else { + widget.controllerIcon.text = ''; + } + }); + }, + ); + }).toList()), + ], + ), + ), + // error text + if (indexIconSelector == null) + Padding( + padding: const EdgeInsets.only(left: 12.0), + child: Text( + AppLocalizations.of(context)!.formIconError, + style: TextStyle( + color: Theme.of(context).colorScheme.error, fontSize: 12), + ), + ), + const SizedBox( + height: 15, + ) + ], + ); + } + + /// return a button to delete the device + /// * [textTheme] the text theme of the current context + Widget buildDeleteButton() { + return SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.0)), + ), + onPressed: () { + showDeleteDialog(); + }, + child: Text(AppLocalizations.of(context)!.formDeleteAlertTitle, + style: const TextStyle(color: Colors.white)), + ), + ); + } + + void showDeleteDialog() { + showDialog( + context: context, + builder: (BuildContext context) { + return customDualChoiceAlertdialog( + title: AppLocalizations.of(context)!.formDeleteAlertTitle, + icon: Icons.delete_outlined, + iconColor: Theme.of(context).colorScheme.error, + child: Text.rich(TextSpan( + children: [ + TextSpan(text: AppLocalizations.of(context)!.formDeleteAlertText), + if (widget.controllerName.text.isNotEmpty) + TextSpan( + text: " ${widget.controllerName.text}", + style: const TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: '?'), + ], + )), + leftText: AppLocalizations.of(context)!.cancel, + leftOnPressed: () { + Navigator.of(context).pop(); + }, + rightText: AppLocalizations.of(context)!.formDeleteAlertDelete, + rightColor: Theme.of(context).colorScheme.error, + rightOnPressed: () async { + Navigator.popUntil(context, (route) => route.isFirst); + List device = await widget.dataOperationOnDelete(); + // sent device to callback function in order to update the UI + widget.onSubmitDeviceCallback(device); + }, + ); + }, + ); + } + + /// returns a custom choice chip with the given text and icon + /// * [updateValue] the value which is updated when the chip is selected + /// * [text] the text of the chip + /// * [icon] the icon of the chip + Row buildChoiceChipContent(int? updateValue, String text, {IconData? icon}) { + return Row( + children: [ + Text(text), + if (icon != null) const SizedBox(width: 10.0), + if (icon != null) Icon(icon), + ], + ); + } +} + +/// An implementation of the [ModularBottomFormPage] for adding a new [NetworkDevice] from the [DiscoverPage] +class NetworkDeviceFormPage extends ModularBottomFormPage { + NetworkDeviceFormPage( + {super.key, + required super.device, + required super.title, + required super.onSubmitDeviceCallback}); + + @override + Future> dataOperationOnSave() async { + List devices = await deviceStorage.addDevice( + getDevice as NetworkDevice, + ); + return devices; + } +} + +/// An implementation of the [ModularBottomFormPage] for editing an already existing [StorageDevice] from the [HomePage] +class EditDeviceFormPage extends ModularBottomFormPage { + EditDeviceFormPage( + {super.key, + required super.device, + required super.title, + required super.onSubmitDeviceCallback}) + : super(deleteButton: true); + + @override + Future> dataOperationOnSave() async { + List devices = await deviceStorage.updateDevice( + getDevice as StorageDevice, + ); + return devices; + } +} + +/// return a validator function which can be passed to a [TextFormField] in order to validate the Input. +/// [regEx] is the RegEx to be evaluated, [msg] ist the error message being shown, if the input doesn't satisfy the RegEx +String? Function(String?) createValidator(String regEx, String msg) => + (String? value) => !RegExp(regEx).hasMatch(value!) ? msg : null; diff --git a/lib/screens/home/discover.dart b/lib/screens/home/discover.dart new file mode 100644 index 0000000..2e7ef5a --- /dev/null +++ b/lib/screens/home/discover.dart @@ -0,0 +1,189 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:network_info_plus/network_info_plus.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:simple_wake_on_lan/constants.dart'; +import 'bottom_sheet_form.dart'; +import '../../services/data.dart'; +import '../../services/network.dart'; +import '../../widgets/layout_elements.dart'; +import '../../widgets/universal_ui_components.dart'; + +class DiscoverPage extends StatefulWidget { + final Function(List) updateDevicesList; + + const DiscoverPage({Key? key, required this.updateDevicesList}) + : super(key: key); + + @override + State createState() => _DiscoverPageState(); +} + +class _DiscoverPageState extends State { + @override + void initState() { + super.initState(); + _deviceDiscovery(); + } + + // variables for discovering network devices and showing the progress in the ui + StreamSubscription? _subscription; + final List _devices = []; + double _progress = 0.0; + + // method to discover devices on the network + Future _deviceDiscovery() async { + setState(() { + _devices.clear(); + _progress = 0.0; + }); + + String? wifiIP = await (NetworkInfo().getWifiIP()); + final String subnet = wifiIP!.substring(0, wifiIP.lastIndexOf('.')); + final stream = findDevicesInNetwork(subnet, (progress) { + if (!mounted) { + // Exit the loop if the widget is no longer mounted. + return; + } + setState(() { + _progress = progress; + }); + }); + + _subscription = stream.listen((device) { + if (!mounted) { + // Exit the loop if the widget is no longer mounted. + _subscription = null; + return; + } + setState(() { + _devices.add(device); + //_devices.sort(); + _devices.sort((NetworkDevice a, NetworkDevice b) => -a.compareTo(b)); + }); + }, onDone: () { + setState(() { + _subscription = null; + }); + }); + } + + @override + void dispose() { + super.dispose(); + _subscription?.cancel(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.discoverTitle), + ), + floatingActionButton: ActionButton( + onPressed: () => showBottomSheet( + title: AppLocalizations.of(context)!.discoverAddCustomAlertTitle, + device: NetworkDevice()), + text: AppLocalizations.of(context)!.discoverAddCustomDeviceButton, + icon: const Icon(Icons.add)), + body: buildListview(), + // This trailing comma makes auto-formatting nicer for build methods. + ); + } + + Widget buildListview() { + return RefreshIndicator( + // on refresh call network method and update the list + onRefresh: () async { + // on refresh should just be called when a scan of the network is done + if (_subscription == null) { + _deviceDiscovery(); + } + }, + child: Column( + children: [ + Visibility( + visible: _subscription != null, + child: LinearProgressIndicator(value: _progress)), + Expanded( + child: ListView( + padding: AppConstants.screenPaddingScrollView, + children: [ + TextTitle( + children: [ + CustomCard( + deviceType: AppConstants.addCustomDeviceType, + title: AppLocalizations.of(context)! + .discoverAddCustomDeviceCard, + trailing: const Icon(Icons.arrow_forward), + onTap: () => showBottomSheet( + title: AppLocalizations.of(context)! + .discoverAddCustomAlertTitle, + device: NetworkDevice())) + ], + ), + TextTitle( + title: + AppLocalizations.of(context)!.discoverNetworkDevicesTitle, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_devices.isNotEmpty) + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: _devices.length, + itemBuilder: (context, index) { + String? title, subtitle; + if (_devices[index].hostName != "") { + title = _devices[index].hostName; + subtitle = _devices[index].ipAddress; + } else { + title = _devices[index].ipAddress; + } + return CustomCard( + title: title, + subtitle: subtitle, + onTap: () => showCustomBottomSheet( + context: context, + formPage: NetworkDeviceFormPage( + title: AppLocalizations.of(context)! + .discoverAddDeviceAlertTitle, + device: _devices[index] + .copyWith(wolPort: 9), + onSubmitDeviceCallback: + widget.updateDevicesList)), + ); + }, + ), + // if (_subscription == null) + ], + ), + ], + ), + ], + ), + ), + ], + ), + ); + } + + void showBottomSheet( + {required String title, required NetworkDevice device, int? port}) { + showModalBottomSheet( + isScrollControlled: true, + // only expand the bottom sheet to 85% of the screen height + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.85, + ), + context: context, + backgroundColor: Theme.of(context).colorScheme.surface, + builder: (context) => NetworkDeviceFormPage( + title: title, + device: device.copyWith(wolPort: port), + onSubmitDeviceCallback: widget.updateDevicesList), + ); + } +} diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart new file mode 100644 index 0000000..cc12192 --- /dev/null +++ b/lib/screens/home/home.dart @@ -0,0 +1,458 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:simple_wake_on_lan/constants.dart'; +import 'package:simple_wake_on_lan/screens/home/discover.dart'; +import '../../widgets/layout_elements.dart'; +import 'bottom_sheet_form.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../../services/database.dart'; +import '../../services/data.dart'; +import '../../services/network.dart'; +import '../../widgets/chip_cards.dart'; +import '../../widgets/universal_ui_components.dart'; + +// This is the type used by the popup menu below. +enum SortingOrder { alphabetical, recently, type } + +class HomePage extends StatefulWidget { + HomePage( + {super.key, + required this.title, + required this.onSelectedMenuChange, + required this.selectedMenu, + required this.onSelectedDeviceTypesChange, + required this.deviceTypesValues}); + + final String title; + + final ValueChanged onSelectedMenuChange; + final SortingOrder selectedMenu; + + final ValueChanged> onSelectedDeviceTypesChange; + final List deviceTypesValues; + + final chipsDeviceTypes = AppConstants().getChipsDeviceTypes(); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + final _deviceStorage = DeviceStorage(); + List _devicesRaw = []; + List _devices = []; + bool _isLoading = false; + + late List deviceTypesValues = widget.deviceTypesValues; + + late SortingOrder selectedMenu = widget.selectedMenu; + + @override + void initState() { + super.initState(); + _loadDevices().then((value) => { + filterDevicesByType(), + sortDevices(), + }); + } + + /// sort Devices by chipsDeviceTypes selection + void filterDevicesByType() { + List sortedDevices = []; + for (StorageDevice device in _devicesRaw) { + if (device.deviceType == null) { + sortedDevices.add(device); + } else { + for (int i = 0; i < widget.chipsDeviceTypes.length; i++) { + if (deviceTypesValues[i] && + device.deviceType == widget.chipsDeviceTypes[i].value) { + sortedDevices.add(device); + break; + } + } + } + } + setState(() { + _devices = sortedDevices; + }); + } + + /// sort devices by selectedMenu value. [alphabetical], [recently] and [type] are possible. + void sortDevices() { + switch (selectedMenu) { + case SortingOrder.alphabetical: + setState(() { + _devices.sort((a, b) => + a.hostName.toLowerCase().compareTo(b.hostName.toLowerCase())); + }); + break; + case SortingOrder.recently: + setState(() { + _devices.sort((a, b) => b.modified.compareTo(a.modified)); + }); + break; + case SortingOrder.type: + setState(() { + _devices.sort((a, b) => a.deviceType == null + ? -1 + : a.deviceType!.compareTo(b.deviceType ?? '')); + }); + break; + } + } + + /// loads a list of devices from the device storage + Future _loadDevices() async { + setState(() { + _isLoading = true; + }); + + try { + final devices = await _deviceStorage.loadDevices(); + setState(() { + _devicesRaw = devices; + }); + } on PlatformException catch (e) { + debugPrint('Failed to load devices: $e'); + } finally { + setState(() { + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + leading: PopupMenuButton( + icon: AppConstants.sort, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10.0), + ), + ), + // The menu should appear below the button. The offset is dependent of the selected menu item so the offset is calculated dependent of + // the current selected menu item. + offset: Offset(0, + 00 + 50.0 * (SortingOrder.values[selectedMenu.index].index + 1)), + initialValue: selectedMenu, + // Callback that sets the selected popup menu item. + onSelected: (SortingOrder item) { + setState(() { + selectedMenu = item; + widget.onSelectedMenuChange(item); + }); + sortDevices(); + }, + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem( + value: SortingOrder.alphabetical, + child: Text('alphabetical'), + ), + const PopupMenuItem( + value: SortingOrder.recently, + child: Text('recently'), + ), + const PopupMenuItem( + value: SortingOrder.type, + child: Text('type'), + ), + ], + ), + ), + floatingActionButton: ActionButton( + onPressed: () async { + final newDevice = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DiscoverPage( + updateDevicesList: updateDevicesList, + )), + ); + if (newDevice != null) { + setState(() { + _devicesRaw.add(newDevice); + }); + } + }, + text: AppLocalizations.of(context)!.homeAddDeviceButton, + icon: AppConstants.add), + body: buildListview(), + ); + } + + updateDevicesList(devices) { + setState(() { + //_devices.add(device); + _devicesRaw = devices; + filterDevicesByType(); + sortDevices(); + }); + } + + Widget buildListview() { + return ListView( + padding: AppConstants.screenPaddingScrollView, + children: [ + TextTitle( + title: AppLocalizations.of(context)!.homeFilterDevicesTitle, + children: [ + SizedBox( + height: 50, + child: filterDevicesChipsV2(), + ), + ], + ), + TextTitle( + title: AppLocalizations.of(context)!.homeDeviceListTitle, + children: [buildDeviceList()], + ), + ], + ); + } + + /// returns a List of Chips for filtering devices + ListView filterDevicesChipsV2() { + List> chipsDeviceTypes = + AppConstants().getChipsDeviceTypes(context: context); + return ListView( + primary: true, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: [ + Wrap( + spacing: 5.0, + children: List.generate(chipsDeviceTypes.length, (index) { + String? label = chipsDeviceTypes[index].label; + IconData? icon = chipsDeviceTypes[index].icon; + return ActionChip( + avatar: Icon(icon), + label: Row( + children: [ + if (label != null) Text(label), + ], + ), + backgroundColor: deviceTypesValues[index] + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.surface, + side: BorderSide( + color: deviceTypesValues[index] + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.secondary, + width: 1.0, + ), + // selected: mode == chipsTheme[index].value, + onPressed: () { + setState(() { + deviceTypesValues[index] = !deviceTypesValues[index]; + filterDevicesByType(); + }); + }, + ); + })), + ], + ); + } + + /// returns a List of Chips for filtering devices + ListView filterDevicesChipsV1() { + List> chipsDeviceTypes = + AppConstants().getChipsDeviceTypes(context: context); + return ListView( + primary: true, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: [ + Wrap( + spacing: 5.0, + children: List.generate(chipsDeviceTypes.length, (index) { + String? label = chipsDeviceTypes[index].label; + IconData? icon = chipsDeviceTypes[index].icon; + return ChoiceChip( + label: IntrinsicWidth( + child: Row( + children: [ + if (label != null) Text(label), + if (icon != null) const SizedBox(width: 10.0), + if (icon != null) Icon(icon), + ], + ), + ), + selected: deviceTypesValues[index], + onSelected: (bool selected) { + setState(() { + deviceTypesValues[index] = selected; + filterDevicesByType(); + }); + }, + ); + })), + ], + ); + } + + /// returns the list of devices + Widget buildDeviceList() { + return _isLoading + ? const Center( + child: CircularProgressIndicator(), + ) + : _devices.isEmpty + ? Text(AppLocalizations.of(context)!.homeNoDevices, + style: Theme.of(context).textTheme.bodyMedium) + : ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: _devices.length, + itemBuilder: (context, index) { + final device = _devices[index]; + return buildDevice(device); + }, + ); + } + + /// returns a single device card + buildDevice(StorageDevice device) { + String title; + String? subtitle; + if (device.hostName != "") { + title = device.hostName; + subtitle = device.ipAddress; + } else { + title = device.ipAddress; + } + return CustomCard( + title: title, + subtitle: subtitle, + deviceType: device.deviceType, + onTap: () { + showDeviceOptionsDialog(device: device); + }, + ); + } + + /// shows the alert dialog for waking and editing the device + showDeviceOptionsDialog({required StorageDevice device}) { + String title = "", subtitle1 = "", subtitle2 = ""; + if (device.hostName != "") { + title = device.hostName; + subtitle1 = "${AppConstants.ipText}: ${device.ipAddress}"; + subtitle2 = "${AppConstants.macText}: ${device.macAddress}"; + } else if (device.macAddress != "") { + title = device.ipAddress; + subtitle1 = "${AppConstants.macText}: ${device.macAddress}"; + } + showDialog( + context: context, + builder: (BuildContext context) { + return deviceInfoDialog( + device: device, + title: title, + subtitle1: subtitle1, + subtitle2: subtitle2); + }); + } + + /// returns the actual alert dialog for waking and editing the device + Widget deviceInfoDialog( + {required StorageDevice device, + required String title, + required String subtitle1, + required String subtitle2}) { + return customDualChoiceAlertdialog( + title: title != "" ? title : null, + child: (subtitle1 != "" || subtitle2 != "") + ? Column( + children: [ + if (subtitle1 != "") Text(subtitle1), + if (subtitle2 != "") Text(subtitle2), + ], + ) + : null, + icon: getIcon(device.deviceType), + leftText: AppLocalizations.of(context)!.homeDeviceCardWakeButton, + rightText: AppLocalizations.of(context)!.homeDeviceCardEditButton, + leftIcon: AppConstants.wakeUp, + rightIcon: AppConstants.edit, + leftOnPressed: () => {Navigator.pop(context), showWakeUpDialog(device)}, + rightOnPressed: () => { + Navigator.of(context).pop(), + showCustomBottomSheet( + context: context, + formPage: EditDeviceFormPage( + title: "Edit Device", + device: device, + onSubmitDeviceCallback: updateDevicesList)) + }); + } + + /// shows the Alert Dialog for waking the device. + /// [device] is the device to wake. + Future showWakeUpDialog(StorageDevice device) { + return showDialog( + context: context, + builder: (context) { + return StreamBuilder>( + stream: sendWolAndGetMessages(device: device.toNetworkDevice()), + builder: (BuildContext context, + AsyncSnapshot> snapshot) { + // set color, text and icon of dialog box according to the arrived messages + Color? color; + String rightText = AppLocalizations.of(context)!.cancel; + IconData? rightIcon = AppConstants.denyIcon; + if (snapshot.hasData && + snapshot.data!.last.type == MsgType.online) { + color = AppConstants.successMessageColor; + rightText = AppLocalizations.of(context)!.done; + rightIcon = AppConstants.checkIcon; + } + + if (snapshot.hasData && + snapshot.data!.last.type == MsgType.error) { + color = Theme.of(context).colorScheme.error; + rightText = AppLocalizations.of(context)!.ok; + rightIcon = null; + } + + return customDualChoiceAlertdialog( + title: AppLocalizations.of(context)!.homeWolCardTitle, + child: snapshot.hasData + ? SizedBox( + width: 200, + child: ListView.separated( + separatorBuilder: (context, index) => + const Divider(), + shrinkWrap: true, + itemCount: snapshot.data!.length, + itemBuilder: (context, index) { + final Message message = snapshot.data![index]; + return Text( + message.text, + style: TextStyle( + color: (message.type == MsgType.error) + ? Theme.of(context).colorScheme.error + : (message.type == MsgType.check || + message.type == MsgType.online) + ? AppConstants.successMessageColor + : null, + ), + ); + }, + ), + ) + : const Center( + child: CircularProgressIndicator(), + ), + icon: AppConstants.wakeUp, + iconColor: color, + rightText: rightText, + rightIcon: rightIcon, + rightOnPressed: () => {Navigator.of(context).pop()}, + ); + }); + }); + } +} diff --git a/lib/screens/settings/data_ops.dart b/lib/screens/settings/data_ops.dart new file mode 100644 index 0000000..78f8fee --- /dev/null +++ b/lib/screens/settings/data_ops.dart @@ -0,0 +1,29 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:share_plus/share_plus.dart'; +import 'dart:io'; +import '../../services/database.dart'; + +Future shareJsonFile() async { + final deviceStorage = DeviceStorage(); + final filePath = await deviceStorage.getFilePath(); + + final file = File(filePath); + if (!await file.exists()) { + await file.create(); + } + + // Share the file using the Share plugin + await Share.shareXFiles([XFile(filePath)], subject: 'devices.json'); +} + +Future getJsonFile() async { + FilePickerResult? result = await FilePicker.platform.pickFiles(); + + if (result == null || result.files.single.path == null) return null; + + File file = File(result.files.single.path!); + + //if (file.path.split('.').last != 'json') return null; + + return file; +} diff --git a/lib/screens/settings/settings.dart b/lib/screens/settings/settings.dart new file mode 100644 index 0000000..85046db --- /dev/null +++ b/lib/screens/settings/settings.dart @@ -0,0 +1,237 @@ +import 'dart:convert'; +// import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:simple_wake_on_lan/constants.dart'; +import 'dart:io'; +import '../../services/database.dart'; +import '../../services/data.dart'; +import '../../widgets/chip_cards.dart'; +import '../../widgets/layout_elements.dart'; +import '../../widgets/universal_ui_components.dart'; +import 'data_ops.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key, required this.title}); + + final String title; + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + int? themeValue = 1; + bool colors = true; + + DeviceStorage deviceStorage = DeviceStorage(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: ListView( + padding: AppConstants.screenPadding, + children: [ + TextTitle( + title: AppLocalizations.of(context)!.settingsAppearanceTitle, + children: [ + // TextSubtitle can't be used as it is a Stateless widget and getThemeSelector() is Stateful when the theme changes + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(AppLocalizations.of(context)!.settingsThemeSelectorTitle, + style: Theme.of(context).textTheme.titleMedium), + const ThemeSwitcher() //getThemeSelector() + ], + ), + + /// TODO Switch to toggle between system colors and app colors (Also define section to choose app colors) + // TextSubtitle( + // title: AppLocalizations.of(context)!.settingsSystemColorsText, + // child: Switch( + // thumbIcon: thumbIcon, + // value: colors, + // onChanged: (bool value) { + // setState(() { + // colors = value; + // }); + // }, + // )) + ], + ), + TextTitle( + title: AppLocalizations.of(context)!.settingsAppDataTitle, + children: [ + SpacedRow( + children: [ + IconTextButton( + text: AppLocalizations.of(context)!.settingsExport, + icon: Icons.arrow_upward_outlined, + onPressed: shareJsonFile, + ), + IconTextButton( + text: AppLocalizations.of(context)!.settingsImport, + icon: Icons.arrow_downward_outlined, + onPressed: importJsonFile, + ), + ], + ), + Row( + children: [ + IconTextButton( + text: AppLocalizations.of(context)!.settingsReset, + icon: Icons.delete_forever_outlined, + onPressed: () { + buildResetDialog(context); + }), + ], + ), + ]), + ], + ), + ); + // This trailing comma makes auto-formatting nicer for build methods. + } + + Future buildResetDialog(BuildContext context) { + return showDialog( + context: context, + builder: (BuildContext context) { + return customDualChoiceAlertdialog( + title: AppLocalizations.of(context)!.settingsReset, + icon: AppConstants.warningIcon, + iconColor: Theme.of(context).colorScheme.error, + child: + Text(AppLocalizations.of(context)!.settingsResetDialogText), + leftText: AppLocalizations.of(context)!.cancel, + leftOnPressed: () => Navigator.pop(context), + rightText: + AppLocalizations.of(context)!.settingsResetDialogButton, + rightOnPressed: () => { + Navigator.pop(context), + deviceStorage.deleteAllDevices(), + }, + rightColor: Theme.of(context).colorScheme.error); + }); + } + + // get the file form the user and show an alert dialog + Future importJsonFile() async { + File? file = await getJsonFile(); + List importedDevices = []; + if (file != null) { + String fileExt = file.path.split('.').last; + if (fileExt != 'json' && context.mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return customDualChoiceAlertdialog( + title: AppLocalizations.of(context)! + .settingsResetDialogWrongFormatTitle, + iconColor: Theme.of(context).colorScheme.error, + child: Text(AppLocalizations.of(context)! + .settingsResetDialogWrongFormatText(fileExt)), + icon: AppConstants.warningIcon, + rightText: AppLocalizations.of(context)!.ok, + rightOnPressed: () => Navigator.pop(context), + ); + }); + return; + } + try { + final fileContents = await file.readAsString(); + final jsonData = json.decode(fileContents) as List; + importedDevices = + jsonData.map((item) => StorageDevice.fromJson(item)).toList(); + } on FileSystemException { + if (context.mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return customDualChoiceAlertdialog( + title: AppLocalizations.of(context)! + .settingsResetDialogWrongJsonFormatTitle, + iconColor: Theme.of(context).colorScheme.error, + child: Text(AppLocalizations.of(context)! + .settingsResetDialogWrongJsonFormatText), + icon: AppConstants.warningIcon, + rightText: AppLocalizations.of(context)!.ok, + rightOnPressed: () => Navigator.pop(context), + ); + }); + } + return; + } + if (context.mounted) { + showDialog( + context: context, + builder: (BuildContext context) { + return customDualChoiceAlertdialog( + title: AppLocalizations.of(context)!.genericWarning, + icon: AppConstants.warningIcon, + iconColor: Theme.of(context).colorScheme.error, + child: Text(AppLocalizations.of(context)! + .settingsResetDialogConfirmText), + leftText: AppLocalizations.of(context)!.cancel, + leftOnPressed: () => Navigator.pop(context), + rightText: AppLocalizations.of(context)! + .settingsResetDialogConfirmButton, + rightOnPressed: () => { + deviceStorage.deleteAllDevices(), + deviceStorage.saveDevices(importedDevices), + Navigator.pop(context), + }, + ); + }); + } + } + } + + // Widget getThemeSelector() { + // List> chipsTheme = + // AppConstants().getChipsTheme(context: context); + // + // return ValueListenableBuilder( + // valueListenable: AdaptiveTheme.of(context).modeChangeNotifier, + // builder: (_, mode, child) { + // // update your UI + // return Wrap( + // spacing: 5.0, + // children: List.generate(chipsTheme.length, (index) { + // String? label = chipsTheme[index].label; + // return ChoiceChip( + // label: Row( + // children: [ + // if (label != null) Text(label), + // //if (icon != null) const SizedBox(width: 10.0), + // //if (icon != null) Icon(icon), + // ], + // ), + // selected: mode == chipsTheme[index].value, + // onSelected: (bool selected) { + // // setState(() { + // // themeValue = index; //selected ? index : null; + // // }); + // AdaptiveTheme.of(context) + // .setThemeMode(chipsTheme[index].value); + // }, + // ); + // })); + // }, + // ); + // } + + // final MaterialStateProperty thumbIcon = + // MaterialStateProperty.resolveWith( + // (Set states) { + // // Thumb icon when the switch is selected. + // if (states.contains(MaterialState.selected)) { + // return const Icon(AppConstants.checkIcon); + // } + // return const Icon(AppConstants.denyIcon); + // }, + // ); +} diff --git a/lib/services/data.dart b/lib/services/data.dart new file mode 100644 index 0000000..834da66 --- /dev/null +++ b/lib/services/data.dart @@ -0,0 +1,195 @@ +import 'package:simple_wake_on_lan/services/utilities.dart'; + +abstract class Device implements Comparable { + final String hostName; + final String ipAddress; + final String macAddress; + final int? wolPort; + final String? deviceType; + + Device( + {required this.hostName, + required this.ipAddress, + required this.macAddress, + this.wolPort, + this.deviceType}); + + Device copyWith({ + String? id, + String? hostName, + String? ipAddress, + String? macAddress, + int? wolPort, + DateTime? modified, + String? deviceType, + }); + + Map toJson(); +} + +class StorageDevice extends Device { + final String id; + final DateTime modified; + + StorageDevice( + {required this.id, + required hostName, + required ipAddress, + required macAddress, + wolPort, + required this.modified, + deviceType}) + : super( + hostName: hostName, + ipAddress: ipAddress, + macAddress: macAddress, + wolPort: wolPort, + deviceType: deviceType); + + @override + int compareTo(NetworkDevice other) { + return ipToNumeric(ipAddress).compareTo(ipToNumeric(other.ipAddress)); + } + + @override + StorageDevice copyWith({ + String? id, + String? hostName, + String? ipAddress, + String? macAddress, + int? wolPort, + DateTime? modified, + String? deviceType, + }) { + return StorageDevice( + id: id ?? this.id, + hostName: hostName ?? this.hostName, + ipAddress: ipAddress ?? this.ipAddress, + macAddress: macAddress ?? this.macAddress, + wolPort: wolPort ?? this.wolPort, + modified: modified ?? this.modified, + deviceType: deviceType ?? this.deviceType, + ); + } + + @override + Map toJson() { + return { + 'id': id, + "hostName": hostName, + "ipAddress": ipAddress, + "macAddress": macAddress, + "wolPort": wolPort, + "deviceType": deviceType, + "modified": modified.toIso8601String(), + }; + } + + factory StorageDevice.fromJson(Map json) { + return StorageDevice( + id: json['id'], + hostName: json['hostName'], + ipAddress: json['ipAddress'], + macAddress: json['macAddress'], + wolPort: json['wolPort'], + modified: DateTime.parse(json['modified']), + deviceType: json['deviceType'], + ); + } + + NetworkDevice toNetworkDevice() { + return NetworkDevice( + hostName: hostName, + ipAddress: ipAddress, + macAddress: macAddress, + wolPort: wolPort, + deviceType: deviceType, + ); + } +} + +class NetworkDevice extends Device { + NetworkDevice( + {hostName = '', ipAddress = '', macAddress = '', wolPort, deviceType}) + : super( + hostName: hostName, + ipAddress: ipAddress, + macAddress: macAddress, + wolPort: wolPort, + deviceType: deviceType); + + @override + int compareTo(NetworkDevice other) { + return ipToNumeric(ipAddress).compareTo(ipToNumeric(other.ipAddress)); + } + + // TODO: not all parameters are necessary + @override + Device copyWith({ + String? id, + String? hostName, + String? ipAddress, + String? macAddress, + int? wolPort, + DateTime? modified, + String? deviceType, + }) { + return NetworkDevice( + hostName: hostName ?? this.hostName, + ipAddress: ipAddress ?? this.ipAddress, + macAddress: macAddress ?? this.macAddress, + wolPort: wolPort ?? this.wolPort, + deviceType: deviceType ?? this.deviceType, + ); + } + + @override + Map toJson() { + return { + 'ipAddress': ipAddress, + 'macAddress': macAddress, + 'hostName': hostName, + "wolPort": wolPort, + "deviceType": deviceType, + }; + } + + static NetworkDevice fromJson(Map json) { + return NetworkDevice( + ipAddress: json['hostName'], + macAddress: json['macAddress'], + hostName: json['name'], + wolPort: json['wolPort'], + deviceType: json['deviceType'], + ); + } + + StorageDevice toStorageDevice({ + required String id, + String? name, + String? ipAddress, + String? macAddress, + int? wolPort, + required DateTime modified, + String? deviceType, + }) { + return StorageDevice( + id: id, + hostName: name ?? hostName, + ipAddress: ipAddress ?? this.ipAddress, + macAddress: macAddress ?? this.macAddress, + wolPort: wolPort ?? this.wolPort, + modified: modified, + deviceType: deviceType ?? this.deviceType, + ); + } +} + +enum MsgType { error, check, ping, online, other } + +class Message { + String text; + MsgType type; + + Message({required this.text, this.type = MsgType.other}); +} diff --git a/lib/services/database.dart b/lib/services/database.dart new file mode 100644 index 0000000..e2b5b5d --- /dev/null +++ b/lib/services/database.dart @@ -0,0 +1,85 @@ +import 'dart:convert'; +import 'package:path_provider/path_provider.dart'; +import 'package:uuid/uuid.dart'; +import 'dart:io'; +import 'data.dart'; + +class DeviceStorage { + static const _fileName = 'devices.json'; + + Future getFilePath() async { + final appDocumentsDirectory = await getApplicationDocumentsDirectory(); + return '${appDocumentsDirectory.path}/$_fileName'; + } + + Future> loadDevices() async { + try { + final filePath = await getFilePath(); + final file = File(filePath); + final fileContents = await file.readAsString(); + final jsonData = json.decode(fileContents) as List; + return jsonData.map((item) => StorageDevice.fromJson(item)).toList(); + } on FileSystemException { + return []; + } on FormatException { + // TODO: show error in ui + return []; + } + } + + /// Saves a list of devices to the file [_fileName] in the app documents directory + /// [devices] the list of devices to save + Future saveDevices(List devices) async { + final filePath = await getFilePath(); + final jsonData = devices.map((item) => item.toJson()).toList(); + final jsonString = json.encode(jsonData); + final file = File(filePath); + await file.writeAsString(jsonString); + } + + /// Adds a new device to the list of devices + /// [device] the device to add + Future> addDevice(NetworkDevice device) async { + final devices = await loadDevices(); + final storageDevice = + device.toStorageDevice(id: const Uuid().v1(), modified: DateTime.now()); + final updatedDevices = [...devices, storageDevice]; + await saveDevices(updatedDevices); + return updatedDevices; + } + + /// Updates a device in the list of devices + /// [updatedDevice] the device to update + Future> updateDevice(StorageDevice updatedDevice) async { + final devices = await loadDevices(); + final updatedDevices = devices.map((device) { + if (device.id == updatedDevice.id) { + return updatedDevice.copyWith(modified: DateTime.now()); + } + return device; + }).toList(); + await saveDevices(updatedDevices); + return updatedDevices; + } + + /// Deletes a device from the list of devices + /// [deviceId] the id of the device to delete + Future> deleteDevice(String deviceId) async { + final devices = await loadDevices(); + final updatedDevices = + devices.where((device) => device.id != deviceId).toList(); + await saveDevices(updatedDevices); + return updatedDevices; + } + + /// Deletes the devices file + Future deleteAllDevices() async { + try { + final filePath = await getFilePath(); + final file = File(filePath); + await file.delete(); + } on FileSystemException { + // ignore + } + } +} diff --git a/lib/services/network.dart b/lib/services/network.dart new file mode 100644 index 0000000..5ab174c --- /dev/null +++ b/lib/services/network.dart @@ -0,0 +1,176 @@ +import 'dart:async'; + +import 'package:dart_ping/dart_ping.dart'; +import 'dart:io'; +import 'package:wake_on_lan/wake_on_lan.dart'; +import 'data.dart'; + +Stream findDevicesInNetwork( + String networkPrefix, + void Function(double) progressCallback, +) { + final controller = StreamController(); + var progress = 0; + const step = 25; + + /* Recursive function which pings a single device and schedules the next ping + step ips away from the current as long as this ip is still within the subnet */ + void pingDevice(int index) async { + final address = '$networkPrefix.$index'; + final ping = Ping(address, count: 1, timeout: 1); + + // Wait for the current ping to complete + await for (final response in ping.stream) { + if (response.response != null && response.error == null) { + // try to get the hostname of the device + String host = ""; + try { + await InternetAddress(address) + .reverse() + .then((value) => host = value.host); + } on SocketException { + host = ""; + } + controller.add(NetworkDevice(ipAddress: address, hostName: host)); + break; + } + } + + // If the end of the subnet is reached, close the stream + if (index == 254) { + controller.close(); + } + + // Increase the progress variable and report the result to the UI + final progressPercent = ++progress / 255; + progressCallback(progressPercent); + + // Schedule the next ping + if (index + step < 255) { + pingDevice(index + step); + } + } + + // Start the initial pings. + for (int i = 1; i <= step; i++) { + pingDevice(i); + } + + return controller.stream; +} + +/// sends the magic packet to the [device] that should receive a magic wol package in order to get woken up +Stream sendWolPackage({required NetworkDevice device}) async* { + // Validate correct formatting of ip and mac addresses + final ip = device.ipAddress; + final mac = device.macAddress; + final int? port = device.wolPort; + bool invalid = false; + + if (!IPv4Address.validate(ip)) { + yield Message(text: "'$ip' is a invalid IPv4 address", type: MsgType.error); + invalid = true; + } + + if (!MACAddress.validate(mac)) { + yield Message(text: "'$mac' is a invalid MAC address", type: MsgType.error); + invalid = true; + } + + //validate port + if (port == null || port < 0 || port > 65535) { + yield Message(text: "'$port' is an invalid port", type: MsgType.error); + invalid = true; + } + + if (invalid) { + // yield Message(text: "There was a error when trying to send a WOL Package to this host", type: MsgType.error); + return; + } + + // if no error occurred: try to send wol package + yield Message(text: "Provided ip and mac address are both valid"); + yield Message(text: "Trying to send a WOL Package"); + IPv4Address ipv4Address = IPv4Address(ip); + MACAddress macAddress = MACAddress(mac); + try { + WakeOnLAN wol = WakeOnLAN(ipv4Address, macAddress, port: port!); + await wol.wake(); + yield Message( + text: "Successfully send WOL package to $ip", type: MsgType.check); + } catch (e) { + yield Message( + text: + "There was a error when trying to send a WOL Package to this host", + type: MsgType.error); + } + + // ping device until it is online + yield Message(text: "Trying to ping device until it is online..."); + bool online = false; + int tries = 0; + while (!online && tries < 10) { + tries++; + yield Message(text: "Sending ping $tries/10", type: MsgType.ping); + + final ping = Ping(ip, count: 1, timeout: 5); + + // Wait for the current ping to complete + await for (final response in ping.stream) { + if (response.response != null && response.error == null) { + online = true; + } + } + } + if (online) { + yield Message(text: "Device is online", type: MsgType.online); + } else { + yield Message(text: "Device is not online", type: MsgType.error); + } +} + +/// returns a list of Messages by using the sendWolPackage function +/// accumulates the messages in a list and yields the list after each message +Stream> sendWolAndGetMessages( + {required NetworkDevice device}) async* { + List messages = []; + await for (Message message in sendWolPackage(device: device)) { + // if last message is ping, replace it with the new one + if (messages.isNotEmpty && + messages.last.type == MsgType.ping && + message.type == MsgType.ping) { + messages.removeLast(); + } + messages.add(message); + yield messages; + } +} + +/// Playground: Test different Discover methods + +// void findDevicesMDNS() async { +// const String name = '_dartobservatory._tcp.local'; +// final MDnsClient client = MDnsClient(); +// // Start the client with default options. +// await client.start(); +// +// // Get the PTR record for the service. +// await for (final PtrResourceRecord ptr in client +// .lookup(ResourceRecordQuery.serverPointer(name))) { +// // Use the domainName from the PTR record to get the SRV record, +// // which will have the port and local hostname. +// // Note that duplicate messages may come through, especially if any +// // other mDNS queries are running elsewhere on the machine. +// await for (final SrvResourceRecord srv in client.lookup( +// ResourceRecordQuery.service(ptr.domainName))) { +// // Domain name will be something like "io.flutter.example@some-iphone.local._dartobservatory._tcp.local" +// final String bundleId = +// ptr.domainName; //.substring(0, ptr.domainName.indexOf('@')); +// // print('Dart observatory instance found at ' +// // '${srv.target}:${srv.port} for "$bundleId".'); +// } +// } +// client.stop(); +// +// // print('Done.'); +// } diff --git a/lib/services/utilities.dart b/lib/services/utilities.dart new file mode 100644 index 0000000..a0adca9 --- /dev/null +++ b/lib/services/utilities.dart @@ -0,0 +1,7 @@ +int ipToNumeric(String ipAddress) { + final parts = ipAddress.split('.'); + final octets = parts.map(int.parse).toList(); + final numeric = + (octets[0] << 24) + (octets[1] << 16) + (octets[2] << 8) + octets[3]; + return numeric; +} diff --git a/lib/widgets/chip_cards.dart b/lib/widgets/chip_cards.dart new file mode 100644 index 0000000..e447aaa --- /dev/null +++ b/lib/widgets/chip_cards.dart @@ -0,0 +1,67 @@ +import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:flutter/material.dart'; +import '../constants.dart'; + +class CustomChoiceChip { + const CustomChoiceChip({this.label, this.icon, required this.value}); + + final String? label; + final IconData? icon; + final T value; +} + +class ThemeSwitcher extends StatelessWidget { + const ThemeSwitcher({super.key}); + + @override + Widget build(BuildContext context) { + List> chipsTheme = + AppConstants().getChipsTheme(context: context); + return ValueListenableBuilder( + valueListenable: AdaptiveTheme.of(context).modeChangeNotifier, + builder: (_, mode, child) { + // update your UI + return Wrap( + spacing: 5.0, + children: List.generate(chipsTheme.length, (index) { + String? label = chipsTheme[index].label; + IconData? icon = chipsTheme[index].icon; + return ActionChip( + avatar: Icon(icon), + label: Row( + children: [ + if (label != null) Text(label), + ], + ), + backgroundColor: mode == chipsTheme[index].value + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.surface, + side: BorderSide( + color: mode == chipsTheme[index].value + ? Theme.of(context).colorScheme.secondaryContainer + : Theme.of(context).colorScheme.secondary, + width: 1.0, + ), + // selected: mode == chipsTheme[index].value, + onPressed: () { + AdaptiveTheme.of(context) + .setThemeMode(chipsTheme[index].value); + }, + ); + })); + }, + ); + } +} + +Widget getIconChip({name = String}) { + return IntrinsicWidth( + child: Row( + children: [ + Text(name), + const SizedBox(width: 10.0), + const Icon(Icons.check_circle_outline) + ], + ), + ); +} diff --git a/lib/widgets/custom_alert_dialog.dart b/lib/widgets/custom_alert_dialog.dart new file mode 100644 index 0000000..961b819 --- /dev/null +++ b/lib/widgets/custom_alert_dialog.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; + +class CustomDialogBox extends StatelessWidget { + final String? title, descriptions, textRight, textLeft; + final void Function()? leftFunc, rightFunc; + final Icon? icon; + + const CustomDialogBox( + {required Key key, + this.title, + this.descriptions, + this.textRight, + this.textLeft, + this.leftFunc, + this.rightFunc, + this.icon}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: contentBox(context), + ); + } + + contentBox(context) { + return Stack( + children: [ + Container( + padding: const EdgeInsets.only( + left: 20, top: 45.0 + 20.0, right: 20, bottom: 20), + margin: const EdgeInsets.only(top: 45), + decoration: BoxDecoration( + shape: BoxShape.rectangle, + color: const Color(0xFF2d3447), + borderRadius: BorderRadius.circular(20), + boxShadow: const [ + BoxShadow( + color: Colors.black, offset: Offset(0, 1), blurRadius: 10), + ]), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title ?? "", + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600, + color: Colors.white), + ), + const SizedBox( + height: 15, + ), + Text( + descriptions ?? "", + style: const TextStyle(fontSize: 14, color: Colors.white), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 22, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: leftFunc ?? + () { + Navigator.of(context).pop(); + }, + child: Text( + textLeft ?? "", + style: const TextStyle(fontSize: 18), + )), + TextButton( + onPressed: rightFunc ?? + () { + Navigator.of(context).pop(); + }, + child: Text( + textRight ?? "", + style: const TextStyle(fontSize: 18, color: Colors.red), + )) + ], + ), + ], + ), + ), + Positioned( + left: 20, + right: 20, + child: CircleAvatar( + backgroundColor: Colors.transparent, + radius: 45, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(45)), + child: icon), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/layout_elements.dart b/lib/widgets/layout_elements.dart new file mode 100644 index 0000000..820d5b4 --- /dev/null +++ b/lib/widgets/layout_elements.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:simple_wake_on_lan/widgets/universal_ui_components.dart'; + +import '../constants.dart'; + +/// +class TextTitle extends StatelessWidget { + final String? title; + final List children; + + const TextTitle({super.key, this.title, required this.children}); + + @override + Widget build(BuildContext context) { + // add a sized box between each button + for (int i = 1; i < children.length; i += 2) { + children.insert(i, const SizedBox(height: 5)); + } + return Padding( + padding: const EdgeInsets.only(top: 12, bottom: 3), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (title != null) + Text( + title!, + style: Theme.of(context).textTheme.headlineSmall, + ), + Padding( + padding: const EdgeInsets.only(left: 5, top: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children)) + ]), + ); + } +} + +/// +class TextSubtitle extends StatelessWidget { + final String title; + final Widget child; + + const TextSubtitle({super.key, required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(title, style: Theme.of(context).textTheme.titleMedium), + child + ], + ); + } +} + +/// A TextBox in the form of a Material Card with text in it +class TextBox extends StatelessWidget { + final String text; + + const TextBox({super.key, required this.text}); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + color: + Theme.of(context).colorScheme.secondaryContainer, //primaryContainer + child: Padding( + padding: const EdgeInsets.all(10), + child: Center(child: Text(text)))); + } +} + +/// TODO control, if the button is expanded or not +class IconTextButton extends StatelessWidget { + final String text; + final IconData icon; + final VoidCallback? onPressed; + + const IconTextButton( + {super.key, required this.text, required this.icon, this.onPressed}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: FilledButton.tonal( + onPressed: onPressed, + child: IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + //crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon(icon), + const SizedBox(width: 10), + Text(text), + ], + ), + ), + ), + ); + } +} + +class ActionButton extends StatelessWidget { + final String text; + final Icon icon; + final VoidCallback? onPressed; + + const ActionButton( + {super.key, required this.text, required this.icon, this.onPressed}); + + @override + Widget build(BuildContext context) { + return FloatingActionButton.extended( + onPressed: onPressed, + tooltip: text, + label: Text(text), + icon: icon, + enableFeedback: true, + ); + } +} + +/// Creates multiple Widgets in a row with a sized box between each button. Recommended to use with 2 Elements +class SpacedRow extends StatelessWidget { + final List children; + + const SpacedRow({super.key, required this.children}); + + @override + Widget build(BuildContext context) { + // add a sized box between each button + for (int i = 1; i < children.length; i += 2) { + children.insert(i, const SizedBox(width: 15)); + } + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: children, + ); + } +} + +class CustomCard extends StatelessWidget { + final VoidCallback? onTap; + final String? title, subtitle, deviceType; + final Widget? trailing; + + const CustomCard( + {super.key, + this.onTap, + this.title, + this.subtitle, + this.deviceType, + this.trailing}); + + @override + Widget build(BuildContext context) { + return Card( + elevation: 0, + color: + Theme.of(context).colorScheme.secondaryContainer, //primaryContainer + child: InkWell( + borderRadius: AppConstants.borderRadius, + onTap: onTap, + child: ListTile( + title: title != null ? Text(title!) : null, + subtitle: subtitle != null ? Text(subtitle!) : null, + minLeadingWidth: 0, + // ignore: sized_box_for_whitespace + leading: deviceType != null && getIcon(deviceType!) != null + ? SizedBox( + height: double.infinity, + child: Icon( + getIcon(deviceType!), + )) + : null, + trailing: trailing, + ), + )); + } +} diff --git a/lib/widgets/universal_ui_components.dart b/lib/widgets/universal_ui_components.dart new file mode 100644 index 0000000..b9c8cab --- /dev/null +++ b/lib/widgets/universal_ui_components.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import '../constants.dart'; +import '../screens/home/bottom_sheet_form.dart'; +import 'chip_cards.dart'; + +void showCustomBottomSheet( + {required BuildContext context, required ModularBottomFormPage formPage}) { + showModalBottomSheet( + isScrollControlled: true, + // only expand the bottom sheet to 85% of the screen height + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.85, + ), + context: context, + backgroundColor: Theme.of(context).colorScheme.surface, + builder: (context) => formPage, + ); +} + +Widget customDualChoiceAlertdialog( + {String? title, + Widget? child, + IconData? icon, + Color? iconColor, + String? leftText, + String? rightText, + Color? leftColor, + Color? rightColor, + IconData? leftIcon, + IconData? rightIcon, + Function()? leftOnPressed, + Function()? rightOnPressed}) { + return customAlertdialog( + title: title, + child: child, + icon: icon, + iconColor: iconColor, + actions: (leftIcon != null || + rightIcon != null || + leftText != null || + rightText != null) + ? [ + TextButton( + onPressed: leftOnPressed, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (leftIcon != null) Icon(leftIcon), + const SizedBox(width: 5), + if (leftText != null) + Text(leftText, style: TextStyle(color: leftColor)), + ], + ), + ), + TextButton( + onPressed: rightOnPressed, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (rightIcon != null) Icon(rightIcon), + const SizedBox(width: 5), + if (rightText != null) + Text( + rightText, + style: TextStyle(color: rightColor), + ), + ], + ), + ), + ] + : null, + ); +} + +Widget customAlertdialog( + {String? title, + Widget? child, + IconData? icon, + Color? iconColor, + List? actions}) { + return AlertDialog( + title: title != null ? Text(title) : null, + icon: icon != null + ? Icon( + icon, + size: 80, + color: iconColor, + ) + : null, + content: SingleChildScrollView( + child: child, + ), + actions: actions, + ); +} + +/// get the icon for a specific [deviceType]. If the device type is not found, return null +/// uses the AppConstants().getChipsDeviceTypes() list and searches for the device type matching the given deviceType +/// orElse in the firstWhere function is needed if the deviceType is not found in the list so no state error is thrown +IconData? getIcon(String? deviceType) { + return AppConstants() + .getChipsDeviceTypes() + .firstWhere((element) => element.value == deviceType, + orElse: () => const CustomChoiceChip(value: '')) + .icon; +} diff --git a/privacyPolicy.md b/privacyPolicy.md new file mode 100644 index 0000000..a56845e --- /dev/null +++ b/privacyPolicy.md @@ -0,0 +1,62 @@ +**Privacy Policy** + +Henrik Herzig built the Sample Wake On Lan app as an Open Source app. This SERVICE is provided by Henrik Herzig at no cost and is intended for use as is. + +This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service. + +If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy. + +**Information Collection and Use** + +For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. The information that I request will be retained on your device and is not collected by me in any way. + +The app does use third-party services that may collect information used to identify you. + +Link to the privacy policy of third-party service providers used by the app + +* [Google Play Services](https://www.google.com/policies/privacy/) + +**Log Data** + +I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third-party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics. + +**Cookies** + +Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory. + +This Service does not use these “cookies” explicitly. However, the app may use third-party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service. + +**Service Providers** + +I may employ third-party companies and individuals due to the following reasons: + +* To facilitate our Service; +* To provide the Service on our behalf; +* To perform Service-related services; or +* To assist us in analyzing how our Service is used. + +I want to inform users of this Service that these third parties have access to their Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose. + +**Security** + +I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security. + +**Links to Other Sites** + +This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. + +**Children’s Privacy** + +These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13 years of age. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do the necessary actions. + +**Changes to This Privacy Policy** + +I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. + +This policy is effective as of 2023-01-11 + +**Contact Us** + +If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at dev.homebrewed@google.com. + +This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.nisrulz.com/) diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..76cb975 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,999 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "405666cd3cf0ee0a48d21ec67e65406aad2c726d9fa58840d3375e7bdcd32a07" + url: "https://pub.dev" + source: hosted + version: "60.0.0" + adaptive_theme: + dependency: "direct main" + description: + name: adaptive_theme + sha256: "61bde10390e937d11d05c6cf0d5cf378a73d49f9a442262e43613dae60ed0b3f" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "1952250bd005bacb895a01bf1b4dc00e3ba1c526cf47dca54dfe24979c65f5b3" + url: "https://pub.dev" + source: hosted + version: "5.12.0" + archive: + dependency: transitive + description: + name: archive + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + url: "https://pub.dev" + source: hosted + version: "3.3.7" + args: + dependency: transitive + description: + name: args + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + async: + dependency: transitive + description: + name: async + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" + source: hosted + version: "2.10.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "7dd62d9faf105c434f3d829bbe9c4be02ec67f5ed94832222116122df67c5452" + url: "https://pub.dev" + source: hosted + version: "8.6.0" + change_app_package_name: + dependency: "direct dev" + description: + name: change_app_package_name + sha256: f9ebaf68a4b5a68c581492579bb68273c523ef325fbf9ce2f1b57fb136ad023b + url: "https://pub.dev" + source: hosted + version: "1.1.0" + characters: + dependency: transitive + description: + name: characters + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" + source: hosted + version: "1.2.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" + source: hosted + version: "4.4.0" + collection: + dependency: transitive + description: + name: collection + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" + source: hosted + version: "1.17.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" + url: "https://pub.dev" + source: hosted + version: "1.6.3" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" + crypto: + dependency: transitive + description: + name: crypto + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" + source: hosted + version: "1.0.5" + dart_ping: + dependency: "direct main" + description: + name: dart_ping + sha256: "623cce963c66643cb26661187651a1c69ec09ac3a8bae3a17717aed0e17c0054" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + dart_ping_ios: + dependency: "direct main" + description: + name: dart_ping_ios + sha256: a79e25f2227add97c90ea64dfe86421d2c6e3c4ba3dc826b648dbf99a4bd476f + url: "https://pub.dev" + source: hosted + version: "2.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + url: "https://pub.dev" + source: hosted + version: "2.3.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" + source: hosted + version: "0.7.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: "0d923fb610d0abf67f2149c3a50ef85f78bebecfc4d645719ca70bcf4abc788f" + url: "https://pub.dev" + source: hosted + version: "5.2.7" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_icmp_ping: + dependency: transitive + description: + name: flutter_icmp_ping + sha256: a06c2255a857c8f9d1b0a68f546b113557e48e7a543f91e38bd66aeab296f3a6 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_launcher_icons: + dependency: "direct dev" + description: + name: flutter_launcher_icons + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" + source: hosted + version: "0.13.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: c224ac897bed083dabf11f238dd11a239809b446740be0c2044608c50029ffdf + url: "https://pub.dev" + source: hosted + version: "2.0.9" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227" + url: "https://pub.dev" + source: hosted + version: "4.0.15" + intl: + dependency: "direct main" + description: + name: intl + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" + source: hosted + version: "0.17.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" + source: hosted + version: "4.8.0" + lan_scanner: + dependency: "direct main" + description: + name: lan_scanner + sha256: cb3ece64ab8eacb5ce367b792e1ceedfbf4d6e74d8bb4184919c7f832742967c + url: "https://pub.dev" + source: hosted + version: "3.5.0" + lints: + dependency: transitive + description: + name: lints + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + url: "https://pub.dev" + source: hosted + version: "0.12.13" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + meta: + dependency: transitive + description: + name: meta + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" + source: hosted + version: "1.8.0" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mockito: + dependency: "direct main" + description: + name: mockito + sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + multicast_dns: + dependency: "direct main" + description: + name: multicast_dns + sha256: "80e54aba906a7cc68fdc6a201e76b135af27155e2f8e958181d85e2b73786591" + url: "https://pub.dev" + source: hosted + version: "0.3.2+3" + network_info_plus: + dependency: "direct main" + description: + name: network_info_plus + sha256: "5e31db917c9dbaaca295685d68d78810a543bd32feb6f1cb7d0c4e5dea924517" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + sha256: "881f5029c5edaf19c616c201d3d8b366c5b1384afd5c1da5a49e4345de82fb8b" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + network_tools: + dependency: "direct main" + description: + name: network_tools + sha256: c4d52c61fed76ce133f1421291f4e8dbca38d758ee87dd3e0821a17d7c671cd5 + url: "https://pub.dev" + source: hosted + version: "3.0.0+3" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" + source: hosted + version: "1.8.2" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 + url: "https://pub.dev" + source: hosted + version: "2.0.14" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" + url: "https://pub.dev" + source: hosted + version: "2.0.24" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + url: "https://pub.dev" + source: hosted + version: "2.1.10" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + url: "https://pub.dev" + source: hosted + version: "2.1.5" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + process_run: + dependency: transitive + description: + name: process_run + sha256: "1142d7f4f0c3f36393a1319406efcf481def2b6d862b2bf600c8ae8fa74d5bd8" + url: "https://pub.dev" + source: hosted + version: "0.12.5+2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "82ddd4ab9260c295e6e39612d4ff00390b9a7a21f1bb1da771e2f232d80ab8a1" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "7fa90471a6875d26ad78c7e4a675874b2043874586891128dc5899662c97db46" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "0c1c16c56c9708aa9c361541a6f0e5cc6fc12a3232d866a687a7b7db30032b07" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: a5fcd2d25eeadbb6589e80198a47d6a464ba3e2049da473943b8af9797900c2d + url: "https://pub.dev" + source: hosted + version: "1.22.0" + test_api: + dependency: transitive + description: + name: test_api + sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + url: "https://pub.dev" + source: hosted + version: "0.4.16" + test_core: + dependency: transitive + description: + name: test_core + sha256: "0ef9755ec6d746951ba0aabe62f874b707690b5ede0fecc818b138fcc9b14888" + url: "https://pub.dev" + source: hosted + version: "0.4.20" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" + source: hosted + version: "6.1.10" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8 + url: "https://pub.dev" + source: hosted + version: "6.0.26" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" + source: hosted + version: "2.0.16" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd + url: "https://pub.dev" + source: hosted + version: "3.0.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + url: "https://pub.dev" + source: hosted + version: "9.4.0" + wake_on_lan: + dependency: "direct main" + description: + name: wake_on_lan + sha256: "5c467487b9ba1b032da782e285b41ae9b247b9c8eb4bfeca8b8fc861ad3dbe31" + url: "https://pub.dev" + source: hosted + version: "3.1.0+6" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + win32: + dependency: transitive + description: + name: win32 + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" + source: hosted + version: "3.1.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + xml: + dependency: transitive + description: + name: xml + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + yaml: + dependency: transitive + description: + name: yaml + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.19.2 <3.0.0" + flutter: ">=3.3.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..c73df49 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,123 @@ +name: simple_wake_on_lan +description: A cross platform app for waking machines on the network with WakeOnLan +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: '>=2.19.2 <3.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # Localisation + flutter_localizations: + sdk: flutter + intl: any + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + + + multicast_dns: ^0.3.2+3 + lan_scanner: ^3.5.0 + network_tools: ^3.0.0+3 + dart_ping: ^7.0.2 + dart_ping_ios: ^2.0.1 + network_info_plus: ^3.0.2 + path_provider: ^2.0.14 + uuid: ^3.0.7 + wake_on_lan: ^3.1.0+6 + share_plus: ^6.3.1 + file_picker: ^5.2.7 + url_launcher: ^6.1.10 + adaptive_theme: ^3.2.0 + mockito: ^5.4.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + + # Generate App Icon for Android and iOS + flutter_launcher_icons: "^0.13.1" + + change_app_package_name: ^1.1.0 + test: ^1.22.0 + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/icon.png" + min_sdk_android: 21 # android min sdk min:16, default 21 + +# 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 packages. +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. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages + generate: true diff --git a/test/unit-tests/database_test.dart b/test/unit-tests/database_test.dart new file mode 100644 index 0000000..d6a3b99 --- /dev/null +++ b/test/unit-tests/database_test.dart @@ -0,0 +1,131 @@ +// import 'package:test/test.dart'; +// import 'package:mockito/mockito.dart'; +// import 'package:path_provider/path_provider.dart'; +// import 'dart:io'; +// import 'dart:convert'; +// import 'package:uuid/uuid.dart'; +// import 'package:simple_wake_on_lan/services/data.dart'; +// import 'package:simple_wake_on_lan/services/database.dart'; +// +// class MockFile extends Mock implements File {} +// +// class MockDirectory extends Mock implements Directory {} +// +// class MockDeviceStorage extends Mock implements DeviceStorage {} +// +// void main() { +// group('DeviceStorage', () { +// final mockFile = MockFile(); +// final mockDirectory = MockDirectory(); +// final mockDeviceStorage = MockDeviceStorage(); +// +// setUp(() { +// when(mockDirectory.path).thenReturn('/path/to/documents'); +// when(mockFile.path).thenReturn('/path/to/documents/devices.json'); +// when(getApplicationDocumentsDirectory()) +// .thenAnswer((_) => Future.value(mockDirectory)); +// }); +// +// test('getFilePath returns the correct file path', () async { +// final deviceStorage = DeviceStorage(); +// final filePath = await deviceStorage.getFilePath(); +// expect(filePath, '/path/to/documents/devices.json'); +// }); +// +// test('loadDevices returns an empty list when the file does not exist', +// () async { +// when(mockFile.readAsString()).thenThrow(FileSystemException()); +// final deviceStorage = DeviceStorage(); +// final devices = await deviceStorage.loadDevices(); +// expect(devices, isEmpty); +// }); +// +// test('loadDevices returns an empty list when the file is empty', () async { +// when(mockFile.readAsString()).thenAnswer((_) => Future.value('')); +// final deviceStorage = DeviceStorage(); +// final devices = await deviceStorage.loadDevices(); +// expect(devices, isEmpty); +// }); +// +// test('loadDevices returns a list of devices when the file is valid', +// () async { +// final jsonData = [ +// {'id': '1', 'name': 'Device 1', 'modified': '2022-01-01T00:00:00.000Z', 'type': 'network'} +// ]; +// final jsonString = json.encode(jsonData); +// when(mockFile.readAsString()).thenAnswer((_) => Future.value(jsonString)); +// final deviceStorage = DeviceStorage(); +// final devices = await deviceStorage.loadDevices(); +// expect(devices, hasLength(1)); +// expect(devices.first.id, '1'); +// expect(devices.first.hostName, 'Device 1'); +// expect(devices.first.modified, DateTime.utc(2022, 1, 1)); +// expect(devices.first.deviceType, 'network'); +// }); +// +// test('saveDevices writes the correct data to the file', () async { +// final device = NetworkDevice(hostName: 'Device 1', ipAddress: '192.168.1.1', macAddress: '00:11:22:33:44:55'); +// final storageDevice = StorageDevice( +// id: '1', hostName: 'Device 1', modified: DateTime.now(), deviceType: 'network', ipAddress: '192.168.1.1', macAddress: '00:11:22:33:44:55'); +// final devices = [storageDevice]; +// final jsonData = [ +// {'id': '1', 'name': 'Device 1', 'modified': storageDevice.modified.toIso8601String(), 'type': 'network', 'ipAddress': '192.168.1.1', 'macAddress': '00:11:22:33:44:55'} +// ]; +// final jsonString = json.encode(jsonData); +// when(mockFile.writeAsString(any)).thenAnswer((_) => Future.value()); +// final deviceStorage = DeviceStorage(); +// await deviceStorage.saveDevices(devices); +// verify(mockFile.writeAsString(jsonString)).called(1); +// }); +// +// test('addDevice adds a device to the list of devices', () async { +// final device = NetworkDevice(id: '1', name: 'Device 1', ipAddress: '192.168.1.1', macAddress: '00:11:22:33:44:55'); +// final storageDevice = StorageDevice( +// id: '1', name: 'Device 1', modified: DateTime.now(), type: 'network', ipAddress: '192.168.1.1', macAddress: '00:11:22:33:44:55'); +// final devices = [storageDevice]; +// when(mockDeviceStorage.loadDevices()).thenAnswer((_) => Future.value(devices)); +// when(mockDeviceStorage.saveDevices(any)).thenAnswer((_) => Future.value()); +// when(Uuid().v1()).thenReturn('2'); +// final updatedDevices = await mockDeviceStorage.addDevice(device); +// expect(updatedDevices, hasLength(2)); +// expect(updatedDevices.last.id, '2'); +// expect(updatedDevices.last.name, 'Device 1'); +// expect(updatedDevices.last.type, 'network'); +// expect(updatedDevices.last.ipAddress, '192.168.1.1'); +// expect(updatedDevices.last.macAddress, '00:11:22:33:44:55'); +// }); +// +// test('updateDevice updates a device in the list of devices', () async { +// final device = StorageDevice( +// id: '1', name: 'Device 1', modified: DateTime.now(), type: 'network', ipAddress: '192.168.1.1', macAddress: '00:11:22:33:44:55'); +// final devices = [device]; +// final updatedDevice = device.copyWith(name: 'Updated Device'); +// when(mockDeviceStorage.loadDevices()).thenAnswer((_) => Future.value(devices)); +// when(mockDeviceStorage.saveDevices(any)).thenAnswer((_) => Future.value()); +// final updatedDevices = await mockDeviceStorage.updateDevice(updatedDevice); +// expect(updatedDevices, hasLength(1)); +// expect(updatedDevices.first.id, '1'); +// expect(updatedDevices.first.name, 'Updated Device'); +// expect(updatedDevices.first.type, 'network'); +// expect(updatedDevices.first.ipAddress, '192.168.1.1'); +// expect(updatedDevices.first.macAddress, '00:11:22:33:44:55'); +// }); +// +// test('deleteDevice deletes a device from the list of devices', () async { +// final device = StorageDevice( +// id: '1', name: 'Device 1', modified: DateTime.now(), type: 'network', ipAddress: '192.168.1.1', macAddress: '00:11:22:33:44:55'); +// final devices = [device]; +// when(mockDeviceStorage.loadDevices()).thenAnswer((_) => Future.value(devices)); +// when(mockDeviceStorage.saveDevices(any)).thenAnswer((_) => Future.value()); +// final updatedDevices = await mockDeviceStorage.deleteDevice('1'); +// expect(updatedDevices, isEmpty); +// }); +// +// test('deleteAllDevices deletes the devices file', () async { +// when(mockFile.delete()).thenAnswer((_) => Future.value()); +// final deviceStorage = DeviceStorage(); +// await deviceStorage.deleteAllDevices(); +// verify(mockFile.delete()).called(1); +// }); +// }); +// } diff --git a/test/widget-test/widget_test.dart b/test/widget-test/widget_test.dart new file mode 100644 index 0000000..afef8e1 --- /dev/null +++ b/test/widget-test/widget_test.dart @@ -0,0 +1,35 @@ +// // This is a basic Flutter widget test. +// // +// // To perform an interaction with a widget in your test, use the WidgetTester +// // utility in the flutter_test package. For example, you can send tap and scroll +// // gestures. You can also use WidgetTester to find child widgets in the widget +// // tree, read text, and verify that the values of widget properties are correct. +// +// import 'package:adaptive_theme/adaptive_theme.dart'; +// import 'package:dart_ping_ios/dart_ping_ios.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_test/flutter_test.dart'; +// +// import 'package:simple_wake_on_lan/main.dart'; +// +// void main() { +// testWidgets('Counter increments smoke test', (WidgetTester tester) async { +// // // Build our app and trigger a frame. +// // DartPingIOS.register(); +// // WidgetsFlutterBinding.ensureInitialized(); +// // final savedThemeMode = await AdaptiveTheme.getThemeMode(); +// // await tester.pumpWidget(MyApp(savedThemeMode: savedThemeMode)); +// +// // // Verify that our counter starts at 0. +// // expect(find.text('0'), findsOneWidget); +// // expect(find.text('1'), findsNothing); +// +// // // Tap the '+' icon and trigger a frame. +// // await tester.tap(find.byIcon(Icons.add)); +// // await tester.pump(); +// +// // // Verify that our counter has incremented. +// // expect(find.text('0'), findsNothing); +// // expect(find.text('1'), findsOneWidget); +// }); +// }