Initial commit : Flutter app nymea energy monitor
- NymeaService : auth complète (Hello → Authenticate → SetNotificationStatus) - Token top-level dans chaque requête JSON-RPC (fix critique GetThings) - Persistance token via shared_preferences par hôte - Dashboard : champs utilisateur/mot de passe dans le dialog de connexion - ThingDetailScreen : renommer, réglages (settingsTypes) et supprimer - NymeaThingClass : champ settingsTypes parsé depuis l'API - NymeaThing : copyWith(name) + settingValue() - Fix overflow _StateChip dans ThingsScreen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
48
.gitignore
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
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-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# 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
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
45
.metadata
Normal file
@ -0,0 +1,45 @@
|
||||
# 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 and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: android
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: ios
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: linux
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: macos
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: web
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
- platform: windows
|
||||
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
|
||||
|
||||
# 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'
|
||||
95
CLAUDE.md
Normal file
@ -0,0 +1,95 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Contexte général
|
||||
|
||||
Dossier parent de tous les projets : `/home/etm/Projects/`
|
||||
|
||||
- **`etm_powersync_app/`** (ce projet) — application Flutter HEMS, client mobile
|
||||
- **`etm-nymea/`** — serveur nymea et plugins associés (C++/Qt), le backend
|
||||
|
||||
Ce projet est le **client Flutter** qui communique avec le serveur nymea via JSON-RPC 2.0.
|
||||
|
||||
---
|
||||
|
||||
## Commandes
|
||||
|
||||
```bash
|
||||
flutter pub get # installer/mettre à jour les dépendances
|
||||
flutter run # lancer sur device/émulateur connecté
|
||||
flutter build apk # build Android release
|
||||
flutter analyze # analyse statique Dart
|
||||
flutter test # lancer les tests
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### State management — un seul Provider
|
||||
|
||||
`NymeaService` (`lib/services/nymea_service.dart`) est le seul `ChangeNotifier`. Il est fourni à la racine dans `main.dart` via `ChangeNotifierProvider`. Tous les écrans le consomment avec `context.watch<NymeaService>()` ou `context.read<NymeaService>()`. Il n'y a aucune autre couche de state management.
|
||||
|
||||
### Navigation
|
||||
|
||||
`MainShell` dans `main.dart` utilise un `IndexedStack` de 5 écrans (l'état est préservé entre les onglets) :
|
||||
|
||||
| Index | Écran | Fichier |
|
||||
|-------|-------|---------|
|
||||
| 0 | Dashboard | `lib/screens/dashboard_screen.dart` |
|
||||
| 1 | Énergie | `lib/screens/energy_screen.dart` |
|
||||
| 2 | Things | `lib/screens/things_screen.dart` |
|
||||
| 3 | A/C | `lib/screens/ac_screen.dart` |
|
||||
| 4 | Favoris | `lib/screens/favorites_screen.dart` |
|
||||
|
||||
### Communication avec nymea
|
||||
|
||||
`NymeaService.connect()` supporte deux transports :
|
||||
|
||||
- **TCP brut** port 2222 (défaut) : nymea parle **en premier** (JSON délimité par `\n`). La fragmentation est gérée via `StringBuffer`.
|
||||
- **WebSocket** port 4444 : c'est le **client qui parle en premier** en envoyant `JSONRPC.Hello`.
|
||||
|
||||
Les requêtes utilisent des IDs auto-incrémentés stockés dans `_pendingRequests: Map<int, Completer>`. Timeout global : 15 secondes. Les notifications push nymea (pas de champ `id`, ont un champ `notification`) sont dispatchées dans `_handleNotification()`.
|
||||
|
||||
**Convention nymea énergie importante** : `currentPowerProduction` est **négatif** (producteur = négatif). Le service convertit en positif dans `_parsePowerBalance()`.
|
||||
|
||||
### Mode simulation
|
||||
|
||||
`NymeaService.startSimulation()` déconnecte toute connexion live, active `_isSimulation = true`, génère des things fictifs et une production PV simulée en sinus (timer 2 s). Le dashboard démarre automatiquement la simulation au premier affichage si non connecté. Toutes les méthodes du service font un garde `if (_isSimulation) return …` pour éviter les vrais appels RPC.
|
||||
|
||||
### Modèles de données
|
||||
|
||||
- **`EnergyData`** (`lib/models/energy_data.dart`) — objet immutable avec `copyWith`, regroupe toutes les métriques énergie temps-réel et journalières + état de la borne EV.
|
||||
- **`NymeaThing` / `NymeaThingClass` / `NymeaStateType` / `NymeaActionType`** (`lib/models/nymea_models.dart`) — miroir de l'API Integrations nymea. `NymeaThingClass.primaryStateType` détermine l'état affiché sur les cards.
|
||||
- **`ThingCategory` + `interfaceToCategoryMap`** (`lib/models/thing_category.dart`) — mappe les interfaces nymea (ex. `"evcharger"`, `"solarinverter"`) vers des catégories d'affichage utilisées par `ThingsScreen`.
|
||||
- **`FavoriteWidget`** (`lib/models/nymea_models.dart`) — descripteurs de widgets stockés en mémoire dans `NymeaService._favoriteWidgets`.
|
||||
|
||||
### Thème
|
||||
|
||||
`AppTheme` (`lib/theme/app_theme.dart`) expose des constantes `Color` nommées : `primaryGreen`, `solarYellow`, `gridGray`, `homeBlue`, `batteryGreen`, `boostRed`, `pvGreen`, `minPvBlue`, etc. Toujours utiliser ces constantes plutôt que des valeurs hex brutes.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Code** : anglais — **Commentaires** : français
|
||||
- **Nommage** : camelCase variables, PascalCase classes
|
||||
- Les écrans utilisent `Consumer<NymeaService>` ou `context.watch` — ne jamais dupliquer l'état du service dans un `State`
|
||||
- **Unités** : Watts (W) pour les puissances instantanées, Wh pour les énergies journalières. L'API nymea retourne des totaux en kWh que le service multiplie par 1000.
|
||||
|
||||
## Méthodes nymea principales
|
||||
|
||||
| Méthode | Rôle |
|
||||
|---------|------|
|
||||
| `JSONRPC.Hello` | Handshake (WebSocket uniquement — envoyé par le client) |
|
||||
| `Integrations.GetThings` | Charger les devices configurés |
|
||||
| `Integrations.GetThingClasses` | Charger les définitions de classes (filtre `thingClassIds` supporté) |
|
||||
| `Integrations.ExecuteAction` | Exécuter une action sur un thing |
|
||||
| `Integrations.GetStateValue` | Lire une valeur d'état |
|
||||
| `Energy.GetPowerBalance` | Snapshot du flux énergétique actuel |
|
||||
| `Energy.GetEnergyLogs` | Historique énergie (échantillons horaires) |
|
||||
| `Energy.SetChargingMode` | Mode borne EV : `pv`, `minpv`, `boost` |
|
||||
|
||||
Notifications push gérées : `Energy.PowerBalanceChanged`, `Energy.RootMeterChanged`, `Integrations.StateChanged`, `Integrations.ThingAdded`, `Integrations.ThingRemoved`.
|
||||
|
||||
## Documentation API
|
||||
|
||||
- `docs/nymea_api.md` — introspection complète de l'API nymea
|
||||
- `docs/nymea_integrations.md` — namespace Integrations en détail
|
||||
17
README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# etm_powersync_app
|
||||
|
||||
A new Flutter project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
This project is a starting point for a Flutter application.
|
||||
|
||||
A few resources to get you started if this is your first Flutter project:
|
||||
|
||||
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
|
||||
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
|
||||
|
||||
For help getting started with Flutter development, view the
|
||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||
samples, guidance on mobile development, and a full API reference.
|
||||
28
analysis_options.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
# This file configures the analyzer, which statically analyzes Dart code to
|
||||
# check for errors, warnings, and lints.
|
||||
#
|
||||
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||
# invoked from the command line by running `flutter analyze`.
|
||||
|
||||
# The following line activates a set of recommended lints for Flutter apps,
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
# included above or to enable additional rules. A list of all available lints
|
||||
# and their documentation is published at https://dart.dev/lints.
|
||||
#
|
||||
# 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
|
||||
14
android/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
gradle-wrapper.jar
|
||||
/.gradle
|
||||
/captures/
|
||||
/gradlew
|
||||
/gradlew.bat
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
44
android/app/build.gradle.kts
Normal file
@ -0,0 +1,44 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.etm_powersync_app"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.example.etm_powersync_app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
45
android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="etm_powersync_app"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@ -0,0 +1,5 @@
|
||||
package com.example.etm_powersync_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
18
android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
24
android/build.gradle.kts
Normal file
@ -0,0 +1,24 @@
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
val newBuildDir: Directory =
|
||||
rootProject.layout.buildDirectory
|
||||
.dir("../../build")
|
||||
.get()
|
||||
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||
|
||||
subprojects {
|
||||
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
}
|
||||
2
android/gradle.properties
Normal file
@ -0,0 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -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-8.14-all.zip
|
||||
26
android/settings.gradle.kts
Normal file
@ -0,0 +1,26 @@
|
||||
pluginManagement {
|
||||
val flutterSdkPath =
|
||||
run {
|
||||
val properties = java.util.Properties()
|
||||
file("local.properties").inputStream().use { properties.load(it) }
|
||||
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||
flutterSdkPath
|
||||
}
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
5938
docs/nymea_api.md
Normal file
10892
docs/nymea_integrations.md
Normal file
34
ios/.gitignore
vendored
Normal file
@ -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
|
||||
24
ios/Flutter/AppFrameworkInfo.plist
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
1
ios/Flutter/Debug.xcconfig
Normal file
@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
1
ios/Flutter/Release.xcconfig
Normal file
@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
620
ios/Runner.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,620 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.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 */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
97C147021CF9000F007C117D /* Info.plist */,
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
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 = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C8080294A63A400263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
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 */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C807F294A63A400263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
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 */
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||
);
|
||||
name = "Thin Binary";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
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 */
|
||||
331C807D294A63A400263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C146FB1CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
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;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
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 = 13.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 = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.etmPowersyncApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.etmPowersyncApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C8089294A63A400263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.etmPowersyncApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C808A294A63A400263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.etmPowersyncApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
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;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
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 = 13.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;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
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;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
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 = 13.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 = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.etmPowersyncApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
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 = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.etmPowersyncApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
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 */
|
||||
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C8088294A63A400263BE5 /* Debug */,
|
||||
331C8089294A63A400263BE5 /* Release */,
|
||||
331C808A294A63A400263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
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 */;
|
||||
}
|
||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
101
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal file
@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
16
ios/Runner/AppDelegate.swift
Normal file
@ -0,0 +1,16 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@ -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.
|
||||
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
26
ios/Runner/Base.lproj/Main.storyboard
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
70
ios/Runner/Info.plist
Normal file
@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Etm Powersync App</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>etm_powersync_app</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
1
ios/Runner/Runner-Bridging-Header.h
Normal file
@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
6
ios/Runner/SceneDelegate.swift
Normal file
@ -0,0 +1,6 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: FlutterSceneDelegate {
|
||||
|
||||
}
|
||||
12
ios/RunnerTests/RunnerTests.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
func testExample() {
|
||||
// If you add code to the Runner application, consider adding tests here.
|
||||
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||
}
|
||||
|
||||
}
|
||||
168
lib/main.dart
Normal file
@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'services/nymea_service.dart';
|
||||
import 'screens/dashboard_screen.dart';
|
||||
import 'screens/energy_screen.dart';
|
||||
import 'screens/things_screen.dart';
|
||||
import 'screens/ac_screen.dart';
|
||||
import 'screens/favorites_screen.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
const SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: Brightness.dark,
|
||||
),
|
||||
);
|
||||
|
||||
runApp(
|
||||
ChangeNotifierProvider(
|
||||
create: (_) => NymeaService(),
|
||||
child: const NymeaEnergyApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NymeaEnergyApp extends StatelessWidget {
|
||||
const NymeaEnergyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Nymea Energy',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.theme,
|
||||
home: const MainShell(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MainShell extends StatefulWidget {
|
||||
const MainShell({super.key});
|
||||
|
||||
@override
|
||||
State<MainShell> createState() => _MainShellState();
|
||||
}
|
||||
|
||||
class _MainShellState extends State<MainShell> {
|
||||
int _currentIndex = 0;
|
||||
|
||||
final List<Widget> _screens = const [
|
||||
DashboardScreen(),
|
||||
EnergyScreen(),
|
||||
ThingsScreen(),
|
||||
ACScreen(),
|
||||
FavoritesScreen(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: _screens,
|
||||
),
|
||||
bottomNavigationBar: _BottomNav(
|
||||
currentIndex: _currentIndex,
|
||||
onTap: (i) => setState(() => _currentIndex = i),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BottomNav extends StatelessWidget {
|
||||
final int currentIndex;
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
const _BottomNav({required this.currentIndex, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = [
|
||||
_NavItem(icon: Icons.home_rounded, label: 'Dashboard'),
|
||||
_NavItem(icon: Icons.bar_chart_rounded, label: 'Énergie'),
|
||||
_NavItem(icon: Icons.device_hub_rounded, label: 'Things'),
|
||||
_NavItem(icon: Icons.ac_unit_rounded, label: 'A/C'),
|
||||
_NavItem(icon: Icons.star_rounded, label: 'Favoris'),
|
||||
];
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha:0.08),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SizedBox(
|
||||
height: 62,
|
||||
child: Row(
|
||||
children: items.asMap().entries.map((e) {
|
||||
final i = e.key;
|
||||
final item = e.value;
|
||||
final isSelected = currentIndex == i;
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => onTap(i),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppTheme.primaryGreen.withValues(alpha:0.12)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
item.icon,
|
||||
color: isSelected
|
||||
? AppTheme.primaryGreen
|
||||
: AppTheme.textLight,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
item.label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isSelected
|
||||
? AppTheme.primaryGreen
|
||||
: AppTheme.textLight,
|
||||
fontWeight: isSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavItem {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
const _NavItem({required this.icon, required this.label});
|
||||
}
|
||||
101
lib/models/energy_data.dart
Normal file
@ -0,0 +1,101 @@
|
||||
class EnergyData {
|
||||
final double pvPower; // Watts - production solaire
|
||||
final double homePower; // Watts - consommation maison
|
||||
final double batteryPower; // Watts - puissance batterie (+ charge, - décharge)
|
||||
final double gridPower; // Watts - réseau (+ injection, - soutif)
|
||||
final double batterySOC; // % - état de charge batterie
|
||||
final double temperature; // °C
|
||||
final String weatherIcon;
|
||||
|
||||
// Journée
|
||||
final double dayProductionWh;
|
||||
final double daySelfConsumptionWh;
|
||||
final double dayGridInjectionWh;
|
||||
final double selfConsumptionRate; // %
|
||||
final double autonomyRate; // %
|
||||
final double dayGains; // €
|
||||
|
||||
// Borne de recharge
|
||||
final ChargingMode chargingMode;
|
||||
final double chargingPower; // kW
|
||||
final double solarSourcePercent; // %
|
||||
final bool gridLimitOk;
|
||||
|
||||
const EnergyData({
|
||||
this.pvPower = 0,
|
||||
this.homePower = 0,
|
||||
this.batteryPower = 0,
|
||||
this.gridPower = 0,
|
||||
this.batterySOC = 0,
|
||||
this.temperature = 0,
|
||||
this.weatherIcon = 'cloud_snow',
|
||||
this.dayProductionWh = 0,
|
||||
this.daySelfConsumptionWh = 0,
|
||||
this.dayGridInjectionWh = 0,
|
||||
this.selfConsumptionRate = 0,
|
||||
this.autonomyRate = 0,
|
||||
this.dayGains = 0,
|
||||
this.chargingMode = ChargingMode.pv,
|
||||
this.chargingPower = 3.6,
|
||||
this.solarSourcePercent = 82,
|
||||
this.gridLimitOk = true,
|
||||
});
|
||||
|
||||
EnergyData copyWith({
|
||||
double? pvPower,
|
||||
double? homePower,
|
||||
double? batteryPower,
|
||||
double? gridPower,
|
||||
double? batterySOC,
|
||||
double? temperature,
|
||||
String? weatherIcon,
|
||||
double? dayProductionWh,
|
||||
double? daySelfConsumptionWh,
|
||||
double? dayGridInjectionWh,
|
||||
double? selfConsumptionRate,
|
||||
double? autonomyRate,
|
||||
double? dayGains,
|
||||
ChargingMode? chargingMode,
|
||||
double? chargingPower,
|
||||
double? solarSourcePercent,
|
||||
bool? gridLimitOk,
|
||||
}) {
|
||||
return EnergyData(
|
||||
pvPower: pvPower ?? this.pvPower,
|
||||
homePower: homePower ?? this.homePower,
|
||||
batteryPower: batteryPower ?? this.batteryPower,
|
||||
gridPower: gridPower ?? this.gridPower,
|
||||
batterySOC: batterySOC ?? this.batterySOC,
|
||||
temperature: temperature ?? this.temperature,
|
||||
weatherIcon: weatherIcon ?? this.weatherIcon,
|
||||
dayProductionWh: dayProductionWh ?? this.dayProductionWh,
|
||||
daySelfConsumptionWh: daySelfConsumptionWh ?? this.daySelfConsumptionWh,
|
||||
dayGridInjectionWh: dayGridInjectionWh ?? this.dayGridInjectionWh,
|
||||
selfConsumptionRate: selfConsumptionRate ?? this.selfConsumptionRate,
|
||||
autonomyRate: autonomyRate ?? this.autonomyRate,
|
||||
dayGains: dayGains ?? this.dayGains,
|
||||
chargingMode: chargingMode ?? this.chargingMode,
|
||||
chargingPower: chargingPower ?? this.chargingPower,
|
||||
solarSourcePercent: solarSourcePercent ?? this.solarSourcePercent,
|
||||
gridLimitOk: gridLimitOk ?? this.gridLimitOk,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ChargingMode { pv, minPv, boost }
|
||||
|
||||
class HistoryPoint {
|
||||
final DateTime time;
|
||||
final double pvWh;
|
||||
final double homeWh;
|
||||
final double gridWh;
|
||||
final double batteryWh;
|
||||
|
||||
const HistoryPoint({
|
||||
required this.time,
|
||||
this.pvWh = 0,
|
||||
this.homeWh = 0,
|
||||
this.gridWh = 0,
|
||||
this.batteryWh = 0,
|
||||
});
|
||||
}
|
||||
247
lib/models/nymea_models.dart
Normal file
@ -0,0 +1,247 @@
|
||||
// ── Thing models ──────────────────────────────────────────────────────────────
|
||||
|
||||
class NymeaThing {
|
||||
final String id;
|
||||
final String name;
|
||||
final String thingClassId;
|
||||
final String setupStatus;
|
||||
final List<Map<String, dynamic>> paramValues;
|
||||
final List<NymeaStateValue> states;
|
||||
|
||||
const NymeaThing({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.thingClassId,
|
||||
required this.setupStatus,
|
||||
required this.paramValues,
|
||||
this.states = const [],
|
||||
});
|
||||
|
||||
factory NymeaThing.fromJson(Map<String, dynamic> j) {
|
||||
return NymeaThing(
|
||||
id: j['id'] as String? ?? '',
|
||||
name: j['name'] as String? ?? 'Unknown',
|
||||
thingClassId: j['thingClassId'] as String? ?? '',
|
||||
setupStatus: j['setupStatus'] as String? ?? '',
|
||||
paramValues: List<Map<String, dynamic>>.from(j['paramValues'] ?? []),
|
||||
states: (j['states'] as List? ?? [])
|
||||
.map((s) => NymeaStateValue.fromJson(s))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
NymeaThing copyWith({String? name, List<NymeaStateValue>? states}) => NymeaThing(
|
||||
id: id,
|
||||
name: name ?? this.name,
|
||||
thingClassId: thingClassId,
|
||||
setupStatus: setupStatus,
|
||||
paramValues: paramValues,
|
||||
states: states ?? this.states,
|
||||
);
|
||||
|
||||
dynamic settingValue(String settingTypeId) {
|
||||
try {
|
||||
return paramValues
|
||||
.firstWhere((p) => p['paramTypeId'] == settingTypeId)['value'];
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
dynamic stateValue(String stateTypeId) {
|
||||
try {
|
||||
return states.firstWhere((s) => s.stateTypeId == stateTypeId).value;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
bool get isSetupComplete => setupStatus == 'ThingSetupStatusComplete';
|
||||
}
|
||||
|
||||
class NymeaStateValue {
|
||||
final String stateTypeId;
|
||||
final dynamic value;
|
||||
|
||||
const NymeaStateValue({required this.stateTypeId, required this.value});
|
||||
|
||||
factory NymeaStateValue.fromJson(Map<String, dynamic> j) => NymeaStateValue(
|
||||
stateTypeId: j['stateTypeId'] as String? ?? '',
|
||||
value: j['value'],
|
||||
);
|
||||
}
|
||||
|
||||
// ── State type definition (from GetThingClasses) ──────────────────────────────
|
||||
class NymeaStateType {
|
||||
final String id;
|
||||
final String name;
|
||||
final String displayName;
|
||||
final String type; // "Double", "Bool", "Int", "String"
|
||||
final String unit; // "W", "V", "A", "Percentage", ""
|
||||
final dynamic defaultValue;
|
||||
final bool writable;
|
||||
|
||||
const NymeaStateType({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.displayName,
|
||||
required this.type,
|
||||
required this.unit,
|
||||
this.defaultValue,
|
||||
this.writable = false,
|
||||
});
|
||||
|
||||
factory NymeaStateType.fromJson(Map<String, dynamic> j) => NymeaStateType(
|
||||
id: j['id'] as String? ?? '',
|
||||
name: j['name'] as String? ?? '',
|
||||
displayName: j['displayName'] as String? ?? j['name'] as String? ?? '',
|
||||
type: j['type'] as String? ?? 'String',
|
||||
unit: j['unit'] as String? ?? '',
|
||||
defaultValue: j['defaultValue'],
|
||||
writable: j['writable'] as bool? ?? false,
|
||||
);
|
||||
|
||||
String formatValue(dynamic value) {
|
||||
if (value == null) return '—';
|
||||
switch (unit) {
|
||||
case 'W': return '${_fmt(value)} W';
|
||||
case 'kW': return '${_fmt(value)} kW';
|
||||
case 'Wh': return '${_fmt(value)} Wh';
|
||||
case 'kWh': return '${_fmt(value)} kWh';
|
||||
case 'V': return '${_fmt(value)} V';
|
||||
case 'A': return '${_fmt(value)} A';
|
||||
case 'Percentage': return '${_fmt(value)} %';
|
||||
case 'Celsius': return '${_fmt(value)} °C';
|
||||
case 'Hz': return '${_fmt(value)} Hz';
|
||||
case 'VoltAmpereReactive': return '${_fmt(value)} VAr';
|
||||
case 'KiloVoltAmpereHour': return '${_fmt(value)} kVAh';
|
||||
case 'Rpm': return '${_fmt(value)} rpm';
|
||||
default:
|
||||
if (type == 'Bool') return (value == true || value == 'true') ? 'Oui' : 'Non';
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
String _fmt(dynamic value) {
|
||||
if (value is double) {
|
||||
if (value.abs() >= 1000) return value.toStringAsFixed(0);
|
||||
if (value.abs() >= 10) return value.toStringAsFixed(1);
|
||||
return value.toStringAsFixed(2);
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Action type definition ─────────────────────────────────────────────────────
|
||||
class NymeaActionType {
|
||||
final String id;
|
||||
final String name;
|
||||
final String displayName;
|
||||
final List<Map<String, dynamic>> paramTypes;
|
||||
|
||||
const NymeaActionType({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.displayName,
|
||||
this.paramTypes = const [],
|
||||
});
|
||||
|
||||
factory NymeaActionType.fromJson(Map<String, dynamic> j) => NymeaActionType(
|
||||
id: j['id'] as String? ?? '',
|
||||
name: j['name'] as String? ?? '',
|
||||
displayName: j['displayName'] as String? ?? j['name'] as String? ?? '',
|
||||
paramTypes: List<Map<String, dynamic>>.from(j['paramTypes'] ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
class NymeaThingClass {
|
||||
final String id;
|
||||
final String name;
|
||||
final String displayName;
|
||||
final List<String> interfaces; // ← clé pour la catégorisation
|
||||
final List<NymeaStateType> stateTypes;
|
||||
final List<NymeaActionType> actionTypes;
|
||||
final List<Map<String, dynamic>> paramTypes;
|
||||
final List<NymeaStateType> settingsTypes;
|
||||
|
||||
const NymeaThingClass({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.displayName,
|
||||
this.interfaces = const [],
|
||||
this.stateTypes = const [],
|
||||
this.actionTypes = const [],
|
||||
this.paramTypes = const [],
|
||||
this.settingsTypes = const [],
|
||||
});
|
||||
|
||||
factory NymeaThingClass.fromJson(Map<String, dynamic> j) => NymeaThingClass(
|
||||
id: j['id'] as String? ?? '',
|
||||
name: j['name'] as String? ?? '',
|
||||
displayName: j['displayName'] as String? ?? '',
|
||||
interfaces: List<String>.from(j['interfaces'] ?? []),
|
||||
stateTypes: (j['stateTypes'] as List? ?? [])
|
||||
.map((s) => NymeaStateType.fromJson(s as Map<String, dynamic>))
|
||||
.toList(),
|
||||
actionTypes: (j['actionTypes'] as List? ?? [])
|
||||
.map((a) => NymeaActionType.fromJson(a as Map<String, dynamic>))
|
||||
.toList(),
|
||||
paramTypes: List<Map<String, dynamic>>.from(j['paramTypes'] ?? []),
|
||||
settingsTypes: (j['settingsTypes'] as List? ?? [])
|
||||
.map((s) => NymeaStateType.fromJson(s as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
/// Retourne la stateType par son id
|
||||
NymeaStateType? stateTypeById(String id) {
|
||||
try { return stateTypes.firstWhere((s) => s.id == id); }
|
||||
catch (_) { return null; }
|
||||
}
|
||||
|
||||
/// Retourne la stateType "principale" à afficher sur la card
|
||||
NymeaStateType? get primaryStateType {
|
||||
// Priorité : power → energy → temperature → connected → on/off → premier
|
||||
const priority = [
|
||||
'currentPower', 'power', 'totalEnergyConsumed', 'totalEnergyProduced',
|
||||
'temperature', 'batterylevel', 'soc', 'connected', 'on',
|
||||
];
|
||||
for (final p in priority) {
|
||||
final found = stateTypes.where(
|
||||
(s) => s.name.toLowerCase() == p.toLowerCase()
|
||||
);
|
||||
if (found.isNotEmpty) return found.first;
|
||||
}
|
||||
return stateTypes.isNotEmpty ? stateTypes.first : null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Favorite Widgets ──────────────────────────────────────────────────────────
|
||||
|
||||
enum FavoriteType {
|
||||
pvPower,
|
||||
homePower,
|
||||
batterySOC,
|
||||
gridPower,
|
||||
rates,
|
||||
evCharger,
|
||||
thingState,
|
||||
chart,
|
||||
}
|
||||
|
||||
class FavoriteWidget {
|
||||
final String id;
|
||||
final FavoriteType type;
|
||||
final String title;
|
||||
final String? thingId;
|
||||
final String? stateTypeId;
|
||||
final Map<String, dynamic> extra;
|
||||
|
||||
const FavoriteWidget({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.title,
|
||||
this.thingId,
|
||||
this.stateTypeId,
|
||||
this.extra = const {},
|
||||
});
|
||||
}
|
||||
187
lib/models/thing_category.dart
Normal file
@ -0,0 +1,187 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'nymea_models.dart';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Catégories affichées dans le Things screen
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
enum ThingCategory {
|
||||
energy, // smartmeter, energymeter, powersocket
|
||||
solar, // solarinverter, solarpanel
|
||||
battery, // battery, energystorage
|
||||
evCharger, // evcharger
|
||||
hvac, // thermostat, heatingzone, ac, pump
|
||||
lighting, // light, dimmablelight, colorlight
|
||||
sensors, // sensor, temperaturesensor, humiditysensor, motionsensor
|
||||
network, // networkdevice, gateway
|
||||
notifications, // pushnotification, email, sms
|
||||
media, // mediadevice, mediaplayer
|
||||
weather, // weather
|
||||
other, // tout le reste
|
||||
}
|
||||
|
||||
class ThingCategoryInfo {
|
||||
final ThingCategory category;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const ThingCategoryInfo({
|
||||
required this.category,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
});
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Mapping interface nymea → catégorie
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const Map<String, ThingCategory> interfaceToCategoryMap = {
|
||||
'smartmeter': ThingCategory.energy,
|
||||
'electricmeter': ThingCategory.energy,
|
||||
'energymeter': ThingCategory.energy,
|
||||
'smartenergymeter': ThingCategory.energy,
|
||||
'extendedsmartmeter': ThingCategory.energy,
|
||||
'powersocket': ThingCategory.energy,
|
||||
'smartplug': ThingCategory.energy,
|
||||
'solarinverter': ThingCategory.solar,
|
||||
'solarpanel': ThingCategory.solar,
|
||||
'inverter': ThingCategory.solar,
|
||||
'battery': ThingCategory.battery,
|
||||
'energystorage': ThingCategory.battery,
|
||||
'batterymonitor': ThingCategory.battery,
|
||||
'evcharger': ThingCategory.evCharger,
|
||||
'wallbox': ThingCategory.evCharger,
|
||||
'thermostat': ThingCategory.hvac,
|
||||
'heatingzone': ThingCategory.hvac,
|
||||
'ac': ThingCategory.hvac,
|
||||
'airconditioner': ThingCategory.hvac,
|
||||
'pump': ThingCategory.hvac,
|
||||
'heatpump': ThingCategory.hvac,
|
||||
'ventilation': ThingCategory.hvac,
|
||||
'boiler': ThingCategory.hvac,
|
||||
'light': ThingCategory.lighting,
|
||||
'dimmablelight': ThingCategory.lighting,
|
||||
'colorlight': ThingCategory.lighting,
|
||||
'colortemperaturelight': ThingCategory.lighting,
|
||||
'sensor': ThingCategory.sensors,
|
||||
'temperaturesensor': ThingCategory.sensors,
|
||||
'humiditysensor': ThingCategory.sensors,
|
||||
'pressuresensor': ThingCategory.sensors,
|
||||
'motionsensor': ThingCategory.sensors,
|
||||
'doorsensor': ThingCategory.sensors,
|
||||
'windowsensor': ThingCategory.sensors,
|
||||
'smokesensor': ThingCategory.sensors,
|
||||
'co2sensor': ThingCategory.sensors,
|
||||
'lightsensor': ThingCategory.sensors,
|
||||
'networkdevice': ThingCategory.network,
|
||||
'gateway': ThingCategory.network,
|
||||
'router': ThingCategory.network,
|
||||
'pushnotification': ThingCategory.notifications,
|
||||
'notificationservice': ThingCategory.notifications,
|
||||
'notification': ThingCategory.notifications,
|
||||
'email': ThingCategory.notifications,
|
||||
'sms': ThingCategory.notifications,
|
||||
'weather': ThingCategory.weather,
|
||||
'weatherstation': ThingCategory.weather,
|
||||
'mediadevice': ThingCategory.media,
|
||||
'mediaplayer': ThingCategory.media,
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Infos visuelles par catégorie
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
const Map<ThingCategory, ThingCategoryInfo> categoryInfoMap = {
|
||||
ThingCategory.energy: ThingCategoryInfo(
|
||||
category: ThingCategory.energy,
|
||||
label: 'Compteurs & Prises',
|
||||
icon: Icons.electrical_services_rounded,
|
||||
color: AppTheme.gridGray,
|
||||
),
|
||||
ThingCategory.solar: ThingCategoryInfo(
|
||||
category: ThingCategory.solar,
|
||||
label: 'Solaire',
|
||||
icon: Icons.solar_power_rounded,
|
||||
color: AppTheme.solarYellow,
|
||||
),
|
||||
ThingCategory.battery: ThingCategoryInfo(
|
||||
category: ThingCategory.battery,
|
||||
label: 'Stockage',
|
||||
icon: Icons.battery_charging_full_rounded,
|
||||
color: AppTheme.batteryGreen,
|
||||
),
|
||||
ThingCategory.evCharger: ThingCategoryInfo(
|
||||
category: ThingCategory.evCharger,
|
||||
label: 'Chargeurs EV',
|
||||
icon: Icons.electric_car_rounded,
|
||||
color: AppTheme.minPvBlue,
|
||||
),
|
||||
ThingCategory.hvac: ThingCategoryInfo(
|
||||
category: ThingCategory.hvac,
|
||||
label: 'Chauffage & Climatisation',
|
||||
icon: Icons.thermostat_rounded,
|
||||
color: Color(0xFFFF7043),
|
||||
),
|
||||
ThingCategory.lighting: ThingCategoryInfo(
|
||||
category: ThingCategory.lighting,
|
||||
label: 'Éclairage',
|
||||
icon: Icons.lightbulb_rounded,
|
||||
color: Color(0xFFFFCA28),
|
||||
),
|
||||
ThingCategory.sensors: ThingCategoryInfo(
|
||||
category: ThingCategory.sensors,
|
||||
label: 'Capteurs',
|
||||
icon: Icons.sensors_rounded,
|
||||
color: Color(0xFF26C6DA),
|
||||
),
|
||||
ThingCategory.network: ThingCategoryInfo(
|
||||
category: ThingCategory.network,
|
||||
label: 'Réseau & Passerelles',
|
||||
icon: Icons.router_rounded,
|
||||
color: Color(0xFF7C4DFF),
|
||||
),
|
||||
ThingCategory.notifications: ThingCategoryInfo(
|
||||
category: ThingCategory.notifications,
|
||||
label: 'Services de notification',
|
||||
icon: Icons.notifications_rounded,
|
||||
color: Color(0xFFEC407A),
|
||||
),
|
||||
ThingCategory.weather: ThingCategoryInfo(
|
||||
category: ThingCategory.weather,
|
||||
label: 'Météo',
|
||||
icon: Icons.wb_cloudy_rounded,
|
||||
color: Color(0xFF42A5F5),
|
||||
),
|
||||
ThingCategory.media: ThingCategoryInfo(
|
||||
category: ThingCategory.media,
|
||||
label: 'Multimédia',
|
||||
icon: Icons.speaker_rounded,
|
||||
color: Color(0xFFAB47BC),
|
||||
),
|
||||
ThingCategory.other: ThingCategoryInfo(
|
||||
category: ThingCategory.other,
|
||||
label: 'Autres',
|
||||
icon: Icons.device_hub_rounded,
|
||||
color: Color(0xFF78909C),
|
||||
),
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Extension utilitaire sur NymeaThingClass
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
extension ThingClassCategoryExt on NymeaThingClass {
|
||||
ThingCategory get category {
|
||||
for (final iface in interfaces) {
|
||||
final cat = interfaceToCategoryMap[iface.toLowerCase()];
|
||||
if (cat != null) return cat;
|
||||
}
|
||||
return ThingCategory.other;
|
||||
}
|
||||
|
||||
ThingCategoryInfo get categoryDetails => categoryInfoMap[category]!;
|
||||
}
|
||||
333
lib/screens/ac_screen.dart
Normal file
@ -0,0 +1,333 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class ACScreen extends StatefulWidget {
|
||||
const ACScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ACScreen> createState() => _ACScreenState();
|
||||
}
|
||||
|
||||
class _ACScreenState extends State<ACScreen> {
|
||||
final List<_ACZone> _zones = [
|
||||
_ACZone(name: 'Salon', currentTemp: 19.5, targetTemp: 21, mode: ACMode.heat, isOn: true),
|
||||
_ACZone(name: 'Chambre', currentTemp: 18.0, targetTemp: 19, mode: ACMode.cool, isOn: false),
|
||||
_ACZone(name: 'Bureau', currentTemp: 20.0, targetTemp: 22, mode: ACMode.heat, isOn: true),
|
||||
_ACZone(name: 'Cuisine', currentTemp: 21.5, targetTemp: 21, mode: ACMode.auto, isOn: false),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
elevation: 0,
|
||||
title: const Text(
|
||||
'Climatisation / Chauffage',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, color: AppTheme.textDark),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.schedule, color: AppTheme.textDark),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _zones.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
return _ZoneCard(
|
||||
zone: _zones[index],
|
||||
onChanged: (updated) {
|
||||
setState(() => _zones[index] = updated);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ACMode { heat, cool, auto, fan }
|
||||
|
||||
class _ACZone {
|
||||
final String name;
|
||||
final double currentTemp;
|
||||
final double targetTemp;
|
||||
final ACMode mode;
|
||||
final bool isOn;
|
||||
|
||||
const _ACZone({
|
||||
required this.name,
|
||||
required this.currentTemp,
|
||||
required this.targetTemp,
|
||||
required this.mode,
|
||||
required this.isOn,
|
||||
});
|
||||
|
||||
_ACZone copyWith({
|
||||
double? targetTemp,
|
||||
ACMode? mode,
|
||||
bool? isOn,
|
||||
}) =>
|
||||
_ACZone(
|
||||
name: name,
|
||||
currentTemp: currentTemp,
|
||||
targetTemp: targetTemp ?? this.targetTemp,
|
||||
mode: mode ?? this.mode,
|
||||
isOn: isOn ?? this.isOn,
|
||||
);
|
||||
}
|
||||
|
||||
class _ZoneCard extends StatelessWidget {
|
||||
final _ACZone zone;
|
||||
final ValueChanged<_ACZone> onChanged;
|
||||
|
||||
const _ZoneCard({required this.zone, required this.onChanged});
|
||||
|
||||
Color get _modeColor {
|
||||
switch (zone.mode) {
|
||||
case ACMode.heat:
|
||||
return Colors.orange;
|
||||
case ACMode.cool:
|
||||
return Colors.cyan;
|
||||
case ACMode.auto:
|
||||
return AppTheme.primaryGreen;
|
||||
case ACMode.fan:
|
||||
return Colors.blueGrey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData get _modeIcon {
|
||||
switch (zone.mode) {
|
||||
case ACMode.heat:
|
||||
return Icons.whatshot_rounded;
|
||||
case ACMode.cool:
|
||||
return Icons.ac_unit_rounded;
|
||||
case ACMode.auto:
|
||||
return Icons.autorenew_rounded;
|
||||
case ACMode.fan:
|
||||
return Icons.air_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
String get _modeLabel {
|
||||
switch (zone.mode) {
|
||||
case ACMode.heat:
|
||||
return 'Chauffage';
|
||||
case ACMode.cool:
|
||||
return 'Clim';
|
||||
case ACMode.auto:
|
||||
return 'Auto';
|
||||
case ACMode.fan:
|
||||
return 'Ventilation';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header row
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: zone.isOn
|
||||
? _modeColor.withValues(alpha:0.15)
|
||||
: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(
|
||||
_modeIcon,
|
||||
color: zone.isOn ? _modeColor : Colors.grey,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(zone.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
fontSize: 16)),
|
||||
Text(
|
||||
zone.isOn ? _modeLabel : 'Éteint',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color:
|
||||
zone.isOn ? _modeColor : AppTheme.textLight),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: zone.isOn,
|
||||
onChanged: (v) => onChanged(zone.copyWith(isOn: v)),
|
||||
activeThumbColor: _modeColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (zone.isOn) ...[
|
||||
const Divider(height: 20),
|
||||
|
||||
// Temperature
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
const Text('Actuelle',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.textLight)),
|
||||
Text(
|
||||
'${zone.currentTemp.toStringAsFixed(1)}°C',
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Icon(Icons.arrow_forward_rounded,
|
||||
color: Colors.grey.shade400),
|
||||
Column(
|
||||
children: [
|
||||
const Text('Cible',
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: AppTheme.textLight)),
|
||||
Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.remove_circle_outline,
|
||||
color: _modeColor),
|
||||
onPressed: () => onChanged(zone.copyWith(
|
||||
targetTemp: zone.targetTemp - 0.5)),
|
||||
),
|
||||
Text(
|
||||
'${zone.targetTemp.toStringAsFixed(1)}°C',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _modeColor,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.add_circle_outline,
|
||||
color: _modeColor),
|
||||
onPressed: () => onChanged(zone.copyWith(
|
||||
targetTemp: zone.targetTemp + 0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Mode selector
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: ACMode.values.map((m) {
|
||||
final selected = zone.mode == m;
|
||||
final color = _modeColorFor(m);
|
||||
final icon = _modeIconFor(m);
|
||||
final label = _modeLabelFor(m);
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: GestureDetector(
|
||||
onTap: () => onChanged(zone.copyWith(mode: m)),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? color.withValues(alpha:0.15)
|
||||
: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: selected
|
||||
? Border.all(color: color, width: 1.5)
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: 18,
|
||||
color: selected ? color : Colors.grey),
|
||||
const SizedBox(height: 2),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color:
|
||||
selected ? color : AppTheme.textLight,
|
||||
fontWeight: selected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Color _modeColorFor(ACMode m) {
|
||||
switch (m) {
|
||||
case ACMode.heat:
|
||||
return Colors.orange;
|
||||
case ACMode.cool:
|
||||
return Colors.cyan;
|
||||
case ACMode.auto:
|
||||
return AppTheme.primaryGreen;
|
||||
case ACMode.fan:
|
||||
return Colors.blueGrey;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _modeIconFor(ACMode m) {
|
||||
switch (m) {
|
||||
case ACMode.heat:
|
||||
return Icons.whatshot_rounded;
|
||||
case ACMode.cool:
|
||||
return Icons.ac_unit_rounded;
|
||||
case ACMode.auto:
|
||||
return Icons.autorenew_rounded;
|
||||
case ACMode.fan:
|
||||
return Icons.air_rounded;
|
||||
}
|
||||
}
|
||||
|
||||
String _modeLabelFor(ACMode m) {
|
||||
switch (m) {
|
||||
case ACMode.heat:
|
||||
return 'Chauf.';
|
||||
case ACMode.cool:
|
||||
return 'Clim';
|
||||
case ACMode.auto:
|
||||
return 'Auto';
|
||||
case ACMode.fan:
|
||||
return 'Vent.';
|
||||
}
|
||||
}
|
||||
}
|
||||
556
lib/screens/dashboard_screen.dart
Normal file
@ -0,0 +1,556 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/nymea_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import '../widgets/energy_flow_widget.dart';
|
||||
import '../widgets/production_card.dart';
|
||||
import '../widgets/ev_charging_card.dart';
|
||||
import '../widgets/gains_card.dart';
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
const DashboardScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||
}
|
||||
|
||||
class _DashboardScreenState extends State<DashboardScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Start simulation on first load
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final service = context.read<NymeaService>();
|
||||
if (!service.connected) {
|
||||
service.startSimulation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<NymeaService>(
|
||||
builder: (context, service, _) {
|
||||
final data = service.energyData;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
body: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
service.startSimulation();
|
||||
},
|
||||
color: AppTheme.primaryGreen,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
// App bar
|
||||
SliverAppBar(
|
||||
floating: true,
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
elevation: 0,
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryGreen,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.bolt, color: Colors.white, size: 22),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'Nymea Energy',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
// Connection status
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
service.connected
|
||||
? Icons.wifi_rounded
|
||||
: Icons.wifi_off_rounded,
|
||||
color: service.connected
|
||||
? AppTheme.primaryGreen
|
||||
: Colors.red,
|
||||
),
|
||||
onPressed: () => _showConnectionDialog(context, service),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.notifications_outlined,
|
||||
color: AppTheme.textDark),
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Energy flow
|
||||
EnergyFlowWidget(data: data),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Self-consumption rates
|
||||
_RatesRow(data: data),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Production card
|
||||
ProductionCard(data: data),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Gains
|
||||
GainsCard(data: data),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// EV Charging
|
||||
EVChargingCard(data: data, service: service),
|
||||
const SizedBox(height: 16),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showConnectionDialog(BuildContext context, NymeaService service) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _ConnectionDialog(service: service),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RatesRow extends StatelessWidget {
|
||||
final dynamic data;
|
||||
|
||||
const _RatesRow({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _RateCard(
|
||||
label: 'Autoconsommation',
|
||||
value: data.selfConsumptionRate,
|
||||
icon: Icons.wb_sunny_rounded,
|
||||
color: AppTheme.solarYellow,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _RateCard(
|
||||
label: 'Autonomie',
|
||||
value: data.autonomyRate,
|
||||
icon: Icons.home_rounded,
|
||||
color: AppTheme.homeBlue,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RateCard extends StatelessWidget {
|
||||
final String label;
|
||||
final double value;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
|
||||
const _RateCard({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 18),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppTheme.textLight),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${value.toStringAsFixed(1)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: (value / 100).clamp(0, 1),
|
||||
backgroundColor: color.withValues(alpha:0.15),
|
||||
valueColor: AlwaysStoppedAnimation(color),
|
||||
minHeight: 6,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Dialog de connexion avec sélection du protocole TCP / WebSocket
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class _ConnectionDialog extends StatefulWidget {
|
||||
final NymeaService service;
|
||||
const _ConnectionDialog({required this.service});
|
||||
|
||||
@override
|
||||
State<_ConnectionDialog> createState() => _ConnectionDialogState();
|
||||
}
|
||||
|
||||
class _ConnectionDialogState extends State<_ConnectionDialog> {
|
||||
late TextEditingController _hostCtrl;
|
||||
late TextEditingController _portCtrl;
|
||||
late TextEditingController _userCtrl;
|
||||
late TextEditingController _passCtrl;
|
||||
late NymeaProtocol _protocol;
|
||||
bool _connecting = false;
|
||||
bool _verbose = false;
|
||||
|
||||
// Ports par défaut selon protocole
|
||||
static const _defaultPorts = {
|
||||
NymeaProtocol.tcpRaw: 2222,
|
||||
NymeaProtocol.webSocket: 4444,
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_protocol = widget.service.protocol;
|
||||
_hostCtrl = TextEditingController(text: widget.service.host);
|
||||
_portCtrl = TextEditingController(text: widget.service.port.toString());
|
||||
_userCtrl = TextEditingController(text: widget.service.username);
|
||||
_passCtrl = TextEditingController(text: widget.service.password);
|
||||
_verbose = widget.service.verboseLog;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hostCtrl.dispose();
|
||||
_portCtrl.dispose();
|
||||
_userCtrl.dispose();
|
||||
_passCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onProtocolChanged(NymeaProtocol p) {
|
||||
setState(() {
|
||||
_protocol = p;
|
||||
// Mettre à jour le port par défaut si l'utilisateur n'a pas changé
|
||||
final currentPort = int.tryParse(_portCtrl.text) ?? 0;
|
||||
final otherDefault = _defaultPorts[_protocol == NymeaProtocol.tcpRaw
|
||||
? NymeaProtocol.webSocket
|
||||
: NymeaProtocol.tcpRaw]!;
|
||||
if (currentPort == otherDefault || currentPort == 0) {
|
||||
_portCtrl.text = _defaultPorts[p]!.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 36, height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryGreen.withValues(alpha:0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.router_rounded,
|
||||
color: AppTheme.primaryGreen, size: 20),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
const Text('Connexion Nymea'),
|
||||
],
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── Sélecteur de protocole ─────────────────────────────────────
|
||||
const Text('Protocole',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textLight)),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_ProtoTab(
|
||||
label: 'TCP brut',
|
||||
subtitle: 'port 2222',
|
||||
icon: Icons.cable_rounded,
|
||||
selected: _protocol == NymeaProtocol.tcpRaw,
|
||||
onTap: () => _onProtocolChanged(NymeaProtocol.tcpRaw),
|
||||
),
|
||||
_ProtoTab(
|
||||
label: 'WebSocket',
|
||||
subtitle: 'port 4444',
|
||||
icon: Icons.wifi_rounded,
|
||||
selected: _protocol == NymeaProtocol.webSocket,
|
||||
onTap: () => _onProtocolChanged(NymeaProtocol.webSocket),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// Info protocole
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryGreen.withValues(alpha:0.07),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_protocol == NymeaProtocol.tcpRaw
|
||||
? '💡 TCP brut : nymea envoie un message de bienvenue dès la connexion. Utilisé par l\'app nymea officielle.'
|
||||
: '💡 WebSocket : le client envoie JSONRPC.Hello en premier. Activez-le dans nymea → Paramètres → Serveur WebSocket.',
|
||||
style: const TextStyle(fontSize: 11, color: AppTheme.textLight),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// ── Hôte ──────────────────────────────────────────────────────
|
||||
TextField(
|
||||
controller: _hostCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Adresse IP / Hôte',
|
||||
prefixIcon: Icon(Icons.dns_outlined),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// ── Port ──────────────────────────────────────────────────────
|
||||
TextField(
|
||||
controller: _portCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Port',
|
||||
prefixIcon: Icon(Icons.settings_ethernet),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// ── Identifiants ──────────────────────────────────────────
|
||||
TextField(
|
||||
controller: _userCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Utilisateur',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: _passCtrl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Mot de passe',
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
border: OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
|
||||
// ── Debug verbose ─────────────────────────────────────────
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.terminal_rounded, size: 16,
|
||||
color: AppTheme.textLight),
|
||||
const SizedBox(width: 8),
|
||||
const Expanded(
|
||||
child: Text('Logs JSON verbose',
|
||||
style: TextStyle(fontSize: 13, color: AppTheme.textDark)),
|
||||
),
|
||||
Switch(
|
||||
value: _verbose,
|
||||
activeThumbColor: AppTheme.primaryGreen,
|
||||
onChanged: (v) {
|
||||
setState(() => _verbose = v);
|
||||
widget.service.verboseLog = v;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ── Erreur ────────────────────────────────────────────────────
|
||||
if (widget.service.connectionError != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.red, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.service.connectionError!,
|
||||
style: const TextStyle(color: Colors.red, fontSize: 11),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
// Mode démo
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.science_outlined, size: 16),
|
||||
label: const Text('Mode démo'),
|
||||
onPressed: () {
|
||||
widget.service.startSimulation();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
// Connecter
|
||||
ElevatedButton.icon(
|
||||
icon: _connecting
|
||||
? const SizedBox(
|
||||
width: 14, height: 14,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Icon(Icons.link_rounded, size: 16),
|
||||
label: const Text('Connecter'),
|
||||
onPressed: _connecting
|
||||
? null
|
||||
: () async {
|
||||
setState(() => _connecting = true);
|
||||
final ok = await widget.service.connect(
|
||||
_hostCtrl.text.trim(),
|
||||
int.tryParse(_portCtrl.text.trim()) ??
|
||||
_defaultPorts[_protocol]!,
|
||||
protocol: _protocol,
|
||||
username: _userCtrl.text.trim(),
|
||||
password: _passCtrl.text,
|
||||
);
|
||||
if (ok && context.mounted) Navigator.pop(context);
|
||||
if (mounted) setState(() => _connecting = false);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProtoTab extends StatelessWidget {
|
||||
final String label;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ProtoTab({
|
||||
required this.label,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
margin: const EdgeInsets.all(4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: selected ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: selected
|
||||
? [BoxShadow(
|
||||
color: Colors.black.withValues(alpha:0.08),
|
||||
blurRadius: 4, offset: const Offset(0, 1))]
|
||||
: null,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon,
|
||||
size: 20,
|
||||
color: selected ? AppTheme.primaryGreen : AppTheme.textLight),
|
||||
const SizedBox(height: 4),
|
||||
Text(label,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: selected ? AppTheme.primaryGreen : AppTheme.textLight)),
|
||||
Text(subtitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 10, color: AppTheme.textLight)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
403
lib/screens/energy_screen.dart
Normal file
@ -0,0 +1,403 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/nymea_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class EnergyScreen extends StatefulWidget {
|
||||
const EnergyScreen({super.key});
|
||||
|
||||
@override
|
||||
State<EnergyScreen> createState() => _EnergyScreenState();
|
||||
}
|
||||
|
||||
class _EnergyScreenState extends State<EnergyScreen> {
|
||||
String _period = 'Jour';
|
||||
final List<String> _periods = ['Jour', 'Semaine', 'Mois', 'Année'];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<NymeaService>(
|
||||
builder: (context, service, _) {
|
||||
final points = service.historyPoints;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
elevation: 0,
|
||||
title: const Text(
|
||||
'Énergie',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, color: AppTheme.textDark),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.file_download_outlined,
|
||||
color: AppTheme.textDark),
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Period selector
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Row(
|
||||
children: _periods.map((p) {
|
||||
final selected = _period == p;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _period = p),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: selected
|
||||
? AppTheme.primaryGreen
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
p,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: selected
|
||||
? Colors.white
|
||||
: AppTheme.textLight,
|
||||
fontWeight: selected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Summary cards
|
||||
_SummaryRow(service: service),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Production chart
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Production & Consommation',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
fontSize: 15)),
|
||||
const SizedBox(height: 4),
|
||||
const _Legend(),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: points.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: BarChart(
|
||||
BarChartData(
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
maxY: 5000,
|
||||
barTouchData: BarTouchData(enabled: true),
|
||||
titlesData: FlTitlesData(
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 24,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final idx = value.toInt();
|
||||
if (idx % 3 != 0 ||
|
||||
idx >= points.length) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return Text(
|
||||
'${points[idx].time.hour}h',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppTheme.textLight),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
getTitlesWidget: (value, meta) => Text(
|
||||
'${(value / 1000).toStringAsFixed(1)}k',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: AppTheme.textLight),
|
||||
),
|
||||
),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles:
|
||||
SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles:
|
||||
SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (value) =>
|
||||
FlLine(
|
||||
color: Colors.grey.shade100,
|
||||
strokeWidth: 1,
|
||||
),
|
||||
),
|
||||
barGroups: points
|
||||
.asMap()
|
||||
.entries
|
||||
.map(
|
||||
(e) => BarChartGroupData(
|
||||
x: e.key,
|
||||
barsSpace: 2,
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
toY: e.value.pvWh,
|
||||
color: AppTheme.solarYellow,
|
||||
width: 5,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(
|
||||
top: Radius.circular(3)),
|
||||
),
|
||||
BarChartRodData(
|
||||
toY: e.value.homeWh,
|
||||
color: AppTheme.homeBlue,
|
||||
width: 5,
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(
|
||||
top: Radius.circular(3)),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Grid line chart
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Import/Export réseau',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
fontSize: 15)),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 160,
|
||||
child: points.isEmpty
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: LineChart(
|
||||
LineChartData(
|
||||
minY: -2000,
|
||||
maxY: 3000,
|
||||
lineTouchData:
|
||||
LineTouchData(enabled: true),
|
||||
titlesData: FlTitlesData(
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 22,
|
||||
getTitlesWidget: (value, meta) {
|
||||
final idx = value.toInt();
|
||||
if (idx % 4 != 0 ||
|
||||
idx >= points.length) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return Text(
|
||||
'${points[idx].time.hour}h',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: AppTheme.textLight),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 42,
|
||||
getTitlesWidget: (v, _) => Text(
|
||||
'${v.toStringAsFixed(0)} W',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: AppTheme.textLight),
|
||||
),
|
||||
),
|
||||
),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles:
|
||||
SideTitles(showTitles: false)),
|
||||
topTitles: const AxisTitles(
|
||||
sideTitles:
|
||||
SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
gridData: FlGridData(
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (v) => FlLine(
|
||||
color: v == 0
|
||||
? Colors.grey.shade400
|
||||
: Colors.grey.shade100,
|
||||
strokeWidth: v == 0 ? 1.5 : 1),
|
||||
),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: points
|
||||
.asMap()
|
||||
.entries
|
||||
.map((e) => FlSpot(
|
||||
e.key.toDouble(),
|
||||
e.value.gridWh))
|
||||
.toList(),
|
||||
isCurved: true,
|
||||
color: Colors.orange,
|
||||
barWidth: 2,
|
||||
belowBarData: BarAreaData(
|
||||
show: true,
|
||||
color: Colors.orange.withValues(alpha:0.1),
|
||||
),
|
||||
dotData: const FlDotData(show: false),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Legend extends StatelessWidget {
|
||||
const _Legend();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
_dot(AppTheme.solarYellow),
|
||||
const SizedBox(width: 4),
|
||||
const Text('Production', style: TextStyle(fontSize: 12, color: AppTheme.textLight)),
|
||||
const SizedBox(width: 16),
|
||||
_dot(AppTheme.homeBlue),
|
||||
const SizedBox(width: 4),
|
||||
const Text('Consommation', style: TextStyle(fontSize: 12, color: AppTheme.textLight)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _dot(Color c) => Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(color: c, shape: BoxShape.circle),
|
||||
);
|
||||
}
|
||||
|
||||
class _SummaryRow extends StatelessWidget {
|
||||
final NymeaService service;
|
||||
|
||||
const _SummaryRow({required this.service});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final d = service.energyData;
|
||||
return Row(
|
||||
children: [
|
||||
_SummaryCard(
|
||||
label: 'Production',
|
||||
value: '${(d.dayProductionWh / 1000).toStringAsFixed(2)} kWh',
|
||||
color: AppTheme.solarYellow,
|
||||
icon: Icons.wb_sunny_rounded),
|
||||
const SizedBox(width: 8),
|
||||
_SummaryCard(
|
||||
label: 'Consommation',
|
||||
value: '${(d.daySelfConsumptionWh / 1000).toStringAsFixed(2)} kWh',
|
||||
color: AppTheme.homeBlue,
|
||||
icon: Icons.home_rounded),
|
||||
const SizedBox(width: 8),
|
||||
_SummaryCard(
|
||||
label: 'Gains',
|
||||
value: '${d.dayGains.toStringAsFixed(2)} €',
|
||||
color: Colors.amber,
|
||||
icon: Icons.euro_rounded),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SummaryCard extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final Color color;
|
||||
final IconData icon;
|
||||
|
||||
const _SummaryCard({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.color,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(height: 6),
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
fontSize: 13)),
|
||||
const SizedBox(height: 2),
|
||||
Text(label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10, color: AppTheme.textLight)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
706
lib/screens/favorites_screen.dart
Normal file
@ -0,0 +1,706 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/energy_data.dart';
|
||||
import '../models/nymea_models.dart';
|
||||
import '../services/nymea_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class FavoritesScreen extends StatelessWidget {
|
||||
const FavoritesScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<NymeaService>(
|
||||
builder: (context, service, _) {
|
||||
final favs = service.favoriteWidgets;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
elevation: 0,
|
||||
title: const Text('Favoris',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark)),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline,
|
||||
color: AppTheme.primaryGreen),
|
||||
onPressed: () => _showAddFavorite(context, service),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: favs.isEmpty
|
||||
? _EmptyState(onAdd: () => _showAddFavorite(context, service))
|
||||
: ReorderableListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
itemCount: favs.length,
|
||||
onReorder: service.reorderFavorites,
|
||||
proxyDecorator: (child, index, animation) => Material(
|
||||
elevation: 6,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: child,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final fav = favs[index];
|
||||
return Padding(
|
||||
key: ValueKey(fav.id),
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: _FavoriteCard(
|
||||
fav: fav,
|
||||
service: service,
|
||||
onRemove: () => service.removeFavorite(fav.id),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddFavorite(BuildContext context, NymeaService service) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
||||
builder: (_) => _AddFavoriteSheet(service: service),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Favorite Card ─────────────────────────────────────────────────────────────
|
||||
|
||||
class _FavoriteCard extends StatelessWidget {
|
||||
final FavoriteWidget fav;
|
||||
final NymeaService service;
|
||||
final VoidCallback onRemove;
|
||||
|
||||
const _FavoriteCard({
|
||||
required this.fav,
|
||||
required this.service,
|
||||
required this.onRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with drag handle + title + remove
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.drag_handle, color: AppTheme.textLight),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(fav.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
fontSize: 15)),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 18, color: AppTheme.textLight),
|
||||
onPressed: onRemove,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Content based on type
|
||||
_buildContent(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContent(BuildContext context) {
|
||||
final d = service.energyData;
|
||||
switch (fav.type) {
|
||||
case FavoriteType.pvPower:
|
||||
return _BigMetric(
|
||||
value: _fmt(d.pvPower),
|
||||
unit: d.pvPower >= 1000 ? 'kW' : 'W',
|
||||
label: 'Production solaire',
|
||||
icon: Icons.wb_sunny_rounded,
|
||||
color: AppTheme.solarYellow,
|
||||
trend: d.pvPower > 100 ? '↑ En production' : 'Pas de production',
|
||||
);
|
||||
|
||||
case FavoriteType.homePower:
|
||||
return _BigMetric(
|
||||
value: _fmt(d.homePower),
|
||||
unit: d.homePower >= 1000 ? 'kW' : 'W',
|
||||
label: 'Consommation maison',
|
||||
icon: Icons.home_rounded,
|
||||
color: AppTheme.homeBlue,
|
||||
trend: 'Dont ${d.selfConsumptionRate.toStringAsFixed(0)}% solaire',
|
||||
);
|
||||
|
||||
case FavoriteType.batterySOC:
|
||||
return _BatteryWidget(data: d);
|
||||
|
||||
case FavoriteType.gridPower:
|
||||
return _BigMetric(
|
||||
value: _fmt(d.gridPower.abs()),
|
||||
unit: d.gridPower.abs() >= 1000 ? 'kW' : 'W',
|
||||
label: d.gridPower >= 0 ? 'Import réseau' : 'Export réseau',
|
||||
icon: Icons.electrical_services_rounded,
|
||||
color: d.gridPower >= 0 ? Colors.orange : AppTheme.primaryGreen,
|
||||
trend: d.gridPower >= 0 ? '↓ Achat réseau' : '↑ Vente réseau',
|
||||
);
|
||||
|
||||
case FavoriteType.rates:
|
||||
return _RatesWidget(data: d);
|
||||
|
||||
case FavoriteType.evCharger:
|
||||
return _EVWidget(data: d, service: service);
|
||||
|
||||
case FavoriteType.thingState:
|
||||
return _ThingStateWidget(fav: fav, service: service);
|
||||
|
||||
case FavoriteType.chart:
|
||||
return _MiniChart(historyPoints: service.historyPoints);
|
||||
}
|
||||
}
|
||||
|
||||
String _fmt(double w) {
|
||||
if (w >= 1000) return (w / 1000).toStringAsFixed(1);
|
||||
return w.toStringAsFixed(0);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Sub-widgets ───────────────────────────────────────────────────────────────
|
||||
|
||||
class _BigMetric extends StatelessWidget {
|
||||
final String value;
|
||||
final String unit;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final String trend;
|
||||
|
||||
const _BigMetric({
|
||||
required this.value,
|
||||
required this.unit,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.trend,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 56,
|
||||
height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha:0.12),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 30),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(value,
|
||||
style: TextStyle(
|
||||
fontSize: 34,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color)),
|
||||
const SizedBox(width: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(unit,
|
||||
style: const TextStyle(
|
||||
fontSize: 16, color: AppTheme.textLight)),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, color: AppTheme.textLight)),
|
||||
const SizedBox(height: 2),
|
||||
Text(trend,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w500)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BatteryWidget extends StatelessWidget {
|
||||
final EnergyData data;
|
||||
const _BatteryWidget({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final soc = data.batterySOC;
|
||||
final isCharging = data.batteryPower > 0;
|
||||
final color = soc > 50
|
||||
? AppTheme.batteryGreen
|
||||
: soc > 20
|
||||
? Colors.orange
|
||||
: Colors.red;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: CircularProgressIndicator(
|
||||
value: soc / 100,
|
||||
strokeWidth: 8,
|
||||
backgroundColor: color.withValues(alpha:0.15),
|
||||
valueColor: AlwaysStoppedAnimation(color),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
Text('${soc.toStringAsFixed(0)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color)),
|
||||
Icon(
|
||||
isCharging
|
||||
? Icons.bolt
|
||||
: Icons.battery_std_rounded,
|
||||
size: 14,
|
||||
color: color),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('État de charge',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, color: AppTheme.textLight)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
isCharging
|
||||
? '⚡ En charge ${_fmt(data.batteryPower)} W'
|
||||
: data.batteryPower < -10
|
||||
? '↓ Décharge ${_fmt(data.batteryPower.abs())} W'
|
||||
: '○ Standby',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _fmt(double w) => w >= 1000
|
||||
? '${(w / 1000).toStringAsFixed(1)} kW'
|
||||
: '${w.toStringAsFixed(0)} W';
|
||||
}
|
||||
|
||||
class _RatesWidget extends StatelessWidget {
|
||||
final EnergyData data;
|
||||
const _RatesWidget({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _RateItem(
|
||||
label: 'Autoconsommation',
|
||||
value: data.selfConsumptionRate,
|
||||
color: AppTheme.solarYellow)),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _RateItem(
|
||||
label: 'Autonomie',
|
||||
value: data.autonomyRate,
|
||||
color: AppTheme.homeBlue)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RateItem extends StatelessWidget {
|
||||
final String label;
|
||||
final double value;
|
||||
final Color color;
|
||||
const _RateItem(
|
||||
{required this.label, required this.value, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text('${value.toStringAsFixed(1)}%',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color)),
|
||||
const SizedBox(height: 4),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: (value / 100).clamp(0, 1),
|
||||
backgroundColor: color.withValues(alpha:0.15),
|
||||
valueColor: AlwaysStoppedAnimation(color),
|
||||
minHeight: 7,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 11, color: AppTheme.textLight),
|
||||
textAlign: TextAlign.center),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EVWidget extends StatelessWidget {
|
||||
final EnergyData data;
|
||||
final NymeaService service;
|
||||
const _EVWidget({required this.data, required this.service});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final modeColors = {
|
||||
ChargingMode.pv: AppTheme.pvGreen,
|
||||
ChargingMode.minPv: AppTheme.minPvBlue,
|
||||
ChargingMode.boost: AppTheme.boostRed,
|
||||
};
|
||||
final modeLabels = {
|
||||
ChargingMode.pv: 'PV',
|
||||
ChargingMode.minPv: 'Min+PV',
|
||||
ChargingMode.boost: 'Boost',
|
||||
};
|
||||
final color = modeColors[data.chargingMode]!;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha:0.12),
|
||||
borderRadius: BorderRadius.circular(14)),
|
||||
child: Icon(Icons.electric_car_rounded, color: color, size: 28),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('${data.chargingPower.toStringAsFixed(1)} kW',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color)),
|
||||
Text('Mode : ${modeLabels[data.chargingMode]}',
|
||||
style: TextStyle(fontSize: 12, color: color)),
|
||||
Text(
|
||||
'${data.solarSourcePercent.toStringAsFixed(0)}% solaire · Réseau ${data.gridLimitOk ? "OK" : "!"}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: AppTheme.textLight)),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Quick mode toggle
|
||||
Column(
|
||||
children: ChargingMode.values.map((m) {
|
||||
final c = modeColors[m]!;
|
||||
return GestureDetector(
|
||||
onTap: () => service.setChargingMode(m),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
width: 8,
|
||||
height: 8,
|
||||
margin: const EdgeInsets.symmetric(vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: data.chargingMode == m ? c : c.withValues(alpha:0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThingStateWidget extends StatelessWidget {
|
||||
final FavoriteWidget fav;
|
||||
final NymeaService service;
|
||||
const _ThingStateWidget(
|
||||
{required this.fav, required this.service});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final thing = service.things.firstWhere(
|
||||
(t) => t.id == fav.thingId,
|
||||
orElse: () => NymeaThing(
|
||||
id: '', name: 'Introuvable', thingClassId: '',
|
||||
setupStatus: '', paramValues: []));
|
||||
|
||||
final value = fav.stateTypeId != null
|
||||
? thing.stateValue(fav.stateTypeId!)
|
||||
: null;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryGreen.withValues(alpha:0.12),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: const Icon(Icons.device_hub_rounded,
|
||||
color: AppTheme.primaryGreen, size: 26),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(thing.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
fontSize: 14)),
|
||||
const SizedBox(height: 4),
|
||||
Text(value?.toString() ?? '—',
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryGreen)),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MiniChart extends StatelessWidget {
|
||||
final List<HistoryPoint> historyPoints;
|
||||
const _MiniChart({required this.historyPoints});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (historyPoints.isEmpty) {
|
||||
return const SizedBox(
|
||||
height: 60,
|
||||
child: Center(child: Text('Pas de données', style: TextStyle(color: AppTheme.textLight))));
|
||||
}
|
||||
final max = historyPoints.map((p) => p.pvWh).reduce((a, b) => a > b ? a : b);
|
||||
|
||||
return SizedBox(
|
||||
height: 60,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: historyPoints.map((p) {
|
||||
final h = max > 0 ? (p.pvWh / max) * 50 : 0.0;
|
||||
return Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 1),
|
||||
child: Container(
|
||||
height: h.clamp(2, 50),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.solarYellow.withValues(alpha:0.7),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(3)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Empty state ───────────────────────────────────────────────────────────────
|
||||
|
||||
class _EmptyState extends StatelessWidget {
|
||||
final VoidCallback onAdd;
|
||||
const _EmptyState({required this.onAdd});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 90,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryGreen.withValues(alpha:0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.star_outline_rounded,
|
||||
color: AppTheme.primaryGreen, size: 48),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Aucun favori',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
color: AppTheme.textDark)),
|
||||
const SizedBox(height: 8),
|
||||
const Text('Ajoutez des widgets pour un accès rapide',
|
||||
style: TextStyle(color: AppTheme.textLight)),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onAdd,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Ajouter un widget'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add favorite sheet ────────────────────────────────────────────────────────
|
||||
|
||||
class _AddFavoriteSheet extends StatelessWidget {
|
||||
final NymeaService service;
|
||||
const _AddFavoriteSheet({required this.service});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final options = [
|
||||
_FavOption(FavoriteType.pvPower, '☀️ Production PV',
|
||||
'Puissance solaire instantanée', Icons.wb_sunny_rounded,
|
||||
AppTheme.solarYellow),
|
||||
_FavOption(FavoriteType.homePower, '🏠 Consommation',
|
||||
'Puissance consommée', Icons.home_rounded, AppTheme.homeBlue),
|
||||
_FavOption(FavoriteType.batterySOC, '🔋 Batterie',
|
||||
'État de charge', Icons.battery_charging_full_rounded,
|
||||
AppTheme.batteryGreen),
|
||||
_FavOption(FavoriteType.gridPower, '⚡ Réseau',
|
||||
'Import / Export réseau', Icons.electrical_services_rounded,
|
||||
AppTheme.gridGray),
|
||||
_FavOption(FavoriteType.rates, '📊 Taux',
|
||||
'Autoconsommation & autonomie', Icons.pie_chart_rounded,
|
||||
Colors.deepPurple),
|
||||
_FavOption(FavoriteType.evCharger, '🚗 Borne EV',
|
||||
'Recharge véhicule', Icons.electric_car_rounded,
|
||||
AppTheme.minPvBlue),
|
||||
_FavOption(FavoriteType.chart, '📈 Graphique',
|
||||
'Production journalière', Icons.bar_chart_rounded,
|
||||
Colors.teal),
|
||||
];
|
||||
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.6,
|
||||
maxChildSize: 0.9,
|
||||
minChildSize: 0.4,
|
||||
expand: false,
|
||||
builder: (_, controller) => Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 8, bottom: 16),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2)),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text('Ajouter un widget',
|
||||
style: TextStyle(
|
||||
fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
controller: controller,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
children: options.map((opt) {
|
||||
final alreadyAdded = service.favoriteWidgets
|
||||
.any((f) => f.type == opt.type);
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: opt.color.withValues(alpha:0.12),
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
child: Icon(opt.icon, color: opt.color),
|
||||
),
|
||||
title: Text(opt.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(opt.subtitle,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
trailing: alreadyAdded
|
||||
? const Icon(Icons.check_circle,
|
||||
color: AppTheme.primaryGreen)
|
||||
: const Icon(Icons.add_circle_outline,
|
||||
color: AppTheme.primaryGreen),
|
||||
onTap: alreadyAdded
|
||||
? null
|
||||
: () {
|
||||
service.addFavorite(FavoriteWidget(
|
||||
id: 'fw_${opt.type.name}_${DateTime.now().millisecondsSinceEpoch}',
|
||||
type: opt.type,
|
||||
title: opt.title.split(' ').skip(1).join(' '),
|
||||
));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FavOption {
|
||||
final FavoriteType type;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
const _FavOption(
|
||||
this.type, this.title, this.subtitle, this.icon, this.color);
|
||||
}
|
||||
655
lib/screens/thing_detail_screen.dart
Normal file
@ -0,0 +1,655 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/nymea_models.dart';
|
||||
import '../models/thing_category.dart';
|
||||
import '../services/nymea_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// ThingDetailScreen — fiche complète d'un appareil
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
class ThingDetailScreen extends StatelessWidget {
|
||||
final NymeaThing thing;
|
||||
|
||||
const ThingDetailScreen({super.key, required this.thing});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final service = context.watch<NymeaService>();
|
||||
|
||||
// Récupérer le thing à jour (les états sont mis à jour en temps réel)
|
||||
final liveThing = service.things.firstWhere(
|
||||
(t) => t.id == thing.id,
|
||||
orElse: () => thing,
|
||||
);
|
||||
|
||||
NymeaThingClass? cls;
|
||||
try {
|
||||
cls = service.thingClasses.firstWhere((c) => c.id == thing.thingClassId);
|
||||
} catch (_) {}
|
||||
|
||||
final catInfo = cls != null
|
||||
? categoryInfoMap[cls.category] ?? categoryInfoMap[ThingCategory.other]!
|
||||
: categoryInfoMap[ThingCategory.other]!;
|
||||
|
||||
// Grouper les états par thème
|
||||
final stateGroups = _groupStates(cls?.stateTypes ?? [], liveThing);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
body: CustomScrollView(
|
||||
slivers: [
|
||||
// ── SliverAppBar avec header coloré ─────────────────────────────
|
||||
SliverAppBar(
|
||||
expandedHeight: 160,
|
||||
pinned: true,
|
||||
backgroundColor: catInfo.color,
|
||||
foregroundColor: Colors.white,
|
||||
actions: [
|
||||
if (!service.isSimulation)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined, color: Colors.white),
|
||||
onPressed: () =>
|
||||
_showRenameDialog(context, service, liveThing),
|
||||
),
|
||||
],
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
catInfo.color,
|
||||
catInfo.color.withValues(alpha: 0.7),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 56, 20, 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Container(
|
||||
width: 56, height: 56,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(catInfo.icon,
|
||||
color: Colors.white, size: 30),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(liveThing.name,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
cls?.displayName ?? catInfo.label,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_OnlineBadge(thing: liveThing, thingClass: cls),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Contenu ──────────────────────────────────────────────────────
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildListDelegate([
|
||||
// Actions rapides (si la classe en a)
|
||||
if (cls != null && cls.actionTypes.isNotEmpty) ...[
|
||||
_ActionsSection(thing: liveThing, thingClass: cls),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// États groupés
|
||||
...stateGroups.entries.map((entry) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_SectionHeader(title: entry.key),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
child: Column(
|
||||
children: entry.value.asMap().entries.map((e) {
|
||||
final isLast = e.key == entry.value.length - 1;
|
||||
return _StateRow(
|
||||
stateType: e.value,
|
||||
value: liveThing.stateValue(e.value.id),
|
||||
isLast: isLast,
|
||||
color: catInfo.color,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
)),
|
||||
|
||||
// Interfaces nymea
|
||||
if (cls != null && cls.interfaces.isNotEmpty) ...[
|
||||
_SectionHeader(title: 'Interfaces'),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8, runSpacing: 6,
|
||||
children: cls.interfaces.map((iface) => Chip(
|
||||
label: Text(iface,
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
backgroundColor: catInfo.color.withValues(alpha: 0.1),
|
||||
side: BorderSide(color: catInfo.color.withValues(alpha: 0.3)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
)).toList(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Section Réglages
|
||||
if (cls != null && cls.settingsTypes.isNotEmpty) ...[
|
||||
const _SectionHeader(title: 'Réglages'),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
child: Column(
|
||||
children: cls.settingsTypes.asMap().entries.map((e) {
|
||||
final isLast = e.key == cls!.settingsTypes.length - 1;
|
||||
final current = liveThing.settingValue(e.value.id);
|
||||
return _SettingRow(
|
||||
settingType: e.value,
|
||||
value: current,
|
||||
isLast: isLast,
|
||||
onEdit: (newVal) => service.setThingSettings(
|
||||
liveThing.id, e.value.id, newVal),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Info technique
|
||||
_SectionHeader(title: 'Informations'),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
child: Column(children: [
|
||||
_InfoRow('ID', liveThing.id, isLast: false),
|
||||
_InfoRow('Classe', liveThing.thingClassId, isLast: false),
|
||||
_InfoRow('Statut setup', liveThing.setupStatus
|
||||
.replaceAll('ThingSetupStatus', ''), isLast: true),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Section Gestion
|
||||
if (!service.isSimulation) ...[
|
||||
const _SectionHeader(title: 'Gestion'),
|
||||
const SizedBox(height: 8),
|
||||
Card(
|
||||
child: Column(children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit_rounded,
|
||||
color: AppTheme.primaryGreen),
|
||||
title: const Text('Renommer'),
|
||||
onTap: () =>
|
||||
_showRenameDialog(context, service, liveThing),
|
||||
),
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline_rounded,
|
||||
color: Colors.red),
|
||||
title: const Text('Supprimer',
|
||||
style: TextStyle(color: Colors.red)),
|
||||
onTap: () =>
|
||||
_showRemoveDialog(context, service, liveThing),
|
||||
),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
const SizedBox(height: 60),
|
||||
]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Groupement des états par thème ────────────────────────────────────────
|
||||
|
||||
Map<String, List<NymeaStateType>> _groupStates(
|
||||
List<NymeaStateType> stateTypes,
|
||||
NymeaThing thing,
|
||||
) {
|
||||
final Map<String, List<NymeaStateType>> groups = {};
|
||||
|
||||
for (final st in stateTypes) {
|
||||
final group = _groupName(st.name);
|
||||
groups.putIfAbsent(group, () => []).add(st);
|
||||
}
|
||||
|
||||
// Trier : Puissance en premier, puis alphabétique
|
||||
final ordered = <String, List<NymeaStateType>>{};
|
||||
const firstGroups = ['Puissance', 'Énergie', 'Mesures électriques', 'État'];
|
||||
for (final g in firstGroups) {
|
||||
if (groups.containsKey(g)) ordered[g] = groups[g]!;
|
||||
}
|
||||
for (final entry in groups.entries) {
|
||||
if (!ordered.containsKey(entry.key)) ordered[entry.key] = entry.value;
|
||||
}
|
||||
|
||||
return ordered;
|
||||
}
|
||||
|
||||
String _groupName(String stateName) {
|
||||
final n = stateName.toLowerCase();
|
||||
if (n.contains('power') || n.contains('currentpower')) { return 'Puissance'; }
|
||||
if (n.contains('energy') || n.contains('total')) { return 'Énergie'; }
|
||||
if (n.contains('voltage') || n.contains('current') || n.contains('frequency')
|
||||
|| n.contains('ampere') || n.contains('volt') || n.contains('hertz')
|
||||
|| n.contains('powerfactor')) { return 'Mesures électriques'; }
|
||||
if (n.contains('temperature') || n.contains('humidity') || n.contains('pressure')
|
||||
|| n.contains('co2') || n.contains('lux')) { return 'Environnement'; }
|
||||
if (n.contains('connected') || n.contains('reachable') || n.contains('status')
|
||||
|| n.contains('mode') || n.contains('on')) { return 'État'; }
|
||||
if (n.contains('soc') || n.contains('battery') || n.contains('charging')) { return 'Batterie'; }
|
||||
return 'Divers';
|
||||
}
|
||||
|
||||
void _showRenameDialog(
|
||||
BuildContext ctx, NymeaService svc, NymeaThing t) {
|
||||
final ctrl = TextEditingController(text: t.name);
|
||||
showDialog(
|
||||
context: ctx,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Renommer'),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c),
|
||||
child: const Text('Annuler')),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final name = ctrl.text.trim();
|
||||
if (name.isNotEmpty) await svc.renameThing(t.id, name);
|
||||
if (c.mounted) Navigator.pop(c);
|
||||
},
|
||||
child: const Text('Renommer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRemoveDialog(
|
||||
BuildContext ctx, NymeaService svc, NymeaThing t) {
|
||||
showDialog(
|
||||
context: ctx,
|
||||
builder: (c) => AlertDialog(
|
||||
title: const Text('Supprimer'),
|
||||
content: Text('Supprimer "${t.name}" définitivement ?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c),
|
||||
child: const Text('Annuler')),
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red, foregroundColor: Colors.white),
|
||||
onPressed: () async {
|
||||
await svc.removeThing(t.id);
|
||||
if (c.mounted) {
|
||||
Navigator.pop(c);
|
||||
Navigator.pop(ctx);
|
||||
}
|
||||
},
|
||||
child: const Text('Supprimer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Section actions ──────────────────────────────────────────────────────────
|
||||
|
||||
class _ActionsSection extends StatelessWidget {
|
||||
final NymeaThing thing;
|
||||
final NymeaThingClass thingClass;
|
||||
const _ActionsSection({required this.thing, required this.thingClass});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final service = context.read<NymeaService>();
|
||||
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
const _SectionHeader(title: 'Actions'),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 10, runSpacing: 8,
|
||||
children: thingClass.actionTypes.map((action) => ElevatedButton.icon(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
),
|
||||
icon: const Icon(Icons.play_arrow_rounded, size: 18),
|
||||
label: Text(action.displayName,
|
||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
onPressed: () => _executeAction(context, service, action),
|
||||
)).toList(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
void _executeAction(BuildContext context, NymeaService service,
|
||||
NymeaActionType action) async {
|
||||
if (action.paramTypes.isEmpty) {
|
||||
// Action sans paramètre → exécuter directement
|
||||
await service.executeAction(
|
||||
thingId: thing.id,
|
||||
actionTypeId: action.id,
|
||||
params: {},
|
||||
);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('Action "${action.displayName}" exécutée'),
|
||||
backgroundColor: AppTheme.primaryGreen,
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// Action avec paramètres → dialog
|
||||
_showActionParamDialog(context, service, action);
|
||||
}
|
||||
}
|
||||
|
||||
void _showActionParamDialog(BuildContext context, NymeaService service,
|
||||
NymeaActionType action) {
|
||||
// Simple pour l'instant : juste afficher les paramètres requis
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(action.displayName),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: action.paramTypes.map((p) => ListTile(
|
||||
dense: true,
|
||||
title: Text(p['displayName']?.toString() ?? p['name']?.toString() ?? ''),
|
||||
subtitle: Text('Type: ${p['type']}'),
|
||||
)).toList(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Fermer')),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Widgets utilitaires ──────────────────────────────────────────────────────
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
const _SectionHeader({required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Text(title,
|
||||
style: const TextStyle(
|
||||
fontSize: 13, fontWeight: FontWeight.w700,
|
||||
color: AppTheme.textLight, letterSpacing: 0.5)),
|
||||
);
|
||||
}
|
||||
|
||||
class _StateRow extends StatelessWidget {
|
||||
final NymeaStateType stateType;
|
||||
final dynamic value;
|
||||
final bool isLast;
|
||||
final Color color;
|
||||
|
||||
const _StateRow({
|
||||
required this.stateType, required this.value,
|
||||
required this.isLast, required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formatted = stateType.formatValue(value);
|
||||
final isBool = stateType.type == 'Bool';
|
||||
|
||||
return Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(stateType.displayName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: AppTheme.textDark,
|
||||
fontWeight: FontWeight.w500)),
|
||||
if (stateType.name != stateType.displayName)
|
||||
Text(stateType.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 10, color: AppTheme.textLight)),
|
||||
]),
|
||||
),
|
||||
if (isBool)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: (value == true || value == 'true')
|
||||
? AppTheme.primaryGreen.withValues(alpha: 0.15)
|
||||
: Colors.grey.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(formatted,
|
||||
style: TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.bold,
|
||||
color: (value == true || value == 'true')
|
||||
? AppTheme.primaryGreen : AppTheme.textLight)),
|
||||
)
|
||||
else
|
||||
Text(formatted,
|
||||
style: TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.bold,
|
||||
color: color)),
|
||||
]),
|
||||
),
|
||||
if (!isLast)
|
||||
const Divider(height: 1, indent: 16, endIndent: 16,
|
||||
color: Color(0xFFEEEEEE)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final bool isLast;
|
||||
const _InfoRow(this.label, this.value, {required this.isLast});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 13, color: AppTheme.textLight)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: SelectableText(value,
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.w500,
|
||||
color: AppTheme.textDark)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
if (!isLast) const Divider(height: 1, indent: 16, endIndent: 16,
|
||||
color: Color(0xFFEEEEEE)),
|
||||
]);
|
||||
}
|
||||
|
||||
class _SettingRow extends StatelessWidget {
|
||||
final NymeaStateType settingType;
|
||||
final dynamic value;
|
||||
final bool isLast;
|
||||
final Future<bool> Function(dynamic) onEdit;
|
||||
|
||||
const _SettingRow({
|
||||
required this.settingType,
|
||||
required this.value,
|
||||
required this.isLast,
|
||||
required this.onEdit,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final formatted = settingType.formatValue(value);
|
||||
return Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(settingType.displayName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: AppTheme.textDark,
|
||||
fontWeight: FontWeight.w500)),
|
||||
if (settingType.name != settingType.displayName)
|
||||
Text(settingType.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 10, color: AppTheme.textLight)),
|
||||
]),
|
||||
),
|
||||
Text(formatted,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: AppTheme.textLight)),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_outlined, size: 18,
|
||||
color: AppTheme.textLight),
|
||||
onPressed: () => _showEditDialog(context),
|
||||
),
|
||||
]),
|
||||
),
|
||||
if (!isLast)
|
||||
const Divider(height: 1, indent: 16, endIndent: 16,
|
||||
color: Color(0xFFEEEEEE)),
|
||||
]);
|
||||
}
|
||||
|
||||
void _showEditDialog(BuildContext context) {
|
||||
if (settingType.type == 'Bool') {
|
||||
final current = value == true || value == 'true';
|
||||
onEdit(!current);
|
||||
return;
|
||||
}
|
||||
final ctrl = TextEditingController(text: value?.toString() ?? '');
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (c) => AlertDialog(
|
||||
title: Text(settingType.displayName),
|
||||
content: TextField(
|
||||
controller: ctrl,
|
||||
autofocus: true,
|
||||
keyboardType: (settingType.type == 'Int' || settingType.type == 'Double')
|
||||
? const TextInputType.numberWithOptions(decimal: true)
|
||||
: TextInputType.text,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
suffixText: settingType.unit,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(c),
|
||||
child: const Text('Annuler')),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
dynamic newVal = ctrl.text.trim();
|
||||
if (settingType.type == 'Int') {
|
||||
newVal = int.tryParse(ctrl.text.trim()) ?? value;
|
||||
} else if (settingType.type == 'Double') {
|
||||
newVal = double.tryParse(ctrl.text.trim()) ?? value;
|
||||
}
|
||||
onEdit(newVal);
|
||||
Navigator.pop(c);
|
||||
},
|
||||
child: const Text('Enregistrer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OnlineBadge extends StatelessWidget {
|
||||
final NymeaThing thing;
|
||||
final NymeaThingClass? thingClass;
|
||||
const _OnlineBadge({required this.thing, required this.thingClass});
|
||||
|
||||
bool _isOnline() {
|
||||
final connType = thingClass?.stateTypes
|
||||
.where((s) => s.name.toLowerCase() == 'connected');
|
||||
if (connType != null && connType.isNotEmpty) {
|
||||
final val = thing.stateValue(connType.first.id);
|
||||
return val == true || val == 'true';
|
||||
}
|
||||
return thing.isSetupComplete;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final online = _isOnline();
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
width: 8, height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: online ? Colors.greenAccent : Colors.grey[300],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(online ? 'En ligne' : 'Hors ligne',
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 12,
|
||||
fontWeight: FontWeight.w600)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
596
lib/screens/things_screen.dart
Normal file
@ -0,0 +1,596 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../models/nymea_models.dart';
|
||||
import '../models/thing_category.dart';
|
||||
import '../services/nymea_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'thing_detail_screen.dart';
|
||||
|
||||
class ThingsScreen extends StatefulWidget {
|
||||
const ThingsScreen({super.key});
|
||||
@override
|
||||
State<ThingsScreen> createState() => _ThingsScreenState();
|
||||
}
|
||||
|
||||
class _ThingsScreenState extends State<ThingsScreen> {
|
||||
final Set<ThingCategory> _collapsed = {};
|
||||
String _searchQuery = '';
|
||||
bool _showSearch = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final service = context.watch<NymeaService>();
|
||||
// Garder tous les things avec un id valide (le filtre setupStatus était trop strict)
|
||||
final things = service.things.where((t) => t.id.isNotEmpty).toList();
|
||||
final thingClasses = service.thingClasses;
|
||||
|
||||
final filtered = _searchQuery.isEmpty
|
||||
? things
|
||||
: things.where((t) =>
|
||||
t.name.toLowerCase().contains(_searchQuery.toLowerCase())).toList();
|
||||
|
||||
final Map<ThingCategory, List<NymeaThing>> grouped = {};
|
||||
for (final thing in filtered) {
|
||||
final cls = _classFor(thing, thingClasses);
|
||||
final cat = cls?.category ?? ThingCategory.other;
|
||||
grouped.putIfAbsent(cat, () => []).add(thing);
|
||||
}
|
||||
|
||||
const orderedCats = [
|
||||
ThingCategory.energy, ThingCategory.solar, ThingCategory.battery,
|
||||
ThingCategory.evCharger, ThingCategory.hvac, ThingCategory.lighting,
|
||||
ThingCategory.sensors, ThingCategory.network, ThingCategory.notifications,
|
||||
ThingCategory.weather, ThingCategory.media, ThingCategory.other,
|
||||
];
|
||||
final cats = orderedCats.where((c) => grouped.containsKey(c)).toList();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
appBar: _buildAppBar(service, things.length),
|
||||
body: !service.isConnected
|
||||
? _buildDisconnected()
|
||||
: (things.isEmpty && !service.thingsLoaded)
|
||||
? _buildLoading()
|
||||
: things.isEmpty
|
||||
? _buildEmpty()
|
||||
: cats.isEmpty
|
||||
// Classes pas encore chargées — afficher things en mode simple
|
||||
? _buildThingsSimple(things, service)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
|
||||
itemCount: cats.length,
|
||||
itemBuilder: (context, i) {
|
||||
final cat = cats[i];
|
||||
return _CategorySection(
|
||||
info: categoryInfoMap[cat]!,
|
||||
things: grouped[cat]!,
|
||||
thingClasses: thingClasses,
|
||||
collapsed: _collapsed.contains(cat),
|
||||
onToggle: () => setState(() {
|
||||
if (_collapsed.contains(cat)) {
|
||||
_collapsed.remove(cat);
|
||||
} else {
|
||||
_collapsed.add(cat);
|
||||
}
|
||||
}),
|
||||
onTap: (t) => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ThingDetailScreen(thing: t)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
AppBar _buildAppBar(NymeaService service, int count) {
|
||||
return AppBar(
|
||||
backgroundColor: AppTheme.backgroundGray,
|
||||
elevation: 0,
|
||||
title: _showSearch
|
||||
? TextField(
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Rechercher un appareil...',
|
||||
border: InputBorder.none,
|
||||
hintStyle: TextStyle(color: AppTheme.textLight),
|
||||
),
|
||||
style: const TextStyle(color: AppTheme.textDark, fontSize: 18),
|
||||
onChanged: (v) => setState(() => _searchQuery = v),
|
||||
)
|
||||
: Row(children: [
|
||||
const Text('Things',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
fontSize: 20)),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryGreen.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text('$count',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryGreen)),
|
||||
),
|
||||
]),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_showSearch ? Icons.close : Icons.search,
|
||||
color: AppTheme.textDark),
|
||||
onPressed: () => setState(() {
|
||||
_showSearch = !_showSearch;
|
||||
_searchQuery = '';
|
||||
}),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.refresh_rounded, color: AppTheme.textDark),
|
||||
onPressed: () => service.refresh(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoading() => const Center(
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
CircularProgressIndicator(color: AppTheme.primaryGreen),
|
||||
SizedBox(height: 16),
|
||||
Text('Chargement des appareils...',
|
||||
style: TextStyle(color: AppTheme.textLight)),
|
||||
]),
|
||||
);
|
||||
|
||||
Widget _buildDisconnected() => Center(
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.cloud_off_rounded, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 12),
|
||||
const Text('Non connecté à nymea',
|
||||
style: TextStyle(color: AppTheme.textLight, fontSize: 16)),
|
||||
]),
|
||||
);
|
||||
|
||||
/// Affichage simple quand les ThingClasses ne sont pas encore chargées
|
||||
Widget _buildThingsSimple(List<NymeaThing> things, NymeaService service) {
|
||||
return ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
|
||||
itemCount: things.length,
|
||||
itemBuilder: (context, i) {
|
||||
final t = things[i];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
width: 40, height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryGreen.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.device_hub_rounded,
|
||||
color: AppTheme.primaryGreen, size: 22),
|
||||
),
|
||||
title: Text(t.name,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
||||
subtitle: Text(t.setupStatus.replaceAll('ThingSetupStatus', ''),
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.textLight)),
|
||||
trailing: t.setupStatus == 'ThingSetupStatusComplete'
|
||||
? Container(
|
||||
width: 8, height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppTheme.primaryGreen,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmpty() => Center(
|
||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.search_off_rounded, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_searchQuery.isEmpty ? 'Aucun appareil' : 'Aucun résultat pour "$_searchQuery"',
|
||||
style: const TextStyle(color: AppTheme.textLight, fontSize: 16),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
NymeaThingClass? _classFor(NymeaThing thing, List<NymeaThingClass> classes) {
|
||||
try { return classes.firstWhere((c) => c.id == thing.thingClassId); }
|
||||
catch (_) { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Section catégorie ────────────────────────────────────────────────────────
|
||||
|
||||
class _CategorySection extends StatelessWidget {
|
||||
final ThingCategoryInfo info;
|
||||
final List<NymeaThing> things;
|
||||
final List<NymeaThingClass> thingClasses;
|
||||
final bool collapsed;
|
||||
final VoidCallback onToggle;
|
||||
final void Function(NymeaThing) onTap;
|
||||
|
||||
const _CategorySection({
|
||||
required this.info, required this.things, required this.thingClasses,
|
||||
required this.collapsed, required this.onToggle, required this.onTap,
|
||||
});
|
||||
|
||||
NymeaThingClass? _cls(NymeaThing t) {
|
||||
try { return thingClasses.firstWhere((c) => c.id == t.thingClassId); }
|
||||
catch (_) { return null; }
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
// ── Header collapsable ─────────────────────────────────────────────────
|
||||
InkWell(
|
||||
onTap: onToggle,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 4),
|
||||
child: Row(children: [
|
||||
Container(
|
||||
width: 34, height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: info.color.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
child: Icon(info.icon, color: info.color, size: 19),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(info.label,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700, fontSize: 13,
|
||||
color: AppTheme.textDark, letterSpacing: 0.2)),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: info.color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text('${things.length}',
|
||||
style: TextStyle(
|
||||
fontSize: 11, fontWeight: FontWeight.bold,
|
||||
color: info.color)),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
AnimatedRotation(
|
||||
turns: collapsed ? -0.25 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: const Icon(Icons.expand_more,
|
||||
color: AppTheme.textLight, size: 20),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
|
||||
// ── Things ────────────────────────────────────────────────────────────
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
crossFadeState: collapsed
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
firstChild: Column(
|
||||
children: [
|
||||
...things.map((t) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: ThingCard(
|
||||
thing: t,
|
||||
thingClass: _cls(t),
|
||||
categoryInfo: info,
|
||||
onTap: () => onTap(t),
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
),
|
||||
secondChild: const SizedBox.shrink(),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── ThingCard ────────────────────────────────────────────────────────────────
|
||||
|
||||
class ThingCard extends StatelessWidget {
|
||||
final NymeaThing thing;
|
||||
final NymeaThingClass? thingClass;
|
||||
final ThingCategoryInfo categoryInfo;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const ThingCard({
|
||||
super.key, required this.thing, required this.thingClass,
|
||||
required this.categoryInfo, required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final primaryType = thingClass?.primaryStateType;
|
||||
final primaryValue = primaryType?.formatValue(thing.stateValue(primaryType.id));
|
||||
final secondaryStates = _secondaryStates();
|
||||
final isOnline = _isOnline();
|
||||
|
||||
return Card(
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
// Ligne principale
|
||||
Row(children: [
|
||||
Container(
|
||||
width: 48, height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: categoryInfo.color.withValues(alpha: isOnline ? 0.15 : 0.07),
|
||||
borderRadius: BorderRadius.circular(13),
|
||||
),
|
||||
child: Icon(categoryInfo.icon,
|
||||
color: isOnline
|
||||
? categoryInfo.color
|
||||
: categoryInfo.color.withValues(alpha: 0.4),
|
||||
size: 26),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(thing.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600, fontSize: 15,
|
||||
color: AppTheme.textDark),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 2),
|
||||
Text(thingClass?.displayName ?? 'Appareil',
|
||||
style: const TextStyle(fontSize: 12, color: AppTheme.textLight),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
]),
|
||||
),
|
||||
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||
if (primaryValue != null)
|
||||
Text(primaryValue,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, fontSize: 16,
|
||||
color: isOnline
|
||||
? categoryInfo.color
|
||||
: AppTheme.textLight)),
|
||||
const SizedBox(height: 4),
|
||||
_StatusDot(isOnline: isOnline),
|
||||
]),
|
||||
const SizedBox(width: 2),
|
||||
_ThingMenu(thing: thing, onDetail: onTap),
|
||||
]),
|
||||
|
||||
// Chips états secondaires
|
||||
if (secondaryStates.isNotEmpty) ...[
|
||||
const SizedBox(height: 10),
|
||||
const Divider(height: 1, color: Color(0xFFEEEEEE)),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
spacing: 7, runSpacing: 6,
|
||||
children: secondaryStates.map((s) => _StateChip(
|
||||
label: s.displayName,
|
||||
value: s.formatValue(thing.stateValue(s.id)),
|
||||
color: categoryInfo.color,
|
||||
)).toList(),
|
||||
),
|
||||
],
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isOnline() {
|
||||
final connType = thingClass?.stateTypes
|
||||
.where((s) => s.name.toLowerCase() == 'connected');
|
||||
if (connType != null && connType.isNotEmpty) {
|
||||
final val = thing.stateValue(connType.first.id);
|
||||
return val == true || val == 'true';
|
||||
}
|
||||
return thing.isSetupComplete;
|
||||
}
|
||||
|
||||
List<NymeaStateType> _secondaryStates() {
|
||||
if (thingClass == null) return [];
|
||||
final primary = thingClass!.primaryStateType;
|
||||
const interesting = [
|
||||
'voltage', 'current', 'frequency', 'power',
|
||||
'totalenergyconsumed', 'totalenergyproduced',
|
||||
'temperature', 'humidity', 'soc',
|
||||
'chargingmode', 'mode', 'maxchargingcurrent', 'status',
|
||||
];
|
||||
return thingClass!.stateTypes
|
||||
.where((s) =>
|
||||
s != primary &&
|
||||
s.name.toLowerCase() != 'connected' &&
|
||||
interesting.any((i) => s.name.toLowerCase().contains(i)))
|
||||
.take(3)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Widgets utilitaires ──────────────────────────────────────────────────────
|
||||
|
||||
class _StatusDot extends StatelessWidget {
|
||||
final bool isOnline;
|
||||
const _StatusDot({required this.isOnline});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
width: 7, height: 7,
|
||||
decoration: BoxDecoration(
|
||||
color: isOnline ? AppTheme.primaryGreen : Colors.grey[400],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isOnline ? 'En ligne' : 'Hors ligne',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isOnline ? AppTheme.primaryGreen : AppTheme.textLight),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
class _StateChip extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final Color color;
|
||||
const _StateChip({required this.label, required this.value, required this.color});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Flexible(
|
||||
child: Text(label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11, color: color.withValues(alpha: 0.8),
|
||||
fontWeight: FontWeight.w500)),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
Flexible(
|
||||
child: Text(value,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(fontSize: 11, color: color, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Menu contextuel ──────────────────────────────────────────────────────────
|
||||
|
||||
class _ThingMenu extends StatelessWidget {
|
||||
final NymeaThing thing;
|
||||
final VoidCallback onDetail;
|
||||
const _ThingMenu({required this.thing, required this.onDetail});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert, color: AppTheme.textLight, size: 20),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(value: 'detail',
|
||||
child: Row(children: [
|
||||
Icon(Icons.info_outline, size: 18, color: AppTheme.textDark),
|
||||
SizedBox(width: 10), Text('Détails & États'),
|
||||
])),
|
||||
const PopupMenuItem(value: 'params',
|
||||
child: Row(children: [
|
||||
Icon(Icons.tune, size: 18, color: AppTheme.textDark),
|
||||
SizedBox(width: 10), Text('Paramètres'),
|
||||
])),
|
||||
const PopupMenuItem(value: 'rename',
|
||||
child: Row(children: [
|
||||
Icon(Icons.edit_outlined, size: 18, color: AppTheme.textDark),
|
||||
SizedBox(width: 10), Text('Renommer'),
|
||||
])),
|
||||
const PopupMenuItem(value: 'favorite',
|
||||
child: Row(children: [
|
||||
Icon(Icons.star_outline, size: 18, color: AppTheme.textDark),
|
||||
SizedBox(width: 10), Text('Ajouter aux favoris'),
|
||||
])),
|
||||
],
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'detail': onDetail(); break;
|
||||
case 'params': _showParamsSheet(context); break;
|
||||
case 'rename': _showRenameDialog(context); break;
|
||||
case 'favorite':
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('${thing.name} ajouté aux favoris'),
|
||||
backgroundColor: AppTheme.primaryGreen,
|
||||
duration: const Duration(seconds: 2),
|
||||
));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showParamsSheet(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
|
||||
builder: (_) => Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
const Icon(Icons.tune, color: AppTheme.primaryGreen),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text('Paramètres — ${thing.name}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold,
|
||||
fontSize: 16, color: AppTheme.textDark))),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
SelectableText('ID: ${thing.id}',
|
||||
style: const TextStyle(fontSize: 10, color: AppTheme.textLight)),
|
||||
const Divider(height: 24),
|
||||
if (thing.paramValues.isEmpty)
|
||||
const Text('Aucun paramètre configuré.',
|
||||
style: TextStyle(color: AppTheme.textLight))
|
||||
else
|
||||
...thing.paramValues.map((p) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(children: [
|
||||
Expanded(child: Text(p['paramTypeId']?.toString() ?? '?',
|
||||
style: const TextStyle(color: AppTheme.textLight))),
|
||||
Text(p['value']?.toString() ?? '—',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600, color: AppTheme.textDark)),
|
||||
]),
|
||||
)),
|
||||
const SizedBox(height: 20),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showRenameDialog(BuildContext context) {
|
||||
final ctrl = TextEditingController(text: thing.name);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Renommer'),
|
||||
content: TextField(
|
||||
controller: ctrl, autofocus: true,
|
||||
decoration: const InputDecoration(labelText: 'Nouveau nom'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text('Annuler')),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
context.read<NymeaService>().renameThing(thing.id, ctrl.text.trim());
|
||||
Navigator.pop(ctx);
|
||||
},
|
||||
child: const Text('Renommer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
946
lib/services/nymea_service.dart
Normal file
@ -0,0 +1,946 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as dev;
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import '../models/energy_data.dart';
|
||||
import '../models/nymea_models.dart';
|
||||
|
||||
// ── Protocole de connexion ─────────────────────────────────────────────────────
|
||||
enum NymeaProtocol {
|
||||
/// TCP brut port 2222 — nymea parle en premier (\n-delimited JSON)
|
||||
tcpRaw,
|
||||
/// WebSocket port 4444 — client envoie JSONRPC.Hello en premier
|
||||
webSocket,
|
||||
}
|
||||
|
||||
/// Service de communication nymea JSON-RPC 2.0
|
||||
/// Supporte TCP brut (port 2222) et WebSocket (port 4444)
|
||||
class NymeaService extends ChangeNotifier {
|
||||
// ── Transport ────────────────────────────────────────────────────────────────
|
||||
Socket? _socket; // TCP raw
|
||||
WebSocketChannel? _wsChannel; // WebSocket
|
||||
StreamSubscription? _wsSub;
|
||||
NymeaProtocol _protocol = NymeaProtocol.tcpRaw;
|
||||
|
||||
int _requestId = 1;
|
||||
final Map<int, Completer<Map<String, dynamic>>> _pendingRequests = {};
|
||||
final StringBuffer _buffer = StringBuffer(); // TCP fragment buffer
|
||||
|
||||
// ── State ────────────────────────────────────────────────────────────────────
|
||||
bool _connected = false;
|
||||
bool _connecting = false;
|
||||
String _host = '192.168.1.106';
|
||||
int _port = 2222;
|
||||
String? _connectionError;
|
||||
String? _nymeaVersion;
|
||||
Timer? _heartbeatTimer;
|
||||
Timer? _simulationTimer;
|
||||
bool _isSimulation = false;
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────────────────────
|
||||
String _token = '';
|
||||
String _username = '';
|
||||
String _password = '';
|
||||
|
||||
// ── Debug verbeux ─────────────────────────────────────────────────────────
|
||||
bool _verboseLog = false; // activé depuis l'UI ou à la connexion
|
||||
bool get verboseLog => _verboseLog;
|
||||
set verboseLog(bool v) {
|
||||
_verboseLog = v;
|
||||
_log('🔧 Verbose logging ${v ? "ON" : "OFF"}', force: true);
|
||||
}
|
||||
|
||||
void _log(String msg, {bool force = false, String? json}) {
|
||||
if (!force && !_verboseLog) return;
|
||||
dev.log(msg, name: 'nymea');
|
||||
if (json != null && _verboseLog) {
|
||||
// Formatter le JSON pour lisibilité
|
||||
try {
|
||||
final parsed = jsonDecode(json);
|
||||
final pretty = const JsonEncoder.withIndent(' ').convert(parsed);
|
||||
dev.log(pretty, name: 'nymea.json');
|
||||
} catch (_) {
|
||||
dev.log(json, name: 'nymea.json');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EnergyData _energyData = const EnergyData();
|
||||
List<HistoryPoint> _historyPoints = [];
|
||||
List<NymeaThing> _things = [];
|
||||
List<NymeaThingClass> _thingClasses = [];
|
||||
List<FavoriteWidget> _favoriteWidgets = [];
|
||||
|
||||
// ── Getters ──────────────────────────────────────────────────────────────────
|
||||
bool get connected => _connected;
|
||||
bool get isConnected => _connected; // alias pour les screens
|
||||
bool get connecting => _connecting;
|
||||
bool get isSimulation => _isSimulation;
|
||||
NymeaProtocol get protocol => _protocol;
|
||||
String get host => _host;
|
||||
int get port => _port;
|
||||
String? get connectionError => _connectionError;
|
||||
String? get nymeaVersion => _nymeaVersion;
|
||||
String get username => _username;
|
||||
String get password => _password;
|
||||
EnergyData get energyData => _energyData;
|
||||
List<HistoryPoint> get historyPoints => _historyPoints;
|
||||
List<NymeaThing> get things => _things;
|
||||
List<NymeaThingClass> get thingClasses => _thingClasses;
|
||||
List<FavoriteWidget> get favoriteWidgets => _favoriteWidgets;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// CONNEXION — entrée publique
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Future<bool> connect(
|
||||
String host,
|
||||
int port, {
|
||||
NymeaProtocol protocol = NymeaProtocol.tcpRaw,
|
||||
String username = '',
|
||||
String password = '',
|
||||
}) async {
|
||||
if (_connecting) return false;
|
||||
_host = host;
|
||||
_port = port;
|
||||
_protocol = protocol;
|
||||
_username = username;
|
||||
_password = password;
|
||||
_connectionError = null;
|
||||
_connecting = true;
|
||||
_isSimulation = false;
|
||||
_simulationTimer?.cancel();
|
||||
_buffer.clear();
|
||||
// Charger le token sauvegardé pour cet hôte
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_token = prefs.getString('nymea_token_$host') ?? '';
|
||||
if (_token.isNotEmpty) {
|
||||
_log('🔑 Token restauré depuis SharedPreferences', force: true);
|
||||
}
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
if (protocol == NymeaProtocol.tcpRaw) {
|
||||
return await _connectTcp(host, port);
|
||||
} else {
|
||||
return await _connectWs(host, port);
|
||||
}
|
||||
} catch (e) {
|
||||
_connectionError = e.toString();
|
||||
_connected = false;
|
||||
_connecting = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── TCP brut : nymea parle en PREMIER ────────────────────────────────────────
|
||||
Future<bool> _connectTcp(String host, int port) async {
|
||||
_log('TCP → $host:$port');
|
||||
_socket = await Socket.connect(host, port,
|
||||
timeout: const Duration(seconds: 8));
|
||||
|
||||
final welcomeCompleter = Completer<Map<String, dynamic>>();
|
||||
bool welcomeReceived = false;
|
||||
|
||||
_socket!.cast<List<int>>().transform(utf8.decoder).listen(
|
||||
(chunk) {
|
||||
_buffer.write(chunk);
|
||||
final raw = _buffer.toString();
|
||||
int start = 0;
|
||||
for (int i = 0; i < raw.length; i++) {
|
||||
if (raw[i] == '\n') {
|
||||
final line = raw.substring(start, i).trim();
|
||||
if (line.isNotEmpty) {
|
||||
try {
|
||||
final data = jsonDecode(line) as Map<String, dynamic>;
|
||||
if (!welcomeReceived) {
|
||||
welcomeReceived = true;
|
||||
_log('← raw TCP line (${line.length}): ${line.substring(0, line.length.clamp(0, 120))}…', force: _verboseLog);
|
||||
if (!welcomeCompleter.isCompleted) {
|
||||
welcomeCompleter.complete(data);
|
||||
}
|
||||
} else {
|
||||
_processMessage(data);
|
||||
}
|
||||
} catch (e) {
|
||||
_log('TCP parse error: $e', force: true);
|
||||
}
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
_buffer.clear();
|
||||
if (start < raw.length) _buffer.write(raw.substring(start));
|
||||
},
|
||||
onError: (e) {
|
||||
if (!welcomeCompleter.isCompleted) welcomeCompleter.completeError(e);
|
||||
_onError(e);
|
||||
},
|
||||
onDone: () {
|
||||
if (!welcomeCompleter.isCompleted) {
|
||||
welcomeCompleter.completeError(
|
||||
Exception('Socket closed before welcome'));
|
||||
}
|
||||
_onDone();
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
// Attendre le message de bienvenue de nymea
|
||||
final welcome = await welcomeCompleter.future
|
||||
.timeout(const Duration(seconds: 8));
|
||||
|
||||
return _finishConnect(welcome);
|
||||
}
|
||||
|
||||
// ── WebSocket : client parle en PREMIER (JSONRPC.Hello) ──────────────────────
|
||||
Future<bool> _connectWs(String host, int port) async {
|
||||
_log('WebSocket → ws://$host:$port');
|
||||
final uri = Uri.parse('ws://$host:$port');
|
||||
_wsChannel = WebSocketChannel.connect(uri);
|
||||
|
||||
// Attendre que la connexion soit établie
|
||||
try {
|
||||
await _wsChannel!.ready.timeout(const Duration(seconds: 8));
|
||||
} catch (_) {
|
||||
// some versions don't have .ready — ignore
|
||||
}
|
||||
|
||||
_wsSub = _wsChannel!.stream.listen(
|
||||
(raw) {
|
||||
try {
|
||||
// nymea peut envoyer String ou binary (Uint8List)
|
||||
String str;
|
||||
if (raw is String) {
|
||||
str = raw;
|
||||
} else if (raw is List<int>) {
|
||||
str = utf8.decode(raw);
|
||||
} else {
|
||||
_log('⚠️ WS frame type inattendu: ${raw.runtimeType}', force: true);
|
||||
return;
|
||||
}
|
||||
_log('← WS raw (${str.length} chars): ${str.substring(0, str.length.clamp(0, 80))}…');
|
||||
final data = jsonDecode(str) as Map<String, dynamic>;
|
||||
_processMessage(data);
|
||||
} catch (e) {
|
||||
_log('❌ WS parse error: $e raw type: ${raw.runtimeType}', force: true);
|
||||
}
|
||||
},
|
||||
onError: (e) {
|
||||
_log('❌ WS stream error: $e', force: true);
|
||||
_onError(e);
|
||||
},
|
||||
onDone: _onDone,
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
_log('📡 WS listener actif, envoi Hello...', force: true);
|
||||
|
||||
// Sur WebSocket, c'est le CLIENT qui envoie Hello en premier
|
||||
final hello = await _sendRequest('JSONRPC.Hello', {})
|
||||
.timeout(const Duration(seconds: 8));
|
||||
|
||||
return _finishConnect(hello);
|
||||
}
|
||||
|
||||
// ── Finalisation commune ─────────────────────────────────────────────────────
|
||||
Future<bool> _finishConnect(Map<String, dynamic> welcome) async {
|
||||
final p = welcome['params'] as Map<String, dynamic>? ?? welcome;
|
||||
_nymeaVersion = p['version'] as String?
|
||||
?? p['serverVersion'] as String?
|
||||
?? 'nymea';
|
||||
|
||||
_log('✅ Connected to nymea $_nymeaVersion'
|
||||
' via ${_protocol == NymeaProtocol.tcpRaw ? "TCP" : "WebSocket"}');
|
||||
|
||||
// Flux auth : si requis ET pas encore authentifié avec token valide
|
||||
if (p['authenticationRequired'] == true && p['authenticated'] != true) {
|
||||
if (_token.isNotEmpty) {
|
||||
_log('🔑 Token existant présent, tentative d\'utilisation...', force: true);
|
||||
// Le token sera inclus dans les requêtes — on verra si ça marche
|
||||
} else if (_username.isNotEmpty) {
|
||||
try {
|
||||
await _doAuthenticate();
|
||||
} catch (e) {
|
||||
_connectionError = e.toString();
|
||||
_connected = false;
|
||||
_connecting = false;
|
||||
notifyListeners();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _setNotificationsEnabled();
|
||||
|
||||
_connected = true;
|
||||
_connecting = false;
|
||||
notifyListeners();
|
||||
|
||||
await _initialLoad();
|
||||
_startHeartbeat();
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _doAuthenticate() async {
|
||||
_log('🔐 JSONRPC.Authenticate → user=$_username', force: true);
|
||||
final r = await _sendRequest('JSONRPC.Authenticate', {
|
||||
'username': _username,
|
||||
'password': _password,
|
||||
'deviceName': 'etm-powersync-app',
|
||||
});
|
||||
_token = r['params']?['token'] as String? ?? '';
|
||||
if (_token.isNotEmpty) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('nymea_token_$_host', _token);
|
||||
_log('✅ Token obtenu et sauvegardé', force: true);
|
||||
} else {
|
||||
_log('❌ Authenticate échoué: ${r["params"]}', force: true);
|
||||
throw Exception('Authentication failed: ${r["params"]}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _setNotificationsEnabled() async {
|
||||
try {
|
||||
await _sendRequest('JSONRPC.SetNotificationStatus', {'enabled': true});
|
||||
} catch (e) {
|
||||
_log('SetNotificationStatus: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Déconnexion ──────────────────────────────────────────────────────────────
|
||||
void disconnect() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_simulationTimer?.cancel();
|
||||
_socket?.destroy();
|
||||
_socket = null;
|
||||
_wsSub?.cancel();
|
||||
_wsChannel?.sink.close();
|
||||
_wsChannel = null;
|
||||
_connected = false;
|
||||
_connecting = false;
|
||||
_isSimulation = false;
|
||||
_buffer.clear();
|
||||
for (final c in _pendingRequests.values) {
|
||||
c.completeError(Exception('Disconnected'));
|
||||
}
|
||||
_pendingRequests.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// JSON-RPC SEND — \n pour TCP, frame WebSocket pour WS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Future<Map<String, dynamic>> _sendRequest(
|
||||
String method, Map<String, dynamic> params) async {
|
||||
if (_socket == null && _wsChannel == null) {
|
||||
throw Exception('Not connected');
|
||||
}
|
||||
final id = _requestId++;
|
||||
final completer = Completer<Map<String, dynamic>>();
|
||||
_pendingRequests[id] = completer;
|
||||
|
||||
final msg = jsonEncode({
|
||||
'id': id, 'method': method, 'params': params,
|
||||
'token': _token,
|
||||
});
|
||||
_log('→ [$id] $method', force: true, json: msg);
|
||||
|
||||
if (_protocol == NymeaProtocol.tcpRaw) {
|
||||
_socket!.write('$msg\n');
|
||||
} else {
|
||||
_wsChannel!.sink.add(msg);
|
||||
}
|
||||
|
||||
// Timeout 15s — évite le blocage si nymea ne répond pas
|
||||
return completer.future.timeout(
|
||||
const Duration(seconds: 15),
|
||||
onTimeout: () {
|
||||
_pendingRequests.remove(id);
|
||||
throw TimeoutException('Request $method timed out', const Duration(seconds: 15));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// MESSAGE PROCESSING (commun TCP + WS)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void _processMessage(Map<String, dynamic> data) {
|
||||
// Réponse à une requête
|
||||
if (data.containsKey('id')) {
|
||||
final id = data['id'];
|
||||
// JSON decode peut retourner num au lieu de int — on normalise
|
||||
final intId = (id is num) ? id.toInt() : null;
|
||||
if (intId != null && _pendingRequests.containsKey(intId)) {
|
||||
_log('← [$intId] réponse status=${data['status'] ?? '?'}',
|
||||
force: true, json: jsonEncode(data));
|
||||
_pendingRequests[intId]!.complete(data);
|
||||
_pendingRequests.remove(intId);
|
||||
} else if (intId != null) {
|
||||
_log('← [$intId] réponse non attendue (déjà timeout?)', force: true);
|
||||
}
|
||||
}
|
||||
// Notification push
|
||||
if (data.containsKey('notification')) {
|
||||
final notification = data['notification'] as String;
|
||||
_log('← push: $notification', force: true,
|
||||
json: _verboseLog ? jsonEncode(data) : null);
|
||||
_handleNotification(
|
||||
notification,
|
||||
data['params'] as Map<String, dynamic>? ?? {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleNotification(
|
||||
String notification, Map<String, dynamic> params) {
|
||||
_log('← push: $notification');
|
||||
switch (notification) {
|
||||
case 'Energy.PowerBalanceChanged':
|
||||
_parsePowerBalance(params);
|
||||
break;
|
||||
case 'Energy.RootMeterChanged':
|
||||
_parseRootMeter(params);
|
||||
break;
|
||||
case 'Integrations.StateChanged':
|
||||
_handleStateChanged(params);
|
||||
break;
|
||||
case 'Integrations.ThingAdded':
|
||||
final t = params['thing'];
|
||||
if (t != null) {
|
||||
_things.add(NymeaThing.fromJson(t as Map<String, dynamic>));
|
||||
notifyListeners();
|
||||
}
|
||||
break;
|
||||
case 'Integrations.ThingRemoved':
|
||||
_things.removeWhere((t) => t.id == params['thingId']);
|
||||
notifyListeners();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleStateChanged(Map<String, dynamic> params) {
|
||||
final thingId = params['thingId'] as String?;
|
||||
final stateTypeId = params['stateTypeId'] as String?;
|
||||
final value = params['value'];
|
||||
if (thingId == null || stateTypeId == null) return;
|
||||
final idx = _things.indexWhere((t) => t.id == thingId);
|
||||
if (idx >= 0) {
|
||||
final states = List<NymeaStateValue>.from(_things[idx].states);
|
||||
final si = states.indexWhere((s) => s.stateTypeId == stateTypeId);
|
||||
if (si >= 0) {
|
||||
states[si] = NymeaStateValue(stateTypeId: stateTypeId, value: value);
|
||||
} else {
|
||||
states.add(NymeaStateValue(stateTypeId: stateTypeId, value: value));
|
||||
}
|
||||
_things[idx] = _things[idx].copyWith(states: states);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void _onError(dynamic error) {
|
||||
_log('❌ Connection error: $error', force: true);
|
||||
_connectionError = error.toString();
|
||||
_connected = false;
|
||||
for (final c in _pendingRequests.values) {
|
||||
c.completeError(error);
|
||||
}
|
||||
_pendingRequests.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _onDone() {
|
||||
_log('🔌 Connection closed', force: true);
|
||||
_connected = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// HEARTBEAT & INITIAL LOAD
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Future<void> _initialLoad() async {
|
||||
await Future.wait([
|
||||
_loadThings(),
|
||||
_loadEnergyStatus(),
|
||||
_loadEnergyHistory(),
|
||||
]);
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
// Polling toutes les 5 secondes pour rafraîchir les données énergie
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 5), (_) async {
|
||||
if (!_connected || _isSimulation) return;
|
||||
try {
|
||||
await _loadEnergyStatus();
|
||||
} catch (_) {
|
||||
_connected = false;
|
||||
notifyListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// ENERGY MANAGER
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Future<void> _loadEnergyStatus() async {
|
||||
try {
|
||||
final r = await _sendRequest('Energy.GetPowerBalance', {});
|
||||
_log('📊 PowerBalance: $r');
|
||||
_parsePowerBalance(r);
|
||||
} catch (e) { _log('GetPowerBalance error: $e'); }
|
||||
}
|
||||
|
||||
// ── nymea 1.14 Energy.PowerBalanceChanged / Energy.GetPowerBalance ───────────
|
||||
// Champs réels confirmés depuis les logs :
|
||||
// currentPowerProduction : W solaire (NÉGATIF! ex: -1190 = 1190W PV)
|
||||
// currentPowerConsumption : W maison (positif)
|
||||
// currentPowerAcquisition : W réseau (positif = import, 0 = export)
|
||||
// currentPowerStorage : W batterie (+ charge, - décharge)
|
||||
// totalProduction : kWh PV cumulé
|
||||
// totalConsumption : kWh maison cumulé
|
||||
// totalAcquisition : kWh réseau importé cumulé
|
||||
// totalReturn : kWh réseau exporté cumulé
|
||||
void _parsePowerBalance(Map<String, dynamic> response) {
|
||||
final p = response['params'] as Map<String, dynamic>? ?? response;
|
||||
_log('📊 Energy fields: ${p.keys.toList()}');
|
||||
|
||||
double? n(String k) => (p[k] as num?)?.toDouble();
|
||||
|
||||
// Production PV est négative dans nymea (convention producteur = négatif)
|
||||
final rawProd = n('currentPowerProduction');
|
||||
final pv = rawProd != null ? rawProd.abs() : 0.0;
|
||||
|
||||
final home = n('currentPowerConsumption') ?? 0.0;
|
||||
final grid = n('currentPowerAcquisition') ?? 0.0; // + import, 0 si export
|
||||
final bat = n('currentPowerStorage') ?? 0.0;
|
||||
|
||||
// Totaux cumulés en kWh → convertir en Wh pour l'affichage
|
||||
final totalProdKwh = n('totalProduction') ?? 0.0;
|
||||
final totalRetKwh = n('totalReturn') ?? 0.0;
|
||||
final totalConsoKwh = n('totalConsumption') ?? 0.0;
|
||||
final totalAcqKwh = n('totalAcquisition') ?? 0.0;
|
||||
|
||||
// Autoconsommation = production - injection réseau
|
||||
final selfConsoKwh = totalProdKwh - totalRetKwh;
|
||||
// Taux autoconsommation = autoconso / production * 100
|
||||
final selfRate = totalProdKwh > 0 ? (selfConsoKwh / totalProdKwh * 100).clamp(0.0, 100.0) : 0.0;
|
||||
// Taux autonomie = (production + batterie utilisée) / conso * 100
|
||||
final autoRate = totalConsoKwh > 0 ? ((totalConsoKwh - totalAcqKwh) / totalConsoKwh * 100).clamp(0.0, 100.0) : 0.0;
|
||||
|
||||
_energyData = _energyData.copyWith(
|
||||
pvPower: pv,
|
||||
homePower: home,
|
||||
gridPower: grid,
|
||||
batteryPower: bat,
|
||||
dayProductionWh: totalProdKwh * 1000,
|
||||
dayGridInjectionWh: totalRetKwh * 1000,
|
||||
daySelfConsumptionWh: selfConsoKwh * 1000,
|
||||
selfConsumptionRate: selfRate,
|
||||
autonomyRate: autoRate,
|
||||
dayGains: totalRetKwh * 0.13 + selfConsoKwh * 0.22, // estimation tarifaire
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _parseRootMeter(Map<String, dynamic> response) {
|
||||
// Dans nymea 1.14, tout est dans PowerBalance — méthode conservée pour compat
|
||||
}
|
||||
|
||||
Future<void> refreshEnergy() => _loadEnergyStatus();
|
||||
|
||||
/// Rafraîchit things + classes + énergie
|
||||
Future<void> refresh() async {
|
||||
await Future.wait([_loadThings(), _loadEnergyStatus()]);
|
||||
}
|
||||
|
||||
Future<void> _loadEnergyHistory() async {
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
final from = DateTime(now.year, now.month, now.day)
|
||||
.millisecondsSinceEpoch ~/ 1000;
|
||||
final to = now.millisecondsSinceEpoch ~/ 1000;
|
||||
final r = await _sendRequest('Energy.GetEnergyLogs', {
|
||||
'sampleRate': 'SampleRateHour',
|
||||
'from': from,
|
||||
'to': to,
|
||||
});
|
||||
final logs = r['params']?['logs'] as List? ?? [];
|
||||
_historyPoints = logs.map((l) {
|
||||
final lm = l as Map<String, dynamic>;
|
||||
return HistoryPoint(
|
||||
time: DateTime.fromMillisecondsSinceEpoch(
|
||||
(lm['timestamp'] as int) * 1000),
|
||||
pvWh: (lm['totalEnergyProduced'] as num?)?.toDouble() ?? 0,
|
||||
homeWh: (lm['totalEnergyConsumed'] as num?)?.toDouble() ?? 0,
|
||||
gridWh: (lm['totalEnergyReturned'] as num?)?.toDouble() ?? 0,
|
||||
);
|
||||
}).toList();
|
||||
notifyListeners();
|
||||
} catch (e) { _log('GetEnergyLogs: $e'); }
|
||||
}
|
||||
|
||||
Future<void> setChargingMode(ChargingMode mode) async {
|
||||
final modeStr = {
|
||||
ChargingMode.pv: 'pv',
|
||||
ChargingMode.minPv: 'minpv',
|
||||
ChargingMode.boost: 'boost',
|
||||
}[mode]!;
|
||||
if (_connected && !_isSimulation) {
|
||||
try {
|
||||
await _sendRequest('Energy.SetChargingMode', {'mode': modeStr});
|
||||
} catch (e) { _log('SetChargingMode: $e'); }
|
||||
}
|
||||
_energyData = _energyData.copyWith(chargingMode: mode);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// THINGS / INTEGRATIONS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool _thingsLoaded = false;
|
||||
bool get thingsLoaded => _thingsLoaded;
|
||||
|
||||
Future<void> _loadThings() async {
|
||||
// ── Étape 1 : charger les things configurés ──────────────────────────────
|
||||
try {
|
||||
_log('📦 GetThings → envoi...', force: true);
|
||||
final r = await _sendRequest('Integrations.GetThings', {});
|
||||
_log('📦 GetThings ← status=${r["status"]} keys=${r.keys.toList()}', force: true);
|
||||
|
||||
final thingsRaw = r['params']?['things']
|
||||
?? r['result']?['things']
|
||||
?? r['things'];
|
||||
|
||||
if (thingsRaw is List) {
|
||||
_things = thingsRaw
|
||||
.map((t) => NymeaThing.fromJson(t as Map<String, dynamic>))
|
||||
.where((t) => t.id.isNotEmpty) // exclure entrées invalides
|
||||
.toList();
|
||||
_log('📦 ✅ ${_things.length} things configurés:', force: true);
|
||||
for (final t in _things) {
|
||||
_log(' 📱 "${t.name}" | classId=${t.thingClassId.substring(0, 8)}… | ${t.setupStatus}', force: true);
|
||||
}
|
||||
} else {
|
||||
_log('📦 ⚠️ GetThings: clé "things" absente — keys=${r.keys.toList()}', force: true);
|
||||
}
|
||||
} catch (e, st) {
|
||||
_log('📦 ❌ GetThings erreur: $e\n$st', force: true);
|
||||
}
|
||||
|
||||
// ── Étape 2 : charger UNIQUEMENT les classes des things présents ──────────
|
||||
// On filtre sur les thingClassIds réellement utilisés → évite les 80+ plugins
|
||||
if (_things.isNotEmpty) {
|
||||
try {
|
||||
final neededClassIds = _things
|
||||
.map((t) => t.thingClassId)
|
||||
.where((id) => id.isNotEmpty)
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
_log('📦 GetThingClasses pour ${neededClassIds.length} classes...', force: true);
|
||||
|
||||
// nymea supporte un filtre optionnel thingClassIds
|
||||
final r = await _sendRequest('Integrations.GetThingClasses', {
|
||||
'thingClassIds': neededClassIds,
|
||||
});
|
||||
|
||||
final classesRaw = r['params']?['thingClasses']
|
||||
?? r['result']?['thingClasses']
|
||||
?? r['thingClasses'];
|
||||
|
||||
if (classesRaw is List && classesRaw.isNotEmpty) {
|
||||
_thingClasses = classesRaw
|
||||
.map((c) => NymeaThingClass.fromJson(c as Map<String, dynamic>))
|
||||
.toList();
|
||||
_log('📦 ✅ ThingClasses: ${_thingClasses.length}', force: true);
|
||||
for (final c in _thingClasses) {
|
||||
_log(' 📋 "${c.name}" | id=${c.id.substring(0, 8)}… | interfaces=${c.interfaces}', force: true);
|
||||
}
|
||||
} else {
|
||||
// Fallback : nymea < 1.6 ne supporte peut-être pas le filtre → tout charger
|
||||
_log('📦 Filtre non supporté, fallback GetThingClasses sans filtre...', force: true);
|
||||
await _loadAllThingClassesFiltered(neededClassIds);
|
||||
}
|
||||
} catch (e) {
|
||||
_log('📦 ❌ GetThingClasses erreur: $e', force: true);
|
||||
// Fallback si le filtre cause une erreur
|
||||
await _loadAllThingClassesFiltered(
|
||||
_things.map((t) => t.thingClassId).toSet().toList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_thingsLoaded = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Fallback : charge toutes les classes puis filtre côté client
|
||||
Future<void> _loadAllThingClassesFiltered(List<String> neededIds) async {
|
||||
try {
|
||||
final r = await _sendRequest('Integrations.GetThingClasses', {});
|
||||
final classesRaw = r['params']?['thingClasses']
|
||||
?? r['result']?['thingClasses']
|
||||
?? r['thingClasses'];
|
||||
|
||||
if (classesRaw is List) {
|
||||
final all = classesRaw
|
||||
.map((c) => NymeaThingClass.fromJson(c as Map<String, dynamic>))
|
||||
.toList();
|
||||
// Garder seulement les classes dont on a besoin
|
||||
_thingClasses = all
|
||||
.where((c) => neededIds.contains(c.id))
|
||||
.toList();
|
||||
_log('📦 Fallback: ${all.length} classes totales → ${_thingClasses.length} utilisées', force: true);
|
||||
for (final c in _thingClasses) {
|
||||
_log(' 📋 "${c.name}" interfaces=${c.interfaces}', force: true);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_log('📦 ❌ Fallback GetThingClasses erreur: $e', force: true);
|
||||
}
|
||||
}
|
||||
Future<void> refreshThings() => _loadThings();
|
||||
|
||||
Future<List<Map<String, dynamic>>> discoverThings(String thingClassId) async {
|
||||
if (_isSimulation) return [];
|
||||
try {
|
||||
final r = await _sendRequest(
|
||||
'Integrations.DiscoverThings', {'thingClassId': thingClassId});
|
||||
return List<Map<String, dynamic>>.from(
|
||||
r['params']?['thingDescriptors'] ?? []);
|
||||
} catch (_) { return []; }
|
||||
}
|
||||
|
||||
Future<bool> addThing({
|
||||
required String thingClassId,
|
||||
required String thingDescriptorId,
|
||||
required String name,
|
||||
}) async {
|
||||
if (_isSimulation) return false;
|
||||
try {
|
||||
final r = await _sendRequest('Integrations.AddThing', {
|
||||
'thingClassId': thingClassId,
|
||||
'name': name,
|
||||
'thingDescriptorId': thingDescriptorId,
|
||||
});
|
||||
if (r['status'] == 'success') { await _loadThings(); return true; }
|
||||
return false;
|
||||
} catch (_) { return false; }
|
||||
}
|
||||
|
||||
Future<bool> removeThing(String thingId) async {
|
||||
if (_isSimulation) return false;
|
||||
try {
|
||||
final r = await _sendRequest(
|
||||
'Integrations.RemoveThing', {'thingId': thingId});
|
||||
if (r['status'] == 'success') {
|
||||
_things.removeWhere((t) => t.id == thingId);
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (_) { return false; }
|
||||
}
|
||||
|
||||
Future<bool> renameThing(String thingId, String newName) async {
|
||||
if (_isSimulation) return false;
|
||||
try {
|
||||
final r = await _sendRequest(
|
||||
'Integrations.EditThing', {'thingId': thingId, 'name': newName});
|
||||
if (r['status'] == 'success') {
|
||||
final idx = _things.indexWhere((t) => t.id == thingId);
|
||||
if (idx >= 0) {
|
||||
_things[idx] = _things[idx].copyWith(name: newName);
|
||||
notifyListeners();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (_) { return false; }
|
||||
}
|
||||
|
||||
Future<bool> setThingSettings(
|
||||
String thingId, String settingTypeId, dynamic value) async {
|
||||
if (_isSimulation) return true;
|
||||
try {
|
||||
final r = await _sendRequest('Integrations.SetThingSettings', {
|
||||
'thingId': thingId,
|
||||
'settings': [{'paramTypeId': settingTypeId, 'value': value}],
|
||||
});
|
||||
return r['params']?['thingError'] == 'ThingErrorNoError';
|
||||
} catch (e) {
|
||||
_log('SetThingSettings: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> executeAction({
|
||||
required String thingId,
|
||||
required String actionTypeId,
|
||||
Map<String, dynamic> params = const {},
|
||||
}) async {
|
||||
if (_isSimulation) return true;
|
||||
try {
|
||||
final r = await _sendRequest('Integrations.ExecuteAction', {
|
||||
'thingId': thingId,
|
||||
'actionTypeId': actionTypeId,
|
||||
'params': params.entries
|
||||
.map((e) => {'paramTypeId': e.key, 'value': e.value})
|
||||
.toList(),
|
||||
});
|
||||
return r['status'] == 'success';
|
||||
} catch (e) { _log('ExecuteAction: $e'); return false; }
|
||||
}
|
||||
|
||||
Future<dynamic> getStateValue(String thingId, String stateTypeId) async {
|
||||
if (_isSimulation) return null;
|
||||
try {
|
||||
final r = await _sendRequest('Integrations.GetStateValue', {
|
||||
'thingId': thingId, 'stateTypeId': stateTypeId,
|
||||
});
|
||||
return r['params']?['value'];
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getRules() async {
|
||||
if (_isSimulation) return [];
|
||||
try {
|
||||
final r = await _sendRequest('Rules.GetRules', {});
|
||||
return List<Map<String, dynamic>>.from(r['params']?['rules'] ?? []);
|
||||
} catch (_) { return []; }
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// FAVORITES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void addFavorite(FavoriteWidget widget) {
|
||||
if (!_favoriteWidgets.any((f) => f.id == widget.id)) {
|
||||
_favoriteWidgets.add(widget);
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
void removeFavorite(String id) {
|
||||
_favoriteWidgets.removeWhere((f) => f.id == id);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void reorderFavorites(int oldIndex, int newIndex) {
|
||||
if (newIndex > oldIndex) newIndex--;
|
||||
final item = _favoriteWidgets.removeAt(oldIndex);
|
||||
_favoriteWidgets.insert(newIndex, item);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// SIMULATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void startSimulation() {
|
||||
disconnect();
|
||||
_connected = true;
|
||||
_isSimulation = true;
|
||||
_nymeaVersion = 'demo 1.8.2';
|
||||
_generateSimulatedThings();
|
||||
_generateHistory();
|
||||
if (_favoriteWidgets.isEmpty) {
|
||||
_favoriteWidgets = [
|
||||
FavoriteWidget(id: 'fw_pv', type: FavoriteType.pvPower, title: 'Production PV'),
|
||||
FavoriteWidget(id: 'fw_home', type: FavoriteType.homePower, title: 'Consommation'),
|
||||
FavoriteWidget(id: 'fw_soc', type: FavoriteType.batterySOC, title: 'Batterie'),
|
||||
FavoriteWidget(id: 'fw_rates', type: FavoriteType.rates, title: 'Taux'),
|
||||
FavoriteWidget(id: 'fw_ev', type: FavoriteType.evCharger, title: 'Borne EV'),
|
||||
];
|
||||
}
|
||||
_simulationTimer?.cancel();
|
||||
_simulationTimer = Timer.periodic(
|
||||
const Duration(seconds: 2), (_) => _simulateData());
|
||||
_simulateData();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _simulateData() {
|
||||
final now = DateTime.now();
|
||||
final hour = now.hour + now.minute / 60.0;
|
||||
final rng = Random();
|
||||
double pv = 0;
|
||||
if (hour >= 7 && hour <= 20) {
|
||||
pv = (3800 * sin((hour - 7) * pi / 13) + rng.nextDouble() * 80 - 40)
|
||||
.clamp(0, 4500);
|
||||
}
|
||||
final home = 200 + rng.nextDouble() * 600;
|
||||
final battery = pv > home + 200 ? (pv - home - 200).clamp(0.0, 2000.0) : 0.0;
|
||||
final batDischarge = home > pv + 100 && _energyData.batterySOC > 10
|
||||
? min(home - pv, 1500.0) : 0.0;
|
||||
final grid = home - pv - battery + batDischarge;
|
||||
final soc = (_energyData.batterySOC +
|
||||
(battery > 0 ? 0.1 : batDischarge > 0 ? -0.05 : 0)).clamp(5.0, 100.0);
|
||||
_energyData = _energyData.copyWith(
|
||||
pvPower: pv, homePower: home,
|
||||
batteryPower: battery > 0 ? battery : (batDischarge > 0 ? -batDischarge : 0),
|
||||
gridPower: grid, batterySOC: soc, temperature: 7,
|
||||
dayProductionWh: pv * 0.5, daySelfConsumptionWh: home * 0.4,
|
||||
dayGridInjectionWh: grid < 0 ? -grid * 0.3 : 0,
|
||||
selfConsumptionRate: pv > 0 ? (min(pv, home) / pv * 100).clamp(0.0, 100.0) : 0.0,
|
||||
autonomyRate: home > 0 ? ((pv + batDischarge) / home * 100).clamp(0.0, 100.0) : 0.0,
|
||||
dayGains: (pv * 0.5 / 1000) * 0.13,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void _generateHistory() {
|
||||
final now = DateTime.now();
|
||||
_historyPoints = List.generate(24, (i) {
|
||||
final h = i.toDouble();
|
||||
final pv = (h >= 7 && h <= 20)
|
||||
? (3000 * sin((h - 7) * pi / 13)).clamp(0.0, 4500.0) : 0.0;
|
||||
return HistoryPoint(
|
||||
time: DateTime(now.year, now.month, now.day, i),
|
||||
pvWh: pv, homeWh: 200 + Random().nextDouble() * 400,
|
||||
gridWh: (200 - pv * 0.3).clamp(0, 500),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _generateSimulatedThings() {
|
||||
_things = [
|
||||
NymeaThing(id: 'sim-inverter', name: 'Onduleur SolarEdge', thingClassId: 'solaredge',
|
||||
setupStatus: 'ThingSetupStatusComplete', paramValues: [],
|
||||
states: [NymeaStateValue(stateTypeId: 'power', value: 4200.0)]),
|
||||
NymeaThing(id: 'sim-battery', name: 'Batterie BYD', thingClassId: 'byd',
|
||||
setupStatus: 'ThingSetupStatusComplete', paramValues: [],
|
||||
states: [NymeaStateValue(stateTypeId: 'soc', value: 42.0)]),
|
||||
NymeaThing(id: 'sim-ev', name: 'Borne Wallbox', thingClassId: 'wallbox',
|
||||
setupStatus: 'ThingSetupStatusComplete', paramValues: [],
|
||||
states: [NymeaStateValue(stateTypeId: 'power', value: 3600.0)]),
|
||||
NymeaThing(id: 'sim-meter', name: 'Compteur Linky', thingClassId: 'linky',
|
||||
setupStatus: 'ThingSetupStatusComplete', paramValues: [],
|
||||
states: [NymeaStateValue(stateTypeId: 'power', value: 180.0)]),
|
||||
NymeaThing(id: 'sim-ac', name: 'Climatiseur Salon', thingClassId: 'mitsubishi',
|
||||
setupStatus: 'ThingSetupStatusComplete', paramValues: [],
|
||||
states: [NymeaStateValue(stateTypeId: 'power', value: false)]),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_simulationTimer?.cancel();
|
||||
_socket?.destroy();
|
||||
_wsSub?.cancel();
|
||||
_wsChannel?.sink.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
36
lib/theme/app_theme.dart
Normal file
@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
// Colors
|
||||
static const Color primaryGreen = Color(0xFF4CAF50);
|
||||
static const Color solarYellow = Color(0xFFF5C842);
|
||||
static const Color gridGray = Color(0xFF5A6473);
|
||||
static const Color homeBlue = Color(0xFF5BA4CF);
|
||||
static const Color batteryGreen = Color(0xFF4CAF50);
|
||||
static const Color accentOrange = Color(0xFFFF7043);
|
||||
static const Color backgroundGray = Color(0xFFF0F2F5);
|
||||
static const Color cardWhite = Color(0xFFFFFFFF);
|
||||
static const Color textDark = Color(0xFF1A1A2E);
|
||||
static const Color textLight = Color(0xFF6B7280);
|
||||
static const Color boostRed = Color(0xFFEF5350);
|
||||
static const Color pvGreen = Color(0xFF43A047);
|
||||
static const Color minPvBlue = Color(0xFF42A5F5);
|
||||
|
||||
static ThemeData get theme => ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: primaryGreen,
|
||||
surface: backgroundGray,
|
||||
),
|
||||
scaffoldBackgroundColor: backgroundGray,
|
||||
cardTheme: CardThemeData(
|
||||
color: cardWhite,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
margin: EdgeInsets.zero,
|
||||
),
|
||||
fontFamily: 'Roboto',
|
||||
useMaterial3: true,
|
||||
);
|
||||
}
|
||||
272
lib/widgets/energy_flow_widget.dart
Normal file
@ -0,0 +1,272 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/energy_data.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class EnergyFlowWidget extends StatelessWidget {
|
||||
final EnergyData data;
|
||||
|
||||
const EnergyFlowWidget({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Données en temps réel',
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${data.temperature.toStringAsFixed(0)}°C',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(Icons.cloud, color: Colors.blueGrey, size: 22),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Energy nodes
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_EnergyNode(
|
||||
icon: Icons.wb_sunny_rounded,
|
||||
color: AppTheme.solarYellow,
|
||||
power: data.pvPower,
|
||||
label: 'Solaire',
|
||||
),
|
||||
_EnergyNode(
|
||||
icon: Icons.home_rounded,
|
||||
color: AppTheme.homeBlue,
|
||||
power: data.homePower,
|
||||
label: 'Maison',
|
||||
),
|
||||
_EnergyNode(
|
||||
icon: Icons.battery_charging_full_rounded,
|
||||
color: AppTheme.batteryGreen,
|
||||
power: data.batteryPower.abs(),
|
||||
label: 'Batterie',
|
||||
badge: '${data.batterySOC.toStringAsFixed(0)}%',
|
||||
),
|
||||
_EnergyNode(
|
||||
icon: Icons.electrical_services_rounded,
|
||||
color: AppTheme.gridGray,
|
||||
power: data.gridPower.abs(),
|
||||
label: data.gridPower >= 0 ? 'Réseau ↓' : 'Réseau ↑',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Flow diagram
|
||||
SizedBox(
|
||||
height: 80,
|
||||
child: CustomPaint(
|
||||
painter: _FlowPainter(data: data),
|
||||
size: Size.infinite,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _EnergyNode extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final double power;
|
||||
final String label;
|
||||
final String? badge;
|
||||
|
||||
const _EnergyNode({
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.power,
|
||||
required this.label,
|
||||
this.badge,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: 30),
|
||||
),
|
||||
if (badge != null)
|
||||
Positioned(
|
||||
bottom: -4,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color, width: 1.5),
|
||||
),
|
||||
child: Text(
|
||||
badge!,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
_formatPower(power),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_powerUnit(power),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textLight,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: AppTheme.textLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPower(double w) {
|
||||
if (w >= 1000) return (w / 1000).toStringAsFixed(1);
|
||||
return w.toStringAsFixed(0);
|
||||
}
|
||||
|
||||
String _powerUnit(double w) => w >= 1000 ? 'kW' : 'W';
|
||||
}
|
||||
|
||||
class _FlowPainter extends CustomPainter {
|
||||
final EnergyData data;
|
||||
|
||||
_FlowPainter({required this.data});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
// Draw animated flow lines between nodes
|
||||
final centerY = size.height * 0.4;
|
||||
final nodePositions = [
|
||||
size.width * 0.125, // PV
|
||||
size.width * 0.375, // Home
|
||||
size.width * 0.625, // Battery
|
||||
size.width * 0.875, // Grid
|
||||
];
|
||||
|
||||
final linePaint = Paint()
|
||||
..strokeWidth = 2
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
// PV -> Home (always if PV > 0)
|
||||
if (data.pvPower > 0) {
|
||||
_drawArrowLine(canvas, Offset(nodePositions[0], centerY),
|
||||
Offset(nodePositions[1], centerY), AppTheme.solarYellow, linePaint);
|
||||
}
|
||||
|
||||
// Home -> Battery (if battery charging)
|
||||
if (data.batteryPower > 0) {
|
||||
_drawArrowLine(canvas, Offset(nodePositions[1], centerY),
|
||||
Offset(nodePositions[2], centerY), AppTheme.batteryGreen, linePaint);
|
||||
}
|
||||
|
||||
// Grid -> Home (if importing)
|
||||
if (data.gridPower > 0) {
|
||||
_drawArrowLine(canvas, Offset(nodePositions[3], centerY),
|
||||
Offset(nodePositions[1], centerY), Colors.orange, linePaint);
|
||||
} else if (data.gridPower < 0) {
|
||||
// Exporting
|
||||
_drawArrowLine(canvas, Offset(nodePositions[2], centerY),
|
||||
Offset(nodePositions[3], centerY), AppTheme.solarYellow, linePaint);
|
||||
}
|
||||
|
||||
// Draw node dots
|
||||
final dotPaint = Paint()..style = PaintingStyle.fill;
|
||||
for (int i = 0; i < nodePositions.length; i++) {
|
||||
final colors = [
|
||||
AppTheme.solarYellow,
|
||||
AppTheme.homeBlue,
|
||||
AppTheme.batteryGreen,
|
||||
AppTheme.gridGray,
|
||||
];
|
||||
dotPaint.color = colors[i];
|
||||
canvas.drawCircle(Offset(nodePositions[i], centerY), 6, dotPaint);
|
||||
}
|
||||
}
|
||||
|
||||
void _drawArrowLine(Canvas canvas, Offset start, Offset end, Color color,
|
||||
Paint paint) {
|
||||
paint.color = color.withValues(alpha:0.7);
|
||||
|
||||
final path = Path()
|
||||
..moveTo(start.dx, start.dy)
|
||||
..lineTo(end.dx, end.dy);
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
// Arrow head
|
||||
final arrowPaint = Paint()
|
||||
..color = color
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
final dx = end.dx - start.dx;
|
||||
final arrowSign = dx > 0 ? 1 : -1;
|
||||
final arrowTip = Offset(end.dx - arrowSign * 8, end.dy);
|
||||
|
||||
final arrowPath = Path()
|
||||
..moveTo(arrowTip.dx + arrowSign * 8, arrowTip.dy)
|
||||
..lineTo(arrowTip.dx, arrowTip.dy - 5)
|
||||
..lineTo(arrowTip.dx, arrowTip.dy + 5)
|
||||
..close();
|
||||
canvas.drawPath(arrowPath, arrowPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_FlowPainter oldDelegate) => true;
|
||||
}
|
||||
425
lib/widgets/ev_charging_card.dart
Normal file
@ -0,0 +1,425 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/energy_data.dart';
|
||||
import '../services/nymea_service.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class EVChargingCard extends StatelessWidget {
|
||||
final EnergyData data;
|
||||
final NymeaService service;
|
||||
|
||||
const EVChargingCard({
|
||||
super.key,
|
||||
required this.data,
|
||||
required this.service,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Recharge du véhicule',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_statusLabel(data.chargingMode),
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppTheme.textLight,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () => _showSettings(context),
|
||||
color: AppTheme.textLight,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Mode buttons
|
||||
Row(
|
||||
children: [
|
||||
_ModeButton(
|
||||
label: 'PV',
|
||||
icon: Icons.wb_sunny_rounded,
|
||||
color: AppTheme.pvGreen,
|
||||
isSelected: data.chargingMode == ChargingMode.pv,
|
||||
onTap: () => service.setChargingMode(ChargingMode.pv),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ModeButton(
|
||||
label: 'Min + PV',
|
||||
icon: Icons.bolt,
|
||||
color: AppTheme.minPvBlue,
|
||||
isSelected: data.chargingMode == ChargingMode.minPv,
|
||||
onTap: () => service.setChargingMode(ChargingMode.minPv),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ModeButton(
|
||||
label: 'Boost',
|
||||
icon: Icons.rocket_launch_rounded,
|
||||
color: AppTheme.boostRed,
|
||||
isSelected: data.chargingMode == ChargingMode.boost,
|
||||
onTap: () => service.setChargingMode(ChargingMode.boost),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Stats
|
||||
_InfoRow(
|
||||
label: 'Puissance borne',
|
||||
value: '${data.chargingPower.toStringAsFixed(1)} kW',
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_InfoRow(
|
||||
label: 'Source',
|
||||
value: '${data.solarSourcePercent.toStringAsFixed(0)}% solaire',
|
||||
valueColor: AppTheme.pvGreen,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_InfoRow(
|
||||
label: 'Limite réseau',
|
||||
value: data.gridLimitOk ? 'OK ✓' : 'Dépassée !',
|
||||
valueColor: data.gridLimitOk ? AppTheme.primaryGreen : AppTheme.boostRed,
|
||||
),
|
||||
|
||||
// Charging indicator
|
||||
const SizedBox(height: 12),
|
||||
if (data.chargingMode != ChargingMode.pv || data.pvPower > 0)
|
||||
_ChargingIndicator(
|
||||
mode: data.chargingMode,
|
||||
pvPower: data.pvPower,
|
||||
chargingPower: data.chargingPower,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _statusLabel(ChargingMode mode) {
|
||||
switch (mode) {
|
||||
case ChargingMode.pv:
|
||||
return 'Surplus PV disponible';
|
||||
case ChargingMode.minPv:
|
||||
return 'Minimum + surplus PV';
|
||||
case ChargingMode.boost:
|
||||
return 'Charge rapide (réseau)';
|
||||
}
|
||||
}
|
||||
|
||||
void _showSettings(BuildContext context) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (_) => const _ChargingSettingsSheet(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ModeButton extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ModeButton({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color : color.withValues(alpha:0.1),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon,
|
||||
color: isSelected ? Colors.white : color, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : color,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoRow extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final Color? valueColor;
|
||||
|
||||
const _InfoRow({required this.label, required this.value, this.valueColor});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(fontSize: 13, color: AppTheme.textLight)),
|
||||
const Text(' : ', style: TextStyle(color: AppTheme.textLight)),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: valueColor ?? AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChargingIndicator extends StatelessWidget {
|
||||
final ChargingMode mode;
|
||||
final double pvPower;
|
||||
final double chargingPower;
|
||||
|
||||
const _ChargingIndicator({
|
||||
required this.mode,
|
||||
required this.pvPower,
|
||||
required this.chargingPower,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final pvPercent = (pvPower / (chargingPower * 1000)).clamp(0.0, 1.0);
|
||||
final gridPercent = mode == ChargingMode.boost ? 1.0 : (1 - pvPercent).clamp(0.0, 1.0);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Répartition charge',
|
||||
style: TextStyle(fontSize: 12, color: AppTheme.textLight)),
|
||||
const SizedBox(height: 6),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: SizedBox(
|
||||
height: 10,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: (pvPercent * 100).toInt(),
|
||||
child: Container(color: AppTheme.solarYellow),
|
||||
),
|
||||
Flexible(
|
||||
flex: (gridPercent * 100).toInt(),
|
||||
child: Container(color: Colors.orange.shade300),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
_legend(AppTheme.solarYellow, 'Solaire'),
|
||||
const SizedBox(width: 12),
|
||||
_legend(Colors.orange.shade300, 'Réseau'),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _legend(Color color, String label) => Row(
|
||||
children: [
|
||||
Container(width: 10, height: 10, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 11, color: AppTheme.textLight)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _ChargingSettingsSheet extends StatefulWidget {
|
||||
const _ChargingSettingsSheet();
|
||||
|
||||
@override
|
||||
State<_ChargingSettingsSheet> createState() => _ChargingSettingsSheetState();
|
||||
}
|
||||
|
||||
class _ChargingSettingsSheetState extends State<_ChargingSettingsSheet> {
|
||||
double _minPower = 6;
|
||||
double _maxPower = 16;
|
||||
bool _scheduleEnabled = false;
|
||||
TimeOfDay _startTime = const TimeOfDay(hour: 22, minute: 0);
|
||||
TimeOfDay _endTime = const TimeOfDay(hour: 6, minute: 0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('Paramètres de charge',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 20),
|
||||
const Text('Courant minimum (A)',
|
||||
style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
Slider(
|
||||
value: _minPower,
|
||||
min: 6,
|
||||
max: 16,
|
||||
divisions: 10,
|
||||
label: '${_minPower.toStringAsFixed(0)} A',
|
||||
onChanged: (v) => setState(() => _minPower = v),
|
||||
activeColor: AppTheme.primaryGreen,
|
||||
),
|
||||
const Text('Courant maximum (A)',
|
||||
style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
Slider(
|
||||
value: _maxPower,
|
||||
min: 6,
|
||||
max: 32,
|
||||
divisions: 26,
|
||||
label: '${_maxPower.toStringAsFixed(0)} A',
|
||||
onChanged: (v) => setState(() => _maxPower = v),
|
||||
activeColor: AppTheme.primaryGreen,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text('Programmation horaire',
|
||||
style: TextStyle(fontWeight: FontWeight.w500)),
|
||||
Switch(
|
||||
value: _scheduleEnabled,
|
||||
onChanged: (v) => setState(() => _scheduleEnabled = v),
|
||||
activeThumbColor: AppTheme.primaryGreen,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_scheduleEnabled) ...[
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _TimePicker(
|
||||
label: 'Début',
|
||||
time: _startTime,
|
||||
onChanged: (t) => setState(() => _startTime = t),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _TimePicker(
|
||||
label: 'Fin',
|
||||
time: _endTime,
|
||||
onChanged: (t) => setState(() => _endTime = t),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: const Text('Enregistrer'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TimePicker extends StatelessWidget {
|
||||
final String label;
|
||||
final TimeOfDay time;
|
||||
final ValueChanged<TimeOfDay> onChanged;
|
||||
|
||||
const _TimePicker({
|
||||
required this.label,
|
||||
required this.time,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final picked = await showTimePicker(context: context, initialTime: time);
|
||||
if (picked != null) onChanged(picked);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time, size: 16, color: AppTheme.textLight),
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(
|
||||
fontSize: 11, color: AppTheme.textLight)),
|
||||
Text(
|
||||
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
63
lib/widgets/gains_card.dart
Normal file
@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../models/energy_data.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class GainsCard extends StatelessWidget {
|
||||
final EnergyData data;
|
||||
|
||||
const GainsCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.amber.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.euro_rounded,
|
||||
color: Colors.amber, size: 22),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
'Gains',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${data.dayGains.toStringAsFixed(2)} €',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.chevron_right, color: AppTheme.textLight),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
177
lib/widgets/production_card.dart
Normal file
@ -0,0 +1,177 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
import '../models/energy_data.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
|
||||
class ProductionCard extends StatelessWidget {
|
||||
final EnergyData data;
|
||||
|
||||
const ProductionCard({super.key, required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Production du jour',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${_formatEnergy(data.dayProductionWh)} Wh',
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
CircularPercentIndicator(
|
||||
radius: 35,
|
||||
lineWidth: 6,
|
||||
percent: (data.selfConsumptionRate / 100).clamp(0, 1),
|
||||
center: Text(
|
||||
'${data.selfConsumptionRate.toStringAsFixed(0)}%',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
progressColor: AppTheme.primaryGreen,
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 24),
|
||||
|
||||
// Self-consumption
|
||||
_StatRow(
|
||||
icon: Icons.home_rounded,
|
||||
iconColor: AppTheme.solarYellow,
|
||||
label: 'Usage personnel',
|
||||
value: '${_formatEnergy(data.daySelfConsumptionWh)} Wh',
|
||||
subLabel: 'Autoconsommation',
|
||||
percent: data.selfConsumptionRate,
|
||||
color: AppTheme.solarYellow,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Grid injection
|
||||
_StatRow(
|
||||
icon: Icons.electrical_services_rounded,
|
||||
iconColor: AppTheme.gridGray,
|
||||
label: 'Vers réseau',
|
||||
value: '${_formatEnergy(data.dayGridInjectionWh)} Wh',
|
||||
subLabel: 'Autonomie',
|
||||
percent: data.autonomyRate,
|
||||
color: AppTheme.homeBlue,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatEnergy(double wh) {
|
||||
if (wh >= 1000) return '${(wh / 1000).toStringAsFixed(2)} k';
|
||||
return wh.toStringAsFixed(0);
|
||||
}
|
||||
}
|
||||
|
||||
class _StatRow extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final String label;
|
||||
final String value;
|
||||
final String subLabel;
|
||||
final double percent;
|
||||
final Color color;
|
||||
|
||||
const _StatRow({
|
||||
required this.icon,
|
||||
required this.iconColor,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.subLabel,
|
||||
required this.percent,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withValues(alpha:0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: iconColor, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14, color: AppTheme.textLight)),
|
||||
Text(value,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.textDark)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'$subLabel : ${percent.toStringAsFixed(0)}%',
|
||||
style: TextStyle(fontSize: 11, color: color),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: LinearProgressIndicator(
|
||||
value: (percent / 100).clamp(0, 1),
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
valueColor: AlwaysStoppedAnimation(color),
|
||||
minHeight: 5,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
1
linux/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
flutter/ephemeral
|
||||
128
linux/CMakeLists.txt
Normal file
@ -0,0 +1,128 @@
|
||||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "etm_powersync_app")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.etm_powersync_app")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Define build configuration options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||
install(FILES "${bundled_library}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
88
linux/flutter/CMakeLists.txt
Normal file
@ -0,0 +1,88 @@
|
||||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
|
||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||
# which isn't available in 3.10.
|
||||
function(list_prepend LIST_NAME PREFIX)
|
||||
set(NEW_LIST "")
|
||||
foreach(element ${${LIST_NAME}})
|
||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||
endforeach(element)
|
||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# === Flutter Library ===
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"fl_basic_message_channel.h"
|
||||
"fl_binary_codec.h"
|
||||
"fl_binary_messenger.h"
|
||||
"fl_dart_project.h"
|
||||
"fl_engine.h"
|
||||
"fl_json_message_codec.h"
|
||||
"fl_json_method_codec.h"
|
||||
"fl_message_codec.h"
|
||||
"fl_method_call.h"
|
||||
"fl_method_channel.h"
|
||||
"fl_method_codec.h"
|
||||
"fl_method_response.h"
|
||||
"fl_plugin_registrar.h"
|
||||
"fl_plugin_registry.h"
|
||||
"fl_standard_message_codec.h"
|
||||
"fl_standard_method_codec.h"
|
||||
"fl_string_codec.h"
|
||||
"fl_value.h"
|
||||
"fl_view.h"
|
||||
"flutter_linux.h"
|
||||
)
|
||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||
target_link_libraries(flutter INTERFACE
|
||||
PkgConfig::GTK
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GIO
|
||||
)
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
)
|
||||
11
linux/flutter/generated_plugin_registrant.cc
Normal file
@ -0,0 +1,11 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
}
|
||||
15
linux/flutter/generated_plugin_registrant.h
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
23
linux/flutter/generated_plugins.cmake
Normal file
@ -0,0 +1,23 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
26
linux/runner/CMakeLists.txt
Normal file
@ -0,0 +1,26 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the application ID.
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Add dependency libraries. Add any application-specific dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
6
linux/runner/main.cc
Normal file
@ -0,0 +1,6 @@
|
||||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
148
linux/runner/my_application.cc
Normal file
@ -0,0 +1,148 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Called when first Flutter frame received.
|
||||
static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "etm_powersync_app");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "etm_powersync_app");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(
|
||||
project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
GdkRGBA background_color;
|
||||
// Background defaults to black, override it here if necessary, e.g. #00000000
|
||||
// for transparent.
|
||||
gdk_rgba_parse(&background_color, "#000000");
|
||||
fl_view_set_background_color(view, &background_color);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
// Show the window when Flutter renders.
|
||||
// Requires the view to be realized so we can start rendering.
|
||||
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
||||
self);
|
||||
gtk_widget_realize(GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application,
|
||||
gchar*** arguments,
|
||||
int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GApplication::startup.
|
||||
static void my_application_startup(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application startup.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||
}
|
||||
|
||||
// Implements GApplication::shutdown.
|
||||
static void my_application_shutdown(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application shutdown.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line =
|
||||
my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
// Set the program name to the application ID, which helps various systems
|
||||
// like GTK and desktop environments map this running application to its
|
||||
// corresponding .desktop file. This ensures better integration by allowing
|
||||
// the application to be recognized beyond its binary name.
|
||||
g_set_prgname(APPLICATION_ID);
|
||||
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID, "flags",
|
||||
G_APPLICATION_NON_UNIQUE, nullptr));
|
||||
}
|
||||
21
linux/runner/my_application.h
Normal file
@ -0,0 +1,21 @@
|
||||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication,
|
||||
my_application,
|
||||
MY,
|
||||
APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
||||
7
macos/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Flutter-related
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
|
||||
# Xcode-related
|
||||
**/dgph
|
||||
**/xcuserdata/
|
||||
1
macos/Flutter/Flutter-Debug.xcconfig
Normal file
@ -0,0 +1 @@
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
1
macos/Flutter/Flutter-Release.xcconfig
Normal file
@ -0,0 +1 @@
|
||||
#include "ephemeral/Flutter-Generated.xcconfig"
|
||||
12
macos/Flutter/GeneratedPluginRegistrant.swift
Normal file
@ -0,0 +1,12 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import shared_preferences_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
705
macos/Runner.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,705 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXAggregateTarget section */
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
|
||||
isa = PBXAggregateTarget;
|
||||
buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
|
||||
buildPhases = (
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */,
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "Flutter Assemble";
|
||||
productName = FLX;
|
||||
};
|
||||
/* End PBXAggregateTarget section */
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; };
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 33CC10EC2044A3C60003C045;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 33CC111A2044C6BA0003C045;
|
||||
remoteInfo = FLX;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
);
|
||||
name = "Bundle Framework";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* etm_powersync_app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "etm_powersync_app.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = "<group>"; };
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = "<group>"; };
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = "<group>"; };
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = "<group>"; };
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33BA886A226E78AF003329D5 /* Configs */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
|
||||
);
|
||||
path = Configs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10E42044A3C60003C045 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33FAB671232836740065AC1E /* Runner */,
|
||||
33CEB47122A05771004F2AC0 /* Flutter */,
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC10EE2044A3C60003C045 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10ED2044A3C60003C045 /* etm_powersync_app.app */,
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CC11242044D66E0003C045 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */,
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */,
|
||||
33CC10F72044A3C60003C045 /* Info.plist */,
|
||||
);
|
||||
name = Resources;
|
||||
path = ..;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33CEB47122A05771004F2AC0 /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
|
||||
33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
|
||||
33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
|
||||
33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
|
||||
);
|
||||
path = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
33FAB671232836740065AC1E /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */,
|
||||
33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */,
|
||||
33E51914231749380026EE4D /* Release.entitlements */,
|
||||
33CC11242044D66E0003C045 /* Resources */,
|
||||
33BA886A226E78AF003329D5 /* Configs */,
|
||||
);
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C80D4294CF70F00263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C80D1294CF70F00263BE5 /* Sources */,
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||
331C80D3294CF70F00263BE5 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
331C80DA294CF71000263BE5 /* PBXTargetDependency */,
|
||||
);
|
||||
name = RunnerTests;
|
||||
productName = RunnerTests;
|
||||
productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
33CC10EC2044A3C60003C045 /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
33CC10E92044A3C60003C045 /* Sources */,
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||
33CC10EB2044A3C60003C045 /* Resources */,
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
productReference = 33CC10ED2044A3C60003C045 /* etm_powersync_app.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
33CC10E52044A3C60003C045 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
331C80D4294CF70F00263BE5 = {
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 33CC10EC2044A3C60003C045;
|
||||
};
|
||||
33CC10EC2044A3C60003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
ProvisioningStyle = Automatic;
|
||||
SystemCapabilities = {
|
||||
com.apple.Sandbox = {
|
||||
enabled = 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
33CC111A2044C6BA0003C045 = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
ProvisioningStyle = Manual;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 33CC10E42044A3C60003C045;
|
||||
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
33CC10EC2044A3C60003C045 /* Runner */,
|
||||
331C80D4294CF70F00263BE5 /* RunnerTests */,
|
||||
33CC111A2044C6BA0003C045 /* Flutter Assemble */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
331C80D3294CF70F00263BE5 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
33CC10EB2044A3C60003C045 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
3399D490228B24CF009A79C7 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
|
||||
};
|
||||
33CC111E2044C6BF0003C045 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterInputs.xcfilelist,
|
||||
);
|
||||
inputPaths = (
|
||||
Flutter/ephemeral/tripwire,
|
||||
);
|
||||
outputFileListPaths = (
|
||||
Flutter/ephemeral/FlutterOutputs.xcfilelist,
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
331C80D1294CF70F00263BE5 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
33CC10E92044A3C60003C045 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
|
||||
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
|
||||
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
331C80DA294CF71000263BE5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 33CC10EC2044A3C60003C045 /* Runner */;
|
||||
targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
|
||||
targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
33CC10F52044A3C60003C045 /* Base */,
|
||||
);
|
||||
name = MainMenu.xib;
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.etmPowersyncApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/etm_powersync_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/etm_powersync_app";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
331C80DC294CF71000263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.etmPowersyncApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/etm_powersync_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/etm_powersync_app";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.etmPowersyncApp.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/etm_powersync_app.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/etm_powersync_app";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CE9231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
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_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
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_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEA231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
338D0CEB231458BD00FA5F75 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
33CC10F92044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
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_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
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_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FA2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
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_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
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_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC10FC2044A3C60003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC10FD2044A3C60003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
33CC111C2044C6BA0003C045 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
33CC111D2044C6BA0003C045 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
331C80DB294CF71000263BE5 /* Debug */,
|
||||
331C80DC294CF71000263BE5 /* Release */,
|
||||
331C80DD294CF71000263BE5 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10F92044A3C60003C045 /* Debug */,
|
||||
33CC10FA2044A3C60003C045 /* Release */,
|
||||
338D0CE9231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC10FC2044A3C60003C045 /* Debug */,
|
||||
33CC10FD2044A3C60003C045 /* Release */,
|
||||
338D0CEA231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
33CC111C2044C6BA0003C045 /* Debug */,
|
||||
33CC111D2044C6BA0003C045 /* Release */,
|
||||
338D0CEB231458BD00FA5F75 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "etm_powersync_app.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "etm_powersync_app.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C80D4294CF70F00263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "etm_powersync_app.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
|
||||
BuildableName = "etm_powersync_app.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
7
macos/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||