Initial version

main
Christoph Giess 2022-10-18 15:07:13 +02:00
parent 9bca2a758e
commit 5e0d78c325
61 changed files with 1939 additions and 6 deletions

47
.gitignore vendored 100644
View File

@ -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

View File

@ -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`.

View File

@ -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

13
android/.gitignore vendored 100644
View File

@ -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

View File

@ -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 '../..'
}

View File

@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.hsma.mars.flutter_demo_gps">
<!-- 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>

View File

@ -0,0 +1,40 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.hsma.mars.flutter_demo_gps">
<application
android:label="flutter_demo_gps"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
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>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

View File

@ -0,0 +1,6 @@
package de.hsma.mars.flutter_demo_gps;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
}

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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>

View 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>

View File

@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.hsma.mars.flutter_demo_gps">
<!-- 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>

View File

@ -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
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

View 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-7.4-all.zip

View File

@ -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"

3
l10n.yaml 100644
View File

@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

View File

@ -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<T extends BlocBase> extends StatefulWidget {
const BlocProvider({
Key? key,
required this.child,
required this.bloc,
}): super(key: key);
final T bloc;
final Widget child;
@override
BlocProviderState<T> createState() => BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
BlocProvider<T>? provider = context.findAncestorWidgetOfExactType<BlocProvider<T>>();
return provider!.bloc;
}
}
class BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
@override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return widget.child;
}
}

View File

@ -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<GpsPoint> _recordedLocations = [];
final StreamController<GpsPoint> _gpsOutController =
StreamController<GpsPoint>.broadcast();
Stream<GpsPoint> get gpsPoint => _gpsOutController.stream;
final StreamController<LocationData> _gpsInController =
StreamController<LocationData>.broadcast();
StreamSink get gpsIn => _gpsInController.sink;
final StreamController<GpsBlocState> _stateOutController =
StreamController<GpsBlocState>.broadcast();
Stream<GpsBlocState> get stateOut => _stateOutController.stream;
final StreamController<GpsBlocAction> _actionInController =
StreamController<GpsBlocAction>.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();
}
}

View File

@ -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<GpsBlocApp> {
Locale locale = AppLocalizations.supportedLocales.first;
StreamSubscription<LocationData>? 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<GpsBloc>(
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),
));
}
}

View File

@ -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<GpsModel>(
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),
)
],
),
),
],
)),
),
);
}
}

View File

@ -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"
}

View File

@ -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, ..."
}
}

View File

@ -0,0 +1,14 @@
{
"@@locale": "he",
"appName": "הג\"י פי אס",
"noData": "אין נתונים",
"date": "תאריכים",
"lat": "רוחב",
"lon": "אורך",
"heading": "כיוון",
"accuracy": "מדויקים",
"speed": "מהירות",
"satNo": "לווינים",
"provider": "הספק"
}

View File

@ -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>';
}

View File

@ -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<GpsPoint> _recordedLocations = [];
bool get isRecording => _recording;
GpsPoint get currentLocation => _currentLocation;
List<GpsPoint> get locations => List.unmodifiable(_recordedLocations);
GpsModel(Stream<LocationData> 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 = [];
}
}

View File

@ -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 ".";
}
}
}

View File

@ -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<GpsPoint> 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<GpsPoint> 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}";
}

View File

@ -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<LocationData> get stream => s.getStream();
}
mixin _Stream {
Stream<LocationData> getStream();
}
class _RealGpsStream implements _Stream {
// Really, really ugly
// - Location package should have been encapsulated
// - Permissions do not belong here
Stream<LocationData> 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<LocationData> getStream() async* {
Map<String, dynamic> 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);
}
}
}

View File

@ -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<double> 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;
}

View File

@ -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")),
],
),
));
}
}

View File

@ -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<GpsBloc>(context);
return Row(
children: [
StreamBuilder<GpsBlocState>(
stream: bloc.stateOut,
initialData: GpsBlocState.notRecording,
builder: (BuildContext context,
AsyncSnapshot<GpsBlocState> 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),
)
],
);
}
}

View File

@ -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<GpsBloc>(context);
return StreamBuilder(
stream: bloc.gpsPoint,
builder: (BuildContext context, AsyncSnapshot<GpsPoint> 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: <Widget>[
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,
),
],
);
}
});
}
}

View File

@ -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<GpsModel>(
builder: (context, model, child) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
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}',
),
],
),
);
}
}

View File

@ -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<GpsBloc>(context);
return StreamBuilder(
stream: bloc.stateOut,
initialData: GpsBlocState.notRecording,
builder: (BuildContext context, AsyncSnapshot<GpsBlocState> snapshot) {
String title = AppLocalizations.of(context)!.appName;
final state =
(snapshot.data == GpsBlocState.recording) ? "- recording" : "";
return Text("$title $state");
});
}
}

View File

@ -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)}";
}

1
linux/.gitignore vendored 100644
View File

@ -0,0 +1 @@
flutter/ephemeral

View File

@ -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 "$<$<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)
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()

View 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}
)

View File

@ -0,0 +1,11 @@
//
// Generated file. Do not edit.
//
// clang-format off
#include "generated_plugin_registrant.h"
void fl_register_plugins(FlPluginRegistry* registry) {
}

View 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_

View 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)

6
linux/main.cc 100644
View 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);
}

View File

@ -0,0 +1,104 @@
#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)
// 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));
}

View File

@ -0,0 +1,18 @@
#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_

35
pubspec.yaml 100644
View File

@ -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

View File

@ -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);
});
});
});
}

View File

@ -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);
});
}

BIN
web/favicon.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

58
web/index.html 100644
View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="flutter_demo_gps">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>flutter_demo_gps</title>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
}
}).then(function(engineInitializer) {
return engineInitializer.initializeEngine();
}).then(function(appRunner) {
return appRunner.runApp();
});
});
</script>
</body>
</html>

35
web/manifest.json 100644
View File

@ -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"
}
]
}