let AI comment everything because well... yeah...
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
// * Enum defining the types of databases used in the app
|
||||
// * - place: Database for camera trap locations
|
||||
// * - excursion: Database for tracking excursions and their data
|
||||
|
||||
enum DatabasesEnum {
|
||||
place,
|
||||
excursion,
|
||||
place, // Camera trap locations database
|
||||
excursion, // Excursions and tracking database
|
||||
}
|
||||
|
||||
@@ -3,25 +3,31 @@ import 'package:flutter/material.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'screens/addCam/add_cam_main.dart';
|
||||
|
||||
// * The homepage where you can choose to add something or view the database entries
|
||||
// * Home screen of the LUPUS app
|
||||
// * Serves as the main navigation hub with sections for:
|
||||
// * - Camera trap management
|
||||
// * - Excursion tracking
|
||||
// * - File operations
|
||||
// * - Settings access
|
||||
|
||||
/// Home screen widget providing access to all main app features
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
// Commented out legacy file sending functionality
|
||||
// void _sendFile() async {
|
||||
// // FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
|
||||
// // if (result != null) {
|
||||
// // File file = File(result.files.single.path!);
|
||||
// // String content = await file.readAsString();
|
||||
|
||||
// // HttpRequest.httpRequest(saveDataString: content);
|
||||
// }
|
||||
// // }
|
||||
// }
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// App bar with settings menu
|
||||
appBar: AppBar(
|
||||
title: const Text("LUPUS"),
|
||||
actions: [
|
||||
@@ -29,28 +35,35 @@ class HomePage extends StatelessWidget {
|
||||
onSelected: (value) {
|
||||
Navigator.pushNamed(context, value.toString());
|
||||
},
|
||||
itemBuilder:
|
||||
(context) => [
|
||||
PopupMenuItem(
|
||||
value: '/settings',
|
||||
child: Text(AppLocalizations.of(context)!.settings),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: '/introScreen',
|
||||
child: Text(AppLocalizations.of(context)!.showloginscreen),
|
||||
),
|
||||
],
|
||||
itemBuilder: (context) => [
|
||||
// Settings menu option
|
||||
PopupMenuItem(
|
||||
value: '/settings',
|
||||
child: Text(AppLocalizations.of(context)!.settings),
|
||||
),
|
||||
// Option to show intro screen
|
||||
PopupMenuItem(
|
||||
value: '/introScreen',
|
||||
child: Text(AppLocalizations.of(context)!.showloginscreen),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Main content area
|
||||
body: Column(
|
||||
children: [
|
||||
// App logo at the top
|
||||
Image.asset('assets/images/reconix_small.png'),
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 30),
|
||||
|
||||
// * Camera Trap Management Section
|
||||
// Button to add new camera location
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(250, 40),
|
||||
@@ -66,6 +79,7 @@ class HomePage extends StatelessWidget {
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.addplace),
|
||||
),
|
||||
// Button to view camera locations
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(250, 40),
|
||||
@@ -73,19 +87,23 @@ class HomePage extends StatelessWidget {
|
||||
onPressed: () => Navigator.pushNamed(context, '/viewCams'),
|
||||
child: Text(AppLocalizations.of(context)!.viewplaces),
|
||||
),
|
||||
|
||||
// Visual section divider
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// * Excursion Management Section
|
||||
// Button to start new excursion
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(250, 40),
|
||||
),
|
||||
onPressed: () => Navigator.pushNamed(context, '/excursion'),
|
||||
child: Text(AppLocalizations.of(context)!.excursion),
|
||||
), // Excursion
|
||||
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Button to view excursions
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(250, 40),
|
||||
@@ -93,10 +111,14 @@ class HomePage extends StatelessWidget {
|
||||
onPressed: () => Navigator.pushNamed(context, '/viewExcursionen'),
|
||||
child: Text(AppLocalizations.of(context)!.viewExcursionen),
|
||||
),
|
||||
|
||||
|
||||
// Visual section divider
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// * File Operations Section
|
||||
// Button to send data files
|
||||
ElevatedButton(
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(250, 40),
|
||||
|
||||
@@ -1,32 +1,65 @@
|
||||
// * Interface defining the contract for database operations
|
||||
// * Used by both PlaceDBHelper and ExcursionDBHelper
|
||||
// * Provides a common set of operations for managing entries and templates
|
||||
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
/// Interface for database operations
|
||||
/// Implements the Repository pattern for database access
|
||||
abstract interface class IDb {
|
||||
/// Get the database instance
|
||||
Future<Database> get dB;
|
||||
|
||||
/// Initialize the database and create necessary tables
|
||||
initDatabases();
|
||||
|
||||
/// Create database schema
|
||||
/// @param excursionDB Database instance
|
||||
/// @param version Schema version number
|
||||
onCreateDatabases(Database excursionDB, int version);
|
||||
|
||||
/// Add a new main entry
|
||||
/// @param excursion Map of entry data
|
||||
/// @return ID of the new entry
|
||||
Future<int> addMainEntry(Map<String, String> excursion);
|
||||
|
||||
/// Update an existing main entry
|
||||
/// @param excursion Map of updated entry data
|
||||
/// @return Number of rows affected
|
||||
Future<int> updateMainEntry(Map<String, String> excursion);
|
||||
|
||||
/// Mark an entry as sent to the server
|
||||
/// @param id ID of the entry to update
|
||||
Future<void> updateSent(int id);
|
||||
|
||||
/// Add a new template entry
|
||||
/// @param templates Map of template data
|
||||
/// @return ID of the new template
|
||||
Future<int> addTemplate(Map<String, String> templates);
|
||||
|
||||
/// Update an existing template
|
||||
/// @param template Map of updated template data
|
||||
Future<void> updateTemplate(Map<String, String> template);
|
||||
|
||||
/// Get all main entries from the database
|
||||
/// @return List of all entries
|
||||
Future<List<Map<String, dynamic>>> getAllMainEntries();
|
||||
|
||||
/// Get all templates from the database
|
||||
/// @return List of all templates
|
||||
Future<List<Map<String, dynamic>>> getAllTemplates();
|
||||
|
||||
/// Delete all main entries from the database
|
||||
Future<void> deleteAllMainEntries();
|
||||
|
||||
/// Delete all templates from the database
|
||||
Future<void> deleteAllTemplates();
|
||||
|
||||
/// Delete a specific template
|
||||
/// @param id ID of the template to delete
|
||||
Future<void> deleteTemplateById(String id);
|
||||
|
||||
/// Delete a specific main entry
|
||||
/// @param id ID of the entry to delete
|
||||
Future<void> deleteMainEntryById(String id);
|
||||
|
||||
}
|
||||
|
||||
@@ -13,22 +13,36 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'home.dart';
|
||||
import 'l10n/l10n.dart';
|
||||
|
||||
// * Main entry point of the LUPUS app
|
||||
// * Configures the app's:
|
||||
// * - Theme and appearance
|
||||
// * - Localization
|
||||
// * - Navigation routes
|
||||
// * - Initial settings
|
||||
|
||||
/// Initialize the app and configure default settings
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Set default values (Propably there is a better way to do this)
|
||||
// Initialize SharedPreferences for app settings
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// isFirstLaunch decides whether the intro screen is shown or not
|
||||
|
||||
// Check if this is the first app launch
|
||||
bool isFirstLaunch = prefs.getBool('isFirstLaunch') ?? true;
|
||||
|
||||
// Set default control periods if not already configured
|
||||
if (prefs.getString("kTage1")?.isEmpty ?? true) await prefs.setString('kTage1', "28");
|
||||
if (prefs.getString("kTage2")?.isEmpty ?? true) await prefs.setString('kTage2', "48");
|
||||
|
||||
// Commented out API addresses for testing purposes
|
||||
// if (prefs.getString("fotofallenApiAddress")?.isEmpty ?? true) await prefs.setString('fotofallenApiAddress', 'http://192.168.1.170/www.dbb-wolf.de/data/app24.php');
|
||||
// if (prefs.getString("exkursionenApiAddress")?.isEmpty ?? true) await prefs.setString('exkursionenApiAddress', 'http://192.168.1.170/www.dbb-wolf.de/data/api_exkursion.php');
|
||||
|
||||
runApp(MyApp(isFirstLaunch: isFirstLaunch));
|
||||
}
|
||||
|
||||
/// Main app widget that configures the app's theme and routing
|
||||
class MyApp extends StatelessWidget {
|
||||
|
||||
final bool isFirstLaunch;
|
||||
const MyApp({super.key, required this.isFirstLaunch});
|
||||
|
||||
@@ -36,13 +50,16 @@ class MyApp extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'LUPUS',
|
||||
// Configure light theme with gold color scheme
|
||||
theme: FlexThemeData.light(scheme: FlexScheme.gold, useMaterial3: true),
|
||||
darkTheme:
|
||||
FlexThemeData.dark(scheme: FlexScheme.greenM3, useMaterial3: true),
|
||||
themeMode: ThemeMode.system,
|
||||
// here the isFirstLaunch comes into play
|
||||
// Configure dark theme with green M3 color scheme
|
||||
darkTheme: FlexThemeData.dark(scheme: FlexScheme.greenM3, useMaterial3: true),
|
||||
themeMode: ThemeMode.system, // Use system theme preference
|
||||
|
||||
// Show intro screen on first launch, otherwise go to home
|
||||
initialRoute: isFirstLaunch ? '/introScreen' : '/home',
|
||||
// Localization settings
|
||||
|
||||
// Configure localization support
|
||||
supportedLocales: L10n.all,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
@@ -50,6 +67,8 @@ class MyApp extends StatelessWidget {
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
|
||||
// Define app navigation routes
|
||||
routes: {
|
||||
'/home': (context) => const HomePage(),
|
||||
'/addCamMain': (context) => const AddCamMain(),
|
||||
|
||||
@@ -4,13 +4,20 @@ import 'package:sqflite/sqflite.dart';
|
||||
import 'dart:io' as io;
|
||||
import 'package:path/path.dart';
|
||||
|
||||
// * Gives the complete functionality for the databases
|
||||
// ! functions may not be named complete correctly
|
||||
// * Database helper for managing excursions
|
||||
// * Provides CRUD operations for both main entries and templates
|
||||
// * Uses SQLite database with two tables:
|
||||
// * - excursion: Stores finalized excursion records
|
||||
// * - excursionTemplates: Stores template entries for quick creation
|
||||
|
||||
/// Database helper class implementing IDb interface
|
||||
/// Handles all database operations for excursions and tracking data
|
||||
class ExcursionDBHelper implements IDb {
|
||||
// Database instance
|
||||
static Database? _excursionDB;
|
||||
|
||||
// checks if the databses are existing and creates them with the initPlaceDatabase function if not
|
||||
/// Get database instance, creating it if necessary
|
||||
/// Returns existing database or initializes a new one
|
||||
@override
|
||||
Future<Database> get dB async {
|
||||
if (_excursionDB != null) {
|
||||
@@ -20,7 +27,8 @@ class ExcursionDBHelper implements IDb {
|
||||
return _excursionDB!;
|
||||
}
|
||||
|
||||
// Creates the databases with help from the _onCreateExcursion function
|
||||
/// Initialize the database
|
||||
/// Creates database file and tables if they don't exist
|
||||
@override
|
||||
initDatabases() async {
|
||||
io.Directory documentsDirectory = await getApplicationCacheDirectory();
|
||||
@@ -33,21 +41,28 @@ class ExcursionDBHelper implements IDb {
|
||||
return excursionDB;
|
||||
}
|
||||
|
||||
// The function that helps
|
||||
/// Create database tables
|
||||
/// Sets up schema for both main entries and templates
|
||||
@override
|
||||
onCreateDatabases(Database excursionDB, int version) async {
|
||||
// Create main excursions table with tracking data fields
|
||||
await excursionDB.execute(
|
||||
'CREATE TABLE excursion (ID INTEGER PRIMARY KEY AUTOINCREMENT, Rudel TEXT, Teilnehmer TEXT, Datum TEXT, Dauer TEXT, MHund INTEGER, MLeine TEXT, BLand TEXT, Lkr TEXT, BeiOrt TEXT, BimaName TEXT, Wetter TEXT, Temperat TEXT, RegenVor TEXT, KmRad TEXT, KmAuto TEXT, KmFuss TEXT, KmTotal TEXT, KmAuProz TEXT, KmFuProz TEXT, KmRaProz TEXT, SpGut TEXT, SpMittel, SpSchlecht TEXT, SpurFund TEXT, SpurLang TEXT, SpurTiere Text, SpSicher TEXT, WelpenSp TEXT, WelpenAnz TEXT, WpSicher TEXT, LosungGes TEXT, LosungAnz TEXT, LosungGen TEXT, UrinAnz TEXT, UrinGen TEXT, OestrAnz TEXT, OestrGen TEXT, HaarAnz TEXT, HaarGen TEXT, LosungKm TEXT, GenetiKm TEXT, Hinweise TEXT, Bemerk TEXT, IntKomm TEXT, BimaNr TEXT, BimaNutzer TEXT, BimaAGV TEXT, FallNum INTEGER, Weg TEXT, Sent INTEGER DEFAULT 0)',
|
||||
);
|
||||
|
||||
// Create templates table (similar structure but without Sent flag)
|
||||
await excursionDB.execute(
|
||||
'CREATE TABLE excursionTemplates (ID INTEGER PRIMARY KEY AUTOINCREMENT, Rudel TEXT, Teilnehmer TEXT, Datum TEXT, Dauer TEXT, MHund INTEGER, MLeine TEXT, BLand TEXT, Lkr TEXT, BeiOrt TEXT, BimaName TEXT, Wetter TEXT, Temperat TEXT, RegenVor TEXT, KmRad TEXT, KmAuto TEXT, KmFuss TEXT, KmTotal TEXT, KmAuProz TEXT, KmFuProz TEXT, KmRaProz TEXT, SpGut TEXT, SpMittel, SpSchlecht TEXT, SpurFund TEXT, SpurLang TEXT, SpurTiere Text, SpSicher TEXT, WelpenSp TEXT, WelpenAnz TEXT, WpSicher TEXT, LosungGes TEXT, LosungAnz TEXT, LosungGen TEXT, UrinAnz TEXT, UrinGen TEXT, OestrAnz TEXT, OestrGen TEXT, HaarAnz TEXT, HaarGen TEXT, LosungKm TEXT, GenetiKm TEXT, Hinweise TEXT, Bemerk TEXT, IntKomm TEXT, BimaNr TEXT, BimaNutzer TEXT, BimaAGV TEXT, FallNum INTEGER, Weg TEXT)',
|
||||
);
|
||||
}
|
||||
|
||||
// Function to add a finished entry and return its ID
|
||||
/// Add a new main entry to the database
|
||||
/// @param excursion Map containing the excursion data
|
||||
/// @return ID of the newly inserted entry
|
||||
@override
|
||||
Future<int> addMainEntry(Map<String, String> excursion) async {
|
||||
var excursionDBClient = await dB;
|
||||
// Commented out code for handling existing entries
|
||||
// final existingID = await excursionDBClient.query(
|
||||
// 'excursion',
|
||||
// where: 'ID = ?',
|
||||
@@ -56,7 +71,7 @@ class ExcursionDBHelper implements IDb {
|
||||
|
||||
// if (existingID.isNotEmpty) {
|
||||
// updateMainEntry(excursion);
|
||||
// return existingID.first['ID'] as int; // Return existing ID
|
||||
// return existingID.first['ID'] as int;
|
||||
// }
|
||||
|
||||
int id = await excursionDBClient.insert(
|
||||
@@ -65,13 +80,15 @@ class ExcursionDBHelper implements IDb {
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
|
||||
return id; // Return the ID of the newly inserted entry
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Update an existing main entry
|
||||
/// @param excursion Map containing the updated excursion data
|
||||
/// @return Number of rows affected
|
||||
@override
|
||||
Future<int> updateMainEntry(Map<String, String> excursion) async {
|
||||
var excursionDBClient = await dB;
|
||||
|
||||
return await excursionDBClient.update(
|
||||
'excursion',
|
||||
excursion,
|
||||
@@ -80,11 +97,11 @@ class ExcursionDBHelper implements IDb {
|
||||
);
|
||||
}
|
||||
|
||||
// function to update the sent value
|
||||
/// Mark an entry as sent to the server
|
||||
/// @param id ID of the entry to update
|
||||
@override
|
||||
Future<void> updateSent(int id) async {
|
||||
var excursionDBClient = await dB;
|
||||
|
||||
await excursionDBClient.update(
|
||||
'excursion',
|
||||
{'Sent': 1},
|
||||
@@ -93,11 +110,13 @@ class ExcursionDBHelper implements IDb {
|
||||
);
|
||||
}
|
||||
|
||||
// same thing as before but with templatews
|
||||
/// Add a new template entry
|
||||
/// @param templates Map containing the template data
|
||||
/// @return ID of the newly inserted template
|
||||
@override
|
||||
Future<int> addTemplate(Map<String, String> templates) async {
|
||||
var excursionDBClient = await dB;
|
||||
|
||||
// Commented out code for handling existing templates
|
||||
// final existingCID = await excursionDBClient.query(
|
||||
// 'excursionTemplates',
|
||||
// where: 'ID = ?',
|
||||
@@ -110,16 +129,15 @@ class ExcursionDBHelper implements IDb {
|
||||
int id = await excursionDBClient.insert(
|
||||
'excursionTemplates',
|
||||
templates,
|
||||
// conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Updates a existing template
|
||||
/// Update an existing template
|
||||
/// @param template Map containing the updated template data
|
||||
@override
|
||||
Future<void> updateTemplate(Map<String, String> template) async {
|
||||
var excursionDBClient = await dB;
|
||||
|
||||
await excursionDBClient.update(
|
||||
'excursionTemplates',
|
||||
template,
|
||||
@@ -128,7 +146,8 @@ class ExcursionDBHelper implements IDb {
|
||||
);
|
||||
}
|
||||
|
||||
// get the finished entries from db
|
||||
/// Retrieve all main entries from the database
|
||||
/// @return List of all excursions or empty list if none exist
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getAllMainEntries() async {
|
||||
var excursionDBClient = await dB;
|
||||
@@ -140,7 +159,8 @@ class ExcursionDBHelper implements IDb {
|
||||
}
|
||||
}
|
||||
|
||||
// get the finished templates from db
|
||||
/// Retrieve all templates from the database
|
||||
/// @return List of all templates or empty list if none exist
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getAllTemplates() async {
|
||||
var excursionDBClient = await dB;
|
||||
@@ -152,21 +172,24 @@ class ExcursionDBHelper implements IDb {
|
||||
}
|
||||
}
|
||||
|
||||
// deletes all finished entries from the db LOCALLY
|
||||
/// Delete all main entries from the local database
|
||||
/// Note: This only affects the local database, not the server
|
||||
@override
|
||||
Future<void> deleteAllMainEntries() async {
|
||||
var excursionDBClient = await dB;
|
||||
await excursionDBClient.delete('excursion');
|
||||
}
|
||||
|
||||
// deletes all templates from the db LOCALLY
|
||||
/// Delete all templates from the local database
|
||||
/// Note: This only affects the local database, not the server
|
||||
@override
|
||||
Future<void> deleteAllTemplates() async {
|
||||
var excursionDBClient = await dB;
|
||||
await excursionDBClient.delete('excursionTemplates');
|
||||
}
|
||||
|
||||
// delete specific template
|
||||
/// Delete a specific template by ID
|
||||
/// @param id ID of the template to delete
|
||||
@override
|
||||
Future<void> deleteTemplateById(String id) async {
|
||||
var excursionDBClient = await dB;
|
||||
@@ -177,7 +200,8 @@ class ExcursionDBHelper implements IDb {
|
||||
);
|
||||
}
|
||||
|
||||
// delete specific excursion
|
||||
/// Delete a specific main entry by ID
|
||||
/// @param id ID of the entry to delete
|
||||
@override
|
||||
Future<void> deleteMainEntryById(String id) async {
|
||||
var excursionDBClient = await dB;
|
||||
|
||||
@@ -4,13 +4,20 @@ import 'package:sqflite/sqflite.dart';
|
||||
import 'dart:io' as io;
|
||||
import 'package:path/path.dart';
|
||||
|
||||
// * Gives the complete functionality for the databases
|
||||
// ! functions may not be named complete correctly
|
||||
// * Database helper for managing camera trap locations (places)
|
||||
// * Provides CRUD operations for both main entries and templates
|
||||
// * Uses SQLite database with two tables:
|
||||
// * - place: Stores finalized camera trap locations
|
||||
// * - placeTemplates: Stores template entries for quick creation
|
||||
|
||||
class PlaceDBHelper implements IDb{
|
||||
/// Database helper class implementing IDb interface
|
||||
/// Handles all database operations for camera trap locations
|
||||
class PlaceDBHelper implements IDb {
|
||||
// Database instance
|
||||
static Database? _dB;
|
||||
|
||||
// checks if the databses are existing and creates them with the initPlaceDatabase function if not
|
||||
/// Get database instance, creating it if necessary
|
||||
/// Returns existing database or initializes a new one
|
||||
@override
|
||||
Future<Database> get dB async {
|
||||
if (_dB != null) {
|
||||
@@ -20,7 +27,8 @@ class PlaceDBHelper implements IDb{
|
||||
return _dB!;
|
||||
}
|
||||
|
||||
// Creates the databases with help from the _onCreatePlace function
|
||||
/// Initialize the database
|
||||
/// Creates database file and tables if they don't exist
|
||||
@override
|
||||
initDatabases() async {
|
||||
io.Directory documentsDirectory = await getApplicationCacheDirectory();
|
||||
@@ -30,19 +38,26 @@ class PlaceDBHelper implements IDb{
|
||||
return placeDB;
|
||||
}
|
||||
|
||||
// The function that helps
|
||||
/// Create database tables
|
||||
/// Sets up schema for both main entries and templates
|
||||
@override
|
||||
onCreateDatabases(Database placeDB, int version) async {
|
||||
// Create main places table
|
||||
await placeDB.execute(
|
||||
'CREATE TABLE place (ID INTEGER PRIMARY KEY AUTOINCREMENT, CID TEXT, Standort TEXT, Rudel TEXT, Datum TEXT, Adresse1 TEXT, Adresse2 TEXT, Adresse3 TEXT, BLand TEXT, Lkr TEXT, BeiOrt TEXT, OrtInfo TEXT, Status TEXT, FFTyp TEXT, FotoFilm TEXT, MEZ TEXT, Platzung TEXT, KSchloNr TEXT, KontDat DATE, Betreuung TEXT, AbbauDat DATE, Auftrag TEXT, KontAbsp TEXT, SonstBem TEXT, FKontakt1 TEXT, FKontakt2 TEXT, FKontakt3 TEXT, KTage1 INTEGER, KTage2 INTEGER, ProtoAm DATE, IntKomm TEXT, DECLNG DECIMALS(4,8), DECLAT DECIMALS(4,8), Sent INTEGER DEFAULT 0)');
|
||||
|
||||
// Create templates table (similar structure but without Sent flag)
|
||||
await placeDB.execute(
|
||||
'CREATE TABLE placeTemplates (ID INTEGER PRIMARY KEY AUTOINCREMENT, CID TEXT, Standort TEXT, Rudel TEXT, Datum TEXT, Adresse1 TEXT, Adresse2 TEXT, Adresse3 TEXT, BLand TEXT, Lkr TEXT, BeiOrt TEXT, OrtInfo TEXT, Status TEXT, FFTyp TEXT, FotoFilm TEXT, MEZ TEXT, Platzung TEXT, KSchloNr TEXT, KontDat DATE, Betreuung TEXT, AbbauDat DATE, Auftrag TEXT, KontAbsp TEXT, SonstBem TEXT, FKontakt1 TEXT, FKontakt2 TEXT, FKontakt3 TEXT, KTage1 INTEGER, KTage2 INTEGER, ProtoAm DATE, IntKomm TEXT, DECLNG DECIMALS(4,8), DECLAT DECIMALS(4,8))');
|
||||
}
|
||||
|
||||
// Function to add a finished entry and return its ID
|
||||
/// Add a new main entry to the database
|
||||
/// @param place Map containing the place data
|
||||
/// @return ID of the newly inserted entry
|
||||
@override
|
||||
Future<int> addMainEntry(Map<String, String> place) async {
|
||||
var placeDBClient = await dB;
|
||||
// Commented out code for handling existing entries
|
||||
// final existingID = await placeDBClient.query(
|
||||
// 'place',
|
||||
// where: 'ID = ?',
|
||||
@@ -51,7 +66,7 @@ class PlaceDBHelper implements IDb{
|
||||
//
|
||||
// if (existingID.isNotEmpty) {
|
||||
// updateMainEntry(place);
|
||||
// return existingID.first['ID'] as int; // Return existing ID
|
||||
// return existingID.first['ID'] as int;
|
||||
// }
|
||||
|
||||
int id = await placeDBClient.insert(
|
||||
@@ -60,31 +75,35 @@ class PlaceDBHelper implements IDb{
|
||||
//conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
|
||||
return id; // Return the ID of the newly inserted entry
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Update an existing main entry
|
||||
/// @param place Map containing the updated place data
|
||||
/// @return Number of rows affected
|
||||
@override
|
||||
Future<int> updateMainEntry(Map<String, String> place) async {
|
||||
var placeDBClient = await dB;
|
||||
|
||||
return await placeDBClient
|
||||
.update('place', place, where: "ID = ?", whereArgs: [place['ID']]);
|
||||
}
|
||||
|
||||
// function to update the sent value
|
||||
/// Mark an entry as sent to the server
|
||||
/// @param id ID of the entry to update
|
||||
@override
|
||||
Future<void> updateSent(int id) async {
|
||||
var placeDBClient = await dB;
|
||||
|
||||
await placeDBClient.update('place', {'Sent': 1},
|
||||
where: 'ID = ?', whereArgs: [id]);
|
||||
}
|
||||
|
||||
// same thing as before but with templatews
|
||||
/// Add a new template entry
|
||||
/// @param templates Map containing the template data
|
||||
/// @return ID of the newly inserted template
|
||||
@override
|
||||
Future<int> addTemplate(Map<String, String> templates) async {
|
||||
var placeDBClient = await dB;
|
||||
|
||||
// Commented out code for handling existing templates
|
||||
// final existingCID = await placeDBClient.query(
|
||||
// 'placeTemplates',
|
||||
// where: 'ID = ?',
|
||||
@@ -97,16 +116,15 @@ class PlaceDBHelper implements IDb{
|
||||
int id = await placeDBClient.insert(
|
||||
'placeTemplates',
|
||||
templates,
|
||||
// conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Updates a existing template
|
||||
/// Update an existing template
|
||||
/// @param template Map containing the updated template data
|
||||
@override
|
||||
Future<void> updateTemplate(Map<String, String> template) async {
|
||||
var placeDBClient = await dB;
|
||||
|
||||
await placeDBClient.update(
|
||||
'placeTemplates',
|
||||
template,
|
||||
@@ -115,7 +133,8 @@ class PlaceDBHelper implements IDb{
|
||||
);
|
||||
}
|
||||
|
||||
// get the finished entries from db
|
||||
/// Retrieve all main entries from the database
|
||||
/// @return List of all places or empty list if none exist
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getAllMainEntries() async {
|
||||
var placeDBClient = await dB;
|
||||
@@ -127,7 +146,8 @@ class PlaceDBHelper implements IDb{
|
||||
}
|
||||
}
|
||||
|
||||
// get the finished templates from db
|
||||
/// Retrieve all templates from the database
|
||||
/// @return List of all templates or empty list if none exist
|
||||
@override
|
||||
Future<List<Map<String, dynamic>>> getAllTemplates() async {
|
||||
var placeDBClient = await dB;
|
||||
@@ -139,21 +159,24 @@ class PlaceDBHelper implements IDb{
|
||||
}
|
||||
}
|
||||
|
||||
// deletes all finished entries from the db LOCALLY
|
||||
/// Delete all main entries from the local database
|
||||
/// Note: This only affects the local database, not the server
|
||||
@override
|
||||
Future<void> deleteAllMainEntries() async {
|
||||
var placeDBClient = await dB;
|
||||
await placeDBClient.delete('place');
|
||||
}
|
||||
|
||||
// deletes all templates from the db LOCALLY
|
||||
/// Delete all templates from the local database
|
||||
/// Note: This only affects the local database, not the server
|
||||
@override
|
||||
Future<void> deleteAllTemplates() async {
|
||||
var placeDBClient = await dB;
|
||||
await placeDBClient.delete('placeTemplates');
|
||||
}
|
||||
|
||||
// delete specific template
|
||||
/// Delete a specific template by ID
|
||||
/// @param id ID of the template to delete
|
||||
@override
|
||||
Future<void> deleteTemplateById(String id) async {
|
||||
var placeDBClient = await dB;
|
||||
@@ -164,7 +187,8 @@ class PlaceDBHelper implements IDb{
|
||||
);
|
||||
}
|
||||
|
||||
// delete specific place
|
||||
/// Delete a specific main entry by ID
|
||||
/// @param id ID of the entry to delete
|
||||
@override
|
||||
Future<void> deleteMainEntryById(String id) async {
|
||||
var placeDBClient = await dB;
|
||||
|
||||
@@ -22,9 +22,14 @@ import 'widgets/mez.dart';
|
||||
import 'widgets/platzung.dart';
|
||||
import 'widgets/status.dart';
|
||||
|
||||
/// Widget for adding or editing camera trap locations
|
||||
/// Supports template creation, editing existing entries, and new entries
|
||||
class AddCamMain extends StatefulWidget {
|
||||
/// Whether this form is being used to create a template
|
||||
final bool isTemplate;
|
||||
/// Whether the entry has been sent to the server
|
||||
final bool isSent;
|
||||
/// Existing data to populate the form with (for editing)
|
||||
final Map<String, dynamic>? existingData;
|
||||
|
||||
const AddCamMain({
|
||||
@@ -38,14 +43,17 @@ class AddCamMain extends StatefulWidget {
|
||||
State<AddCamMain> createState() => _AddCamMainState();
|
||||
}
|
||||
|
||||
/// State class for the camera trap location form
|
||||
class _AddCamMainState extends State<AddCamMain> {
|
||||
// var declaration
|
||||
/// Current step in the multi-step form
|
||||
int currentStep = 0;
|
||||
/// Whether this form is being used as a template
|
||||
late bool isTemplate;
|
||||
|
||||
/// Current GPS position, initialized with default values for Germany
|
||||
Position currentPosition = Position(
|
||||
longitude: 10.0,
|
||||
latitude: 51.0,
|
||||
longitude: 10.0, // Default longitude (roughly center of Germany)
|
||||
latitude: 51.0, // Default latitude (roughly center of Germany)
|
||||
timestamp: DateTime.now(),
|
||||
accuracy: 0.0,
|
||||
altitude: 0.0,
|
||||
@@ -56,6 +64,12 @@ class _AddCamMainState extends State<AddCamMain> {
|
||||
headingAccuracy: 0.0,
|
||||
);
|
||||
|
||||
/// Map containing all form fields with their controllers and required status
|
||||
/// Organized by steps:
|
||||
/// - Step 1: Basic information (ID, Status, etc.)
|
||||
/// - Step 2: Location information
|
||||
/// - Step 3: Dates and control periods
|
||||
/// - Step 4: Contact information
|
||||
Map<String, Map<String, dynamic>> rmap = {
|
||||
"ID": {"controller": TextEditingController(), "required": false},
|
||||
// Step 1
|
||||
@@ -102,6 +116,8 @@ class _AddCamMainState extends State<AddCamMain> {
|
||||
"Sent": {"controller": TextEditingController(), "required": false},
|
||||
};
|
||||
|
||||
/// Retrieves all field values as a map
|
||||
/// @return Map of field names to their current values
|
||||
Map<String, String> getFieldsText() {
|
||||
Map<String, String> puff = {};
|
||||
|
||||
@@ -112,8 +128,12 @@ class _AddCamMainState extends State<AddCamMain> {
|
||||
return puff;
|
||||
}
|
||||
|
||||
/// Flag indicating whether position is currently being loaded
|
||||
bool isLoadingPosition = false;
|
||||
|
||||
/// Initializes the GPS position
|
||||
/// Handles location permissions and device settings
|
||||
/// @return Future<Position> The determined position
|
||||
Future<Position> _initializePosition() async {
|
||||
try {
|
||||
final position = await GeolocatorService.deteterminePosition();
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
|
||||
// Karte
|
||||
// ! completely new page
|
||||
|
||||
|
||||
// Status
|
||||
|
||||
|
||||
// STTyp
|
||||
|
||||
|
||||
// platzung
|
||||
|
||||
|
||||
// FotoFilm
|
||||
|
||||
|
||||
// MEZ
|
||||
|
||||
|
||||
// KontDat
|
||||
|
||||
|
||||
// AbbauDat
|
||||
@@ -1,19 +1,34 @@
|
||||
// * Service for handling GPS location functionality
|
||||
// * Provides methods for:
|
||||
// * - Location permission handling
|
||||
// * - GPS service status checks
|
||||
// * - Position determination
|
||||
// * - Always-on location checks
|
||||
|
||||
import 'package:fforte/screens/addCam/exceptions/location_disabled_exception.dart';
|
||||
import 'package:fforte/screens/addCam/exceptions/location_forbidden_exception.dart';
|
||||
import 'package:fforte/screens/excursion/exceptions/need_always_location_exception.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
/// Service class for handling all GPS location related functionality
|
||||
class GeolocatorService {
|
||||
// determine live position with checks for denied permission and turned off location service
|
||||
/// Determine the current device position with permission checks
|
||||
/// @param alwaysOnNeeded Whether the app needs always-on location permission
|
||||
/// @throws LocationDisabledException if location services are disabled
|
||||
/// @throws LocationForbiddenException if location permission is denied
|
||||
/// @throws NeedAlwaysLocation if always-on permission is needed but not granted
|
||||
/// @return Future<Position> The current GPS position
|
||||
static Future<Position> deteterminePosition({bool alwaysOnNeeded = false}) async {
|
||||
bool locationEnabled;
|
||||
LocationPermission permissionGiven;
|
||||
|
||||
// Check if location services are enabled
|
||||
locationEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!locationEnabled) {
|
||||
throw LocationDisabledException();
|
||||
}
|
||||
|
||||
// Check and request location permissions if needed
|
||||
permissionGiven = await Geolocator.checkPermission();
|
||||
if (permissionGiven == LocationPermission.denied) {
|
||||
permissionGiven = await Geolocator.requestPermission();
|
||||
@@ -22,13 +37,16 @@ class GeolocatorService {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for always-on permission if required
|
||||
if (alwaysOnNeeded && permissionGiven != LocationPermission.always) {
|
||||
throw NeedAlwaysLocation();
|
||||
}
|
||||
|
||||
return await Geolocator.getCurrentPosition();
|
||||
return await Geolocator.getCurrentPosition();
|
||||
}
|
||||
|
||||
/// Check if always-on location permission is enabled
|
||||
/// @return Future<bool> True if always-on permission is granted or location is disabled
|
||||
static Future<bool> alwaysPositionEnabled() async {
|
||||
LocationPermission permissionGiven = await Geolocator.checkPermission();
|
||||
bool locationEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// * Widget for managing camera trap dismantling dates
|
||||
// * Features:
|
||||
// * - Date picker for selecting dismantling date
|
||||
// * - Date display and reset functionality
|
||||
// * - Localized text support
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for selecting and displaying camera trap dismantling dates
|
||||
/// Allows users to pick a date or clear the selection
|
||||
class AbbauDat extends StatefulWidget {
|
||||
/// Initial dismantling date, can be null if not set
|
||||
final DateTime? initAbbauDat;
|
||||
/// Callback function when date is changed
|
||||
final Function(DateTime) onDateChanged;
|
||||
|
||||
const AbbauDat({super.key, required this.initAbbauDat, required this.onDateChanged});
|
||||
@@ -11,7 +21,9 @@ class AbbauDat extends StatefulWidget {
|
||||
State<AbbauDat> createState() => _AbbauDatState();
|
||||
}
|
||||
|
||||
/// State class for the dismantling date widget
|
||||
class _AbbauDatState extends State<AbbauDat> {
|
||||
/// Currently selected dismantling date
|
||||
DateTime? abbauDat;
|
||||
|
||||
@override
|
||||
@@ -25,6 +37,7 @@ class _AbbauDatState extends State<AbbauDat> {
|
||||
return Row(
|
||||
children: [
|
||||
Column(children: [
|
||||
// Date picker button
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: ElevatedButton(
|
||||
@@ -38,34 +51,34 @@ class _AbbauDatState extends State<AbbauDat> {
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
Builder(builder: (context) {
|
||||
if (abbauDat != null) {
|
||||
return Text(
|
||||
'${abbauDat?.day}. ${abbauDat?.month}. ${abbauDat?.year}');
|
||||
} else {
|
||||
return Text(AppLocalizations.of(context)!.nichts);
|
||||
}
|
||||
}),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
abbauDat = null;
|
||||
});
|
||||
},
|
||||
child: const Text("X"))
|
||||
]),
|
||||
],
|
||||
)
|
||||
const SizedBox(width: 10),
|
||||
// Display selected date or "nothing" text
|
||||
Builder(builder: (context) {
|
||||
if (abbauDat != null) {
|
||||
return Text(
|
||||
'${abbauDat?.day}. ${abbauDat?.month}. ${abbauDat?.year}');
|
||||
} else {
|
||||
return Text(AppLocalizations.of(context)!.nichts);
|
||||
}
|
||||
}),
|
||||
const SizedBox(width: 10),
|
||||
// Clear date button
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
abbauDat = null;
|
||||
});
|
||||
},
|
||||
child: const Text("X"))
|
||||
]),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Show date picker dialog and return selected date
|
||||
/// @return Future<DateTime?> Selected date or null if cancelled
|
||||
Future<DateTime?> pickDate() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// * Widget for selecting camera trap media type
|
||||
// * Provides radio button selection between:
|
||||
// * - Photo mode
|
||||
// * - Film/Video mode
|
||||
// * Includes localization support
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for selecting between photo and film/video mode
|
||||
/// Uses radio buttons for selection with localized labels
|
||||
class FotoFilm extends StatefulWidget {
|
||||
/// Callback function when media type selection changes
|
||||
final Function(String) onFotoFilmChanged;
|
||||
/// Initial media type selection ('foto' by default)
|
||||
final String initialFotoFilm;
|
||||
|
||||
const FotoFilm(
|
||||
@@ -14,7 +24,9 @@ class FotoFilm extends StatefulWidget {
|
||||
State<FotoFilm> createState() => _FotoFilmState();
|
||||
}
|
||||
|
||||
/// State class for the photo/film selection widget
|
||||
class _FotoFilmState extends State<FotoFilm> {
|
||||
/// Currently selected media type
|
||||
String? _selectedFotoFilm;
|
||||
|
||||
@override
|
||||
@@ -27,6 +39,7 @@ class _FotoFilmState extends State<FotoFilm> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Photo mode radio button
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.foto),
|
||||
@@ -41,6 +54,7 @@ class _FotoFilmState extends State<FotoFilm> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Film/Video mode radio button
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.film),
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
// import 'package:fforte/screens/helper/snack_bar_helper.dart';
|
||||
// * Interactive map widget for camera trap location selection
|
||||
// * Features:
|
||||
// * - OpenStreetMap integration
|
||||
// * - Location marker placement
|
||||
// * - GPS coordinates display and saving
|
||||
// * - Localized interface
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
@@ -6,11 +12,18 @@ import 'package:latlong2/latlong.dart';
|
||||
// import 'package:geocoding/geocoding.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for displaying and interacting with the map
|
||||
/// Allows users to select and save camera trap locations
|
||||
class Karte extends StatefulWidget {
|
||||
/// Controller for nearby location name
|
||||
final TextEditingController beiOrtC;
|
||||
/// Controller for location details
|
||||
final TextEditingController ortInfoC;
|
||||
/// Controller for longitude coordinate
|
||||
final TextEditingController decLngC;
|
||||
/// Controller for latitude coordinate
|
||||
final TextEditingController decLatC;
|
||||
/// Current GPS position
|
||||
final Position currentPosition;
|
||||
|
||||
const Karte(
|
||||
@@ -25,15 +38,20 @@ class Karte extends StatefulWidget {
|
||||
KarteState createState() => KarteState();
|
||||
}
|
||||
|
||||
/// State class for the map widget
|
||||
class KarteState extends State<Karte> {
|
||||
/// Current marker on the map
|
||||
Marker? currentMarker;
|
||||
/// Selected position coordinates
|
||||
LatLng? selectedPosition;
|
||||
/// Whether the save button should be visible
|
||||
bool saveVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Initialize marker at current position
|
||||
currentMarker = Marker(
|
||||
point: LatLng(
|
||||
widget.currentPosition.latitude, widget.currentPosition.longitude),
|
||||
@@ -50,6 +68,7 @@ class KarteState extends State<Karte> {
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.map),
|
||||
actions: [
|
||||
// Save location button
|
||||
Visibility(
|
||||
visible: saveVisible,
|
||||
child: Padding(
|
||||
@@ -76,6 +95,7 @@ class KarteState extends State<Karte> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Map display with OpenStreetMap tiles
|
||||
body: FlutterMap(
|
||||
mapController: MapController(),
|
||||
options: MapOptions(
|
||||
@@ -89,15 +109,21 @@ class KarteState extends State<Karte> {
|
||||
onTap: _handleTap,
|
||||
),
|
||||
children: [
|
||||
// OpenStreetMap tile layer
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'de.lupus.apps',
|
||||
),
|
||||
// Marker layer
|
||||
MarkerLayer(markers: [currentMarker!]),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
/// Handle tap events on the map
|
||||
/// Creates a new marker at the tapped location
|
||||
/// @param position The tap position on the screen
|
||||
/// @param latlng The geographical coordinates of the tap
|
||||
_handleTap(TapPosition position, LatLng latlng) {
|
||||
setState(() {
|
||||
currentMarker = Marker(
|
||||
@@ -111,7 +137,7 @@ class KarteState extends State<Karte> {
|
||||
);
|
||||
// selectedPosition = latlng;
|
||||
saveVisible = true;
|
||||
});
|
||||
});
|
||||
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
// content: Text(
|
||||
// "${AppLocalizations.of(context)!.markerSet}\n${selectedPosition!.latitude}\n${selectedPosition!.longitude}")));
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// * Widget for managing camera trap control/check dates
|
||||
// * Features:
|
||||
// * - Date picker for selecting control dates
|
||||
// * - Date display in localized format
|
||||
// * - Callback support for date changes
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for selecting and displaying camera trap control dates
|
||||
/// Used to schedule when the camera trap should be checked
|
||||
class KontDat extends StatefulWidget {
|
||||
/// Initial control date, if any
|
||||
final DateTime? initKontDat;
|
||||
/// Callback function when date is changed
|
||||
final Function(DateTime) onDateChanged;
|
||||
|
||||
const KontDat(
|
||||
@@ -12,7 +22,9 @@ class KontDat extends StatefulWidget {
|
||||
State<KontDat> createState() => _KontDatState();
|
||||
}
|
||||
|
||||
/// State class for the control date widget
|
||||
class _KontDatState extends State<KontDat> {
|
||||
/// Currently selected control date
|
||||
DateTime? kontDat;
|
||||
|
||||
@override
|
||||
@@ -26,6 +38,7 @@ class _KontDatState extends State<KontDat> {
|
||||
return Row(
|
||||
children: [
|
||||
Row(children: [
|
||||
// Date picker button
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: ElevatedButton(
|
||||
@@ -39,9 +52,8 @@ class _KontDatState extends State<KontDat> {
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.pickkontdat)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
// Display selected date in DD.MM.YYYY format
|
||||
Text(
|
||||
'${kontDat?.day}. ${kontDat?.month}. ${kontDat?.year}',
|
||||
),
|
||||
@@ -50,6 +62,8 @@ class _KontDatState extends State<KontDat> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Show date picker dialog and return selected date
|
||||
/// @return Future<DateTime?> Selected date or null if cancelled
|
||||
Future<DateTime?> pickDate() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// * Widget for selecting time zone settings (MEZ/MESZ)
|
||||
// * Features:
|
||||
// * - Radio button selection between summer and winter time
|
||||
// * - Localized labels for time zones
|
||||
// * - Default selection support
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for selecting between summer time (MESZ) and winter time (MEZ)
|
||||
/// Used to configure camera trap time settings
|
||||
class MEZ extends StatefulWidget {
|
||||
/// Callback function when time zone selection changes
|
||||
final Function(String) onMEZChanged;
|
||||
/// Initial time zone selection ('sommerzeit' by default)
|
||||
final String initialMEZ;
|
||||
|
||||
const MEZ(
|
||||
@@ -12,7 +22,9 @@ class MEZ extends StatefulWidget {
|
||||
State<MEZ> createState() => _MEZState();
|
||||
}
|
||||
|
||||
/// State class for the time zone selection widget
|
||||
class _MEZState extends State<MEZ> {
|
||||
/// Currently selected time zone
|
||||
String? _selectedMEZ;
|
||||
|
||||
@override
|
||||
@@ -25,6 +37,7 @@ class _MEZState extends State<MEZ> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Summer time (MESZ) radio button
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.sommerzeit),
|
||||
@@ -39,6 +52,7 @@ class _MEZState extends State<MEZ> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Winter time (MEZ) radio button
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.winterzeit),
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
// * Widget for selecting camera trap placement type
|
||||
// * Features:
|
||||
// * - Multiple placement options via radio buttons
|
||||
// * - Localized labels for each placement type
|
||||
// * - Support for initial selection
|
||||
// * Available placement types:
|
||||
// * - Bait station (Kirrung)
|
||||
// * - Water source (Wasserstelle)
|
||||
// * - Forest (Wald)
|
||||
// * - Game pass (Wildwechsel)
|
||||
// * - Path/Road (Weg/Straße)
|
||||
// * - Farm/Garden (Hof/Garten)
|
||||
// * - Meadow/Field (Wiese/Feld/Offenfläche)
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for selecting the type of location where the camera trap is placed
|
||||
/// Provides various predefined placement options common in wildlife monitoring
|
||||
class Platzung extends StatefulWidget {
|
||||
/// Callback function when placement type selection changes
|
||||
final Function(String) onPlatzungChanged;
|
||||
/// Initial placement type selection
|
||||
final String? initialPlatzung;
|
||||
|
||||
const Platzung({
|
||||
@@ -15,7 +33,9 @@ class Platzung extends StatefulWidget {
|
||||
State<Platzung> createState() => _PlatzungState();
|
||||
}
|
||||
|
||||
/// State class for the placement type selection widget
|
||||
class _PlatzungState extends State<Platzung> {
|
||||
/// Currently selected placement type
|
||||
String? _selectedPlatzung;
|
||||
|
||||
@override
|
||||
@@ -30,6 +50,7 @@ class _PlatzungState extends State<Platzung> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Bait station placement option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.kirrung),
|
||||
@@ -44,6 +65,7 @@ class _PlatzungState extends State<Platzung> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Water source placement option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.wasserstelle),
|
||||
@@ -58,6 +80,7 @@ class _PlatzungState extends State<Platzung> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Forest placement option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.wald),
|
||||
@@ -72,6 +95,7 @@ class _PlatzungState extends State<Platzung> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Game pass placement option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.wildwechsel),
|
||||
@@ -86,6 +110,7 @@ class _PlatzungState extends State<Platzung> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Path/Road placement option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.wegstrasse),
|
||||
@@ -100,6 +125,7 @@ class _PlatzungState extends State<Platzung> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Farm/Garden placement option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.hofgarten),
|
||||
@@ -114,6 +140,7 @@ class _PlatzungState extends State<Platzung> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Meadow/Field placement option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.wiesefeld),
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// * Widget for selecting camera trap status
|
||||
// * Features:
|
||||
// * - Radio button selection between active and inactive
|
||||
// * - Localized status labels
|
||||
// * - Default selection support
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for selecting the operational status of a camera trap
|
||||
/// Allows toggling between active and inactive states
|
||||
class Status extends StatefulWidget {
|
||||
/// Callback function when status selection changes
|
||||
final Function(String) onStatusChanged;
|
||||
/// Initial status selection ('Aktiv' by default)
|
||||
final String initialStatus;
|
||||
|
||||
const Status(
|
||||
@@ -12,7 +22,9 @@ class Status extends StatefulWidget {
|
||||
State<Status> createState() => _StatusState();
|
||||
}
|
||||
|
||||
/// State class for the status selection widget
|
||||
class _StatusState extends State<Status> {
|
||||
/// Currently selected status
|
||||
String? _selectedStatus;
|
||||
|
||||
@override
|
||||
@@ -25,6 +37,7 @@ class _StatusState extends State<Status> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Active status option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.aktiv),
|
||||
@@ -39,6 +52,7 @@ class _StatusState extends State<Status> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Inactive status option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.inaktiv),
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
// * Widget for selecting the sampling type for camera trap monitoring
|
||||
// * Features:
|
||||
// * - Two sampling modes: opportunistic and systematic
|
||||
// * - Radio button selection interface
|
||||
// * - State management for selection
|
||||
// * - Callback for selection changes
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for managing sampling type selection
|
||||
/// Provides choice between opportunistic and systematic sampling
|
||||
class STTyp extends StatefulWidget {
|
||||
/// Callback function when sampling type changes
|
||||
final Function(String) onSTTypChanged;
|
||||
/// Initial sampling type value
|
||||
final String initialSTTyp;
|
||||
|
||||
const STTyp(
|
||||
@@ -14,7 +25,9 @@ class STTyp extends StatefulWidget {
|
||||
State<STTyp> createState() => _STTypState();
|
||||
}
|
||||
|
||||
/// State class for the sampling type selection widget
|
||||
class _STTypState extends State<STTyp> {
|
||||
/// Currently selected sampling type
|
||||
String? _selectedSTTyp;
|
||||
|
||||
@override
|
||||
@@ -27,6 +40,7 @@ class _STTypState extends State<STTyp> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Opportunistic sampling option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.opportunistisch),
|
||||
@@ -41,6 +55,7 @@ class _STTypState extends State<STTyp> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Systematic sampling option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.systematisch),
|
||||
|
||||
@@ -24,9 +24,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Main widget for managing excursion data entry
|
||||
/// Provides a multi-step form interface for recording excursion details
|
||||
class ExcursionMain extends StatefulWidget {
|
||||
/// Whether this is a template excursion
|
||||
final bool isTemplate;
|
||||
/// Whether the excursion data has been sent to the server
|
||||
final bool isSent;
|
||||
/// Existing excursion data for editing
|
||||
final Map<String, dynamic>? existingData;
|
||||
|
||||
const ExcursionMain({
|
||||
@@ -40,9 +45,13 @@ class ExcursionMain extends StatefulWidget {
|
||||
State<ExcursionMain> createState() => _ExcursionMainState();
|
||||
}
|
||||
|
||||
/// State class for the main excursion screen
|
||||
class _ExcursionMainState extends State<ExcursionMain> {
|
||||
/// Current step in the form
|
||||
int currentStep = 0;
|
||||
/// Whether this is a template excursion
|
||||
late bool isTemplate;
|
||||
/// Current GPS position
|
||||
Position currentPosition = Position(
|
||||
longitude: 10.0,
|
||||
latitude: 51.0,
|
||||
@@ -56,12 +65,14 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
headingAccuracy: 0.0,
|
||||
);
|
||||
|
||||
/// Whether to show extended BImA information
|
||||
bool bimaExtended = false;
|
||||
|
||||
// all TextEditingController because its easier
|
||||
/// Map of all form fields and their controllers
|
||||
/// Each field has a controller and required flag
|
||||
Map<String, Map<String, dynamic>> rmap = {
|
||||
"ID": {"controller": TextEditingController(), "required": false},
|
||||
// Step 1
|
||||
// Step 1 - Basic Information
|
||||
"Datum": {"controller": TextEditingController(), "required": false},
|
||||
"Rudel": {"controller": TextEditingController(), "required": false},
|
||||
"Teilnehmer": {"controller": TextEditingController(), "required": false},
|
||||
@@ -76,7 +87,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
"BimaNutzer": {"controller": TextEditingController(), "required": false},
|
||||
"BimaAGV": {"controller": TextEditingController(), "required": false},
|
||||
|
||||
// Step 2
|
||||
// Step 2 - Environmental Conditions and Observations
|
||||
"Weg": {"controller": TextEditingController(), "required": false},
|
||||
"Wetter": {"controller": TextEditingController(), "required": false},
|
||||
"Temperat": {"controller": TextEditingController(), "required": false},
|
||||
@@ -89,7 +100,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
"KmFuProz": {"controller": TextEditingController(), "required": false},
|
||||
"KmRaProz": {"controller": TextEditingController(), "required": false},
|
||||
|
||||
// Spur maybe own step?
|
||||
// Track Findings
|
||||
"SpGut": {"controller": TextEditingController(), "required": false},
|
||||
"SpMittel": {"controller": TextEditingController(), "required": false},
|
||||
"SpSchlecht": {"controller": TextEditingController(), "required": false},
|
||||
@@ -101,6 +112,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
"WelpenAnz": {"controller": TextEditingController(), "required": false},
|
||||
"WpSicher": {"controller": TextEditingController(), "required": false},
|
||||
|
||||
// Sample Counts
|
||||
"LosungGes": {"controller": TextEditingController(), "required": false},
|
||||
"LosungAnz": {"controller": TextEditingController(), "required": false},
|
||||
"LosungGen": {"controller": TextEditingController(), "required": false},
|
||||
@@ -114,7 +126,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
"GenetiKm": {"controller": TextEditingController(), "required": false},
|
||||
"Hinweise": {"controller": TextEditingController(), "required": false},
|
||||
|
||||
// Step 3
|
||||
// Step 3 - Notes and Communication
|
||||
"Bemerk": {"controller": TextEditingController(), "required": false},
|
||||
"IntKomm": {"controller": TextEditingController(), "required": false},
|
||||
"FallNum": {"controller": TextEditingController(), "required": false},
|
||||
@@ -123,6 +135,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Initialize location services
|
||||
GeolocatorService.deteterminePosition(
|
||||
alwaysOnNeeded: false,
|
||||
).then((result) => currentPosition = result).catchError((error) async {
|
||||
@@ -156,13 +169,14 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
return currentPosition;
|
||||
});
|
||||
|
||||
// Load existing data or set defaults
|
||||
if (widget.existingData?.isNotEmpty ?? false) {
|
||||
for (var key in widget.existingData!.keys) {
|
||||
rmap[key]!["controller"]!.text =
|
||||
widget.existingData?[key].toString() ?? "";
|
||||
}
|
||||
} else {
|
||||
// Set BLand and default values if there is no existing data
|
||||
// Set default state and date
|
||||
SharedPreferences.getInstance().then((prefs) {
|
||||
rmap["BLand"]!["controller"]!.text = prefs.getString('bLand') ?? "";
|
||||
});
|
||||
@@ -178,12 +192,15 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Dispose all controllers
|
||||
for (String key in rmap.keys) {
|
||||
rmap[key]!["controller"].dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Get all form field values as a map
|
||||
/// @return Map<String, String> Map of field names to values
|
||||
Map<String, String> getFieldsText() {
|
||||
Map<String, String> puff = {};
|
||||
|
||||
@@ -196,12 +213,14 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
/// Build the steps for the form
|
||||
/// @return List<Step> List of form steps
|
||||
List<Step> getSteps() => [
|
||||
Step(
|
||||
title: Text(AppLocalizations.of(context)!.dateandtime),
|
||||
content: Column(
|
||||
children: [
|
||||
// ---------- Date
|
||||
// Date picker
|
||||
Datum(
|
||||
initDatum: DateTime.now(),
|
||||
onDateChanged: (date) {
|
||||
@@ -210,7 +229,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
name: AppLocalizations.of(context)!.date,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Pack
|
||||
// Pack/Group field
|
||||
VarTextField(
|
||||
textController: rmap["Rudel"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.rudel,
|
||||
@@ -219,7 +238,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Participants
|
||||
// Participants field
|
||||
VarTextField(
|
||||
textController: rmap["Teilnehmer"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.teilnehmer,
|
||||
@@ -228,7 +247,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Duration
|
||||
// Duration field
|
||||
VarTextField(
|
||||
textController: rmap["Dauer"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.dauer,
|
||||
@@ -237,13 +256,13 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// ---------- Dog(leash)
|
||||
// Dog and leash selection
|
||||
HundULeine(
|
||||
mHund: rmap["MHund"]!["controller"]!,
|
||||
mLeine: rmap["MLeine"]!["controller"]!,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- State
|
||||
// State field
|
||||
VarTextField(
|
||||
textController: rmap["BLand"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.bland,
|
||||
@@ -252,7 +271,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Country
|
||||
// County field
|
||||
VarTextField(
|
||||
textController: rmap["Lkr"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.lkr,
|
||||
@@ -261,7 +280,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- By State
|
||||
// Nearby location field
|
||||
VarTextField(
|
||||
textController: rmap["BeiOrt"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.beiort,
|
||||
@@ -270,7 +289,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Bima number
|
||||
// BImA information section
|
||||
const Divider(),
|
||||
const SizedBox(height: 10),
|
||||
ClipRRect(
|
||||
@@ -307,7 +326,6 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Bima name
|
||||
VarTextField(
|
||||
textController:
|
||||
rmap["BimaName"]!["controller"]!,
|
||||
@@ -318,7 +336,6 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Bima user
|
||||
BimaNutzer(
|
||||
onBimaNutzerChanged: (value) {
|
||||
setState(() {
|
||||
@@ -328,7 +345,6 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Bima AGV
|
||||
VarTextField(
|
||||
textController: rmap["BimaAGV"]!["controller"]!,
|
||||
localization:
|
||||
@@ -351,7 +367,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
title: Text(AppLocalizations.of(context)!.umstaendeUndAktionen),
|
||||
content: Column(
|
||||
children: [
|
||||
// ---------- Tracking
|
||||
// GPS tracking button
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
// Check for always permission before starting tracking
|
||||
@@ -426,7 +442,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// ---------- Weather
|
||||
// Weather field
|
||||
VarTextField(
|
||||
textController: rmap["Wetter"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.wetter,
|
||||
@@ -435,7 +451,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Temperature
|
||||
// Temperature field
|
||||
VarTextField(
|
||||
textController: rmap["Temperat"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.temperatur,
|
||||
@@ -444,11 +460,11 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// ---------- Last precipitation
|
||||
// Last precipitation selection
|
||||
LetzterNiederschlag(
|
||||
controller: rmap["RegenVor"]!["controller"]!),
|
||||
const SizedBox(height: 20),
|
||||
// ---------- Track conditions
|
||||
// Distance and track conditions
|
||||
StreckeUSpurbedingungen(
|
||||
kmAutoController: rmap["KmAuto"]!["controller"]!,
|
||||
kmFussController: rmap["KmFuss"]!["controller"]!,
|
||||
@@ -459,7 +475,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
// ---------- Track found
|
||||
// Track findings
|
||||
SpurGefunden(
|
||||
spurFund: rmap["SpurFund"]!["controller"]!,
|
||||
spurLang: rmap["SpurLang"]!["controller"]!,
|
||||
@@ -471,7 +487,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
// ---------- Counts
|
||||
// Sample counts
|
||||
Anzahlen(
|
||||
losungAnz: rmap["LosungAnz"]!["controller"]!,
|
||||
losungGes: rmap["LosungGes"]!["controller"]!,
|
||||
@@ -486,7 +502,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
const SizedBox(height: 20),
|
||||
const Divider(),
|
||||
const SizedBox(height: 20),
|
||||
// ---------- Clues
|
||||
// Observations section
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(
|
||||
@@ -502,7 +518,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
title: Text(AppLocalizations.of(context)!.intkomm),
|
||||
content: Column(
|
||||
children: [
|
||||
// ---------- Remarks
|
||||
// Remarks field
|
||||
VarTextField(
|
||||
textController: rmap["Bemerk"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.sonstbemerkungen,
|
||||
@@ -511,7 +527,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
dbDesignation: DatabasesEnum.excursion,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// ---------- Internal communication
|
||||
// Internal communication field
|
||||
VarTextField(
|
||||
textController: rmap["IntKomm"]!["controller"]!,
|
||||
localization: AppLocalizations.of(context)!.intkomm,
|
||||
@@ -583,7 +599,6 @@ class _ExcursionMainState extends State<ExcursionMain> {
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.excursion),
|
||||
actions: [
|
||||
// Text(TrackingService().isTracking ? "Tracking" : "Not tracking")
|
||||
Image.asset(
|
||||
TrackingService().isTracking
|
||||
? "assets/icons/tracking_on.png"
|
||||
|
||||
@@ -1,15 +1,34 @@
|
||||
// * Widget for tracking various wildlife monitoring quantities
|
||||
// * Features:
|
||||
// * - Tracking of droppings (Losung) counts and samples
|
||||
// * - Urine marking spot counts and samples
|
||||
// * - Estrus blood sample tracking
|
||||
// * - Hair sample tracking
|
||||
// * All fields support genetic sample counting
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for managing counts of various wildlife monitoring samples
|
||||
/// Provides input fields for different types of samples and their genetic subsets
|
||||
class Anzahlen extends StatefulWidget {
|
||||
/// Controller for number of droppings found
|
||||
final TextEditingController losungAnz;
|
||||
/// Controller for number of droppings collected
|
||||
final TextEditingController losungGes;
|
||||
/// Controller for number of genetic samples from droppings
|
||||
final TextEditingController losungGen;
|
||||
/// Controller for number of urine marking spots
|
||||
final TextEditingController urinAnz;
|
||||
/// Controller for number of genetic samples from urine
|
||||
final TextEditingController urinGen;
|
||||
/// Controller for number of estrus blood spots
|
||||
final TextEditingController oestrAnz;
|
||||
/// Controller for number of genetic samples from estrus blood
|
||||
final TextEditingController oestrGen;
|
||||
/// Controller for number of hair samples
|
||||
final TextEditingController haarAnz;
|
||||
/// Controller for number of genetic samples from hair
|
||||
final TextEditingController haarGen;
|
||||
|
||||
const Anzahlen(
|
||||
@@ -28,6 +47,7 @@ class Anzahlen extends StatefulWidget {
|
||||
AnzahlenState createState() => AnzahlenState();
|
||||
}
|
||||
|
||||
/// State class for the quantity tracking widget
|
||||
class AnzahlenState extends State<Anzahlen> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -37,6 +57,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
// Droppings count section
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -46,9 +67,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.anzahlLosungen)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft, child: TextField(
|
||||
@@ -57,9 +76,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
onTap: () => widget.losungAnz.selection = TextSelection(baseOffset: 0, extentOffset: widget.losungAnz.value.text.length),
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Align(
|
||||
@@ -67,9 +84,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.davonEingesammelt)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft, child: TextField(
|
||||
@@ -78,11 +93,10 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
onTap: () => widget.losungGes.selection = TextSelection(baseOffset: 0, extentOffset: widget.losungGes.value.text.length),
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
// Genetic samples from droppings
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
@@ -94,9 +108,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
AppLocalizations.of(context)!.davonGenetikproben),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft, child: TextField(
|
||||
@@ -107,9 +119,8 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 40,
|
||||
),
|
||||
const Divider(height: 40),
|
||||
// Urine marking spots section
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -119,9 +130,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.anzahlUrinMakierstellen)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft, child: TextField(
|
||||
@@ -130,9 +139,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
onTap: () => widget.urinAnz.selection = TextSelection(baseOffset: 0, extentOffset: widget.urinAnz.value.text.length),
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Align(
|
||||
@@ -140,9 +147,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.davonGenetikproben)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft, child: TextField(
|
||||
@@ -151,14 +156,11 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
onTap: () => widget.urinGen.selection = TextSelection(baseOffset: 0, extentOffset: widget.urinGen.value.text.length),
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 40,
|
||||
),
|
||||
const Divider(height: 40),
|
||||
// Estrus blood section
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -168,9 +170,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.anzahlOestrusblut)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft, child: TextField(
|
||||
@@ -179,9 +179,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
onTap: () => widget.oestrAnz.selection = TextSelection(baseOffset: 0, extentOffset: widget.oestrAnz.value.text.length),
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Align(
|
||||
@@ -189,9 +187,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.davonGenetikproben)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft, child: TextField(
|
||||
@@ -200,14 +196,11 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
onTap: () => widget.oestrGen.selection = TextSelection(baseOffset: 0, extentOffset: widget.oestrGen.value.text.length),
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
const Divider(
|
||||
height: 40,
|
||||
),
|
||||
const Divider(height: 40),
|
||||
// Hair samples section
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -217,9 +210,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.anzahlHaarproben)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft, child: TextField(
|
||||
@@ -228,9 +219,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
onTap: () => widget.haarAnz.selection = TextSelection(baseOffset: 0, extentOffset: widget.haarAnz.value.text.length),
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Align(
|
||||
@@ -238,9 +227,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.davonGenetikproben)),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft, child: TextField(
|
||||
@@ -249,9 +236,7 @@ class AnzahlenState extends State<Anzahlen> {
|
||||
onTap: () => widget.haarGen.selection = TextSelection(baseOffset: 0, extentOffset: widget.haarGen.value.text.length),
|
||||
)),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
// * Widget for selecting BImA (Bundesanstalt für Immobilienaufgaben) property user type
|
||||
// * Features:
|
||||
// * - Radio button selection for different user categories
|
||||
// * - Localized labels for each user type
|
||||
// * Available user types:
|
||||
// * - Bundeswehr (German Armed Forces)
|
||||
// * - Gaststreitkräfte (Foreign Armed Forces)
|
||||
// * - NNE Bund (Federal non-civil use)
|
||||
// * - Geschäftsliegenschaft/AGV (Commercial property)
|
||||
// * - kein (none)
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
// Bundeswehr
|
||||
// Gastreitkraefte
|
||||
// NNE Bund
|
||||
// Geschaeftsliegenschaft/AGV
|
||||
// kein
|
||||
|
||||
/// Widget for selecting the type of BImA property user
|
||||
/// Used to categorize the property where monitoring takes place
|
||||
class BimaNutzer extends StatefulWidget {
|
||||
/// Callback function when user type selection changes
|
||||
final Function(String) onBimaNutzerChanged;
|
||||
/// Initial user type selection ('Bundeswehr' by default)
|
||||
final String initialStatus;
|
||||
|
||||
const BimaNutzer(
|
||||
@@ -18,7 +27,9 @@ class BimaNutzer extends StatefulWidget {
|
||||
State<BimaNutzer> createState() => _StatusState();
|
||||
}
|
||||
|
||||
/// State class for the BImA user selection widget
|
||||
class _StatusState extends State<BimaNutzer> {
|
||||
/// Currently selected user type
|
||||
String? _selectedStatus;
|
||||
|
||||
@override
|
||||
@@ -31,6 +42,7 @@ class _StatusState extends State<BimaNutzer> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// German Armed Forces option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.bundeswehr),
|
||||
@@ -45,6 +57,7 @@ class _StatusState extends State<BimaNutzer> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Foreign Armed Forces option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.gaststreitkraefte),
|
||||
@@ -59,6 +72,7 @@ class _StatusState extends State<BimaNutzer> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Federal non-civil use option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.nneBund),
|
||||
@@ -73,6 +87,7 @@ class _StatusState extends State<BimaNutzer> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Commercial property option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.geschaeftsliegenschaftAGV),
|
||||
@@ -87,6 +102,7 @@ class _StatusState extends State<BimaNutzer> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// No user option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.kein),
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
// * Widget for managing wildlife observation hints and indicators
|
||||
// * Features:
|
||||
// * - Checkbox selection for common observation types
|
||||
// * - Custom text input for additional observations
|
||||
// * - Automatic text aggregation of selected items
|
||||
// * Available observation types:
|
||||
// * - Resting places (Liegestelle)
|
||||
// * - Animal carcasses (Wildtierkadaver)
|
||||
// * - Direct sightings (Sichtung)
|
||||
// * - Howling (Heulen)
|
||||
// * - Other observations (Sonstiges)
|
||||
|
||||
import 'package:fforte/enums/databases.dart';
|
||||
import 'package:fforte/screens/sharedWidgets/var_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for recording various types of wildlife observations
|
||||
/// Combines predefined categories with custom input options
|
||||
class Hinweise extends StatefulWidget {
|
||||
/// Controller for the combined observation text
|
||||
final TextEditingController hinweise;
|
||||
|
||||
const Hinweise({super.key, required this.hinweise});
|
||||
@@ -11,21 +26,29 @@ class Hinweise extends StatefulWidget {
|
||||
@override
|
||||
State<Hinweise> createState() => _HinweiseState();
|
||||
}
|
||||
class _HinweiseState extends State<Hinweise> {
|
||||
// Vars for Checkboxes
|
||||
late bool liegestelleChecked;
|
||||
late bool kadaverChecked;
|
||||
late bool sichtungChecked;
|
||||
late bool heulenChecked;
|
||||
bool sonstigesChecked = false;
|
||||
|
||||
// for sonstiges textfield
|
||||
/// State class for the observations widget
|
||||
class _HinweiseState extends State<Hinweise> {
|
||||
// Checkbox state variables
|
||||
/// Whether resting place was observed
|
||||
late bool liegestelleChecked;
|
||||
/// Whether animal carcass was found
|
||||
late bool kadaverChecked;
|
||||
/// Whether direct sighting occurred
|
||||
late bool sichtungChecked;
|
||||
/// Whether howling was heard
|
||||
late bool heulenChecked;
|
||||
/// Whether other observations exist
|
||||
bool sonstigesChecked = false;
|
||||
|
||||
/// Controller for additional observations text
|
||||
TextEditingController sonstigesController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
sonstigesController.addListener(updateController);
|
||||
|
||||
// Initialize checkboxes based on existing text
|
||||
liegestelleChecked = widget.hinweise.text.contains("Liegestelle") ? true : false;
|
||||
kadaverChecked = widget.hinweise.text.contains("Wildtierkadaver") ? true : false;
|
||||
sichtungChecked = widget.hinweise.text.contains("Sichtung") ? true : false;
|
||||
@@ -33,6 +56,7 @@ class _HinweiseState extends State<Hinweise> {
|
||||
|
||||
bool firstRun = true;
|
||||
|
||||
// Parse existing other observations
|
||||
for (String val in widget.hinweise.text.split(",")) {
|
||||
if (val != "Liegestelle" && val != "Wildtierkadaver" && val != "Sichtung" && val != "Heulen" && val != "") {
|
||||
sonstigesChecked = true;
|
||||
@@ -51,6 +75,8 @@ class _HinweiseState extends State<Hinweise> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Update the main controller text based on selected options
|
||||
/// Combines all checked items and additional text into a comma-separated string
|
||||
void updateController() {
|
||||
Map<String, bool> props = {
|
||||
"Liegestelle": liegestelleChecked,
|
||||
@@ -63,6 +89,7 @@ class _HinweiseState extends State<Hinweise> {
|
||||
|
||||
widget.hinweise.text = "";
|
||||
|
||||
// Build combined text from selected options
|
||||
for (String key in props.keys) {
|
||||
if (!firstRun && props[key]!) {
|
||||
widget.hinweise.text += ",";
|
||||
@@ -74,7 +101,6 @@ class _HinweiseState extends State<Hinweise> {
|
||||
} else if (props[key]!){
|
||||
widget.hinweise.text += key;
|
||||
}
|
||||
|
||||
}
|
||||
debugPrint(widget.hinweise.text);
|
||||
}
|
||||
@@ -83,6 +109,7 @@ class _HinweiseState extends State<Hinweise> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Resting place checkbox
|
||||
CheckboxListTile(
|
||||
title: Text(AppLocalizations.of(context)!.liegestelle),
|
||||
value: liegestelleChecked,
|
||||
@@ -90,6 +117,7 @@ class _HinweiseState extends State<Hinweise> {
|
||||
setState(() => liegestelleChecked = value ?? false);
|
||||
updateController();
|
||||
}),
|
||||
// Animal carcass checkbox
|
||||
CheckboxListTile(
|
||||
title: Text(AppLocalizations.of(context)!.wildtierKadaver),
|
||||
value: kadaverChecked,
|
||||
@@ -97,6 +125,7 @@ class _HinweiseState extends State<Hinweise> {
|
||||
setState(() => kadaverChecked = value ?? false);
|
||||
updateController();
|
||||
}),
|
||||
// Direct sighting checkbox
|
||||
CheckboxListTile(
|
||||
title: Text(AppLocalizations.of(context)!.sichtung),
|
||||
value: sichtungChecked,
|
||||
@@ -104,6 +133,7 @@ class _HinweiseState extends State<Hinweise> {
|
||||
setState(() => sichtungChecked = value ?? false);
|
||||
updateController();
|
||||
}),
|
||||
// Howling checkbox
|
||||
CheckboxListTile(
|
||||
title: Text(AppLocalizations.of(context)!.heulen),
|
||||
value: heulenChecked,
|
||||
@@ -111,6 +141,7 @@ class _HinweiseState extends State<Hinweise> {
|
||||
setState(() => heulenChecked = value ?? false);
|
||||
updateController();
|
||||
}),
|
||||
// Other observations checkbox and input field
|
||||
CheckboxListTile(
|
||||
title: Text(AppLocalizations.of(context)!.sonstiges),
|
||||
value: sonstigesChecked,
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
// * Widget for recording presence of dogs and their leash status during excursions
|
||||
// * Features:
|
||||
// * - Dog presence selection (yes/no)
|
||||
// * - Conditional leash status selection
|
||||
// * - State persistence via text controllers
|
||||
// * - Localized labels
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for managing information about dogs during wildlife monitoring
|
||||
/// Tracks both presence of dogs and whether they are leashed
|
||||
class HundULeine extends StatefulWidget {
|
||||
// 1. with dog (ja, bzw name oder nein) 2. with leash
|
||||
// if nothing selected null
|
||||
/// Controller for dog presence status (yes/name or no)
|
||||
final TextEditingController mHund;
|
||||
/// Controller for leash status
|
||||
final TextEditingController mLeine;
|
||||
|
||||
const HundULeine({super.key, required this.mHund, required this.mLeine});
|
||||
@@ -13,13 +22,18 @@ class HundULeine extends StatefulWidget {
|
||||
HundULeineState createState() => HundULeineState();
|
||||
}
|
||||
|
||||
/// State class for the dog and leash selection widget
|
||||
class HundULeineState extends State<HundULeine> {
|
||||
/// Currently selected dog presence value
|
||||
late String _selectedMHundValue;
|
||||
/// Currently selected leash status value
|
||||
late String _selectedMLeineValue;
|
||||
/// Whether to show leash selection options
|
||||
bool visible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Initialize dog presence selection
|
||||
if (widget.mHund.text == "") {
|
||||
_selectedMHundValue = "nein";
|
||||
} else {
|
||||
@@ -27,6 +41,7 @@ class HundULeineState extends State<HundULeine> {
|
||||
visible = true;
|
||||
}
|
||||
|
||||
// Initialize leash status selection
|
||||
if (widget.mLeine.text == "") {
|
||||
_selectedMLeineValue = "nein";
|
||||
} else {
|
||||
@@ -36,6 +51,9 @@ class HundULeineState extends State<HundULeine> {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
/// Update widget state and controller values when selections change
|
||||
/// @param mHund New dog presence value
|
||||
/// @param mLeine New leash status value
|
||||
void onValueChanged(String mHund, String mLeine) {
|
||||
setState(() {
|
||||
visible = mHund == "ja" ? true : false;
|
||||
@@ -50,10 +68,12 @@ class HundULeineState extends State<HundULeine> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Dog presence section header
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(AppLocalizations.of(context)!.mHund),
|
||||
),
|
||||
// Dog presence - Yes option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.ja),
|
||||
@@ -65,6 +85,7 @@ class HundULeineState extends State<HundULeine> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Dog presence - No option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.nein),
|
||||
@@ -76,19 +97,14 @@ class HundULeineState extends State<HundULeine> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Conditional leash status section
|
||||
if (visible) ...[
|
||||
// TextField(
|
||||
// controller: controller,
|
||||
// onChanged: (value) {
|
||||
// onValueChanged("ja", _selectedMLeineValue);
|
||||
// },
|
||||
// decoration:
|
||||
// InputDecoration(hintText: AppLocalizations.of(context)!.name),
|
||||
// ),
|
||||
// Leash status section header
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(AppLocalizations.of(context)!.mLeine),
|
||||
),
|
||||
// Leash status - Yes option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.ja),
|
||||
@@ -100,6 +116,7 @@ class HundULeineState extends State<HundULeine> {
|
||||
},
|
||||
),
|
||||
),
|
||||
// Leash status - No option
|
||||
ListTile(
|
||||
visualDensity: const VisualDensity(vertical: -4),
|
||||
title: Text(AppLocalizations.of(context)!.nein),
|
||||
|
||||
@@ -1,7 +1,24 @@
|
||||
// * Widget for selecting the timing of last precipitation
|
||||
// * Features:
|
||||
// * - Dropdown menu for time selection
|
||||
// * - Multiple predefined time ranges
|
||||
// * - Localized time descriptions
|
||||
// * Available time ranges:
|
||||
// * - Currently raining
|
||||
// * - Same morning
|
||||
// * - Last night
|
||||
// * - Previous day/evening
|
||||
// * - 2-3 days ago
|
||||
// * - 4-6 days ago
|
||||
// * - 1 week or more
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for recording when the last precipitation occurred
|
||||
/// Used to track weather conditions during wildlife monitoring
|
||||
class LetzterNiederschlag extends StatefulWidget {
|
||||
/// Controller for storing the selected precipitation timing
|
||||
final TextEditingController controller;
|
||||
|
||||
const LetzterNiederschlag({super.key, required this.controller});
|
||||
@@ -10,11 +27,14 @@ class LetzterNiederschlag extends StatefulWidget {
|
||||
LetzterNiederschlagState createState() => LetzterNiederschlagState();
|
||||
}
|
||||
|
||||
/// State class for the last precipitation selection widget
|
||||
class LetzterNiederschlagState extends State<LetzterNiederschlag> {
|
||||
late String? selectedValue; // Variable für den ausgewählten Wert
|
||||
/// Currently selected precipitation timing value
|
||||
late String? selectedValue;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Initialize selection from controller
|
||||
if (widget.controller.text == "") {
|
||||
selectedValue = null;
|
||||
} else {
|
||||
@@ -36,30 +56,37 @@ class LetzterNiederschlagState extends State<LetzterNiederschlag> {
|
||||
});
|
||||
},
|
||||
items: [
|
||||
// Currently raining option
|
||||
DropdownMenuItem<String>(
|
||||
value: "aktuell",
|
||||
child: Text(AppLocalizations.of(context)!.aktuell),
|
||||
),
|
||||
// Same morning option
|
||||
DropdownMenuItem<String>(
|
||||
value: "am selben Morgen",
|
||||
child: Text(AppLocalizations.of(context)!.selberMorgen),
|
||||
),
|
||||
// Last night option
|
||||
DropdownMenuItem<String>(
|
||||
value: "in der Nacht",
|
||||
child: Text(AppLocalizations.of(context)!.letzteNacht),
|
||||
),
|
||||
// Previous day/evening option
|
||||
DropdownMenuItem<String>(
|
||||
value: "am Tag oder Abend zuvor",
|
||||
child: Text(AppLocalizations.of(context)!.vortag),
|
||||
),
|
||||
// 2-3 days ago option
|
||||
DropdownMenuItem<String>(
|
||||
value: "vor 2 bis 3 Tagen",
|
||||
child: Text(AppLocalizations.of(context)!.vor23Tagen),
|
||||
),
|
||||
// 4-6 days ago option
|
||||
DropdownMenuItem<String>(
|
||||
value: "vor 4 bis 6 Tagen",
|
||||
child: Text(AppLocalizations.of(context)!.vor46Tagen),
|
||||
),
|
||||
// 1 week or more option
|
||||
DropdownMenuItem<String>(
|
||||
value: ">=1 Woche",
|
||||
child: Text(AppLocalizations.of(context)!.vor1Woche),
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
// * Widget for recording wildlife track findings during excursions
|
||||
// * Features:
|
||||
// * - Track presence recording
|
||||
// * - Track length measurement
|
||||
// * - Animal count estimation
|
||||
// * - Confidence level indication
|
||||
// * - Separate tracking for cubs/pups
|
||||
// * - Nested visibility based on selections
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for managing wildlife track findings and measurements
|
||||
/// Includes options for both adult and cub/pup tracks
|
||||
class SpurGefunden extends StatefulWidget {
|
||||
/// Controller for track presence status
|
||||
final TextEditingController spurFund;
|
||||
/// Controller for total track length
|
||||
final TextEditingController spurLang;
|
||||
/// Controller for estimated number of animals
|
||||
final TextEditingController spurTiere;
|
||||
/// Controller for track identification confidence
|
||||
final TextEditingController spSicher;
|
||||
/// Controller for cub/pup track length
|
||||
final TextEditingController welpenSp;
|
||||
/// Controller for estimated number of cubs/pups
|
||||
final TextEditingController welpenAnz;
|
||||
/// Controller for cub/pup track identification confidence
|
||||
final TextEditingController wpSicher;
|
||||
|
||||
const SpurGefunden({
|
||||
@@ -25,14 +43,20 @@ class SpurGefunden extends StatefulWidget {
|
||||
State<SpurGefunden> createState() => _SpurGefundenState();
|
||||
}
|
||||
|
||||
/// State class for the track findings widget
|
||||
class _SpurGefundenState extends State<SpurGefunden> {
|
||||
/// Whether any tracks were found
|
||||
late bool _spurFundChecked;
|
||||
/// Whether adult track identification is confident
|
||||
bool _spSicher = false;
|
||||
/// Whether cub/pup track identification is confident
|
||||
bool _wpSicher = false;
|
||||
/// Whether cub/pup tracks were found
|
||||
late bool _welpenSpFundChecked;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Initialize track finding states
|
||||
if (widget.spurFund.text == "") {
|
||||
_spurFundChecked = false;
|
||||
_welpenSpFundChecked = false;
|
||||
@@ -53,6 +77,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Track presence checkbox
|
||||
Row(
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.spurGefunden),
|
||||
@@ -65,22 +90,14 @@ class _SpurGefundenState extends State<SpurGefunden> {
|
||||
});
|
||||
},
|
||||
),
|
||||
// Text(AppLocalizations.of(context)!.welpenSpurGefunden),
|
||||
// Checkbox(
|
||||
// value: _welpenSpFundChecked,
|
||||
// onChanged: (val) {
|
||||
// setState(() {
|
||||
// _welpenSpFundChecked = val ?? false;
|
||||
// widget.welpenSp.text = val ?? false ? "WelpenSp" : "";
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
],
|
||||
),
|
||||
// Track details section (visible when tracks found)
|
||||
Visibility(
|
||||
visible: _spurFundChecked,
|
||||
child: Column(
|
||||
children: [
|
||||
// Total track length input
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -101,7 +118,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// const SizedBox(height: 10),
|
||||
// Estimated animal count input
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -124,6 +141,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Track identification confidence
|
||||
Row(
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.sicher),
|
||||
@@ -139,7 +157,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// const SizedBox(height: 10),
|
||||
// Cub/pup track presence checkbox
|
||||
Row(
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.welpenSpurGefunden),
|
||||
@@ -153,10 +171,12 @@ class _SpurGefundenState extends State<SpurGefunden> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Cub/pup track details section
|
||||
Visibility(
|
||||
visible: _welpenSpFundChecked,
|
||||
child: Column(
|
||||
children: [
|
||||
// Cub/pup track length input
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -179,9 +199,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// const SizedBox(height: 10),
|
||||
|
||||
// Estimated cub/pup count input
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -204,6 +222,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Cub/pup track identification confidence
|
||||
Row(
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.sicher),
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
// * Widget for recording travel distances and track conditions during excursions
|
||||
// * Features:
|
||||
// * - Distance tracking by transportation mode (car, foot, bicycle)
|
||||
// * - Track condition assessment (good, medium, poor)
|
||||
// * - Automatic validation of total distances
|
||||
// * - Input validation with user feedback
|
||||
|
||||
import 'package:fforte/screens/helper/snack_bar_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
|
||||
/// Widget for managing travel distances and track conditions
|
||||
/// Tracks both how the distance was covered and the quality of tracks found
|
||||
class StreckeUSpurbedingungen extends StatefulWidget {
|
||||
/// Controller for distance traveled by car
|
||||
final TextEditingController kmAutoController;
|
||||
/// Controller for distance traveled on foot
|
||||
final TextEditingController kmFussController;
|
||||
/// Controller for distance traveled by bicycle
|
||||
final TextEditingController kmRadController;
|
||||
/// Controller for distance with good track conditions
|
||||
final TextEditingController spGutController;
|
||||
/// Controller for distance with medium track conditions
|
||||
final TextEditingController spMittelController;
|
||||
/// Controller for distance with poor track conditions
|
||||
final TextEditingController spSchlechtController;
|
||||
|
||||
const StreckeUSpurbedingungen({
|
||||
@@ -24,6 +39,7 @@ class StreckeUSpurbedingungen extends StatefulWidget {
|
||||
StreckeUSpurbedingungenState createState() => StreckeUSpurbedingungenState();
|
||||
}
|
||||
|
||||
/// State class for the distance and track conditions widget
|
||||
class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
// vars for percent text fields
|
||||
// String carPercent = "0";
|
||||
@@ -44,7 +60,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
// widget.kmFussController.addListener(onDistanceTravledUpdated);
|
||||
// widget.kmRadController.addListener(onDistanceTravledUpdated);
|
||||
|
||||
// if one of the values is "" the excursion is edited for the first time. On which value i check here is unnecessarry
|
||||
// Initialize distance values if not set
|
||||
if (widget.kmAutoController.text == "") {
|
||||
widget.kmAutoController.text = "0";
|
||||
widget.kmFussController.text = "0";
|
||||
@@ -56,7 +72,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
widget.spMittelController.addListener(onTrackConditionsUpdated);
|
||||
widget.spSchlechtController.addListener(onTrackConditionsUpdated);
|
||||
|
||||
// if one of the values is "" the excursion is edited for the first time. On which value i check here is unnecessarry
|
||||
// Initialize track condition values if not set
|
||||
if (widget.spGutController.text == "") {
|
||||
widget.spGutController.text = "0";
|
||||
widget.spMittelController.text = "0";
|
||||
@@ -87,20 +103,25 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
// }
|
||||
// }
|
||||
|
||||
/// Validate that track condition distances don't exceed total travel distance
|
||||
/// Shows warning if track conditions total is greater than distance traveled
|
||||
void onTrackConditionsUpdated() {
|
||||
try {
|
||||
// Parse track condition distances
|
||||
double kmGood = double.parse(widget.spGutController.text);
|
||||
double kmMiddle = double.parse(widget.spMittelController.text);
|
||||
double kmBad = double.parse(widget.spSchlechtController.text);
|
||||
|
||||
// Parse travel distances
|
||||
double kmAuto = double.parse(widget.kmAutoController.text);
|
||||
double kmFuss = double.parse(widget.kmFussController.text);
|
||||
double kmRad = double.parse(widget.kmRadController.text);
|
||||
|
||||
// Calculate totals
|
||||
double gesConditionsKm = (kmGood + kmMiddle + kmBad);
|
||||
double gesDistanceKm = (kmAuto + kmFuss + kmRad);
|
||||
|
||||
|
||||
// Show warning if track conditions exceed distance
|
||||
if (gesConditionsKm > gesDistanceKm) {
|
||||
SnackBarHelper.showSnackBarMessage(context, AppLocalizations.of(context)!.bedingungenGroesserAlsStrecke);
|
||||
}
|
||||
@@ -115,6 +136,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
// Travel distance section header
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(
|
||||
@@ -124,8 +146,10 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Travel distance inputs
|
||||
Row(
|
||||
children: [
|
||||
// Car distance input
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@@ -145,6 +169,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Foot distance input
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@@ -164,6 +189,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
),
|
||||
),
|
||||
|
||||
// Bicycle distance input
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
@@ -189,6 +215,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Track conditions section header
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(
|
||||
@@ -198,8 +225,10 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
),
|
||||
const SizedBox(height: 10,),
|
||||
|
||||
// Track condition inputs
|
||||
Row(
|
||||
children: [
|
||||
// Good conditions input
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -215,6 +244,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Medium conditions input
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -230,6 +260,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Poor conditions input
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
// * Widget for GPS tracking during wildlife monitoring excursions
|
||||
// * Features:
|
||||
// * - Real-time location tracking
|
||||
// * - Track visualization on map
|
||||
// * - Distance calculation
|
||||
// * - Location accuracy monitoring
|
||||
// * - Track recording controls (start/pause/stop)
|
||||
// * - Track data persistence
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
@@ -11,27 +20,40 @@ import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Widget for managing GPS tracking functionality
|
||||
/// Provides map visualization and tracking controls
|
||||
class Tracking extends StatefulWidget {
|
||||
/// Initial position for the tracking session
|
||||
final Position startPosition;
|
||||
/// Controller for storing the tracked path
|
||||
final TextEditingController weg;
|
||||
|
||||
const Tracking({super.key, required this.startPosition, required this.weg});
|
||||
|
||||
@override
|
||||
State<Tracking> createState() => _TrackingState();
|
||||
}
|
||||
|
||||
/// State class for the tracking widget
|
||||
class _TrackingState extends State<Tracking> {
|
||||
/// Service for managing tracking functionality
|
||||
final TrackingService _trackingService = TrackingService();
|
||||
/// Current GPS position
|
||||
Position? currentPosition;
|
||||
/// Controller for the map widget
|
||||
MapController mapController = MapController();
|
||||
/// Subscription for position updates
|
||||
StreamSubscription? _positionSubscription;
|
||||
/// Subscription for tracking statistics updates
|
||||
StreamSubscription? _statsSubscription;
|
||||
/// Current tracking statistics
|
||||
TrackingStats? _currentStats;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Load existing track if available
|
||||
if (widget.weg.text.isNotEmpty) {
|
||||
for (var element in widget.weg.text.split(";")) {
|
||||
List<String> posSplit = element.split(",");
|
||||
@@ -50,24 +72,27 @@ class _TrackingState extends State<Tracking> {
|
||||
|
||||
currentPosition = widget.startPosition;
|
||||
|
||||
// Initialisiere die Statistiken sofort
|
||||
// Initialize tracking statistics
|
||||
setState(() {
|
||||
_currentStats = _trackingService.currentStats;
|
||||
});
|
||||
_trackingService.requestStatsUpdate();
|
||||
|
||||
// Subscribe to position updates
|
||||
_positionSubscription = _trackingService.positionStream$.listen((position) {
|
||||
setState(() {
|
||||
currentPosition = position;
|
||||
});
|
||||
});
|
||||
|
||||
// Subscribe to statistics updates
|
||||
_statsSubscription = _trackingService.statsStream$.listen((stats) {
|
||||
setState(() {
|
||||
_currentStats = stats;
|
||||
});
|
||||
});
|
||||
|
||||
// Check location permissions
|
||||
GeolocatorService.alwaysPositionEnabled().then((value) {
|
||||
if (!value && mounted) {
|
||||
Navigator.of(context).pop();
|
||||
@@ -84,6 +109,9 @@ class _TrackingState extends State<Tracking> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Format distance for display
|
||||
/// @param meters Distance in meters
|
||||
/// @return Formatted distance string with appropriate unit
|
||||
String _formatDistance(double meters) {
|
||||
if (meters >= 1000) {
|
||||
return '${(meters / 1000).toStringAsFixed(2)} km';
|
||||
@@ -99,6 +127,7 @@ class _TrackingState extends State<Tracking> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.tracking),
|
||||
// Display tracking statistics if available
|
||||
if (_currentStats != null)
|
||||
DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodySmall!,
|
||||
@@ -125,6 +154,7 @@ class _TrackingState extends State<Tracking> {
|
||||
icon: Icon(Icons.arrow_back_rounded)
|
||||
),
|
||||
actions: [
|
||||
// Delete track button (only when not tracking)
|
||||
if (!_trackingService.isTracking)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
@@ -140,6 +170,7 @@ class _TrackingState extends State<Tracking> {
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
),
|
||||
),
|
||||
// Stop tracking button (only when tracking)
|
||||
if (_trackingService.isTracking)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -149,6 +180,7 @@ class _TrackingState extends State<Tracking> {
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.trackingStop),
|
||||
),
|
||||
// Start/Pause tracking button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@@ -165,6 +197,7 @@ class _TrackingState extends State<Tracking> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Center on current location button
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
mapController.move(
|
||||
@@ -177,6 +210,7 @@ class _TrackingState extends State<Tracking> {
|
||||
},
|
||||
child: Icon(Icons.my_location),
|
||||
),
|
||||
// Map display
|
||||
body: FlutterMap(
|
||||
mapController: mapController,
|
||||
options: MapOptions(
|
||||
@@ -192,10 +226,12 @@ class _TrackingState extends State<Tracking> {
|
||||
initialZoom: 16.0,
|
||||
),
|
||||
children: [
|
||||
// Base map layer
|
||||
TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'de.lupus.apps',
|
||||
),
|
||||
// Track path layer
|
||||
if (_trackingService.pathList.isNotEmpty)
|
||||
PolylineLayer(
|
||||
polylines: [
|
||||
@@ -206,6 +242,7 @@ class _TrackingState extends State<Tracking> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Current position accuracy circle
|
||||
if (currentPosition != null)
|
||||
CircleLayer(
|
||||
circles: [
|
||||
@@ -231,6 +268,7 @@ class _TrackingState extends State<Tracking> {
|
||||
),
|
||||
],
|
||||
),
|
||||
// Current location marker
|
||||
CurrentLocationLayer(),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
// * Helper class for managing entry-related dialogs
|
||||
// * Provides various dialog types:
|
||||
// * - Template creation dialog
|
||||
// * - Save options dialog
|
||||
// * - Server error handling dialog
|
||||
// * - Location settings dialog
|
||||
// * - Route deletion confirmation dialog
|
||||
|
||||
import 'package:fforte/enums/databases.dart';
|
||||
import 'package:fforte/screens/helper/snack_bar_helper.dart';
|
||||
import 'package:fforte/screens/sharedMethods/http_request.dart';
|
||||
@@ -8,8 +16,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
/// Helper class for managing various dialogs related to adding and saving entries
|
||||
class AddEntriesDialogHelper {
|
||||
// Function to show the dialog where the user has to choose if he want to safe his values as a template
|
||||
/// Show dialog for saving current data as a template
|
||||
/// @param context The BuildContext to show the dialog in
|
||||
/// @param saveData Map containing the data to save
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
static Future<void> showTemplateDialog(
|
||||
BuildContext context,
|
||||
Map<String, String> saveData,
|
||||
@@ -17,17 +29,19 @@ class AddEntriesDialogHelper {
|
||||
) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierDismissible: false, // User must make a choice
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.fieldEmpty),
|
||||
actions: <Widget>[
|
||||
// Cancel button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
),
|
||||
// Save as template button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
saveTemplate(saveData, dbType);
|
||||
@@ -45,6 +59,12 @@ class AddEntriesDialogHelper {
|
||||
);
|
||||
}
|
||||
|
||||
/// Show error dialog when server communication fails
|
||||
/// Offers options to retry or cancel
|
||||
/// @param context The BuildContext to show the dialog in
|
||||
/// @param saveData Map containing the data to save
|
||||
/// @param isTemplate Whether this is a template entry
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
static Future<dynamic> showServerErrorDialog(
|
||||
BuildContext context,
|
||||
Map<String, String> saveData,
|
||||
@@ -68,6 +88,7 @@ class AddEntriesDialogHelper {
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
// Retry button
|
||||
if (!isLoading)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -79,19 +100,18 @@ class AddEntriesDialogHelper {
|
||||
|
||||
if (errorCode == 200 && context.mounted) {
|
||||
Navigator.pop(context);
|
||||
// saveData(true);
|
||||
SaveMainEntryMethod.saveEntry(
|
||||
entryData: saveData,
|
||||
isTemplate: isTemplate,
|
||||
dbType: dbType,
|
||||
sent: true
|
||||
);
|
||||
|
||||
showSuccessDialog(context);
|
||||
}
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.sendagain),
|
||||
),
|
||||
// Cancel button
|
||||
if (!isLoading)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -107,6 +127,17 @@ class AddEntriesDialogHelper {
|
||||
);
|
||||
}
|
||||
|
||||
/// Show dialog with various save options
|
||||
/// Options include:
|
||||
/// - Save as template
|
||||
/// - Send to server
|
||||
/// - Save as file
|
||||
/// - Save locally only
|
||||
/// @param context The BuildContext to show the dialog in
|
||||
/// @param saveData Map containing the data to save
|
||||
/// @param isTemplate Whether this is a template entry
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
/// @return bool Whether the operation was completed successfully
|
||||
static Future<bool> showSaveOptionsDialog(
|
||||
BuildContext context,
|
||||
Map<String, String> saveData,
|
||||
@@ -118,7 +149,7 @@ class AddEntriesDialogHelper {
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierDismissible: false, // User must make a choice
|
||||
builder: (BuildContext context) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
@@ -135,6 +166,7 @@ class AddEntriesDialogHelper {
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
// Save as template button
|
||||
if (!isLoading)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -156,6 +188,7 @@ class AddEntriesDialogHelper {
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.template),
|
||||
),
|
||||
// Send to server button
|
||||
if (!isLoading)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -192,6 +225,7 @@ class AddEntriesDialogHelper {
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.sendtoserver),
|
||||
),
|
||||
// Save as file button
|
||||
if (!isLoading)
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -223,7 +257,7 @@ class AddEntriesDialogHelper {
|
||||
pop = true;
|
||||
}
|
||||
} catch (_) {
|
||||
// User cancelled the dialog
|
||||
// User cancelled the file save dialog
|
||||
}
|
||||
setState(() => isLoading = false);
|
||||
} catch (e) {
|
||||
@@ -238,6 +272,7 @@ class AddEntriesDialogHelper {
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.saveasfile),
|
||||
),
|
||||
// Save locally only button
|
||||
if (!isLoading)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -265,6 +300,7 @@ class AddEntriesDialogHelper {
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.justsave),
|
||||
),
|
||||
// Cancel button
|
||||
if (!isLoading)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -282,6 +318,8 @@ class AddEntriesDialogHelper {
|
||||
return pop;
|
||||
}
|
||||
|
||||
/// Show success dialog after successful save operation
|
||||
/// @param context The BuildContext to show the dialog in
|
||||
static Future<void> showSuccessDialog(BuildContext context) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
@@ -305,8 +343,10 @@ class AddEntriesDialogHelper {
|
||||
);
|
||||
}
|
||||
|
||||
/// Show dialog requesting location permission settings
|
||||
/// @param context The BuildContext to show the dialog in
|
||||
/// @return bool Whether the settings were changed
|
||||
static Future<bool> locationSettingsDialog(BuildContext context) async {
|
||||
|
||||
bool reload = false;
|
||||
|
||||
await showDialog(
|
||||
@@ -316,6 +356,7 @@ class AddEntriesDialogHelper {
|
||||
return AlertDialog(
|
||||
content: Text(AppLocalizations.of(context)!.needsAlwaysLocation),
|
||||
actions: [
|
||||
// Open settings button
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await Geolocator.openAppSettings();
|
||||
@@ -324,6 +365,7 @@ class AddEntriesDialogHelper {
|
||||
},
|
||||
child: Text("Ok"),
|
||||
),
|
||||
// Cancel button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
@@ -337,6 +379,9 @@ class AddEntriesDialogHelper {
|
||||
return reload;
|
||||
}
|
||||
|
||||
/// Show confirmation dialog for deleting entire route
|
||||
/// @param context The BuildContext to show the dialog in
|
||||
/// @return bool Whether deletion was confirmed
|
||||
static Future<bool> deleteCompleteRouteDialog(BuildContext context) async {
|
||||
bool confirmed = false;
|
||||
|
||||
@@ -354,6 +399,7 @@ class AddEntriesDialogHelper {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
// Confirm delete button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
confirmed = true;
|
||||
@@ -361,6 +407,7 @@ class AddEntriesDialogHelper {
|
||||
},
|
||||
child: Text("Ok"),
|
||||
),
|
||||
// Cancel button
|
||||
TextButton(
|
||||
onPressed: () => {Navigator.of(context).pop()},
|
||||
child: Text(AppLocalizations.of(context)!.cancel),
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
// * Helper class for displaying snackbar messages
|
||||
// * Provides a consistent way to show temporary notifications
|
||||
// * throughout the app
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Utility class for showing snackbar messages
|
||||
/// Contains static methods to display notifications
|
||||
class SnackBarHelper {
|
||||
/// Display a snackbar message at the bottom of the screen
|
||||
/// @param context The BuildContext to show the snackbar in
|
||||
/// @param message The text message to display
|
||||
static void showSnackBarMessage(BuildContext context, String message) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(SnackBar(content: Text(message)));
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
// * Helper class for displaying confirmation dialogs
|
||||
// * Used when viewing and managing database entries
|
||||
// * Provides dialogs for deleting entries and templates
|
||||
|
||||
import 'package:fforte/enums/databases.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
import 'package:fforte/screens/sharedMethods/delete_main_entries.dart';
|
||||
import 'package:fforte/screens/sharedMethods/delete_templates.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Helper class for managing confirmation dialogs
|
||||
/// Contains static methods for showing delete confirmation dialogs
|
||||
class ViewEntriesDialogHelper {
|
||||
/// Show confirmation dialog for deleting all main entries
|
||||
/// @param context The BuildContext to show the dialog in
|
||||
/// @param dbType The type of database (place/excursion) to delete from
|
||||
static Future<void> deleteAllMainEntries(
|
||||
BuildContext context,
|
||||
DatabasesEnum dbType,
|
||||
) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierDismissible: false, // User must make a choice
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.deleteEverything),
|
||||
@@ -23,14 +32,15 @@ class ViewEntriesDialogHelper {
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
// Delete confirmation button
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await DeleteMainEntries.deleteAll(dbType);
|
||||
|
||||
if (context.mounted) Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.deleteEverything),
|
||||
),
|
||||
// Cancel button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
@@ -43,13 +53,16 @@ class ViewEntriesDialogHelper {
|
||||
);
|
||||
}
|
||||
|
||||
/// Show confirmation dialog for deleting all templates
|
||||
/// @param context The BuildContext to show the dialog in
|
||||
/// @param dbType The type of database (place/excursion) to delete from
|
||||
static Future<void> deleteAllTemplates(
|
||||
BuildContext context,
|
||||
DatabasesEnum dbType,
|
||||
) async {
|
||||
return showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierDismissible: false, // User must make a choice
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.deleteEverything),
|
||||
@@ -61,6 +74,7 @@ class ViewEntriesDialogHelper {
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
// Delete confirmation button
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await DeleteTemplates.deleteAll(dbType);
|
||||
@@ -68,6 +82,7 @@ class ViewEntriesDialogHelper {
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.deleteEverything),
|
||||
),
|
||||
// Cancel button
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
@@ -79,5 +94,4 @@ class ViewEntriesDialogHelper {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
// * Initial setup screen shown on first app launch
|
||||
// * Allows users to configure:
|
||||
// * - Username/Address
|
||||
// * - Region settings
|
||||
// * - API endpoints for server communication
|
||||
// * Settings are persisted using SharedPreferences
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Widget for initial app configuration
|
||||
class IntroScreen extends StatefulWidget {
|
||||
const IntroScreen({super.key});
|
||||
|
||||
@@ -9,65 +17,52 @@ class IntroScreen extends StatefulWidget {
|
||||
State<IntroScreen> createState() => _IntroScreenState();
|
||||
}
|
||||
|
||||
/// State class for the intro screen
|
||||
class _IntroScreenState extends State<IntroScreen> {
|
||||
TextEditingController addresse1C = TextEditingController();
|
||||
TextEditingController bLandC = TextEditingController();
|
||||
TextEditingController ffApiAddress = TextEditingController();
|
||||
TextEditingController exApiAddress = TextEditingController();
|
||||
|
||||
String selectedFFApiAddress = "https://data.dbb-wolf.de/app24.php";
|
||||
String selectedEXApiAddress = "https://data.dbb-wolf.de/api_exkursion.php";
|
||||
String? selectedBLand = "Sachsen";
|
||||
// Text controllers for input fields
|
||||
final TextEditingController addresse1C = TextEditingController();
|
||||
final TextEditingController bLandC = TextEditingController();
|
||||
final TextEditingController ffApiAddress = TextEditingController();
|
||||
final TextEditingController exApiAddress = TextEditingController();
|
||||
|
||||
/// Save configuration data to SharedPreferences
|
||||
Future<void> _saveData() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Save user inputs
|
||||
await prefs.setString('addresse1', addresse1C.text);
|
||||
await prefs.setString('bLand', bLandC.text);
|
||||
await prefs.setBool('isFirstLaunch', false);
|
||||
await prefs.setString('fotofallenApiAddress', ffApiAddress.text);
|
||||
await prefs.setString('exkursionenApiAddress', exApiAddress.text);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPrefs();
|
||||
}
|
||||
|
||||
void _loadPrefs() {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
ffApiAddress.text = prefs.getString('fotofallenApiAddress') ?? "https://data.dbb-wolf.de/app24.php";
|
||||
exApiAddress.text = prefs.getString('exkursionenApiAddress') ?? "https://data.dbb-wolf.de/api_exkursion.php";
|
||||
addresse1C.text = prefs.getString('addresse1') ?? "";
|
||||
bLandC.text = prefs.getString('bLand') ?? "Sachsen";
|
||||
});
|
||||
});
|
||||
|
||||
// Mark app as initialized
|
||||
await prefs.setBool('isFirstLaunch', false);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('LUPUS'),
|
||||
title: const Text('LUPUS'),
|
||||
),
|
||||
body: Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(31),
|
||||
child: Column(
|
||||
children: [
|
||||
// Username/Address input
|
||||
TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: AppLocalizations.of(context)!.benutzername),
|
||||
hintText: AppLocalizations.of(context)!.benutzername
|
||||
),
|
||||
controller: addresse1C,
|
||||
onChanged: (value) => setState(() {
|
||||
addresse1C.text = value;
|
||||
}),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Region settings
|
||||
Column(
|
||||
children: [
|
||||
Row(
|
||||
@@ -79,193 +74,58 @@ class _IntroScreenState extends State<IntroScreen> {
|
||||
controller: bLandC,
|
||||
),
|
||||
),
|
||||
// Expanded(
|
||||
// flex: 1,
|
||||
// child: PopupMenuButton(
|
||||
// icon: const Icon(Icons.arrow_drop_down),
|
||||
// initialValue: selectedBLand,
|
||||
// onSelected: (value) {
|
||||
// setState(() {
|
||||
// selectedBLand = value;
|
||||
// bLandC.text = value;
|
||||
// });
|
||||
// },
|
||||
// itemBuilder: (context) => const <PopupMenuEntry>[
|
||||
// PopupMenuItem(
|
||||
// value: 'Baden-Württemberg',
|
||||
// child: Text('Baden-Württemberg'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Bayern',
|
||||
// child: Text('Bayern'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Berlin',
|
||||
// child: Text('Berlin'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Brandenburg',
|
||||
// child: Text('Brandenburg'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Bremen',
|
||||
// child: Text('Bremen'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Hamburg',
|
||||
// child: Text('Hamburg'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Hessen',
|
||||
// child: Text('Hessen'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Mecklenburg-Vorpommern',
|
||||
// child: Text('Mecklenburg-Vorpommern'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Niedersachsen',
|
||||
// child: Text('Niedersachsen'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Nordrhein-Westfalen',
|
||||
// child: Text('Nordrhein-Westfalen'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Rheinland-Pfalz',
|
||||
// child: Text('Rheinland-Pfalz'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Saarland',
|
||||
// child: Text('Saarland'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Sachsen',
|
||||
// child: Text('Sachsen'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Sachsen-Anhalt',
|
||||
// child: Text('Sachsen-Anhalt'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Schleswig-Holstein',
|
||||
// child: Text('Schleswig-Holstein'),
|
||||
// ),
|
||||
// PopupMenuItem(
|
||||
// value: 'Thüringen',
|
||||
// child: Text('Thüringen'),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 35,
|
||||
),
|
||||
const SizedBox(height: 35),
|
||||
|
||||
// Camera trap API endpoint
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(AppLocalizations.of(context)!.ffApiAddress)),
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(AppLocalizations.of(context)!.ffApiAddress)
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
// Expanded(
|
||||
// flex: 4,
|
||||
// child: TextField(
|
||||
// decoration: InputDecoration(
|
||||
// hintText:
|
||||
// AppLocalizations.of(context)!.ffApiAddress),
|
||||
// controller: ffApiAddress,
|
||||
// ),
|
||||
// ),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: ffApiAddress,
|
||||
),
|
||||
// child: PopupMenuButton(
|
||||
// icon: const Icon(Icons.arrow_drop_down),
|
||||
// initialValue: selectedFFApiAddress,
|
||||
// onSelected: (value) {
|
||||
// setState(() {
|
||||
// selectedFFApiAddress = value;
|
||||
// ffApiAddress.text = value;
|
||||
// });
|
||||
// },
|
||||
// itemBuilder: (context) => <PopupMenuEntry>[
|
||||
// // PopupMenuItem(
|
||||
// // value:
|
||||
// // "http://192.168.1.106/www.dbb-wolf.de/data/app24.php",
|
||||
// // child:
|
||||
// // Text(AppLocalizations.of(context)!.test)),
|
||||
// PopupMenuItem(
|
||||
// value: "https://data.dbb-wolf.de/app24.php",
|
||||
// child: Text(
|
||||
// AppLocalizations.of(context)!.notest))
|
||||
// ],
|
||||
// ),
|
||||
//)
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Excursion API endpoint
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(AppLocalizations.of(context)!.exApiAddress)),
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Text(AppLocalizations.of(context)!.exApiAddress)
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
// Expanded(
|
||||
// flex: 4,
|
||||
// child: TextField(
|
||||
// decoration: InputDecoration(
|
||||
// hintText:
|
||||
// AppLocalizations.of(context)!.exApiAddress),
|
||||
// controller: exApiAddress,
|
||||
// ),
|
||||
// ),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: exApiAddress,
|
||||
),
|
||||
// child: PopupMenuButton(
|
||||
// icon: const Icon(Icons.arrow_drop_down),
|
||||
// initialValue: selectedEXApiAddress,
|
||||
// onSelected: (value) {
|
||||
// setState(() {
|
||||
// selectedEXApiAddress = value;
|
||||
// exApiAddress.text = value;
|
||||
// });
|
||||
// },
|
||||
// itemBuilder: (context) => <PopupMenuEntry>[
|
||||
// // PopupMenuItem(
|
||||
// // value:
|
||||
// // "http://192.168.1.106/www.dbb-wolf.de/data/app24.php",
|
||||
// // child:
|
||||
// // Text(AppLocalizations.of(context)!.test)),
|
||||
// PopupMenuItem(
|
||||
// value:
|
||||
// "https://data.dbb-wolf.de/api_exkursion.php",
|
||||
// child: Text(
|
||||
// AppLocalizations.of(context)!.notest))
|
||||
// ],
|
||||
// )
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 15,
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Continue button - saves settings and goes to home
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
_saveData();
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context, '/home', (route) => false);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.continueB))
|
||||
onPressed: () {
|
||||
_saveData();
|
||||
Navigator.pushNamedAndRemoveUntil(
|
||||
context,
|
||||
'/home',
|
||||
(route) => false,
|
||||
);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.continueB)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
// * Settings screen for the LUPUS app
|
||||
// * Allows configuration of:
|
||||
// * - File storage location
|
||||
// * - GPS tracking interval
|
||||
// * All settings are persisted using SharedPreferences
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fforte/l10n/app_localizations.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
// * Widget for the settings screen
|
||||
class Settings extends StatefulWidget {
|
||||
const Settings({super.key});
|
||||
|
||||
@@ -9,22 +16,28 @@ class Settings extends StatefulWidget {
|
||||
State<Settings> createState() => _SettingsState();
|
||||
}
|
||||
|
||||
// * State class for the settings screen
|
||||
class _SettingsState extends State<Settings> {
|
||||
// Default tracking interval in seconds
|
||||
int _trackingInterval = 60;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadSettings();
|
||||
_loadSettings(); // Load saved settings on start
|
||||
}
|
||||
|
||||
// * Load settings from SharedPreferences
|
||||
Future<void> _loadSettings() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
setState(() {
|
||||
// Load tracking interval or use default (60 seconds)
|
||||
_trackingInterval = prefs.getInt('trackingInterval') ?? 60;
|
||||
});
|
||||
}
|
||||
|
||||
// * Save new tracking interval
|
||||
// * @param value The new interval in seconds
|
||||
Future<void> _saveTrackingInterval(int value) async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt('trackingInterval', value);
|
||||
@@ -33,6 +46,8 @@ class _SettingsState extends State<Settings> {
|
||||
});
|
||||
}
|
||||
|
||||
// * Get configured save directory
|
||||
// * @return The configured directory or empty string
|
||||
Future<String> _getSaveDir() async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final String saveDir = prefs.getString('saveDir') ?? "";
|
||||
@@ -42,15 +57,22 @@ class _SettingsState extends State<Settings> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings),),
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.settings),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(AppLocalizations.of(context)!.filelocation, style: const TextStyle(fontSize: 20),),
|
||||
// * File location section
|
||||
Text(
|
||||
AppLocalizations.of(context)!.filelocation,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
// Display current save directory
|
||||
FutureBuilder(
|
||||
future: _getSaveDir(),
|
||||
future: _getSaveDir(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text(snapshot.data ?? "");
|
||||
@@ -59,15 +81,22 @@ class _SettingsState extends State<Settings> {
|
||||
}
|
||||
}
|
||||
),
|
||||
// Button to open directory selection
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
onPressed: () {},
|
||||
child: Text(AppLocalizations.of(context)!.open)
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// * Tracking interval section
|
||||
const Text(
|
||||
'Tracking Interval (Sekunden)',
|
||||
style: TextStyle(fontSize: 20),
|
||||
),
|
||||
// Slider for interval adjustment
|
||||
// - Minimum: 10 seconds
|
||||
// - Maximum: 300 seconds (5 minutes)
|
||||
// - 29 divisions for precise control
|
||||
Slider(
|
||||
value: _trackingInterval.toDouble(),
|
||||
min: 10,
|
||||
@@ -78,6 +107,7 @@ class _SettingsState extends State<Settings> {
|
||||
_saveTrackingInterval(value.round());
|
||||
},
|
||||
),
|
||||
// Display current interval
|
||||
Text('Aktuelles Intervall: $_trackingInterval Sekunden'),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
class CheckRequired {
|
||||
static bool checkRequired(Map<String, Map<String, dynamic>> fieldsList) {
|
||||
for (String key in fieldsList.keys) {
|
||||
if (fieldsList[key]!["required"]! && fieldsList[key]!["controller"]!.text.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// * Utility class for validating required form fields
|
||||
// * Used to check if all required fields have been filled out
|
||||
// * before saving or submitting form data
|
||||
|
||||
return false;
|
||||
/// Helper class for form field validation
|
||||
class CheckRequired {
|
||||
/// Check if any required fields are empty
|
||||
/// @param fieldsList Map of field definitions with their required status and controllers
|
||||
/// @return true if any required field is empty, false otherwise
|
||||
static bool checkRequired(Map<String, Map<String, dynamic>> fieldsList) {
|
||||
// Iterate through all fields
|
||||
for (String key in fieldsList.keys) {
|
||||
// Check if field is required and empty
|
||||
if (fieldsList[key]!["required"]! && fieldsList[key]!["controller"]!.text.isEmpty) {
|
||||
return true; // Found an empty required field
|
||||
}
|
||||
}
|
||||
|
||||
return false; // All required fields are filled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
// * Shared methods for deleting main entries from the database
|
||||
// * Provides functionality for:
|
||||
// * - Deleting all entries of a specific type
|
||||
// * - Deleting a single entry by ID
|
||||
|
||||
import 'package:fforte/enums/databases.dart';
|
||||
import 'package:fforte/interfaces/i_db.dart';
|
||||
import 'package:fforte/methods/excursion_db_helper.dart';
|
||||
import 'package:fforte/methods/place_db_helper.dart';
|
||||
|
||||
/// Helper class for deleting main entries from the database
|
||||
class DeleteMainEntries {
|
||||
/// Delete all main entries of a specific type
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
static Future<void> deleteAll(DatabasesEnum dbType) async {
|
||||
// Select appropriate database helper
|
||||
IDb? db;
|
||||
|
||||
if (dbType == DatabasesEnum.place) {
|
||||
@@ -15,7 +24,11 @@ class DeleteMainEntries {
|
||||
await db!.deleteAllMainEntries();
|
||||
}
|
||||
|
||||
/// Delete a single main entry by ID
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
/// @param id ID of the entry to delete
|
||||
static Future<void> deleteSingle(DatabasesEnum dbType, int id) async {
|
||||
// Select appropriate database helper
|
||||
IDb? db;
|
||||
|
||||
if (dbType == DatabasesEnum.place) {
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
// * Shared methods for deleting templates from the database
|
||||
// * Provides functionality for:
|
||||
// * - Deleting all templates of a specific type
|
||||
// * - Deleting a single template by ID
|
||||
|
||||
import 'package:fforte/enums/databases.dart';
|
||||
import 'package:fforte/interfaces/i_db.dart';
|
||||
import 'package:fforte/methods/excursion_db_helper.dart';
|
||||
import 'package:fforte/methods/place_db_helper.dart';
|
||||
|
||||
/// Helper class for deleting templates from the database
|
||||
class DeleteTemplates {
|
||||
|
||||
/// Delete a single template by ID
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
/// @param id ID of the template to delete
|
||||
static Future<void> deleteSingle(DatabasesEnum dbType, String id) async {
|
||||
// Select appropriate database helper
|
||||
IDb? db;
|
||||
|
||||
if (dbType == DatabasesEnum.place) {
|
||||
@@ -17,7 +26,10 @@ class DeleteTemplates {
|
||||
await db!.deleteTemplateById(id);
|
||||
}
|
||||
|
||||
/// Delete all templates of a specific type
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
static Future<void> deleteAll(DatabasesEnum dbType) async {
|
||||
// Select appropriate database helper
|
||||
IDb? db;
|
||||
|
||||
if (dbType == DatabasesEnum.place) {
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
// * Service for handling HTTP requests to the backend API
|
||||
// * Features:
|
||||
// * - Support for camera trap and excursion data endpoints
|
||||
// * - Configurable timeouts
|
||||
// * - Error handling
|
||||
// * - JSON data formatting
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Service class for making HTTP requests to the backend
|
||||
/// Handles both camera trap and excursion data submissions
|
||||
class HttpRequestService {
|
||||
/// Send data to the appropriate backend endpoint
|
||||
/// @param saveDataMap Optional map of data to send
|
||||
/// @param saveDataString Optional string of data to send
|
||||
/// @return Future<int> HTTP status code of the response
|
||||
static Future<int> httpRequest({Map<String, String>? saveDataMap, String? saveDataString}) async {
|
||||
// print(jsonEncode(place));
|
||||
|
||||
@@ -18,6 +31,7 @@ class HttpRequestService {
|
||||
Response(requestOptions: RequestOptions(path: ''), statusCode: 400);
|
||||
|
||||
try {
|
||||
// Choose endpoint based on data type (camera trap vs excursion)
|
||||
if (saveDataMap != null && saveDataMap.containsKey("CID") || saveDataString != null && saveDataString.contains("CID")) {
|
||||
response = await dio.post(prefs.getString('fotofallenApiAddress') ?? "",
|
||||
data: saveDataMap == null ? saveDataString : jsonEncode(saveDataMap));
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
// * Shared method for saving entries to text files
|
||||
// * Allows users to:
|
||||
// * - Select a save directory
|
||||
// * - Save entries as JSON files
|
||||
// * - Persist the chosen directory for future use
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -7,7 +13,14 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Helper class for saving entries to files
|
||||
class SaveFileMethod {
|
||||
/// Save an entry to a text file in JSON format
|
||||
/// @param place Map containing the entry data
|
||||
/// @param id ID of the entry
|
||||
/// @param fileNameLocalization Localized prefix for the filename
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
/// @throws FileDialogCancelled if user cancels directory selection
|
||||
static Future<void> saveFile(
|
||||
Map<String, String> place,
|
||||
int id,
|
||||
@@ -15,24 +28,32 @@ class SaveFileMethod {
|
||||
DatabasesEnum dbType,
|
||||
) async {
|
||||
try {
|
||||
// Let user select save directory
|
||||
String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
|
||||
|
||||
if (selectedDirectory == null) {
|
||||
throw FileDialogCancelled();
|
||||
}
|
||||
|
||||
// Save entry as JSON
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String jsonPlace = jsonEncode(place);
|
||||
|
||||
// Remember selected directory for future use
|
||||
await prefs.setString('saveDir', selectedDirectory);
|
||||
|
||||
// Create file with format: prefix-id-identifier.txt
|
||||
// For places: identifier = CID
|
||||
// For excursions: identifier = date
|
||||
File file = File(
|
||||
'$selectedDirectory/$fileNameLocalization-$id-${dbType == DatabasesEnum.place ? place["CID"] : place["Datum"]!.split(" ").first}.txt',
|
||||
);
|
||||
|
||||
// Write JSON data to file
|
||||
await file.writeAsString(jsonPlace);
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
rethrow; // Re-throw to allow proper error handling by caller
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,60 @@
|
||||
// * Shared method for saving main entries to the database
|
||||
// * Handles both place and excursion entries
|
||||
// * Supports:
|
||||
// * - Creating new entries
|
||||
// * - Converting templates to entries
|
||||
// * - Updating existing entries
|
||||
// * - Marking entries as sent to server
|
||||
|
||||
import 'package:fforte/enums/databases.dart';
|
||||
import 'package:fforte/interfaces/i_db.dart';
|
||||
import 'package:fforte/methods/excursion_db_helper.dart';
|
||||
import 'package:fforte/methods/place_db_helper.dart';
|
||||
|
||||
/// Helper class for saving main entries to the database
|
||||
class SaveMainEntryMethod {
|
||||
/// Save or update a main entry in the database
|
||||
/// @param entryData Map containing the entry data
|
||||
/// @param isTemplate Whether this is being converted from a template
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
/// @param sent Whether the entry has been sent to the server
|
||||
/// @return ID of the saved entry
|
||||
static Future<int> saveEntry({
|
||||
required Map<String, String> entryData,
|
||||
required bool isTemplate,
|
||||
required DatabasesEnum dbType,
|
||||
bool sent = false,
|
||||
}) async {
|
||||
|
||||
// Select appropriate database helper
|
||||
IDb? placeDB;
|
||||
|
||||
if (dbType == DatabasesEnum.place) {
|
||||
placeDB = PlaceDBHelper();
|
||||
placeDB = PlaceDBHelper();
|
||||
} else if (dbType == DatabasesEnum.excursion) {
|
||||
placeDB = ExcursionDBHelper();
|
||||
placeDB = ExcursionDBHelper();
|
||||
}
|
||||
|
||||
// If converting from template, delete the template first
|
||||
if (isTemplate) await placeDB!.deleteTemplateById(entryData["ID"]!);
|
||||
|
||||
// Handle new entry creation vs update
|
||||
int entryId;
|
||||
if (entryData["ID"] == "" || isTemplate) {
|
||||
// Create new entry
|
||||
entryData.remove("ID");
|
||||
entryId = await placeDB!.addMainEntry(entryData);
|
||||
// Commented out template deletion by CID
|
||||
// await placeDB.deleteTemplateById(entryData["CID"]!);
|
||||
} else {
|
||||
// Update existing entry
|
||||
entryId = await placeDB!.updateMainEntry(entryData);
|
||||
}
|
||||
|
||||
// Update sent status if entry was sent to server
|
||||
if (sent == true) {
|
||||
placeDB.updateSent(entryId); // Update 'Sent' using the correct ID
|
||||
placeDB.updateSent(entryId);
|
||||
}
|
||||
|
||||
return entryId;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
// * Shared method for saving templates to the database
|
||||
// * Handles both place and excursion templates
|
||||
// * Supports both creating new templates and updating existing ones
|
||||
|
||||
import 'package:fforte/enums/databases.dart';
|
||||
import 'package:fforte/interfaces/i_db.dart';
|
||||
import 'package:fforte/methods/excursion_db_helper.dart';
|
||||
import 'package:fforte/methods/place_db_helper.dart';
|
||||
|
||||
/// Save or update a template in the database
|
||||
/// @param templateData Map containing the template data
|
||||
/// @param dbType The type of database (place/excursion)
|
||||
/// @return ID of the saved template, or -1 if operation failed
|
||||
Future<int> saveTemplate(Map<String, String> templateData, DatabasesEnum dbType,) async {
|
||||
// Select appropriate database helper
|
||||
IDb dbHelper;
|
||||
int id =templateData["ID"]! != "" ? int.parse(templateData["ID"]!) : -1;
|
||||
int id = templateData["ID"]! != "" ? int.parse(templateData["ID"]!) : -1;
|
||||
|
||||
if (dbType == DatabasesEnum.place) {
|
||||
dbHelper = PlaceDBHelper();
|
||||
} else if (dbType == DatabasesEnum.excursion) {
|
||||
dbHelper = ExcursionDBHelper();
|
||||
} else {
|
||||
return -1;
|
||||
return -1; // Invalid database type
|
||||
}
|
||||
|
||||
// Remove sent status as it's not needed for templates
|
||||
templateData.remove("Sent");
|
||||
|
||||
// Handle new template creation vs update
|
||||
if (templateData["ID"]! == "" || templateData["ID"]! == "-1") {
|
||||
// Create new template
|
||||
templateData.remove("ID");
|
||||
id = await dbHelper.addTemplate(templateData);
|
||||
} else {
|
||||
// Update existing template
|
||||
await dbHelper.updateTemplate(templateData);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
// * Shared method for sending files to the server
|
||||
// * Allows users to:
|
||||
// * - Select a file using the system file picker
|
||||
// * - Send the file contents to the server
|
||||
// * Legacy widget implementation is kept for reference
|
||||
|
||||
import 'package:fforte/screens/sharedMethods/http_request.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'dart:io';
|
||||
|
||||
/// Helper class for sending files to the server
|
||||
class SendFile {
|
||||
/// Let user pick a file and send its contents to the server
|
||||
/// Uses the system file picker for file selection
|
||||
/// Sends file content using the HttpRequestService
|
||||
static Future<void> sendFile() async {
|
||||
File? pickedFile;
|
||||
|
||||
// Open file picker dialog
|
||||
FilePickerResult? result = await FilePicker.platform.pickFiles();
|
||||
|
||||
if (result != null) {
|
||||
// Read and send file contents
|
||||
pickedFile = File(result.files.single.path!);
|
||||
String fileContent = await pickedFile.readAsString();
|
||||
await HttpRequestService.httpRequest(saveDataString: fileContent);
|
||||
@@ -17,64 +28,67 @@ class SendFile {
|
||||
}
|
||||
}
|
||||
|
||||
// class SendFile extends StatefulWidget {
|
||||
// const SendFile({super.key});
|
||||
//
|
||||
// @override
|
||||
// State<SendFile> createState() => _SendFileState();
|
||||
// }
|
||||
//
|
||||
// class _SendFileState extends State<SendFile> {
|
||||
// File? pickedFile;
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return Scaffold(
|
||||
// appBar: AppBar(),
|
||||
// body: Column(
|
||||
// children: [
|
||||
// ElevatedButton(
|
||||
// onPressed: () async {
|
||||
// FilePickerResult? result =
|
||||
// await FilePicker.platform.pickFiles();
|
||||
//
|
||||
// if (result != null) {
|
||||
// pickedFile = File(result.files.single.path!);
|
||||
// } else {
|
||||
// pickedFile = File("");
|
||||
// }
|
||||
// },
|
||||
// child: Text(AppLocalizations.of(context)!.pickfile)),
|
||||
// Text(pickedFile.toString()),
|
||||
// ElevatedButton(
|
||||
// onPressed: () async {
|
||||
// final dio = Dio();
|
||||
// final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
// String? fileContent = await pickedFile?.readAsString();
|
||||
//
|
||||
// dio.options.responseType = ResponseType.plain;
|
||||
// Response response = Response(
|
||||
// requestOptions: RequestOptions(path: ''), statusCode: 400);
|
||||
//
|
||||
// try {
|
||||
// response = await dio.post(prefs.getString('apiAddress') ?? "",
|
||||
// data: jsonEncode(fileContent));
|
||||
// } on DioException catch (e) {
|
||||
// if (e.response?.statusCode == 500) {
|
||||
// /* print('-------------------------');
|
||||
// print('code 500'); */
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// if (response.statusCode == 201) {
|
||||
// // print(response.statusCode);
|
||||
// } else {
|
||||
// //print(response.statusCode);
|
||||
// }
|
||||
// },
|
||||
// child: Text(AppLocalizations.of(context)!.sendtoserver))
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// * Legacy widget implementation kept for reference
|
||||
// * This was a stateful widget version of the file sender
|
||||
// * with additional UI elements and error handling
|
||||
/*
|
||||
class SendFile extends StatefulWidget {
|
||||
const SendFile({super.key});
|
||||
|
||||
@override
|
||||
State<SendFile> createState() => _SendFileState();
|
||||
}
|
||||
|
||||
class _SendFileState extends State<SendFile> {
|
||||
File? pickedFile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Column(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
FilePickerResult? result =
|
||||
await FilePicker.platform.pickFiles();
|
||||
|
||||
if (result != null) {
|
||||
pickedFile = File(result.files.single.path!);
|
||||
} else {
|
||||
pickedFile = File("");
|
||||
}
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.pickfile)),
|
||||
Text(pickedFile.toString()),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final dio = Dio();
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
String? fileContent = await pickedFile?.readAsString();
|
||||
|
||||
dio.options.responseType = ResponseType.plain;
|
||||
Response response = Response(
|
||||
requestOptions: RequestOptions(path: ''), statusCode: 400);
|
||||
|
||||
try {
|
||||
response = await dio.post(prefs.getString('apiAddress') ?? "",
|
||||
data: jsonEncode(fileContent));
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 500) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (response.statusCode == 201) {
|
||||
// Success handling was here
|
||||
} else {
|
||||
// Error handling was here
|
||||
}
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.sendtoserver))
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
// * Shared widget for date selection across the application
|
||||
// * Features:
|
||||
// * - Date picker dialog interface
|
||||
// * - Formatted date display
|
||||
// * - Customizable button label
|
||||
// * - Date range validation
|
||||
// * - Callback for date changes
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Widget for managing date selection
|
||||
/// Provides a button to open date picker and displays selected date
|
||||
class Datum extends StatefulWidget {
|
||||
/// Initial date value
|
||||
final DateTime? initDatum;
|
||||
/// Callback function when date changes
|
||||
final Function(DateTime) onDateChanged;
|
||||
/// Label for the date picker button
|
||||
final String name;
|
||||
|
||||
const Datum(
|
||||
@@ -12,7 +25,9 @@ class Datum extends StatefulWidget {
|
||||
State<Datum> createState() => _DatumState();
|
||||
}
|
||||
|
||||
/// State class for the date selection widget
|
||||
class _DatumState extends State<Datum> {
|
||||
/// Currently selected date
|
||||
DateTime? datum;
|
||||
|
||||
@override
|
||||
@@ -26,6 +41,7 @@ class _DatumState extends State<Datum> {
|
||||
return Row(
|
||||
children: [
|
||||
Row(children: [
|
||||
// Date picker button
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: ElevatedButton(
|
||||
@@ -40,6 +56,7 @@ class _DatumState extends State<Datum> {
|
||||
const SizedBox(
|
||||
width: 10,
|
||||
),
|
||||
// Formatted date display
|
||||
Text(
|
||||
'${datum?.day}. ${datum?.month}. ${datum?.year}',
|
||||
),
|
||||
@@ -48,6 +65,8 @@ class _DatumState extends State<Datum> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows date picker dialog and returns selected date
|
||||
/// @return Future<DateTime?> Selected date or null if cancelled
|
||||
Future<DateTime?> pickDate() async {
|
||||
final date = await showDatePicker(
|
||||
context: context,
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
// * Shared widget for text input fields with database integration
|
||||
// * Features:
|
||||
// * - Customizable text input field
|
||||
// * - Database value suggestions
|
||||
// * - Required field validation
|
||||
// * - Default value support
|
||||
// * - Visual feedback for validation state
|
||||
// * - Dropdown for previous entries
|
||||
|
||||
import 'package:fforte/enums/databases.dart';
|
||||
import 'package:fforte/methods/excursion_db_helper.dart';
|
||||
import 'package:fforte/methods/place_db_helper.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Widget for managing text input with database integration
|
||||
/// Provides suggestions from previous entries and validation
|
||||
class VarTextField extends StatefulWidget {
|
||||
/// Controller for the text input
|
||||
final TextEditingController textController;
|
||||
/// Localized label/hint text
|
||||
final String localization;
|
||||
/// Database type (place or excursion)
|
||||
final DatabasesEnum dbDesignation;
|
||||
/// Database field name
|
||||
final String dbName;
|
||||
/// Default value key for preferences
|
||||
final String? defaultValue;
|
||||
/// Whether the field is required
|
||||
final bool required;
|
||||
|
||||
const VarTextField({
|
||||
@@ -26,24 +43,31 @@ class VarTextField extends StatefulWidget {
|
||||
State<VarTextField> createState() => _VarTextFieldState();
|
||||
}
|
||||
|
||||
/// State class for the variable text field widget
|
||||
class _VarTextFieldState extends State<VarTextField> {
|
||||
/// List of previous values from database
|
||||
List<String> dbVar = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Load default value if field is empty
|
||||
if (widget.textController.text == "" && widget.defaultValue != null) {
|
||||
_loadPref();
|
||||
}
|
||||
|
||||
// Load previous values from database
|
||||
_loadData().then((e) => dbVar = e);
|
||||
}
|
||||
|
||||
/// Load previous values from the appropriate database
|
||||
/// @return Future<List<String>> List of previous values
|
||||
Future<List<String>> _loadData() async {
|
||||
List<Map<String, dynamic>> entries = [];
|
||||
List<Map<String, dynamic>> templatesEntries = [];
|
||||
|
||||
// Get entries from appropriate database
|
||||
if (widget.dbDesignation == DatabasesEnum.place) {
|
||||
entries = await PlaceDBHelper().getAllMainEntries();
|
||||
templatesEntries = await PlaceDBHelper().getAllTemplates();
|
||||
@@ -54,6 +78,7 @@ class _VarTextFieldState extends State<VarTextField> {
|
||||
|
||||
List<String> erg = [];
|
||||
|
||||
// Extract values for this field from entries
|
||||
for (var element in entries) {
|
||||
for (var key in element.keys) {
|
||||
if (key == widget.dbName && element[key].toString() != "") {
|
||||
@@ -62,6 +87,7 @@ class _VarTextFieldState extends State<VarTextField> {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract values from templates
|
||||
for (var element in templatesEntries) {
|
||||
for (var key in element.keys) {
|
||||
if (key == widget.dbName && element[key].toString() != "") {
|
||||
@@ -73,6 +99,7 @@ class _VarTextFieldState extends State<VarTextField> {
|
||||
return erg;
|
||||
}
|
||||
|
||||
/// Load default value from preferences
|
||||
void _loadPref() {
|
||||
Future.delayed(Duration.zero, () async {
|
||||
SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
@@ -87,6 +114,7 @@ class _VarTextFieldState extends State<VarTextField> {
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
// Text input field
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: TextField(
|
||||
@@ -100,6 +128,7 @@ class _VarTextFieldState extends State<VarTextField> {
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.localization,
|
||||
// Border color based on required status and value
|
||||
enabledBorder:
|
||||
widget.required
|
||||
? (widget.textController.text.isEmpty
|
||||
@@ -128,6 +157,7 @@ class _VarTextFieldState extends State<VarTextField> {
|
||||
),
|
||||
),
|
||||
const Expanded(child: SizedBox(width: 15)),
|
||||
// Dropdown for previous values
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Align(
|
||||
|
||||
@@ -337,65 +337,6 @@ class _ViewEntriesState extends State<ViewEntries> {
|
||||
MarkerLayer(markers: marker),
|
||||
],
|
||||
),
|
||||
// ),
|
||||
// child: FutureBuilder(
|
||||
// future: mainEntries,
|
||||
// builder: (context, snapshot) {
|
||||
// if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
// return const CircularProgressIndicator();
|
||||
// } else if (snapshot.hasError) {
|
||||
// return Text("Error ${snapshot.error}");
|
||||
// } else {
|
||||
// if (snapshot.data != null) {
|
||||
// markers =
|
||||
// snapshot.data!.map((e) {
|
||||
// return Marker(
|
||||
// width: 80.0,
|
||||
// height: 80.0,
|
||||
// point: LatLng(
|
||||
// double.parse(e['DECLAT'].toString()),
|
||||
// double.parse(e['DECLNG'].toString()),
|
||||
// ),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// const Icon(
|
||||
// Icons.location_on,
|
||||
// color: Colors.red,
|
||||
// ),
|
||||
// Text(
|
||||
// "ID: ${e['ID'].toString()}",
|
||||
// style: const TextStyle(color: Colors.black),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }).toList();
|
||||
// }
|
||||
// return FlutterMap(
|
||||
// options: MapOptions(
|
||||
// initialCenter:
|
||||
// markers.isEmpty
|
||||
// ? const LatLng(50, 10)
|
||||
// : markers.first.point,
|
||||
// interactionOptions: const InteractionOptions(
|
||||
// flags:
|
||||
// InteractiveFlag.pinchZoom |
|
||||
// InteractiveFlag.drag |
|
||||
// InteractiveFlag.pinchMove,
|
||||
// ),
|
||||
// ),
|
||||
// children: [
|
||||
// TileLayer(
|
||||
// urlTemplate:
|
||||
// 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
// userAgentPackageName: 'com.example.app',
|
||||
// ),
|
||||
// MarkerLayer(markers: markers),
|
||||
// ],
|
||||
// );
|
||||
// } // REMOVE
|
||||
// }, // REMOVE
|
||||
// ), // REMOVE
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,44 +1,69 @@
|
||||
// * Service for managing local notifications in the app
|
||||
// * Handles notification permissions, creation, and management
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
// * Notification service class that handles all notification functionality
|
||||
class NotificationService {
|
||||
// Plugin instance for local notifications
|
||||
final notifiactionPlugin = FlutterLocalNotificationsPlugin();
|
||||
|
||||
// Initialization status flag
|
||||
bool _isInitialized = false;
|
||||
|
||||
// Getter for initialization status
|
||||
bool get isInitialized => _isInitialized;
|
||||
|
||||
// * Initialize the notification service
|
||||
// * - Requests notification permissions
|
||||
// * - Configures Android-specific settings
|
||||
// * - Initializes the notification plugin
|
||||
Future<void> initNotification() async {
|
||||
// Prevent multiple initializations
|
||||
if (_isInitialized) return;
|
||||
|
||||
// Request permissions for Android notifications
|
||||
await notifiactionPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()!.requestNotificationsPermission();
|
||||
|
||||
|
||||
// Android-specific initialization settings
|
||||
const initSettingsAndroid = AndroidInitializationSettings(
|
||||
'@mipmap/ic_launcher',
|
||||
'@mipmap/ic_launcher', // App icon for notifications
|
||||
);
|
||||
|
||||
// Overall initialization settings
|
||||
const initSettings = InitializationSettings(android: initSettingsAndroid);
|
||||
|
||||
// Initialize plugin
|
||||
await notifiactionPlugin.initialize(initSettings);
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
// * Create default notification settings
|
||||
// * Configures a notification with:
|
||||
// * - Low importance and priority
|
||||
// * - Ongoing status
|
||||
// * - Specific channel for tracking
|
||||
NotificationDetails notificationDetails() {
|
||||
return const NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
"tracking0",
|
||||
"tracking ongoing",
|
||||
"tracking0", // Channel ID
|
||||
"tracking ongoing", // Channel name
|
||||
importance: Importance.low,
|
||||
priority: Priority.low,
|
||||
ongoing: true,
|
||||
ongoing: true, // Notification persists
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// * Show a new notification
|
||||
// * @param id The notification ID (default: 0)
|
||||
// * @param title The notification title
|
||||
Future<void> showNotification({int id = 0, String? title}) async {
|
||||
return notifiactionPlugin.show(id, title, "", notificationDetails());
|
||||
}
|
||||
|
||||
// * Delete an existing notification
|
||||
// * @param id The ID of the notification to delete (default: 0)
|
||||
Future<void> deleteNotification({id = 0}) async {
|
||||
await notifiactionPlugin.cancel(id);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ import 'package:geolocator/geolocator.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Service, with the Singleton design pattern, that runs the geolocator service that tracks the position of the device.
|
||||
/// This is needed for excursions
|
||||
/// Service that runs the geolocator service to track the device's position
|
||||
/// Uses the Singleton design pattern to ensure only one tracking instance exists
|
||||
/// This service is essential for excursion tracking functionality
|
||||
///
|
||||
/// Start the tracking service via [startTracking]
|
||||
/// Manage the position stream via [pauseTracking], [stopTracking] and [resumeTracking]
|
||||
/// Start tracking via [startTracking]
|
||||
/// Control tracking via [pauseTracking], [stopTracking], and [resumeTracking]
|
||||
class TrackingService {
|
||||
// Singleton stuff
|
||||
// Singleton implementation
|
||||
static TrackingService? _instance;
|
||||
|
||||
factory TrackingService() {
|
||||
@@ -24,7 +25,7 @@ class TrackingService {
|
||||
|
||||
TrackingService._internal();
|
||||
|
||||
/// Resets all values, making it possible to start tracking again.
|
||||
/// Reset all tracking values for a fresh start
|
||||
static void resetInstance() {
|
||||
if (_instance != null) {
|
||||
_instance!.dispose();
|
||||
@@ -32,36 +33,38 @@ class TrackingService {
|
||||
}
|
||||
}
|
||||
|
||||
// Variables
|
||||
// - Stores the tracked coordinates
|
||||
// Core tracking variables
|
||||
// Stores tracked GPS coordinates
|
||||
List<LatLng> pathList = [];
|
||||
// - Stores all gotten accuracies
|
||||
// Stores GPS accuracy values
|
||||
List<double> accuracyList = [];
|
||||
// - Stores timer so that is responsible vor the periodically tracking
|
||||
// Timer for periodic tracking
|
||||
Timer? _positionTimer;
|
||||
// Current tracking status
|
||||
bool isTracking = false;
|
||||
// - Some more Singleton stuff (i guess. Vibecoded it because of lack of time)
|
||||
// Context for UI interactions
|
||||
BuildContext? _lastContext;
|
||||
// Stream controllers for position and stats updates
|
||||
final _positionController = StreamController<Position>.broadcast();
|
||||
final _statsController = StreamController<TrackingStats>.broadcast();
|
||||
|
||||
// - Stores the last measured accuracy so that it can be displayed in the excursions view
|
||||
double? currentAccuracy;
|
||||
|
||||
// - Getter
|
||||
// Stream getters
|
||||
Stream<Position> get positionStream$ => _positionController.stream;
|
||||
Stream<TrackingStats> get statsStream$ => _statsController.stream;
|
||||
|
||||
// Name says it all
|
||||
// Last measured GPS accuracy for display
|
||||
double? currentAccuracy;
|
||||
|
||||
/// Calculate median accuracy from a list of accuracy values
|
||||
/// This is preferred over mean because initial accuracy values can be very high (~9000m)
|
||||
double _calculateMedianAccuracy(List<double> accuracies) {
|
||||
// if one or less values for accuracy are available return that accuracy or 0
|
||||
if (accuracies.isEmpty) return 0;
|
||||
if (accuracies.length == 1) return accuracies.first;
|
||||
|
||||
// Copy the list so that the original data doesnt get modified
|
||||
// Copy list to preserve original data
|
||||
var sorted = List<double>.from(accuracies)..sort();
|
||||
|
||||
// Calculates median (not arithmetic mean!!). That is because often the firsed tracked accuracy is about 9000m
|
||||
// Calculate median
|
||||
if (sorted.length % 2 == 0) {
|
||||
int midIndex = sorted.length ~/ 2;
|
||||
return (sorted[midIndex - 1] + sorted[midIndex]) / 2;
|
||||
@@ -70,13 +73,20 @@ class TrackingService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts tracking
|
||||
/// Start position tracking
|
||||
/// - Initializes high-accuracy GPS tracking
|
||||
/// - Sets up periodic position updates
|
||||
/// - Shows tracking notification
|
||||
Future<void> startTracking(BuildContext context) async {
|
||||
if (isTracking) return;
|
||||
|
||||
// Configure high-accuracy GPS settings
|
||||
final LocationSettings locationSettings =
|
||||
LocationSettings(accuracy: LocationAccuracy.high);
|
||||
|
||||
_lastContext = context;
|
||||
|
||||
// Initialize and show tracking notification
|
||||
await NotificationService().initNotification();
|
||||
if (context.mounted) {
|
||||
NotificationService().showNotification(
|
||||
@@ -84,23 +94,25 @@ class TrackingService {
|
||||
);
|
||||
}
|
||||
|
||||
// Get tracking interval from settings
|
||||
// Load tracking interval from settings
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final intervalSeconds = prefs.getInt('trackingInterval') ?? 60;
|
||||
|
||||
// Create a timer that triggers position updates
|
||||
// Set up periodic position updates
|
||||
_positionTimer =
|
||||
Timer.periodic(Duration(seconds: intervalSeconds), (_) async {
|
||||
try {
|
||||
final Position position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: locationSettings);
|
||||
|
||||
// Store position and accuracy data
|
||||
pathList.add(LatLng(position.latitude, position.longitude));
|
||||
accuracyList.add(position.accuracy);
|
||||
currentAccuracy = position.accuracy;
|
||||
_positionController.add(position);
|
||||
_updateStats();
|
||||
} catch (e) {
|
||||
// Handle errors with notification
|
||||
NotificationService().deleteNotification();
|
||||
NotificationService().showNotification(title: "ERROR: $e");
|
||||
}
|
||||
@@ -124,9 +136,15 @@ class TrackingService {
|
||||
isTracking = true;
|
||||
}
|
||||
|
||||
// Last calculated tracking statistics
|
||||
TrackingStats? _lastStats;
|
||||
TrackingStats? get currentStats => _lastStats;
|
||||
|
||||
/// Update tracking statistics
|
||||
/// Calculates:
|
||||
/// - Current GPS accuracy
|
||||
/// - Average accuracy
|
||||
/// - Total distance traveled
|
||||
void _updateStats() {
|
||||
if (pathList.isEmpty) {
|
||||
_lastStats = TrackingStats(
|
||||
@@ -137,6 +155,7 @@ class TrackingService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate total distance
|
||||
double totalDistance = 0;
|
||||
for (int i = 1; i < pathList.length; i++) {
|
||||
totalDistance += _calculateDistance(
|
||||
@@ -155,19 +174,24 @@ class TrackingService {
|
||||
_statsController.add(_lastStats!);
|
||||
}
|
||||
|
||||
/// Request a manual stats update
|
||||
void requestStatsUpdate() {
|
||||
_updateStats();
|
||||
}
|
||||
|
||||
/// Calculate distance between two GPS coordinates using the Haversine formula
|
||||
/// @return Distance in meters
|
||||
double _calculateDistance(
|
||||
double lat1, double lon1, double lat2, double lon2) {
|
||||
const double earthRadius = 6371000; // Erdradius in Metern
|
||||
const double earthRadius = 6371000; // Earth radius in meters
|
||||
|
||||
// Convert coordinates to radians
|
||||
double lat1Rad = lat1 * math.pi / 180;
|
||||
double lat2Rad = lat2 * math.pi / 180;
|
||||
double deltaLat = (lat2 - lat1) * math.pi / 180;
|
||||
double deltaLon = (lon2 - lon1) * math.pi / 180;
|
||||
|
||||
// Haversine formula calculation
|
||||
double a = math.sin(deltaLat / 2) * math.sin(deltaLat / 2) +
|
||||
math.cos(lat1Rad) *
|
||||
math.cos(lat2Rad) *
|
||||
@@ -178,11 +202,13 @@ class TrackingService {
|
||||
return earthRadius * c;
|
||||
}
|
||||
|
||||
/// Temporarily pause tracking
|
||||
void pauseTracking() {
|
||||
_positionTimer?.cancel();
|
||||
isTracking = false;
|
||||
}
|
||||
|
||||
/// Resume paused tracking
|
||||
void resumeTracking() {
|
||||
if (!isTracking && _lastContext != null) {
|
||||
startTracking(_lastContext!);
|
||||
@@ -190,6 +216,7 @@ class TrackingService {
|
||||
isTracking = true;
|
||||
}
|
||||
|
||||
/// Stop tracking completely and clear current state
|
||||
void stopTracking() {
|
||||
_positionTimer?.cancel();
|
||||
NotificationService().deleteNotification();
|
||||
@@ -199,6 +226,7 @@ class TrackingService {
|
||||
_lastContext = null;
|
||||
}
|
||||
|
||||
/// Clear all recorded position data
|
||||
void clearPath() {
|
||||
pathList.clear();
|
||||
accuracyList.clear();
|
||||
@@ -206,10 +234,13 @@ class TrackingService {
|
||||
_updateStats();
|
||||
}
|
||||
|
||||
/// Convert tracked path to string format
|
||||
/// Format: "latitude,longitude;latitude,longitude;..."
|
||||
String getPathAsString() {
|
||||
return pathList.map((pos) => "${pos.latitude},${pos.longitude}").join(";");
|
||||
}
|
||||
|
||||
/// Clean up resources
|
||||
void dispose() {
|
||||
stopTracking();
|
||||
_positionController.close();
|
||||
@@ -217,6 +248,11 @@ class TrackingService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Data class for tracking statistics
|
||||
/// Contains:
|
||||
/// - Current GPS accuracy
|
||||
/// - Average accuracy
|
||||
/// - Total distance in meters
|
||||
class TrackingStats {
|
||||
final double currentAccuracy;
|
||||
final double averageAccuracy;
|
||||
|
||||
Reference in New Issue
Block a user