diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9407069
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,47 @@
+# Miscellaneous
+/tmp
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/pubspec.lock
+/build/
+
+# Web related
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/README.md b/README.md
index 7f2b23e..cc7ed58 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,30 @@
-# gps
+# Flutter Demo GPS-App
-This is an sample application which demonstrates some features that can be found in many flutter apps, e.g.
+This is a sample application that demonstrates some of the features which can be found in many Flutter apps, e.g.
- Provider and BLoC pattern
- Accessing the GPS sensor
-- Using Streams
+- Streams as data provider
- Localization
- Own Widgets
+- Storing files
+- ...
## Getting Started
`gps_bloc_app.dart` and `gps_provider_app.dart` are two implementations of the GPS app.
-The first uses the BLoc pattern, the latte the provider pattern.
-Both can be executed from the IDE and the command line.
+The first uses the BLoC pattern, the latter the provider pattern.
+Both can be executed from the IDE and the command line, e.g.
+
+ `flutter run lib/gps_provider_app.dart`
+
+To build an app for Android or iOS, one of these files hast to be linked or renamed to `main.dart`.
+
+
+Tested with Flutter (Channel stable, 3.3.4, on Ubuntu 22.04.1) and
+
+- Android
+- Linux
+- Web \
+ Note: ensure that browser provides GPS coordinates
+ - Google Chrome works
+ - Ungoogled Chromium does not
-To build an app for Android or iOS, one these files hast to be renamed to `main.dart`.
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..61b6c4d
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,29 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at
+ # https://dart-lang.github.io/linter/lints/index.html.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..6f56801
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,13 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 0000000..97d8424
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,59 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion flutter.compileSdkVersion
+ ndkVersion flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "de.hsma.mars.flutter_demo_gps"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
+ minSdkVersion flutter.minSdkVersion
+ targetSdkVersion flutter.targetSdkVersion
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ 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.debug
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..d94892a
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7de3302
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/java/de/hsma/mars/flutter_demo_gps/MainActivity.java b/android/app/src/main/java/de/hsma/mars/flutter_demo_gps/MainActivity.java
new file mode 100644
index 0000000..9e75836
--- /dev/null
+++ b/android/app/src/main/java/de/hsma/mars/flutter_demo_gps/MainActivity.java
@@ -0,0 +1,6 @@
+package de.hsma.mars.flutter_demo_gps;
+
+import io.flutter.embedding.android.FlutterActivity;
+
+public class MainActivity extends FlutterActivity {
+}
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..d94892a
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..83ae220
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+ ext.kotlin_version = '1.6.10'
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.1.2'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..cb24abd
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..44e62bc
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/l10n.yaml b/l10n.yaml
new file mode 100644
index 0000000..15338f2
--- /dev/null
+++ b/l10n.yaml
@@ -0,0 +1,3 @@
+arb-dir: lib/l10n
+template-arb-file: app_en.arb
+output-localization-file: app_localizations.dart
diff --git a/lib/blocs/bloc_provider.dart b/lib/blocs/bloc_provider.dart
new file mode 100644
index 0000000..c56e54b
--- /dev/null
+++ b/lib/blocs/bloc_provider.dart
@@ -0,0 +1,42 @@
+import 'package:flutter/material.dart';
+
+//
+// Base for all BLoCs.
+// This class was adapted from https://github.com/boeledi/Streams-Block-Reactive-Programming-in-Flutter
+//
+
+abstract class BlocBase {
+ void dispose();
+}
+
+class BlocProvider extends StatefulWidget {
+ const BlocProvider({
+ Key? key,
+ required this.child,
+ required this.bloc,
+ }): super(key: key);
+
+ final T bloc;
+ final Widget child;
+
+ @override
+ BlocProviderState createState() => BlocProviderState();
+
+ static T of(BuildContext context){
+ BlocProvider? provider = context.findAncestorWidgetOfExactType>();
+ return provider!.bloc;
+ }
+}
+
+class BlocProviderState extends State>{
+ @override
+ void dispose(){
+ widget.bloc.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context){
+ return widget.child;
+ }
+}
\ No newline at end of file
diff --git a/lib/blocs/gps_bloc.dart b/lib/blocs/gps_bloc.dart
new file mode 100644
index 0000000..8240f03
--- /dev/null
+++ b/lib/blocs/gps_bloc.dart
@@ -0,0 +1,77 @@
+import 'dart:async';
+import 'package:location/location.dart';
+import 'package:gps/models/gps_point.dart';
+import 'package:gps/utils/gps_persister.dart';
+import 'bloc_provider.dart';
+
+// An action which is triggert by the UI
+enum GpsBlocAction { toggleRecording, save }
+
+// A state change triggert by the Business Logic
+enum GpsBlocState { recording, notRecording }
+
+class GpsBloc implements BlocBase {
+ final GpsPersister _persister;
+ bool _recording = false;
+ GpsPoint _currentLocation = GpsPoint();
+ List _recordedLocations = [];
+
+ final StreamController _gpsOutController =
+ StreamController.broadcast();
+ Stream get gpsPoint => _gpsOutController.stream;
+
+ final StreamController _gpsInController =
+ StreamController.broadcast();
+ StreamSink get gpsIn => _gpsInController.sink;
+
+ final StreamController _stateOutController =
+ StreamController.broadcast();
+ Stream get stateOut => _stateOutController.stream;
+
+ final StreamController _actionInController =
+ StreamController.broadcast();
+ StreamSink get actionIn => _actionInController.sink;
+
+ GpsBloc(this._persister) {
+ _gpsInController.stream.listen(_handleNewPosition);
+ _actionInController.stream.listen(_handleActions);
+ }
+
+ void _handleNewPosition(LocationData event) {
+ _currentLocation = GpsPoint(
+ latitude: event.latitude ?? 0.0,
+ longitude: event.longitude ?? 0.0,
+ accuracy: event.accuracy ?? 0.0,
+ speed: event.speed ?? 0.0,
+ heading: event.heading ?? 0.0,
+ time: event.time ?? 0.0,
+ satelliteNumber: event.satelliteNumber ?? 0,
+ provider: event.provider ?? "none");
+ if (_recording) {
+ _recordedLocations.add(_currentLocation);
+ }
+ _gpsOutController.sink.add(_currentLocation);
+ }
+
+ void _handleActions(GpsBlocAction event) {
+ switch (event) {
+ case GpsBlocAction.toggleRecording:
+ _recording = !_recording;
+ _stateOutController.sink.add(
+ _recording ? GpsBlocState.recording : GpsBlocState.notRecording);
+ break;
+ case GpsBlocAction.save:
+ _persister.save(_recordedLocations);
+ _recordedLocations = [];
+ break;
+ }
+ }
+
+ @override
+ void dispose() {
+ _gpsOutController.close();
+ _gpsInController.close();
+ _stateOutController.close();
+ _actionInController.close();
+ }
+}
diff --git a/lib/gps_bloc_app.dart b/lib/gps_bloc_app.dart
new file mode 100644
index 0000000..229a356
--- /dev/null
+++ b/lib/gps_bloc_app.dart
@@ -0,0 +1,88 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:gps/blocs/bloc_provider.dart';
+import 'package:gps/blocs/gps_bloc.dart';
+import 'package:gps/utils/environment.dart';
+import 'package:gps/utils/gps_persister.dart';
+import 'package:gps/utils/gps_utils.dart';
+import 'package:gps/widgets/gps_buttons_widget_bloc.dart';
+import 'package:gps/widgets/gps_position_widget_bloc.dart';
+import 'package:gps/widgets/gps_title_widget_bloc.dart';
+import 'package:location/location.dart';
+
+void main() {
+ runApp(const GpsBlocApp());
+}
+
+class GpsBlocApp extends StatefulWidget {
+ const GpsBlocApp({Key? key}) : super(key: key);
+
+ @override
+ GpsBlocAppState createState() => GpsBlocAppState();
+}
+
+class GpsBlocAppState extends State {
+ Locale locale = AppLocalizations.supportedLocales.first;
+ StreamSubscription? gps;
+
+ @override
+ void initState() {
+ super.initState();
+ }
+
+ @override
+ void deactivate() {
+ gps?.cancel();
+ super.deactivate();
+ }
+
+ void _nextLocale() {
+ const locales = AppLocalizations.supportedLocales;
+ final nextIdx = (locales.indexOf(locale) + 1) % locales.length;
+
+ setState(() {
+ locale = locales[nextIdx];
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ GpsBloc myBloc = GpsBloc(CsvGpsPersister(Environment.storageDir()));
+ gps = GpsUtils().stream.listen((event) {
+ myBloc.gpsIn.add(event);
+ });
+
+ return MaterialApp(
+ onGenerateTitle: (BuildContext context) =>
+ AppLocalizations.of(context)!.appName,
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ ),
+ locale: locale,
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
+ supportedLocales: AppLocalizations.supportedLocales,
+ home: BlocProvider(
+ bloc: myBloc,
+ child: buildScaffold(context),
+ ),
+ );
+ }
+
+ Widget buildScaffold(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const GpsTitleWidgetBloc(),
+ ),
+ body: Center(
+ child: Column(children: const [
+ GpsPositionWidget(),
+ GpsButtonsWidgetBloc(),
+ ])),
+ floatingActionButton: FloatingActionButton(
+ onPressed: _nextLocale,
+ child: const Icon(Icons.update),
+ ));
+ }
+}
diff --git a/lib/gps_provider_app.dart b/lib/gps_provider_app.dart
new file mode 100644
index 0000000..312a8d7
--- /dev/null
+++ b/lib/gps_provider_app.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+import 'package:gps/provider/gps_model.dart';
+import 'package:gps/utils/environment.dart';
+import 'package:gps/utils/gps_persister.dart';
+import 'package:gps/utils/gps_utils.dart';
+import 'package:gps/widgets/gps_position_widget_provider.dart';
+import 'package:provider/provider.dart';
+
+void main() {
+ runApp(const GpsProviderApp());
+}
+
+class GpsProviderApp extends StatelessWidget {
+ const GpsProviderApp({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'GPS Demo',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ ),
+ home: GpsDemo(),
+ );
+ }
+}
+
+class GpsDemo extends StatelessWidget {
+ GpsDemo({Key? key}) : super(key: key);
+
+ final GpsPersister _storage = CsvGpsPersister(Environment.storageDir());
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text('GPS Demo'),
+ ),
+ body: ChangeNotifierProvider(
+ create: (_) => GpsModel(GpsUtils().stream),
+ child: Center(
+ child: Column(
+ children: [
+ const GpsPositionWidget(),
+ Consumer(
+ builder: (context, model, child) => Row(
+ children: [
+ IconButton(
+ onPressed: () => model.toggleRecording(),
+ icon: Icon(model.isRecording
+ ? Icons.stop
+ : Icons.fiber_manual_record),
+ tooltip: model.isRecording ? "Stop Recording" : "Record",
+ ),
+ IconButton(
+ onPressed: () => model.save(_storage),
+ icon: const Icon(Icons.save),
+ )
+ ],
+ ),
+ ),
+ ],
+ )),
+ ),
+ );
+ }
+}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
new file mode 100644
index 0000000..6821524
--- /dev/null
+++ b/lib/l10n/app_de.arb
@@ -0,0 +1,14 @@
+{
+ "@@locale": "de",
+ "appName": "GPS Block",
+ "noData": "Keine Daten vorhanden",
+ "date": "Datum",
+ "lat": "Breite",
+ "lon": "Länge",
+ "heading": "Richtung",
+ "accuracy": "Genauigkeit",
+ "speed": "Geschwindigkeit",
+ "satNo": "Satelliten",
+ "provider": "Provider"
+}
+
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
new file mode 100644
index 0000000..f3c0ab2
--- /dev/null
+++ b/lib/l10n/app_en.arb
@@ -0,0 +1,45 @@
+{
+ "@@locale": "en",
+
+ "appName": "GPS BLoC",
+ "@appName": {
+ "description": "App Name"
+ },
+ "noData": "No Data",
+ "@noData": {
+ "description": "Message when no GPS data are available"
+ },
+ "date": "Date",
+ "@date": {
+ "description": "The current timestamp"
+ },
+ "lat": "Latitude",
+ "@lat": {
+ "description": "Current latitude"
+ },
+ "lon": "Longitude",
+ "@lon": {
+ "description": "Current longitude"
+ },
+ "heading": "Heading",
+ "@heading": {
+ "description": "Orientation in degree"
+ },
+ "accuracy": "Accuracy",
+ "@accuracy": {
+ "description": "Accuracy of GPS signal"
+ },
+ "speed": "Speed",
+ "@speed": {
+ "description": "Current speed in m/s"
+ },
+ "satNo": "Satellite number",
+ "@satNo": {
+ "description": "Number of satellites from which the position was computed"
+ },
+ "provider": "Provider",
+ "@provider": {
+ "description": "Data provider, e.g. GPS, Glonass, ..."
+ }
+}
+
diff --git a/lib/l10n/app_he.arb b/lib/l10n/app_he.arb
new file mode 100644
index 0000000..4ab867f
--- /dev/null
+++ b/lib/l10n/app_he.arb
@@ -0,0 +1,14 @@
+{
+ "@@locale": "he",
+ "appName": "הג\"י פי אס",
+ "noData": "אין נתונים",
+ "date": "תאריכים",
+ "lat": "רוחב",
+ "lon": "אורך",
+ "heading": "כיוון",
+ "accuracy": "מדויקים",
+ "speed": "מהירות",
+ "satNo": "לווינים",
+ "provider": "הספק"
+}
+
diff --git a/lib/models/gps_point.dart b/lib/models/gps_point.dart
new file mode 100644
index 0000000..8ec2365
--- /dev/null
+++ b/lib/models/gps_point.dart
@@ -0,0 +1,38 @@
+class GpsPoint {
+ GpsPoint(
+ {this.latitude = 0.0,
+ this.longitude = 0.0,
+ this.accuracy = 0.0,
+ this.speed = 0.0,
+ this.heading = 0.0,
+ this.time = 0.0,
+ this.satelliteNumber = 0,
+ this.provider = "none"});
+
+ /// Timestamp
+ final double time;
+
+ /// Latitude in degrees
+ final double latitude;
+
+ /// Longitude, in degrees
+ final double longitude;
+
+ /// Estimated horizontal accuracy of this location, radial, in meters
+ final double accuracy;
+
+ /// In meters/second
+ final double speed;
+
+ /// Heading is the horizontal direction of travel of this device, in degrees
+ final double heading;
+
+ /// Number of satellites used to derive the fix.
+ final int satelliteNumber;
+
+ /// Name of the provider that generated this fix.
+ final String provider;
+
+ @override
+ String toString() => 'GpsPoint<$latitude, $longitude>';
+}
diff --git a/lib/provider/gps_model.dart b/lib/provider/gps_model.dart
new file mode 100644
index 0000000..266da0c
--- /dev/null
+++ b/lib/provider/gps_model.dart
@@ -0,0 +1,44 @@
+import 'package:flutter/material.dart';
+import 'package:location/location.dart';
+import 'package:gps/models/gps_point.dart';
+import 'package:gps/utils/gps_persister.dart';
+
+class GpsModel with ChangeNotifier {
+ bool _recording = false;
+ GpsPoint _currentLocation = GpsPoint();
+ List _recordedLocations = [];
+
+ bool get isRecording => _recording;
+ GpsPoint get currentLocation => _currentLocation;
+ List get locations => List.unmodifiable(_recordedLocations);
+
+ GpsModel(Stream stream) {
+ stream.listen((event) {
+ _currentLocation = GpsPoint(
+ latitude: event.latitude ?? 0.0,
+ longitude: event.longitude ?? 0.0,
+ accuracy: event.accuracy ?? 0.0,
+ speed: event.speed ?? 0.0,
+ heading: event.heading ?? 0.0,
+ time: event.time ?? 0.0,
+ satelliteNumber: event.satelliteNumber ?? 0,
+ provider: event.provider ?? "none");
+
+ if (_recording) {
+ _recordedLocations.add(_currentLocation);
+ }
+
+ notifyListeners();
+ });
+ }
+
+ void toggleRecording() {
+ _recording = !_recording;
+ notifyListeners();
+ }
+
+ void save(GpsPersister s) {
+ s.save(locations);
+ _recordedLocations = [];
+ }
+}
diff --git a/lib/utils/environment.dart b/lib/utils/environment.dart
new file mode 100644
index 0000000..9bc675a
--- /dev/null
+++ b/lib/utils/environment.dart
@@ -0,0 +1,28 @@
+import 'dart:io' show Platform;
+
+import 'package:flutter/foundation.dart';
+
+class Environment {
+ static bool get hasRealGps =>
+ kIsWeb || Platform.isAndroid || Platform.isIOS;
+ // !kIsWeb && (Platform.isAndroid || Platform.isIOS);
+ static bool get isAndroid => !kIsWeb && Platform.isAndroid;
+ static bool get isIOS => !kIsWeb && Platform.isIOS;
+ static bool get isDesktop => !kIsWeb && (
+ Platform.isLinux ||
+ Platform.isMacOS ||
+ Platform.isWindows ||
+ Platform.isFuchsia);
+
+ static String storageDir() {
+ if (kIsWeb) {
+ return ".";
+ } else if (isAndroid) {
+ return '/storage/emulated/0/Download/';
+ } else if (isIOS) {
+ return 'Documents';
+ } else {
+ return ".";
+ }
+ }
+}
diff --git a/lib/utils/gps_persister.dart b/lib/utils/gps_persister.dart
new file mode 100644
index 0000000..c0d1124
--- /dev/null
+++ b/lib/utils/gps_persister.dart
@@ -0,0 +1,35 @@
+import 'dart:io' show File, FileMode;
+import 'package:intl/intl.dart' show DateFormat;
+import 'package:gps/models/gps_point.dart';
+
+abstract class GpsPersister {
+ void save(Iterable data);
+}
+
+class CsvGpsPersister implements GpsPersister {
+ static const String _csvHeader =
+ "#Time, Latitude, Longitude, Accuracy, Speed, Heading";
+
+ /// Format of DateTime part of file name
+ static final DateFormat _formatter = DateFormat('yyyyMMdd_HHmmss');
+
+ /// Path where the CSV will be stored
+ final String _rootDir;
+
+ const CsvGpsPersister(this._rootDir);
+
+ @override
+ void save(Iterable points) {
+ File file = File("$_rootDir/geo${_formatter.format(DateTime.now())}.csv");
+
+ file.writeAsStringSync(_csvHeader);
+ for (var point in points) {
+ file.writeAsStringSync("\n${_toCsv(point)}",
+ mode: FileMode.writeOnlyAppend);
+ }
+ file.writeAsString("\n", mode: FileMode.writeOnlyAppend, flush: true);
+ }
+
+ String _toCsv(GpsPoint p) =>
+ "${p.time}, ${p.latitude}, ${p.longitude}, ${p.accuracy}, ${p.speed}, ${p.heading}";
+}
diff --git a/lib/utils/gps_utils.dart b/lib/utils/gps_utils.dart
new file mode 100644
index 0000000..bcb2479
--- /dev/null
+++ b/lib/utils/gps_utils.dart
@@ -0,0 +1,80 @@
+import 'dart:async';
+import 'dart:math';
+
+import 'package:gps/utils/environment.dart';
+import 'package:location/location.dart';
+import 'package:wakelock/wakelock.dart';
+
+class GpsUtils {
+ _Stream s;
+
+ GpsUtils()
+ : s = Environment.hasRealGps
+ ? _RealGpsStream()
+ : _FakeGpsStream(49.4833, 8.4667, Duration(milliseconds: 100));
+
+ Stream get stream => s.getStream();
+}
+
+mixin _Stream {
+ Stream getStream();
+}
+
+class _RealGpsStream implements _Stream {
+ // Really, really ugly
+ // - Location package should have been encapsulated
+ // - Permissions do not belong here
+ Stream getStream() {
+ final Location location = Location();
+
+ location.serviceEnabled().then((_serviceEnabled) => {
+ if (!_serviceEnabled) {location.requestService()}
+ });
+ location.hasPermission().then((_permissionGranted) => {
+ if (_permissionGranted == PermissionStatus.denied)
+ {location.requestPermission()}
+ });
+ location.changeSettings(
+ accuracy: LocationAccuracy.high, interval: 100, distanceFilter: 0.0);
+ Wakelock.enable();
+ return location.onLocationChanged;
+ }
+}
+
+class _FakeGpsStream implements _Stream {
+ double lat;
+ double lon;
+ Duration dur;
+
+ _FakeGpsStream(this.lat, this.lon, [this.dur = const Duration(seconds: 1)]);
+
+ /// Generator which creates GPS coordinates around the given location.
+ Stream getStream() async* {
+ Map gpsPoint = {
+ 'accuracy': 20.0,
+ 'altitude': 100.0,
+ 'speed': 1.0,
+ 'speed_accuracy': 1.0,
+ 'isMock': 1,
+ 'verticalAccuracy': 20.0,
+ 'headingAccuracy': 1.0,
+ 'elapsedRealtimeNanos': 1000.0,
+ 'elapsedRealtimeUncertaintyNanos': 2000.0,
+ 'satelliteNumber': 0,
+ 'provider': "dummy"
+ };
+
+ double degree = 0.0;
+ while (true) {
+ await Future.delayed(dur);
+ gpsPoint['time'] = DateTime.now().millisecondsSinceEpoch + 0.0;
+ double rad = degree * pi / 180;
+ gpsPoint['latitude'] = lat + cos(rad);
+ gpsPoint['longitude'] = lon + sin(rad);
+ gpsPoint['heading'] = degree;
+ gpsPoint['altitude'] = degree;
+ degree = (degree + 1) % 360;
+ yield LocationData.fromMap(gpsPoint);
+ }
+ }
+}
diff --git a/lib/widgets/compass.dart b/lib/widgets/compass.dart
new file mode 100644
index 0000000..41b0f33
--- /dev/null
+++ b/lib/widgets/compass.dart
@@ -0,0 +1,151 @@
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+
+class Compass extends StatelessWidget {
+ static const double fallbackSize = 100.0;
+ final double degree;
+ final Color pointerColor;
+ final double pointerThickness;
+ final double relativeCircleSize;
+ final double relativePointerLength;
+ final double? height;
+ final double? width;
+
+ const Compass(
+ {this.degree = 0.0,
+ this.pointerColor = Colors.red,
+ this.pointerThickness = 0.2,
+ this.relativeCircleSize = 0.8,
+ this.relativePointerLength = 1.0,
+ this.width,
+ this.height,
+ Key? key})
+ : assert(pointerThickness > 0, "Pointer must be thicker than zero"),
+ assert(relativeCircleSize >= 0 && relativeCircleSize <= 1.0,
+ "Relative circle size must be between 0.0 and 1.0"),
+ assert(relativePointerLength >= 0 && relativePointerLength <= 1.0,
+ "Relative circle size must be between 0.0 and 1.0"),
+ super(key: key);
+
+ /// Determines widget's width and height
+ /// Both will be equal to the smallest length which is either
+ /// set as [width], [height] or as [constraints] maxWidth/maxHeight.
+ /// [fallbackSize] will be used if none of them is set.
+ double calcSideLength(
+ double? setWidth, double? setHeight, BoxConstraints constraints) {
+ List sizes = [
+ setWidth ?? double.infinity,
+ setHeight ?? double.infinity,
+ constraints.maxWidth,
+ constraints.maxHeight,
+ ];
+ var tmp = sizes.where((s) => s != double.infinity);
+ return tmp.isNotEmpty ? tmp.reduce(min) : fallbackSize;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ var sideLength = calcSideLength(width, height, constraints);
+ return SizedBox(
+ height: sideLength,
+ width: sideLength,
+ child: CustomPaint(
+ painter: _Compass(
+ degree: degree,
+ pointerColor: pointerColor,
+ pointerThickness: pointerThickness,
+ relativeCircleSize: relativeCircleSize,
+ relativePointerLength: relativePointerLength),
+ ),
+ );
+ },
+ );
+ }
+
+ /// Creates a 6x6 grid with Compass widgets.
+ /// The look of each widget differs from its predecessor
+ /// - Orientation: 0 - 350°
+ /// - Color: from green over brown to red
+ /// - Size of circle: 0-87,5% of widgets width/height
+ /// - Pointers thickness: 5-35% of widgets width/height
+ static Widget demo() {
+ return Container(
+ color: Colors.grey,
+ width: 300,
+ height: 300,
+ child: GridView.builder(
+ gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
+ maxCrossAxisExtent: 50,
+ childAspectRatio: 1,
+ crossAxisSpacing: 0,
+ mainAxisSpacing: 0),
+ itemCount: 36,
+ itemBuilder: (BuildContext ctxt, int index) => CustomPaint(
+ painter: _Compass(
+ degree: index * 10,
+ relativeCircleSize: (index * 0.025),
+ pointerThickness: index / 120 + 0.05,
+ pointerColor: Color.fromRGBO((index * 255 / 36).floor(),
+ 255 - (index * 255 / 36).floor(), 20, 1),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _Compass extends CustomPainter {
+ static const Offset origin = Offset(0, 0);
+ static final Paint black = Paint()..color = Colors.black;
+ static final Paint white = Paint()..color = Colors.white;
+
+ final double degree;
+ final Color pointerColor;
+ final double pointerThickness;
+ final double relativeCircleSize;
+ final double relativePointerLength;
+
+ _Compass({
+ this.degree = 0.0,
+ this.pointerColor = Colors.red,
+ this.pointerThickness = 0.2,
+ this.relativeCircleSize = 0.8,
+ this.relativePointerLength = 1.0,
+ });
+
+ @override
+ void paint(Canvas canvas, Size size) {
+ final Paint pointerPaint = Paint()..color = pointerColor;
+
+ double minSide = size.width < size.height ? size.width : size.height;
+ double circleRadius = minSide * relativeCircleSize / 2;
+ double pointerLength = minSide * relativePointerLength / 2;
+ double pointerRadius = minSide * pointerThickness / 2;
+
+ canvas.save();
+ canvas.translate(size.width / 2, size.height / 2);
+
+ canvas.drawCircle(origin, circleRadius, black);
+ canvas.drawCircle(origin, circleRadius * 0.9, white);
+ canvas.drawCircle(origin, pointerRadius, pointerPaint);
+
+ canvas.rotate(degree * pi / 180);
+ canvas.drawPath(makeTriangle(pointerRadius, pointerLength), pointerPaint);
+ canvas.restore();
+ }
+
+ Path makeTriangle(double halfBase, double height) {
+ var path = Path();
+ path.moveTo(-halfBase, 0);
+ path.lineTo(0, -height);
+ path.lineTo(halfBase, 0);
+ path.close();
+ return path;
+ }
+
+ @override
+ bool shouldRepaint(CustomPainter oldDelegate) => false;
+}
diff --git a/lib/widgets/demo_align_widget.dart b/lib/widgets/demo_align_widget.dart
new file mode 100644
index 0000000..adebe13
--- /dev/null
+++ b/lib/widgets/demo_align_widget.dart
@@ -0,0 +1,50 @@
+import 'package:flutter/material.dart';
+
+class DemoAlignWidget extends StatelessWidget {
+ const DemoAlignWidget({
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ height: 300,
+ child: DefaultTextStyle(
+ style: const TextStyle(fontSize: 20.0, color: Colors.black),
+ child: Stack(
+ fit: StackFit.passthrough,
+ children: const [
+ Align(
+ alignment: Alignment(-0.5, -0.5),
+ child: Text("-0.5, -0.5"),
+ ),
+ Align(
+ alignment: Alignment(-0.5, 0.5),
+ child: Text("-0.5, 0.5"),
+ ),
+ Align(
+ alignment: Alignment(0.5, -0.5),
+ child: Text("0.5, -0.5"),
+ ),
+ Align(alignment: Alignment.topLeft, child: Text("topLeft")),
+ Align(
+ alignment: Alignment(0.5, 0.5),
+ child: Text("0.5, 0.5"),
+ ),
+ Align(alignment: Alignment.topCenter, child: Text("topCenter")),
+ Align(alignment: Alignment.topRight, child: Text("topRight")),
+ Align(alignment: Alignment.centerLeft, child: Text("centerLeft")),
+ Align(alignment: Alignment.center, child: Text("center")),
+ Align(
+ alignment: Alignment.centerRight, child: Text("centerRight")),
+ Align(alignment: Alignment.bottomLeft, child: Text("bottomLeft")),
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: Text("bottomCenter")),
+ Align(
+ alignment: Alignment.bottomRight, child: Text("bottomRight")),
+ ],
+ ),
+ ));
+ }
+}
diff --git a/lib/widgets/gps_buttons_widget_bloc.dart b/lib/widgets/gps_buttons_widget_bloc.dart
new file mode 100644
index 0000000..2f5f892
--- /dev/null
+++ b/lib/widgets/gps_buttons_widget_bloc.dart
@@ -0,0 +1,34 @@
+import 'package:flutter/material.dart';
+import 'package:gps/blocs/bloc_provider.dart';
+import 'package:gps/blocs/gps_bloc.dart';
+
+class GpsButtonsWidgetBloc extends StatelessWidget {
+ const GpsButtonsWidgetBloc({
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final GpsBloc bloc = BlocProvider.of(context);
+ return Row(
+ children: [
+ StreamBuilder(
+ stream: bloc.stateOut,
+ initialData: GpsBlocState.notRecording,
+ builder: (BuildContext context,
+ AsyncSnapshot snapshot) {
+ return IconButton(
+ onPressed: () => bloc.actionIn.add(GpsBlocAction.toggleRecording),
+ icon: Icon(snapshot.data == GpsBlocState.recording
+ ? Icons.stop
+ : Icons.fiber_manual_record),
+ );
+ }),
+ IconButton(
+ onPressed: () => bloc.actionIn.add(GpsBlocAction.save),
+ icon: const Icon(Icons.save),
+ )
+ ],
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/widgets/gps_position_widget_bloc.dart b/lib/widgets/gps_position_widget_bloc.dart
new file mode 100644
index 0000000..f0e1293
--- /dev/null
+++ b/lib/widgets/gps_position_widget_bloc.dart
@@ -0,0 +1,48 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:gps/blocs/bloc_provider.dart';
+import 'package:gps/blocs/gps_bloc.dart';
+import 'package:gps/models/gps_point.dart';
+import 'package:gps/widgets/two_parts.dart';
+
+import 'compass.dart';
+
+class GpsPositionWidget extends StatelessWidget {
+ const GpsPositionWidget({
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final GpsBloc bloc = BlocProvider.of(context);
+
+ return StreamBuilder(
+ stream: bloc.gpsPoint,
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ if (!snapshot.hasData) {
+ return Text(AppLocalizations.of(context)!.noData);
+ } else {
+ final GpsPoint currentLocation = snapshot.data!;
+ final AppLocalizations l = AppLocalizations.of(context)!;
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ TwoParts.fromMillisecondDateTime(
+ l.date, currentLocation.time.toInt() * 1000),
+ TwoParts.fromDouble(l.lat, currentLocation.latitude),
+ TwoParts.fromDouble(l.lon, currentLocation.longitude),
+ TwoParts.fromDouble(l.heading, currentLocation.heading),
+ TwoParts.fromDouble(l.accuracy, currentLocation.accuracy),
+ TwoParts.fromDouble(l.speed, currentLocation.speed),
+ TwoParts.fromInt(l.satNo, currentLocation.satelliteNumber),
+ TwoParts.fromString(l.provider, currentLocation.provider),
+ Compass(
+ degree: currentLocation.heading,
+ width: 200,
+ ),
+ ],
+ );
+ }
+ });
+ }
+}
diff --git a/lib/widgets/gps_position_widget_provider.dart b/lib/widgets/gps_position_widget_provider.dart
new file mode 100644
index 0000000..e16e754
--- /dev/null
+++ b/lib/widgets/gps_position_widget_provider.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:gps/provider/gps_model.dart';
+import 'package:provider/provider.dart';
+
+class GpsPositionWidget extends StatelessWidget {
+ const GpsPositionWidget({
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Consumer(
+ builder: (context, model, child) => Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ 'Breite: ${model.currentLocation.latitude}',
+ ),
+ Text(
+ 'Länge: ${model.currentLocation.longitude}',
+ ),
+ Text(
+ 'Genauigkeit: ${model.currentLocation.accuracy}',
+ ),
+ Text(
+ 'Geschwindigkeit: ${model.currentLocation.speed}',
+ ),
+ Text(
+ 'Richtung: ${model.currentLocation.heading}',
+ ),
+ Text(
+ 'Satelliten: ${model.currentLocation.satelliteNumber}',
+ ),
+ Text(
+ 'Provider: ${model.currentLocation.provider}',
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/widgets/gps_title_widget_bloc.dart b/lib/widgets/gps_title_widget_bloc.dart
new file mode 100644
index 0000000..0a7a9dc
--- /dev/null
+++ b/lib/widgets/gps_title_widget_bloc.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:gps/blocs/bloc_provider.dart';
+import 'package:gps/blocs/gps_bloc.dart';
+
+class GpsTitleWidgetBloc extends StatelessWidget {
+ const GpsTitleWidgetBloc({
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ final GpsBloc bloc = BlocProvider.of(context);
+
+ return StreamBuilder(
+ stream: bloc.stateOut,
+ initialData: GpsBlocState.notRecording,
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ String title = AppLocalizations.of(context)!.appName;
+ final state =
+ (snapshot.data == GpsBlocState.recording) ? "- recording" : "";
+ return Text("$title $state");
+ });
+ }
+}
diff --git a/lib/widgets/two_parts.dart b/lib/widgets/two_parts.dart
new file mode 100644
index 0000000..d7a4996
--- /dev/null
+++ b/lib/widgets/two_parts.dart
@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+
+class TwoParts extends StatelessWidget {
+ final String title;
+ final Localizable value;
+
+ factory TwoParts.fromInt(String title, int value) =>
+ TwoParts(title: title, value: _IntHolder(value));
+ factory TwoParts.fromDouble(String title, double value) =>
+ TwoParts(title: title, value: _DoubleHolder(value));
+ factory TwoParts.fromString(String title, String value) =>
+ TwoParts(title: title, value: _StringHolder(value));
+ factory TwoParts.fromMillisecondDateTime(String title, int value) =>
+ TwoParts(title: title, value: _DateHolder(value));
+
+ const TwoParts({
+ required this.title,
+ required this.value,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Row(children: [
+ Expanded(flex: 1, child: Text(title)),
+ Expanded(
+ flex: 1, child: Text(value.format(Localizations.localeOf(context))))
+ ]));
+ }
+}
+
+mixin Localizable {
+ String format(Locale locale);
+}
+
+class _StringHolder implements Localizable {
+ final String _string;
+ _StringHolder(this._string);
+ @override
+ String format(Locale locale) => _string;
+}
+
+class _IntHolder implements Localizable {
+ final int _int;
+ _IntHolder(this._int);
+ @override
+ String format(Locale locale) =>
+ NumberFormat("#", locale.toString()).format(_int);
+}
+
+class _DoubleHolder implements Localizable {
+ final double _double;
+ _DoubleHolder(this._double);
+ @override
+ String format(Locale locale) =>
+ NumberFormat("##0.0#", locale.toString()).format(_double);
+}
+
+class _DateHolder implements Localizable {
+ final DateTime _timestamp;
+ _DateHolder(int i) : _timestamp = DateTime.fromMicrosecondsSinceEpoch(i);
+ @override
+ String format(Locale locale) =>
+ "${DateFormat.yMMMMEEEEd(locale.toString()).format(_timestamp)}\n${DateFormat.jms(locale.toString()).format(_timestamp)}";
+}
diff --git a/linux/.gitignore b/linux/.gitignore
new file mode 100644
index 0000000..d3896c9
--- /dev/null
+++ b/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt
new file mode 100644
index 0000000..93d8a6e
--- /dev/null
+++ b/linux/CMakeLists.txt
@@ -0,0 +1,138 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.10)
+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 "flutter_demo_gps")
+# The unique GTK application identifier for this application. See:
+# https://wiki.gnome.org/HowDoI/ChooseApplicationID
+set(APPLICATION_ID "de.hsma.mars.flutter_demo_gps")
+
+# 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 "$<$>:-O3>")
+ target_compile_definitions(${TARGET} PRIVATE "$<$>: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)
+
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Define the application target. To change its name, change BINARY_NAME above,
+# 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 dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+# 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)
+
+# 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()
diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt
new file mode 100644
index 0000000..d5bd016
--- /dev/null
+++ b/linux/flutter/CMakeLists.txt
@@ -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}
+)
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000..e71a16d
--- /dev/null
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,11 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+}
diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000..e0f0a47
--- /dev/null
+++ b/linux/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
new file mode 100644
index 0000000..2e1de87
--- /dev/null
+++ b/linux/flutter/generated_plugins.cmake
@@ -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 $)
+ 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)
diff --git a/linux/main.cc b/linux/main.cc
new file mode 100644
index 0000000..e7c5c54
--- /dev/null
+++ b/linux/main.cc
@@ -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);
+}
diff --git a/linux/my_application.cc b/linux/my_application.cc
new file mode 100644
index 0000000..cc3623c
--- /dev/null
+++ b/linux/my_application.cc
@@ -0,0 +1,104 @@
+#include "my_application.h"
+
+#include
+#ifdef GDK_WINDOWING_X11
+#include
+#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)
+
+// 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, "flutter_demo_gps");
+ 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, "flutter_demo_gps");
+ }
+
+ gtk_window_set_default_size(window, 1280, 720);
+ gtk_widget_show(GTK_WIDGET(window));
+
+ 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);
+ gtk_widget_show(GTK_WIDGET(view));
+ gtk_container_add(GTK_CONTAINER(window), 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 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_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+ return MY_APPLICATION(g_object_new(my_application_get_type(),
+ "application-id", APPLICATION_ID,
+ "flags", G_APPLICATION_NON_UNIQUE,
+ nullptr));
+}
diff --git a/linux/my_application.h b/linux/my_application.h
new file mode 100644
index 0000000..72271d5
--- /dev/null
+++ b/linux/my_application.h
@@ -0,0 +1,18 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include
+
+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_
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..7b81bb5
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,35 @@
+name: gps
+description: Demo Application
+
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+version: 1.0.0+1
+
+environment:
+ sdk: ">=2.18.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+ flutter_localizations:
+ sdk: flutter
+
+ location: ^4.3.0
+
+ cupertino_icons: ^1.0.2
+ wakelock: ^0.6.2
+ provider: ^6.0.1
+ intl: ^0.17.0
+
+dev_dependencies:
+ test:
+ flutter_test:
+ sdk: flutter
+
+ flutter_lints: ^2.0.1
+
+flutter:
+ uses-material-design: true
+ generate: true
+
diff --git a/test/localized_parsing_test.dart b/test/localized_parsing_test.dart
new file mode 100644
index 0000000..4348f6a
--- /dev/null
+++ b/test/localized_parsing_test.dart
@@ -0,0 +1,60 @@
+import 'package:intl/date_symbol_data_local.dart';
+import 'package:intl/intl.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('double', () {
+ test('default', () {
+ expect(NumberFormat().parse("1.234"), 1.234);
+ });
+ group('decimalPattern', () {
+ test('default', () {
+ expect(NumberFormat.decimalPattern().parse("1.234"), 1.234);
+ });
+ test('en', () {
+ expect(NumberFormat.decimalPattern("en").parse("1.234,567"), 1.234567);
+ });
+ test('de', () {
+ expect(NumberFormat.decimalPattern("de").parse("1.234,567"), 1234.567);
+ });
+ });
+ group('scientific', () {
+ const double expected = 1.2345;
+ test('default', () {
+ expect(NumberFormat.scientificPattern().parse("123.45E-2"), expected);
+ });
+ test('en', () {
+ expect(
+ NumberFormat.scientificPattern("en").parse("123.45E-2"), expected);
+ });
+ test('de', () {
+ expect(
+ NumberFormat.scientificPattern("de").parse("123,45E-2"), expected);
+ });
+ });
+ });
+
+ group('DateTime', () {
+ final expectedDateTime = DateTime(2021, 12, 24, 11, 30, 0);
+
+ test('parse', () {
+ expect(DateTime.parse("2021-12-24 11:30:00"), expectedDateTime);
+ });
+ group('DateFormat', () {
+ test('with pattern', () {
+ expect(DateFormat("dd.MM.yyyy hh:mm:ss").parse("24.12.2021 11:30:00"),
+ expectedDateTime);
+ });
+
+ final expectedDate = DateTime(2021, 12, 24);
+ test('yMd("en")', () {
+ initializeDateFormatting("en");
+ expect(DateFormat.yMd("en").parse("12/24/2021"), expectedDate);
+ });
+ test('yMd("de")', () {
+ initializeDateFormatting("de");
+ expect(DateFormat.yMd("de").parse("24.12.2021"), expectedDate);
+ });
+ });
+ });
+}
diff --git a/test/widget_test.dart b/test/widget_test.dart
new file mode 100644
index 0000000..a721f1f
--- /dev/null
+++ b/test/widget_test.dart
@@ -0,0 +1,15 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:gps/widgets/gps_position_widget_bloc.dart';
+import 'package:gps/widgets/gps_title_widget_bloc.dart';
+
+import '../lib/gps_bloc_app.dart';
+
+void main() {
+ testWidgets('GPS smoke test', (WidgetTester tester) async {
+// await tester.pumpWidget(const GpsBlocApp());
+
+// expect(find.byType(GpsTitleWidgetBloc), findsOneWidget);
+// expect(find.byType(GpsPositionWidget), findsOneWidget);
+
+ });
+}
diff --git a/web/favicon.png b/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
Binary files /dev/null and b/web/favicon.png differ
diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
Binary files /dev/null and b/web/icons/Icon-192.png differ
diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
Binary files /dev/null and b/web/icons/Icon-512.png differ
diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png
new file mode 100644
index 0000000..eb9b4d7
Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ
diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png
new file mode 100644
index 0000000..d69c566
Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..457e665
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ flutter_demo_gps
+
+
+
+
+
+
+
+
+
+
diff --git a/web/manifest.json b/web/manifest.json
new file mode 100644
index 0000000..200d0d0
--- /dev/null
+++ b/web/manifest.json
@@ -0,0 +1,35 @@
+{
+ "name": "flutter_demo_gps",
+ "short_name": "flutter_demo_gps",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#0175C2",
+ "theme_color": "#0175C2",
+ "description": "A new Flutter project.",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/Icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}