├── .github
└── workflows
│ └── ubuntu.yml
├── .gitignore
├── .metadata
├── CITATION.cff
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── seemoo_lab_21_22
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── OpenHaystackIcon.png
└── accessory_icons
│ ├── bug_report.png
│ ├── business_center.png
│ ├── credit_card.png
│ ├── directions_car.png
│ ├── directions_walk.png
│ ├── favorite.png
│ ├── language.png
│ ├── pedal_bike.png
│ ├── pets.png
│ ├── place.png
│ ├── push_pin.png
│ ├── redeem.png
│ ├── school.png
│ ├── visibility.png
│ ├── vpn_key.png
│ └── work.png
├── lib
├── accessory
│ ├── accessory_color_selector.dart
│ ├── accessory_detail.dart
│ ├── accessory_dto.dart
│ ├── accessory_icon.dart
│ ├── accessory_icon_model.dart
│ ├── accessory_icon_selector.dart
│ ├── accessory_list.dart
│ ├── accessory_list_item.dart
│ ├── accessory_list_item_placeholder.dart
│ ├── accessory_model.dart
│ ├── accessory_registry.dart
│ └── no_accessories.dart
├── dashboard
│ ├── accessory_map_list_vert.dart
│ ├── dashboard_desktop.dart
│ └── dashboard_mobile.dart
├── deployment
│ ├── code_block.dart
│ ├── deployment_details.dart
│ ├── deployment_email.dart
│ ├── deployment_esp32.dart
│ ├── deployment_instructions.dart
│ ├── deployment_linux_hci.dart
│ ├── deployment_nrf51.dart
│ └── hyperlink.dart
├── ffi
│ ├── bridge_generated.dart
│ ├── bridge_generated.io.dart
│ ├── bridge_generated.web.dart
│ ├── ffi.dart
│ └── ffi_web.dart
├── findMy
│ ├── decrypt_reports.dart
│ ├── find_my_controller.dart
│ ├── models.dart
│ └── reports_fetcher.dart
├── history
│ ├── accessory_history.dart
│ └── days_selection_slider.dart
├── item_management
│ ├── accessory_color_input.dart
│ ├── accessory_icon_input.dart
│ ├── accessory_id_input.dart
│ ├── accessory_name_input.dart
│ ├── accessory_pk_input.dart
│ ├── item_creation.dart
│ ├── item_export.dart
│ ├── item_file_import.dart
│ ├── item_import.dart
│ ├── item_management.dart
│ ├── loading_spinner.dart
│ └── new_item_action.dart
├── location
│ └── location_model.dart
├── main.dart
├── map
│ └── map.dart
├── placeholder
│ ├── avatar_placeholder.dart
│ └── text_placeholder.dart
├── preferences
│ ├── preferences_page.dart
│ └── user_preferences_model.dart
└── splashscreen.dart
├── native
├── Cargo.lock
├── Cargo.toml
└── src
│ ├── api.rs
│ ├── bridge_generated.io.rs
│ ├── bridge_generated.rs
│ ├── bridge_generated.web.rs
│ └── lib.rs
├── pubspec.lock
├── pubspec.yaml
├── test
└── widget_test.dart
└── web
├── favicon.png
├── icons
├── Icon-192.png
├── Icon-512.png
├── Icon-maskable-192.png
└── Icon-maskable-512.png
├── index.html
└── manifest.json
/.github/workflows/ubuntu.yml:
--------------------------------------------------------------------------------
1 | name: Release (Android, Web)
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - uses: actions/setup-java@v2
11 | with:
12 | distribution: "zulu"
13 | java-version: "21"
14 | cache: "gradle"
15 | - uses: subosito/flutter-action@v2
16 | with:
17 | channel: stable
18 | flutter-version: 3.29.1
19 | cache: true
20 | - uses: dtolnay/rust-toolchain@master
21 | with:
22 | toolchain: nightly
23 | targets: aarch64-linux-android,armv7-linux-androideabi,x86_64-linux-android
24 | - name: Setup Rust tools for Android build
25 | run: |
26 | cargo install cargo-ndk
27 | - name: Setup Keystore
28 | env:
29 | ANDROID_KEYSTORE: ${{ secrets.ANDROID_KEYSTORE }}
30 | ANDROID_KEY_PROPERTIES: ${{ secrets.ANDROID_KEY_PROPERTIES }}
31 | run: |
32 | echo $ANDROID_KEYSTORE | base64 --decode > ./android/app/key.jks
33 | echo $ANDROID_KEY_PROPERTIES | base64 --decode > ./android/key.properties
34 | - name: Build for Android
35 | env:
36 | SDK_REGISTRY_TOKEN: ${{ secrets.MAP_SDK_SECRET_KEY }}
37 | MAP_SDK_PUBLIC_KEY: ${{ secrets.MAP_SDK_PUBLIC_KEY }}
38 | run: |
39 | flutter pub get
40 | flutter build apk --release --dart-define=MAP_SDK_PUBLIC_KEY="$MAP_SDK_PUBLIC_KEY"
41 | - name: Upload Android Artifacts
42 | uses: actions/upload-artifact@v4
43 | with:
44 | name: android
45 | path: ./build/app/outputs/flutter-apk/app-release.apk
46 | - name: Setup Rust tools for Web build
47 | run: |
48 | rustup override set nightly
49 | rustup component add rust-src
50 | rustup target add wasm32-unknown-unknown
51 | cargo install wasm-pack
52 | - name: Build for Web
53 | env:
54 | RUSTUP_TOOLCHAIN: nightly
55 | RUSTFLAGS: -C target-feature=+atomics,+bulk-memory,+mutable-globals
56 | SDK_REGISTRY_TOKEN: ${{ secrets.MAP_SDK_SECRET_KEY }}
57 | MAP_SDK_PUBLIC_KEY: ${{ secrets.MAP_SDK_PUBLIC_KEY }}
58 | run: |
59 | wasm-pack build -t no-modules -d ./../web/pkg --no-typescript --out-name native native -- -Z build-std=std,panic_abort
60 | flutter build web --release --dart-define=MAP_SDK_PUBLIC_KEY="$MAP_SDK_PUBLIC_KEY"
61 | - name: Upload Web Artifacts
62 | uses: actions/upload-artifact@v4
63 | with:
64 | name: web
65 | path: ./build/web
66 | - name: Publish to Cloudflare Pages
67 | uses: cloudflare/pages-action@1
68 | with:
69 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
70 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
71 | projectName: openhaystack
72 | directory: ./build/web
73 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | **/ios/Flutter/.last_build_id
26 | .dart_tool/
27 | .flutter-plugins
28 | .flutter-plugins-dependencies
29 | .packages
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Web related
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Obfuscation related
40 | app.*.map.json
41 |
42 | # Android Studio will place build artifacts here
43 | /android/app/debug
44 | /android/app/profile
45 | /android/app/release
46 |
47 | # FFI
48 | jniLibs
49 |
50 | # Rust
51 | /native/target
52 |
53 | # VSCode
54 | .vscode
55 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 18116933e77adc82f80866c928266a5b4f1ed645
8 | channel: stable
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/CITATION.cff:
--------------------------------------------------------------------------------
1 | # This CITATION.cff file was generated with cffinit.
2 | # Visit https://bit.ly/cffinit to generate yours today!
3 |
4 | cff-version: 1.2.0
5 | title: OpenHaystack
6 | message: 'If you use this software, please cite it as below.'
7 | type: software
8 | authors:
9 | - given-names: Alexander
10 | family-names: Heinrich
11 | affiliation: 'SEEMOO, TU Darmstadt'
12 | orcid: 'https://orcid.org/0000-0002-1150-1922'
13 | - given-names: Milan
14 | family-names: Stute
15 | affiliation: 'SEEMOO, TU Darmstadt'
16 | orcid: 'https://orcid.org/0000-0003-4921-8476'
17 | - given-names: Matthias
18 | family-names: Hollick
19 | affiliation: 'SEEMOO, TU Darmstadt'
20 | orcid: 'https://orcid.org/0000-0002-9163-5989'
21 | repository-code: 'https://github.com/seemoo-lab/openhaystack'
22 | abstract: >-
23 | OpenHaystack is a framework for tracking personal
24 | Bluetooth devices via Apple's massive Find My network. Use
25 | it to create your own tracking tags that you can append to
26 | physical objects (keyrings, backpacks, ...) or integrate
27 | it into other Bluetooth-capable devices such as notebooks.
28 | license: AGPL-3.0
29 | commit: 7d72fa1ac19d2a9f6dec43011be07df8976a8b02
30 | version: 0.5.3
31 | date-released: '2023-10-09'
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a fork of [SEEMOO's Openhaystack mobile app](https://github.com/seemoo-lab/openhaystack/tree/main/OpenHaystack)
2 |
3 | # Main improvements over the original project
4 | - Vector map: smoother, faster and prettier map
5 | - Performance improvements: less than 2 seconds to decrypt 2000 history locations (even on your browser!)
6 | - Server endpoint as user preference: bring your how Openhaystack server without recompiling
7 | - Better history visualization: color encoded points
8 | - Web version: try it now at https://find.willian.wang/
9 |
10 | # About OpenHaystack
11 | OpenHaystack is a project that allows location tracking of Bluetooth Low Energy (BLE) devices over Apples Find My Network.
12 |
13 | # About the code
14 | 🚧
15 |
16 | For now, it's possible to build it by reproducing the [Github Actions workflow](https://github.com/wangwillian0/openhaystack/blob/main/.github/workflows/ubuntu.yml)
17 |
18 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 |
28 | # Additional information about this file can be found at
29 | # https://dart.dev/guides/language/analysis-options
30 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | id "dev.flutter.flutter-gradle-plugin"
5 | }
6 |
7 | def keystorePropertiesFile = rootProject.file("key.properties")
8 | def keystoreProperties = new Properties()
9 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
10 |
11 | def localProperties = new Properties()
12 | def localPropertiesFile = rootProject.file('local.properties')
13 | if (localPropertiesFile.exists()) {
14 | localPropertiesFile.withReader('UTF-8') { reader ->
15 | localProperties.load(reader)
16 | }
17 | }
18 |
19 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
20 | if (flutterVersionCode == null) {
21 | flutterVersionCode = '1'
22 | }
23 |
24 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
25 | if (flutterVersionName == null) {
26 | flutterVersionName = '1.0'
27 | }
28 |
29 | android {
30 | compileSdkVersion 35
31 |
32 | kotlinOptions {
33 | jvmTarget = '1.8'
34 | }
35 |
36 | sourceSets {
37 | main.java.srcDirs += 'src/main/kotlin'
38 | }
39 |
40 | defaultConfig {
41 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
42 | applicationId "de.seemoo.android.openhaystack"
43 | minSdkVersion 21
44 | targetSdkVersion 30
45 | versionCode flutterVersionCode.toInteger()
46 | versionName flutterVersionName
47 | }
48 |
49 | signingConfigs {
50 | release {
51 | storeFile file(keystoreProperties['storeFile'])
52 | storePassword keystoreProperties['storePassword']
53 | keyAlias keystoreProperties['keyAlias']
54 | keyPassword keystoreProperties['keyPassword']
55 | }
56 | }
57 |
58 | buildTypes {
59 | release {
60 | minifyEnabled false
61 | shrinkResources false
62 | signingConfig signingConfigs.release
63 | }
64 | }
65 | }
66 |
67 | flutter {
68 | source '../..'
69 | }
70 |
71 | [
72 | Debug: null,
73 | Profile: '--release',
74 | Release: '--release'
75 | ].each {
76 | def taskPostfix = it.key
77 | def profileMode = it.value
78 | tasks.whenTaskAdded { task ->
79 | if (task.name == "javaPreCompile$taskPostfix") {
80 | task.dependsOn "cargoBuild$taskPostfix"
81 | }
82 | }
83 | tasks.register("cargoBuild$taskPostfix", Exec) {
84 | workingDir "../../native"
85 | commandLine 'cargo', 'ndk',
86 | '-t', 'armeabi-v7a',
87 | '-t', 'arm64-v8a',
88 | '-t', 'x86_64',
89 | '-o', '../android/app/src/main/jniLibs', 'build'
90 | if (profileMode != null) {
91 | args profileMode
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
10 |
17 |
21 |
25 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
41 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/seemoo_lab_21_22/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package de.seemoo.android.openhaystack
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | rootProject.buildDir = '../build'
9 | subprojects {
10 | afterEvaluate { project ->
11 | if (project.hasProperty('android')) {
12 | project.android {
13 | if (namespace == null) {
14 | namespace project.group
15 | }
16 | }
17 | }
18 | }
19 | project.buildDir = "${rootProject.buildDir}/${project.name}"
20 | project.evaluationDependsOn(':app')
21 | }
22 |
23 | tasks.register("clean", Delete) {
24 | delete rootProject.buildDir
25 | }
26 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Apr 28 22:51:27 BRT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def flutterSdkPath = {
3 | def properties = new Properties()
4 | file("local.properties").withInputStream { properties.load(it) }
5 | def flutterSdkPath = properties.getProperty("flutter.sdk")
6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7 | return flutterSdkPath
8 | }()
9 |
10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
11 |
12 | repositories {
13 | google()
14 | mavenCentral()
15 | gradlePluginPortal()
16 | }
17 | }
18 |
19 | plugins {
20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
21 | id "com.android.application" version "8.2.1" apply false
22 | id "org.jetbrains.kotlin.android" version "1.8.20" apply false
23 | }
24 |
25 | include ":app"
26 |
--------------------------------------------------------------------------------
/assets/OpenHaystackIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/OpenHaystackIcon.png
--------------------------------------------------------------------------------
/assets/accessory_icons/bug_report.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/bug_report.png
--------------------------------------------------------------------------------
/assets/accessory_icons/business_center.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/business_center.png
--------------------------------------------------------------------------------
/assets/accessory_icons/credit_card.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/credit_card.png
--------------------------------------------------------------------------------
/assets/accessory_icons/directions_car.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/directions_car.png
--------------------------------------------------------------------------------
/assets/accessory_icons/directions_walk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/directions_walk.png
--------------------------------------------------------------------------------
/assets/accessory_icons/favorite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/favorite.png
--------------------------------------------------------------------------------
/assets/accessory_icons/language.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/language.png
--------------------------------------------------------------------------------
/assets/accessory_icons/pedal_bike.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/pedal_bike.png
--------------------------------------------------------------------------------
/assets/accessory_icons/pets.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/pets.png
--------------------------------------------------------------------------------
/assets/accessory_icons/place.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/place.png
--------------------------------------------------------------------------------
/assets/accessory_icons/push_pin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/push_pin.png
--------------------------------------------------------------------------------
/assets/accessory_icons/redeem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/redeem.png
--------------------------------------------------------------------------------
/assets/accessory_icons/school.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/school.png
--------------------------------------------------------------------------------
/assets/accessory_icons/visibility.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/visibility.png
--------------------------------------------------------------------------------
/assets/accessory_icons/vpn_key.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/vpn_key.png
--------------------------------------------------------------------------------
/assets/accessory_icons/work.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wangwillian0/openhaystack/24a641649e3fdeb6145832e2b4881545e01111a3/assets/accessory_icons/work.png
--------------------------------------------------------------------------------
/lib/accessory/accessory_color_selector.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_colorpicker/flutter_colorpicker.dart';
3 |
4 | class AccessoryColorSelector extends StatelessWidget {
5 |
6 | /// This shows a color selector.
7 | ///
8 | /// The color can be selected via a color field or by inputing explicit
9 | /// RGB values.
10 | const AccessoryColorSelector({ Key? key }) : super(key: key);
11 |
12 | /// Displays the color selector with the [initialColor] preselected.
13 | ///
14 | /// The selected color is returned if the user selects the save option.
15 | /// Otherwise the selection is discarded with a null return value.
16 | static Future showColorSelection(BuildContext context, Color initialColor) async {
17 | Color currentColor = initialColor;
18 | return await showDialog(
19 | context: context,
20 | builder: (BuildContext context) {
21 | return AlertDialog(
22 | title: const Text('Pick a color'),
23 | content: SingleChildScrollView(
24 | child: ColorPicker(
25 | hexInputBar: true,
26 | pickerColor: currentColor,
27 | onColorChanged: (Color newColor) {
28 | currentColor = newColor;
29 | },
30 | )
31 | ),
32 | actions: [
33 | ElevatedButton(
34 | child: const Text('Save'),
35 | onPressed: () {
36 | Navigator.pop(context, currentColor);
37 | },
38 | ),
39 | ],
40 | );
41 | },
42 | );
43 | }
44 |
45 | @override
46 | Widget build(BuildContext context) {
47 | throw UnimplementedError();
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_detail.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 | import 'package:openhaystack_mobile/accessory/accessory_color_selector.dart';
4 | import 'package:openhaystack_mobile/accessory/accessory_icon.dart';
5 | import 'package:openhaystack_mobile/accessory/accessory_icon_selector.dart';
6 | import 'package:openhaystack_mobile/accessory/accessory_model.dart';
7 | import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
8 | import 'package:openhaystack_mobile/item_management/accessory_name_input.dart';
9 |
10 | class AccessoryDetail extends StatefulWidget {
11 | Accessory accessory;
12 |
13 | /// A page displaying the editable information of a specific [accessory].
14 | ///
15 | /// This shows the editable information of a specific [accessory] and
16 | /// allows the user to edit them.
17 | AccessoryDetail({
18 | Key? key,
19 | required this.accessory,
20 | }) : super(key: key);
21 |
22 | @override
23 | _AccessoryDetailState createState() => _AccessoryDetailState();
24 | }
25 |
26 | class _AccessoryDetailState extends State {
27 | // An accessory storing the changed values.
28 | late Accessory newAccessory;
29 | final _formKey = GlobalKey();
30 |
31 | @override
32 | void initState() {
33 | // Initialize changed accessory with existing accessory properties.
34 | newAccessory = widget.accessory.clone();
35 | super.initState();
36 | }
37 |
38 | @override
39 | Widget build(BuildContext context) {
40 | return Scaffold(
41 | appBar: AppBar(
42 | title: Text(widget.accessory.name),
43 | ),
44 | body: SingleChildScrollView(
45 | child: Form(
46 | key: _formKey,
47 | child: Column(
48 | children: [
49 | Center(
50 | child: Stack(
51 | children: [
52 | Padding(
53 | padding: const EdgeInsets.all(20),
54 | child: AccessoryIcon(
55 | size: 100,
56 | icon: newAccessory.icon,
57 | color: newAccessory.color,
58 | ),
59 | ),
60 | Positioned(
61 | bottom: 0,
62 | right: 0,
63 | child: Padding(
64 | padding: const EdgeInsets.all(10.0),
65 | child: Container(
66 | decoration: const BoxDecoration(
67 | color: Color.fromARGB(255, 200, 200, 200),
68 | shape: BoxShape.circle,
69 | ),
70 | child: IconButton(
71 | onPressed: () async {
72 | // Show icon selection
73 | String? selectedIcon = await AccessoryIconSelector
74 | .showIconSelection(context, newAccessory.rawIcon, newAccessory.color);
75 | if (selectedIcon != null) {
76 | setState(() {
77 | newAccessory.setIcon(selectedIcon);
78 | });
79 |
80 | // Show color selection only when icon is selected
81 | Color? selectedColor = await AccessoryColorSelector
82 | .showColorSelection(context, newAccessory.color);
83 | if (selectedColor != null) {
84 | setState(() {
85 | newAccessory.color = selectedColor;
86 | });
87 | }
88 | }
89 | },
90 | icon: const Icon(Icons.edit),
91 | ),
92 | ),
93 | ),
94 | ),
95 | ],
96 | ),
97 | ),
98 | AccessoryNameInput(
99 | initialValue: newAccessory.name,
100 | onChanged: (value) {
101 | setState(() {
102 | newAccessory.name = value;
103 | });
104 | },
105 | ),
106 | SwitchListTile(
107 | value: newAccessory.isActive,
108 | title: const Text('Is Active'),
109 | onChanged: (checked) {
110 | setState(() {
111 | newAccessory.isActive = checked;
112 | });
113 | },
114 | ),
115 | SwitchListTile(
116 | value: newAccessory.isDeployed,
117 | title: const Text('Is Deployed'),
118 | onChanged: (checked) {
119 | setState(() {
120 | newAccessory.isDeployed = checked;
121 | });
122 | },
123 | ),
124 | ListTile(
125 | title: OutlinedButton(
126 | child: const Text('Save'),
127 | onPressed: _formKey.currentState == null || !_formKey.currentState!.validate()
128 | ? null : () {
129 | if (_formKey.currentState != null && _formKey.currentState!.validate()) {
130 | // Update accessory with changed values
131 | var accessoryRegistry = Provider.of(context, listen: false);
132 | accessoryRegistry.editAccessory(widget.accessory, newAccessory);
133 | ScaffoldMessenger.of(context).showSnackBar(
134 | const SnackBar(
135 | content: Text('Changes saved!'),
136 | ),
137 | );
138 | }
139 | },
140 | ),
141 | ),
142 | ListTile(
143 | title: ElevatedButton(
144 | style: ButtonStyle(
145 | backgroundColor: MaterialStateProperty.resolveWith(
146 | (Set states) {
147 | return Theme.of(context).colorScheme.error;
148 | },
149 | ),
150 | ),
151 | child: const Text('Delete Accessory', style: TextStyle(color: Colors.white),),
152 | onPressed: () {
153 | // Delete accessory
154 | var accessoryRegistry = Provider.of(context, listen: false);
155 | accessoryRegistry.removeAccessory(widget.accessory);
156 | Navigator.pop(context);
157 | },
158 | ),
159 | ),
160 | ],
161 | ),
162 | ),
163 | ),
164 | );
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_dto.dart:
--------------------------------------------------------------------------------
1 | /// This class is used for de-/serializing data to the JSON transfer format.
2 | class AccessoryDTO {
3 | int id;
4 | List colorComponents;
5 | String name;
6 | double? lastDerivationTimestamp;
7 | String? symmetricKey;
8 | int? updateInterval;
9 | String privateKey;
10 | String icon;
11 | bool isDeployed;
12 | String colorSpaceName;
13 | bool usesDerivation;
14 | String? oldestRelevantSymmetricKey;
15 | bool isActive;
16 |
17 | /// Creates a transfer object to serialize to the JSON export format.
18 | ///
19 | /// This implements the [toJson] method used by the Dart JSON serializer.
20 | /// ```dart
21 | /// var accessoryDTO = AccessoryDTO(...);
22 | /// jsonEncode(accessoryDTO);
23 | /// ```
24 | AccessoryDTO({
25 | required this.id,
26 | required this.colorComponents,
27 | required this.name,
28 | this.lastDerivationTimestamp,
29 | this.symmetricKey,
30 | this.updateInterval,
31 | required this.privateKey,
32 | required this.icon,
33 | required this.isDeployed,
34 | required this.colorSpaceName,
35 | required this.usesDerivation,
36 | this.oldestRelevantSymmetricKey,
37 | required this.isActive,
38 | });
39 |
40 | /// Creates a transfer object from deserialized JSON data.
41 | ///
42 | /// The data is only decoded and not processed further.
43 | ///
44 | /// Typically used with JSON decoder.
45 | /// ```dart
46 | /// String json = '...';
47 | /// var accessoryDTO = AccessoryDTO.fromJSON(jsonDecode(json));
48 | /// ```
49 | ///
50 | /// This implements the [toJson] method used by the Dart JSON serializer.
51 | /// ```dart
52 | /// var accessoryDTO = AccessoryDTO(...);
53 | /// jsonEncode(accessoryDTO);
54 | /// ```
55 | AccessoryDTO.fromJson(Map json)
56 | : id = json['id'],
57 | colorComponents = List.from(json['colorComponents'])
58 | .map((val) => double.parse(val.toString())).toList(),
59 | name = json['name'],
60 | lastDerivationTimestamp = json['lastDerivationTimestamp'] ?? 0,
61 | symmetricKey = json['symmetricKey'] ?? '',
62 | updateInterval = json['updateInterval'] ?? 0,
63 | privateKey = json['privateKey'],
64 | icon = json['icon'],
65 | isDeployed = json['isDeployed'],
66 | colorSpaceName = json['colorSpaceName'],
67 | usesDerivation = json['usesDerivation'] ?? false,
68 | oldestRelevantSymmetricKey = json['oldestRelevantSymmetricKey'] ?? '',
69 | isActive = json['isActive'];
70 |
71 | /// Creates a JSON map of the serialized transfer object.
72 | ///
73 | /// Typically used by JSON encoder.
74 | /// ```dart
75 | /// var accessoryDTO = AccessoryDTO(...);
76 | /// jsonEncode(accessoryDTO);
77 | /// ```
78 | Map toJson() => usesDerivation ? {
79 | // With derivation
80 | 'id': id,
81 | 'colorComponents': colorComponents,
82 | 'name': name,
83 | 'lastDerivationTimestamp': lastDerivationTimestamp,
84 | 'symmetricKey': symmetricKey,
85 | 'updateInterval': updateInterval,
86 | 'privateKey': privateKey,
87 | 'icon': icon,
88 | 'isDeployed': isDeployed,
89 | 'colorSpaceName': colorSpaceName,
90 | 'usesDerivation': usesDerivation,
91 | 'oldestRelevantSymmetricKey': oldestRelevantSymmetricKey,
92 | 'isActive': isActive,
93 | } : {
94 | // Without derivation (skip rolling key params)
95 | 'id': id,
96 | 'colorComponents': colorComponents,
97 | 'name': name,
98 | 'privateKey': privateKey,
99 | 'icon': icon,
100 | 'isDeployed': isDeployed,
101 | 'colorSpaceName': colorSpaceName,
102 | 'usesDerivation': usesDerivation,
103 | 'isActive': isActive,
104 | };
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_icon.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter/foundation.dart';
3 |
4 | class AccessoryIcon extends StatelessWidget {
5 | /// The icon to display.
6 | final IconData icon;
7 | /// The color of the surrounding ring.
8 | final Color color;
9 | /// The size of the icon.
10 | final double size;
11 |
12 | /// Displays the icon in a colored ring.
13 | ///
14 | /// The default size can be adjusted by setting the [size] parameter.
15 | const AccessoryIcon({
16 | Key? key,
17 | this.icon = Icons.help,
18 | this.color = Colors.grey,
19 | this.size = 24,
20 | }) : super(key: key);
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return Container(
25 | decoration: BoxDecoration(
26 | color: Theme.of(context).colorScheme.surface,
27 | shape: BoxShape.circle,
28 | border: Border.all(width: size / 6, color: color),
29 | ),
30 | child: Padding(
31 | padding: EdgeInsets.all(size / 12),
32 | child: Icon(
33 | icon,
34 | size: size,
35 | color: Theme.of(context).colorScheme.onSurface,
36 | ),
37 | ),
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_icon_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class AccessoryIconModel {
4 | /// A list of all available icons
5 | static const List icons = [
6 | "credit_card", "business_center", "work", "vpn_key",
7 | "place", "push_pin", "language", "school",
8 | "redeem", "directions_car", "pedal_bike", "directions_walk",
9 | "favorite", "pets", "bug_report", "visibility",
10 | ];
11 |
12 | /// A mapping from the cupertino icon names to the material icon names.
13 | ///
14 | /// If the icons do not match, so a similar replacement is used.
15 | static const iconMapping = {
16 | 'credit_card': Icons.credit_card,
17 | 'business_center': Icons.business_center,
18 | 'work': Icons.work,
19 | 'vpn_key': Icons.vpn_key,
20 | 'place': Icons.place,
21 | 'push_pin': Icons.push_pin,
22 | 'language': Icons.language,
23 | 'school': Icons.school,
24 | 'redeem': Icons.redeem,
25 | 'directions_car': Icons.directions_car,
26 | 'pedal_bike': Icons.pedal_bike,
27 | 'directions_walk': Icons.directions_walk,
28 | 'favorite': Icons.favorite,
29 | 'pets': Icons.pets,
30 | 'bug_report': Icons.bug_report,
31 | 'visibility': Icons.visibility,
32 | };
33 |
34 | /// Looks up the equivalent material icon for the cupertino icon [iconName].
35 | static IconData? mapIcon(String iconName) {
36 | return iconMapping[iconName];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_icon_selector.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:openhaystack_mobile/accessory/accessory_icon_model.dart';
5 |
6 | typedef IconChangeListener = void Function(String? newValue);
7 |
8 | class AccessoryIconSelector extends StatelessWidget {
9 | /// The existing icon used previously.
10 | final String icon;
11 | /// The existing color used previously.
12 | final Color color;
13 | /// A callback being called when the icon changes.
14 | final IconChangeListener iconChanged;
15 |
16 | /// This show an icon selector.
17 | ///
18 | /// The icon can be selected from a list of available icons.
19 | /// The icons are handled by the cupertino icon names.
20 | const AccessoryIconSelector({
21 | Key? key,
22 | required this.icon,
23 | required this.color,
24 | required this.iconChanged,
25 | }) : super(key: key);
26 |
27 | /// Displays the icon selector with the [currentIcon] preselected in the [highlighColor].
28 | ///
29 | /// The selected icon as a cupertino icon name is returned if the user selects an icon.
30 | /// Otherwise the selection is discarded and a null value is returned.
31 | static Future showIconSelection(BuildContext context, String currentIcon, Color highlighColor) async {
32 | return await showDialog(
33 | context: context,
34 | builder: (BuildContext context) {
35 | return LayoutBuilder(
36 | builder: (context, constraints) => Dialog(
37 | child: GridView.count(
38 | primary: false,
39 | padding: const EdgeInsets.all(20),
40 | crossAxisSpacing: 10,
41 | mainAxisSpacing: 10,
42 | shrinkWrap: true,
43 | crossAxisCount: min((constraints.maxWidth / 80).floor(), 8),
44 | semanticChildCount: AccessoryIconModel.icons.length,
45 | children: AccessoryIconModel.icons
46 | .map((value) => IconButton(
47 | icon: Icon(AccessoryIconModel.mapIcon(value)),
48 | color: value == currentIcon ? highlighColor : null,
49 | onPressed: () { Navigator.pop(context, value); },
50 | )).toList(),
51 | ),
52 | ),
53 | );
54 | }
55 | );
56 | }
57 |
58 | @override
59 | Widget build(BuildContext context) {
60 | return Container(
61 | decoration: const BoxDecoration(
62 | color: Color.fromARGB(255, 200, 200, 200),
63 | shape: BoxShape.circle,
64 | ),
65 | child: IconButton(
66 | onPressed: () async {
67 | String? selectedIcon = await showIconSelection(context, icon, color);
68 | if (selectedIcon != null) {
69 | iconChanged(selectedIcon);
70 | }
71 | },
72 | icon: Icon(AccessoryIconModel.mapIcon(icon)),
73 | ),
74 | );
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_list.dart:
--------------------------------------------------------------------------------
1 | import 'dart:math';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter/foundation.dart';
5 | import 'package:flutter_slidable/flutter_slidable.dart';
6 | import 'package:maps_launcher/maps_launcher.dart';
7 | import 'package:provider/provider.dart';
8 | import 'package:mapbox_gl/mapbox_gl.dart';
9 | import 'package:geolocator/geolocator.dart';
10 | import 'package:openhaystack_mobile/accessory/accessory_list_item.dart';
11 | import 'package:openhaystack_mobile/accessory/accessory_list_item_placeholder.dart';
12 | import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
13 | import 'package:openhaystack_mobile/accessory/no_accessories.dart';
14 | import 'package:openhaystack_mobile/history/accessory_history.dart';
15 | import 'package:openhaystack_mobile/location/location_model.dart';
16 |
17 | class AccessoryList extends StatefulWidget {
18 | final AsyncCallback loadLocationUpdates;
19 | final void Function(LatLng point)? centerOnPoint;
20 |
21 | /// Display a location overview all accessories in a concise list form.
22 | ///
23 | /// For each accessory the name and last known locaiton information is shown.
24 | /// Uses the accessories in the [AccessoryRegistry].
25 | const AccessoryList({
26 | Key? key,
27 | required this.loadLocationUpdates,
28 | this.centerOnPoint,
29 | }): super(key: key);
30 |
31 | @override
32 | _AccessoryListState createState() => _AccessoryListState();
33 | }
34 |
35 | class _AccessoryListState extends State {
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | return Consumer2(
40 | builder: (context, accessoryRegistry, locationModel, child) {
41 | var accessories = accessoryRegistry.accessories;
42 |
43 | // Show placeholder while accessories are loading
44 | if (accessoryRegistry.loading){
45 | return LayoutBuilder(
46 | builder: (context, constraints) {
47 | // Show as many accessory placeholder fitting into the vertical space.
48 | // Minimum one, maximum 6 placeholders
49 | var nrOfEntries = min(max((constraints.maxHeight / 64).floor(), 1), 6);
50 | List placeholderList = [];
51 | for (int i = 0; i < nrOfEntries; i++) {
52 | placeholderList.add(const AccessoryListItemPlaceholder());
53 | }
54 | return Scrollbar(
55 | child: ListView(
56 | children: placeholderList,
57 | ),
58 | );
59 | }
60 | );
61 | }
62 |
63 | if (accessories.isEmpty) {
64 | return const NoAccessoriesPlaceholder();
65 | }
66 |
67 | // TODO: Refresh Indicator for desktop
68 | // Use pull to refresh method
69 | return SlidableAutoCloseBehavior(child:
70 | RefreshIndicator(
71 | onRefresh: widget.loadLocationUpdates,
72 | child: Scrollbar(
73 | child: ListView(
74 | children: accessories.map((accessory) {
75 | // Calculate distance from users devices location
76 | Widget? trailing;
77 | if (locationModel.here != null && accessory.lastLocation != null) {
78 | final double km = GeolocatorPlatform.instance.distanceBetween(
79 | locationModel.here!.latitude, locationModel.here!.longitude,
80 | accessory.lastLocation!.latitude, accessory.lastLocation!.longitude
81 | ) / 1000;
82 | trailing = Text('${km.toStringAsFixed(3)} km');
83 | }
84 | // Get human readable location
85 | return Slidable(
86 | endActionPane: ActionPane(
87 | motion: const DrawerMotion(),
88 | children: [
89 | if (accessory.isDeployed) SlidableAction(
90 | onPressed: (context) async {
91 | if (accessory.lastLocation != null && accessory.isDeployed) {
92 | var loc = accessory.lastLocation!;
93 | await MapsLauncher.launchCoordinates(
94 | loc.latitude, loc.longitude, accessory.name);
95 | }
96 | },
97 | backgroundColor: Colors.blue,
98 | foregroundColor: Colors.white,
99 | icon: Icons.directions,
100 | label: 'Navigate',
101 | ),
102 | if (accessory.isDeployed) SlidableAction(
103 | onPressed: (context) {
104 | Navigator.push(
105 | context,
106 | MaterialPageRoute(builder: (context) => AccessoryHistory(
107 | accessory: accessory,
108 | )),
109 | );
110 | },
111 | backgroundColor: Colors.orange,
112 | foregroundColor: Colors.white,
113 | icon: Icons.history,
114 | label: 'History',
115 | ),
116 | if (!accessory.isDeployed) SlidableAction(
117 | onPressed: (context) {
118 | var accessoryRegistry = Provider.of(context, listen: false);
119 | var newAccessory = accessory.clone();
120 | newAccessory.isDeployed = true;
121 | accessoryRegistry.editAccessory(accessory, newAccessory);
122 | },
123 | backgroundColor: Colors.green,
124 | foregroundColor: Colors.white,
125 | icon: Icons.upload_file,
126 | label: 'Deploy',
127 | ),
128 | ],
129 | ),
130 | child: Builder(
131 | builder: (context) {
132 | return AccessoryListItem(
133 | accessory: accessory,
134 | distance: trailing,
135 | herePlace: locationModel.herePlace,
136 | onTap: () {
137 | var lastLocation = accessory.lastLocation;
138 | if (lastLocation != null) {
139 | widget.centerOnPoint?.call(lastLocation);
140 | }
141 | },
142 | onLongPress: Slidable.of(context)?.openEndActionPane,
143 | );
144 | }
145 | ),
146 | );
147 | }).toList(),
148 | ),
149 | ),
150 | ),
151 | );
152 | },
153 | );
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_list_item.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:geocoding/geocoding.dart';
3 | import 'package:openhaystack_mobile/accessory/accessory_icon.dart';
4 | import 'package:openhaystack_mobile/accessory/accessory_model.dart';
5 | import 'package:intl/intl.dart';
6 |
7 | class AccessoryListItem extends StatelessWidget {
8 | /// The accessory to display the information for.
9 | final Accessory accessory;
10 | /// A trailing distance information widget.
11 | final Widget? distance;
12 | /// Address information about the accessories location.
13 | final Placemark? herePlace;
14 | final VoidCallback onTap;
15 | final VoidCallback? onLongPress;
16 |
17 | /// Displays the location of an accessory as a concise list item.
18 | ///
19 | /// Shows the icon and name of the accessory, as well as the current
20 | /// location and distance to the user's location (if known; `distance != null`)
21 | const AccessoryListItem({
22 | Key? key,
23 | required this.accessory,
24 | required this.onTap,
25 | this.onLongPress,
26 | this.distance,
27 | this.herePlace,
28 | }) : super(key: key);
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | return FutureBuilder(
33 | future: accessory.place,
34 | builder: (BuildContext context, AsyncSnapshot snapshot) {
35 | // Format the location of the accessory. Use in this order:
36 | // * Address if known
37 | // * Coordinates (latitude & longitude) if known
38 | // * `Unknown` if unknown
39 | String locationString = accessory.lastLocation != null
40 | ? '${accessory.lastLocation!.latitude}, ${accessory.lastLocation!.longitude}'
41 | : 'Unknown';
42 | if (snapshot.hasData && snapshot.data != null) {
43 | Placemark place = snapshot.data!;
44 | locationString = '${place.locality}, ${place.administrativeArea}';
45 | if (herePlace != null && herePlace!.country != place.country) {
46 | locationString = '${place.locality}, ${place.country}';
47 | }
48 | }
49 | // Format published date in a human readable way
50 | String? dateString = accessory.datePublished != null
51 | ? ' · ${DateFormat('dd.MM.yyyy HH:mm').format(accessory.datePublished!)}'
52 | : '';
53 | return ListTile(
54 | onTap: onTap,
55 | onLongPress: onLongPress,
56 | title: Text(
57 | accessory.name + (accessory.isDeployed ? '' : ' (not deployed)'),
58 | style: TextStyle(
59 | color: accessory.isDeployed
60 | ? Theme.of(context).colorScheme.onSurface
61 | : Theme.of(context).disabledColor,
62 | ),
63 | ),
64 | subtitle: Text(locationString + dateString),
65 | trailing: distance,
66 | dense: true,
67 | leading: AccessoryIcon(
68 | icon: accessory.icon,
69 | color: accessory.color,
70 | ),
71 | );
72 | },
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_list_item_placeholder.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:openhaystack_mobile/accessory/accessory_list_item.dart';
3 | import 'package:openhaystack_mobile/placeholder/avatar_placeholder.dart';
4 | import 'package:openhaystack_mobile/placeholder/text_placeholder.dart';
5 |
6 | class AccessoryListItemPlaceholder extends StatelessWidget {
7 |
8 | /// A placeholder for an [AccessoryListItem] showing a loading animation.
9 | const AccessoryListItemPlaceholder({
10 | Key? key,
11 | }) : super(key: key);
12 |
13 | @override
14 | Widget build(BuildContext context) {
15 | // Uses a similar layout to the actual accessory list item
16 | return const ListTile(
17 | title: TextPlaceholder(),
18 | subtitle: TextPlaceholder(),
19 | dense: true,
20 | leading: AvatarPlaceholder(),
21 | trailing: TextPlaceholder(width: 60),
22 | );
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_model.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:geocoding/geocoding.dart';
3 | import 'package:mapbox_gl/mapbox_gl.dart';
4 | import 'package:openhaystack_mobile/accessory/accessory_icon_model.dart';
5 | import 'package:openhaystack_mobile/findMy/find_my_controller.dart';
6 | import 'package:openhaystack_mobile/location/location_model.dart';
7 |
8 | class Pair {
9 | final T1 a;
10 | final T2 b;
11 |
12 | Pair(this.a, this.b);
13 | }
14 |
15 |
16 | const defaultIcon = Icons.push_pin;
17 |
18 |
19 | class Accessory {
20 | /// The ID of the accessory key.
21 | String id;
22 | /// A hash of the public key.
23 | /// An identifier for the private key stored separately in the key store.
24 | String hashedPublicKey;
25 | /// If the accessory uses rolling keys.
26 | bool usesDerivation;
27 |
28 | // Parameters for rolling keys (only relevant is usesDerivation == true)
29 | String? symmetricKey;
30 | double? lastDerivationTimestamp;
31 | int? updateInterval;
32 | String? oldestRelevantSymmetricKey;
33 |
34 | /// The display name of the accessory.
35 | String name;
36 | /// The display icon of the accessory.
37 | String iconString;
38 | /// The display color of the accessory.
39 | Color color;
40 |
41 | /// If the accessory is active.
42 | bool isActive;
43 | /// If the accessory is already deployed
44 | /// (and could therefore send locations).
45 | bool isDeployed;
46 |
47 | /// The timestamp of the last known location
48 | /// (null if no location known).
49 | DateTime? datePublished;
50 | /// The last known locations coordinates
51 | /// (null if no location known).
52 | LatLng? _lastLocation;
53 |
54 | /// A list of known locations over time.
55 | List> locationHistory = [];
56 |
57 | /// Stores address information about the current location.
58 | Future place = Future.value(null);
59 |
60 |
61 | /// Creates an accessory with the given properties.
62 | Accessory({
63 | required this.id,
64 | required this.name,
65 | required this.hashedPublicKey,
66 | required this.datePublished,
67 | this.isActive = false,
68 | this.isDeployed = false,
69 | LatLng? lastLocation,
70 | String icon = 'push_pin',
71 | this.color = Colors.grey,
72 | this.usesDerivation = false,
73 | this.symmetricKey,
74 | this.lastDerivationTimestamp,
75 | this.updateInterval,
76 | this.oldestRelevantSymmetricKey,
77 | }): iconString = icon, _lastLocation = lastLocation, super() {
78 | _init();
79 | }
80 |
81 | void _init() {
82 | if (_lastLocation != null) {
83 | place = LocationModel.getAddress(_lastLocation!);
84 | }
85 | }
86 |
87 | /// Creates a new accessory with exactly the same properties of this accessory.
88 | Accessory clone() {
89 | return Accessory(
90 | datePublished: datePublished,
91 | id: id,
92 | name: name,
93 | hashedPublicKey: hashedPublicKey,
94 | color: color,
95 | icon: iconString,
96 | isActive: isActive,
97 | isDeployed: isDeployed,
98 | lastLocation: lastLocation,
99 | usesDerivation: usesDerivation,
100 | symmetricKey: symmetricKey,
101 | lastDerivationTimestamp: lastDerivationTimestamp,
102 | updateInterval: updateInterval,
103 | oldestRelevantSymmetricKey: oldestRelevantSymmetricKey,
104 | );
105 | }
106 |
107 | /// Updates the properties of this accessor with the new values of the [newAccessory].
108 | void update(Accessory newAccessory) {
109 | datePublished = newAccessory.datePublished;
110 | id = newAccessory.id;
111 | name = newAccessory.name;
112 | hashedPublicKey = newAccessory.hashedPublicKey;
113 | color = newAccessory.color;
114 | iconString = newAccessory.iconString;
115 | isActive = newAccessory.isActive;
116 | isDeployed = newAccessory.isDeployed;
117 | lastLocation = newAccessory.lastLocation;
118 | }
119 |
120 | /// The last known location of the accessory.
121 | LatLng? get lastLocation {
122 | return _lastLocation;
123 | }
124 |
125 | /// The last known location of the accessory.
126 | set lastLocation(LatLng? newLocation) {
127 | _lastLocation = newLocation;
128 | if (_lastLocation != null) {
129 | place = LocationModel.getAddress(_lastLocation!);
130 | }
131 | }
132 |
133 | /// The display icon of the accessory.
134 | IconData get icon {
135 | IconData? icon = AccessoryIconModel.mapIcon(iconString);
136 | return icon ?? defaultIcon;
137 | }
138 |
139 | /// The cupertino icon name.
140 | String get rawIcon {
141 | return iconString;
142 | }
143 |
144 | /// The display icon of the accessory.
145 | setIcon (String icon) {
146 | iconString = icon;
147 | }
148 |
149 | /// Creates an accessory from deserialized JSON data.
150 | ///
151 | /// Uses the same format as in [toJson]
152 | ///
153 | /// Typically used with JSON decoder.
154 | /// ```dart
155 | /// String json = '...';
156 | /// var accessoryDTO = Accessory.fromJSON(jsonDecode(json));
157 | /// ```
158 | Accessory.fromJson(Map json)
159 | : id = json['id'],
160 | name = json['name'],
161 | hashedPublicKey = json['hashedPublicKey'],
162 | datePublished = json['datePublished'] != null
163 | ? DateTime.fromMillisecondsSinceEpoch(json['datePublished']) : null,
164 | _lastLocation = json['latitude'] != null && json['longitude'] != null
165 | ? LatLng(json['latitude'].toDouble(), json['longitude'].toDouble()) : null,
166 | isActive = json['isActive'],
167 | isDeployed = json['isDeployed'],
168 | iconString = json['icon'],
169 | color = Color(int.parse(json['color'], radix: 16)),
170 | usesDerivation = json['usesDerivation'] ?? false,
171 | symmetricKey = json['symmetricKey'],
172 | lastDerivationTimestamp = json['lastDerivationTimestamp'],
173 | updateInterval = json['updateInterval'],
174 | oldestRelevantSymmetricKey = json['oldestRelevantSymmetricKey'] {
175 | _init();
176 | }
177 |
178 | /// Creates a JSON map of the serialized accessory.
179 | ///
180 | /// Uses the same format as in [Accessory.fromJson].
181 | ///
182 | /// Typically used by JSON encoder.
183 | /// ```dart
184 | /// var accessory = Accessory(...);
185 | /// jsonEncode(accessory);
186 | /// ```
187 | Map toJson() => {
188 | 'id': id,
189 | 'name': name,
190 | 'hashedPublicKey': hashedPublicKey,
191 | 'datePublished': datePublished?.millisecondsSinceEpoch,
192 | 'latitude': _lastLocation?.latitude,
193 | 'longitude': _lastLocation?.longitude,
194 | 'isActive': isActive,
195 | 'isDeployed': isDeployed,
196 | 'icon': iconString,
197 | 'color': color.toString().split('(0x')[1].split(')')[0],
198 | 'usesDerivation': usesDerivation,
199 | 'symmetricKey': symmetricKey,
200 | 'lastDerivationTimestamp': lastDerivationTimestamp,
201 | 'updateInterval': updateInterval,
202 | 'oldestRelevantSymmetricKey': oldestRelevantSymmetricKey,
203 | };
204 |
205 | /// Returns the Base64 encoded hash of the advertisement key
206 | /// (used to fetch location reports).
207 | Future getHashedAdvertisementKey() async {
208 | var keyPair = await FindMyController.getKeyPair(hashedPublicKey);
209 | return keyPair.getHashedAdvertisementKey();
210 | }
211 |
212 | /// Returns the Base64 encoded advertisement key
213 | /// (sent out by the accessory via BLE).
214 | Future getAdvertisementKey() async {
215 | var keyPair = await FindMyController.getKeyPair(hashedPublicKey);
216 | return keyPair.getBase64AdvertisementKey();
217 | }
218 |
219 | /// Returns the Base64 encoded private key.
220 | Future getPrivateKey() async {
221 | var keyPair = await FindMyController.getKeyPair(hashedPublicKey);
222 | return keyPair.getBase64PrivateKey();
223 | }
224 |
225 | }
226 |
--------------------------------------------------------------------------------
/lib/accessory/accessory_registry.dart:
--------------------------------------------------------------------------------
1 | import 'dart:collection';
2 | import 'dart:convert';
3 | import 'package:flutter/foundation.dart';
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_secure_storage/flutter_secure_storage.dart';
6 | import 'package:openhaystack_mobile/accessory/accessory_model.dart';
7 | import 'package:mapbox_gl/mapbox_gl.dart';
8 | import 'package:openhaystack_mobile/findMy/find_my_controller.dart';
9 | import 'package:openhaystack_mobile/findMy/models.dart';
10 |
11 | const accessoryStorageKey = 'ACCESSORIES';
12 |
13 | class AccessoryRegistry extends ChangeNotifier {
14 |
15 | final _storage = const FlutterSecureStorage();
16 | final _findMyController = FindMyController();
17 | List _accessories = [];
18 | bool loading = false;
19 | bool initialLoadFinished = false;
20 |
21 | /// Creates the accessory registry.
22 | ///
23 | /// This is used to manage the accessories of the user.
24 | AccessoryRegistry() : super();
25 |
26 | /// A list of the user's accessories.
27 | UnmodifiableListView get accessories => UnmodifiableListView(_accessories);
28 |
29 | /// Loads the user's accessories from persistent storage.
30 | Future loadAccessories() async {
31 | loading = true;
32 | String? serialized = await _storage.read(key: accessoryStorageKey);
33 | if (serialized != null) {
34 | List accessoryJson = json.decode(serialized);
35 | List loadedAccessories =
36 | accessoryJson.map((val) => Accessory.fromJson(val)).toList();
37 | _accessories = loadedAccessories;
38 | } else {
39 | _accessories = [];
40 | }
41 |
42 | // For Debugging:
43 | // await overwriteEverythingWithDemoDataForDebugging();
44 |
45 | loading = false;
46 |
47 | notifyListeners();
48 | }
49 |
50 | /// __USE ONLY FOR DEBUGGING PURPOSES__
51 | ///
52 | /// __ALL PERSISTENT DATA WILL BE LOST!__
53 | ///
54 | /// Overwrites all accessories in this registry with demo data for testing.
55 | Future overwriteEverythingWithDemoDataForDebugging() async {
56 | // Delete everything to start with a fresh set of demo accessories
57 | await _storage.deleteAll();
58 |
59 | // Load demo accessories
60 | List demoAccessories = [
61 | Accessory(hashedPublicKey: 'TrnHrAM0ZrFSDeq1NN7ppmh0zYJotYiO09alVVF1mPI=',
62 | id: '-5952179461995674635', name: 'Raspberry Pi', color: Colors.green,
63 | datePublished: DateTime.fromMillisecondsSinceEpoch(1636390931651),
64 | icon: 'redeem', lastLocation: LatLng(-23.559389, -46.731839)),
65 | Accessory(hashedPublicKey: 'TrnHrAM0ZrFSDeq1NN7ppmh0zYJotYiO09alVVF1mPI=',
66 | id: '-5952179461995674635', name: 'My Bag', color: Colors.blue,
67 | datePublished: DateTime.fromMillisecondsSinceEpoch(1636390931651),
68 | icon: 'business_center', lastLocation: LatLng(-23.559389, -46.731839)),
69 | Accessory(hashedPublicKey: 'TrnHrAM0ZrFSDeq1NN7ppmh0zYJotYiO09alVVF1mPI=',
70 | id: '-5952179461995674635', name: 'Car', color: Colors.red,
71 | datePublished: DateTime.fromMillisecondsSinceEpoch(1636390931651),
72 | icon: 'directions_car', lastLocation: LatLng(-23.559389, -46.731839)),
73 | ];
74 | _accessories = demoAccessories;
75 |
76 | // Store demo accessories for later use
77 | await _storeAccessories();
78 |
79 | // Import private key for demo accessories
80 | // Public key hash is TrnHrAM0ZrFSDeq1NN7ppmh0zYJotYiO09alVVF1mPI=
81 | await FindMyController.importKeyPair('siykvOCIEQRVDwrbjyZUXuBwsMi0Htm7IBmBIg==');
82 | }
83 |
84 | /// Fetches new location reports and matches them to their accessory.
85 | Future loadLocationReports() async {
86 | List>> runningLocationRequests = [];
87 |
88 | // request location updates for all accessories simultaneously
89 | List currentAccessories = accessories;
90 | for (var i = 0; i < currentAccessories.length; i++) {
91 | var accessory = currentAccessories.elementAt(i);
92 |
93 | var keyPair = await FindMyController.getKeyPair(accessory.hashedPublicKey);
94 | var locationRequest = FindMyController.computeResults(keyPair);
95 | runningLocationRequests.add(locationRequest);
96 | }
97 |
98 | // wait for location updates to succeed and update state afterwards
99 | var reportsForAccessories = await Future.wait(runningLocationRequests);
100 | for (var i = 0; i < currentAccessories.length; i++) {
101 | var accessory = currentAccessories.elementAt(i);
102 | var reports = reportsForAccessories.elementAt(i);
103 |
104 | debugPrint("Found ${reports.length} reports for accessory '${accessory.name}'");
105 |
106 | accessory.locationHistory = reports
107 | .where((report) => report.latitude.abs() <= 90 && report.longitude.abs() < 90 )
108 | .map((report) => Pair(
109 | LatLng(report.latitude, report.longitude),
110 | report.timestamp ?? report.published,
111 | ))
112 | .toList();
113 |
114 | reports.sort((a, b) => (b.timestamp ?? DateTime(0)).compareTo(a.timestamp ?? DateTime(0)));
115 |
116 | if (reports.isNotEmpty) {
117 | var lastReport = reports.first;
118 | accessory.lastLocation = LatLng(lastReport.latitude, lastReport.longitude);
119 | accessory.datePublished = lastReport.timestamp ?? lastReport.published;
120 | }
121 | }
122 |
123 | // Store updated lastLocation and datePublished for accessories
124 | _storeAccessories();
125 |
126 | initialLoadFinished = true;
127 | notifyListeners();
128 | }
129 |
130 | /// Stores the user's accessories in persistent storage.
131 | Future _storeAccessories() async {
132 | List jsonList = _accessories.map(jsonEncode).toList();
133 | await _storage.write(key: accessoryStorageKey, value: jsonList.toString());
134 | }
135 |
136 | /// Adds a new accessory to this registry.
137 | void addAccessory(Accessory accessory) {
138 | _accessories.add(accessory);
139 | _storeAccessories();
140 | notifyListeners();
141 | }
142 |
143 | /// Removes [accessory] from this registry.
144 | void removeAccessory(Accessory accessory) {
145 | _accessories.remove(accessory);
146 | // TODO: remove private key from keychain
147 | _storeAccessories();
148 | notifyListeners();
149 | }
150 |
151 | /// Updates [oldAccessory] with the values from [newAccessory].
152 | void editAccessory(Accessory oldAccessory, Accessory newAccessory) {
153 | oldAccessory.update(newAccessory);
154 | _storeAccessories();
155 | notifyListeners();
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/lib/accessory/no_accessories.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:openhaystack_mobile/item_management/new_item_action.dart';
3 |
4 | class NoAccessoriesPlaceholder extends StatelessWidget {
5 |
6 | /// Displays a message that no accessories are present.
7 | ///
8 | /// Allows the user to quickly add a new accessory.
9 | const NoAccessoriesPlaceholder({ Key? key }) : super(key: key);
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Center(
14 | child: Column(
15 | mainAxisAlignment: MainAxisAlignment.center,
16 | children: const [
17 | Text(
18 | 'There\'s Nothing Here Yet\nAdd an accessory to get started.',
19 | style: TextStyle(
20 | fontSize: 20,
21 | color: Colors.grey,
22 | ),
23 | textAlign: TextAlign.center,
24 | ),
25 | NewKeyAction(mini: true),
26 | ],
27 | ),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/dashboard/accessory_map_list_vert.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:provider/provider.dart';
4 | import 'package:openhaystack_mobile/accessory/accessory_list.dart';
5 | import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
6 | import 'package:openhaystack_mobile/location/location_model.dart';
7 | import 'package:openhaystack_mobile/map/map.dart';
8 | import 'package:mapbox_gl/mapbox_gl.dart';
9 |
10 | class AccessoryMapListVertical extends StatefulWidget {
11 | final AsyncCallback loadLocationUpdates;
12 |
13 | /// Displays a map view and the accessory list in a vertical alignment.
14 | const AccessoryMapListVertical({
15 | Key? key,
16 | required this.loadLocationUpdates,
17 | }) : super(key: key);
18 |
19 | @override
20 | State createState() => _AccessoryMapListVerticalState();
21 | }
22 |
23 | class _AccessoryMapListVerticalState extends State {
24 | MapboxMapController? _mapController;
25 |
26 | void _centerPoint(LatLng point) {
27 | _mapController?.moveCamera(
28 | CameraUpdate.newCameraPosition(
29 | CameraPosition(
30 | target: point,
31 | zoom: 15.0,
32 | ),
33 | )
34 | );
35 | }
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | return Consumer2(
40 | builder: (BuildContext context, AccessoryRegistry accessoryRegistry, LocationModel locationModel, Widget? child) {
41 | return Column(
42 | children: [
43 | Flexible(
44 | fit: FlexFit.tight,
45 | child: AccessoryMap(
46 | onMapCreatedCallback: (controller) {
47 | _mapController = controller;
48 | },
49 | ),
50 | ),
51 | Flexible(
52 | fit: FlexFit.tight,
53 | child: AccessoryList(
54 | loadLocationUpdates: widget.loadLocationUpdates,
55 | centerOnPoint: _centerPoint,
56 | ),
57 | ),
58 | ],
59 | );
60 | },
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/dashboard/dashboard_desktop.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 | import 'package:openhaystack_mobile/accessory/accessory_list.dart';
4 | import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
5 | import 'package:openhaystack_mobile/location/location_model.dart';
6 | import 'package:openhaystack_mobile/map/map.dart';
7 | import 'package:openhaystack_mobile/preferences/preferences_page.dart';
8 | import 'package:openhaystack_mobile/preferences/user_preferences_model.dart';
9 |
10 | class DashboardDesktop extends StatefulWidget {
11 |
12 | /// Displays the layout for the desktop view of the app.
13 | ///
14 | /// The layout is optimized for horizontally aligned larger screens
15 | /// on desktop devices.
16 | const DashboardDesktop({ Key? key }) : super(key: key);
17 |
18 | @override
19 | _DashboardDesktopState createState() => _DashboardDesktopState();
20 | }
21 |
22 | class _DashboardDesktopState extends State {
23 |
24 | @override
25 | void initState() {
26 | super.initState();
27 |
28 | // Initialize models and preferences
29 | var userPreferences = Provider.of(context, listen: false);
30 | var locationModel = Provider.of(context, listen: false);
31 | var locationPreferenceKnown = userPreferences.locationPreferenceKnown ?? false;
32 | var locationAccessWanted = userPreferences.locationAccessWanted ?? false;
33 | if (!locationPreferenceKnown || locationAccessWanted) {
34 | locationModel.requestLocationUpdates();
35 | }
36 |
37 | loadLocationUpdates();
38 | }
39 |
40 | /// Fetch locaiton updates for all accessories.
41 | Future loadLocationUpdates() async {
42 | var accessoryRegistry = Provider.of(context, listen: false);
43 | await accessoryRegistry.loadLocationReports();
44 | }
45 |
46 | @override
47 | Widget build(BuildContext context) {
48 | return Scaffold(
49 | body: Row(
50 | children: [
51 | SizedBox(
52 | width: 400,
53 | child: Column(
54 | children: [
55 | AppBar(
56 | title: const Text('OpenHaystack'),
57 | leading: IconButton(
58 | onPressed: () { /* reload */ },
59 | icon: const Icon(Icons.menu),
60 | ),
61 | actions: [
62 | IconButton(
63 | onPressed: () {
64 | Navigator.push(
65 | context,
66 | MaterialPageRoute(builder: (context) => const PreferencesPage()),
67 | );
68 | },
69 | icon: const Icon(Icons.settings),
70 | ),
71 | ],
72 | ),
73 | const Padding(
74 | padding: EdgeInsets.all(5),
75 | child: Text('My Accessories')
76 | ),
77 | Expanded(
78 | child: AccessoryList(
79 | loadLocationUpdates: loadLocationUpdates,
80 | ),
81 | ),
82 | ],
83 | ),
84 | ),
85 | const Expanded(
86 | child: AccessoryMap(),
87 | ),
88 | ],
89 | ),
90 | );
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/lib/dashboard/dashboard_mobile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:provider/provider.dart';
3 | import 'package:openhaystack_mobile/accessory/accessory_registry.dart';
4 | import 'package:openhaystack_mobile/dashboard/accessory_map_list_vert.dart';
5 | import 'package:openhaystack_mobile/item_management/item_management.dart';
6 | import 'package:openhaystack_mobile/item_management/new_item_action.dart';
7 | import 'package:openhaystack_mobile/location/location_model.dart';
8 | import 'package:openhaystack_mobile/preferences/preferences_page.dart';
9 | import 'package:openhaystack_mobile/preferences/user_preferences_model.dart';
10 |
11 | class DashboardMobile extends StatefulWidget {
12 |
13 | /// Displays the layout for the mobile view of the app.
14 | ///
15 | /// The layout is optimized for a vertically aligned small screens.
16 | /// The functionality is structured in a bottom tab bar for easy access
17 | /// on mobile devices.
18 | const DashboardMobile({ Key? key }) : super(key: key);
19 |
20 | @override
21 | _DashboardMobileState createState() => _DashboardMobileState();
22 | }
23 |
24 | class _DashboardMobileState extends State {
25 |
26 | /// A list of the tabs displayed in the bottom tab bar.
27 | late final List