let AI comment everything because well... yeah...

This commit is contained in:
Nico
2025-06-06 21:00:32 +02:00
parent 9c84d0c375
commit cc110ac104
44 changed files with 1230 additions and 646 deletions

View File

@@ -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 { enum DatabasesEnum {
place, place, // Camera trap locations database
excursion, excursion, // Excursions and tracking database
} }

View File

@@ -3,25 +3,31 @@ import 'package:flutter/material.dart';
import 'l10n/app_localizations.dart'; import 'l10n/app_localizations.dart';
import 'screens/addCam/add_cam_main.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 { class HomePage extends StatelessWidget {
const HomePage({super.key}); const HomePage({super.key});
// Commented out legacy file sending functionality
// void _sendFile() async { // void _sendFile() async {
// // FilePickerResult? result = await FilePicker.platform.pickFiles(); // // FilePickerResult? result = await FilePicker.platform.pickFiles();
// // if (result != null) { // // if (result != null) {
// // File file = File(result.files.single.path!); // // File file = File(result.files.single.path!);
// // String content = await file.readAsString(); // // String content = await file.readAsString();
// // HttpRequest.httpRequest(saveDataString: content); // // HttpRequest.httpRequest(saveDataString: content);
// } // // }
// } // }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
// App bar with settings menu
appBar: AppBar( appBar: AppBar(
title: const Text("LUPUS"), title: const Text("LUPUS"),
actions: [ actions: [
@@ -29,28 +35,35 @@ class HomePage extends StatelessWidget {
onSelected: (value) { onSelected: (value) {
Navigator.pushNamed(context, value.toString()); Navigator.pushNamed(context, value.toString());
}, },
itemBuilder: itemBuilder: (context) => [
(context) => [ // Settings menu option
PopupMenuItem( PopupMenuItem(
value: '/settings', value: '/settings',
child: Text(AppLocalizations.of(context)!.settings), child: Text(AppLocalizations.of(context)!.settings),
), ),
PopupMenuItem( // Option to show intro screen
value: '/introScreen', PopupMenuItem(
child: Text(AppLocalizations.of(context)!.showloginscreen), value: '/introScreen',
), child: Text(AppLocalizations.of(context)!.showloginscreen),
], ),
],
), ),
], ],
), ),
// Main content area
body: Column( body: Column(
children: [ children: [
// App logo at the top
Image.asset('assets/images/reconix_small.png'), Image.asset('assets/images/reconix_small.png'),
Center( Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const SizedBox(height: 30), const SizedBox(height: 30),
// * Camera Trap Management Section
// Button to add new camera location
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(250, 40), minimumSize: const Size(250, 40),
@@ -66,6 +79,7 @@ class HomePage extends StatelessWidget {
}, },
child: Text(AppLocalizations.of(context)!.addplace), child: Text(AppLocalizations.of(context)!.addplace),
), ),
// Button to view camera locations
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(250, 40), minimumSize: const Size(250, 40),
@@ -73,19 +87,23 @@ class HomePage extends StatelessWidget {
onPressed: () => Navigator.pushNamed(context, '/viewCams'), onPressed: () => Navigator.pushNamed(context, '/viewCams'),
child: Text(AppLocalizations.of(context)!.viewplaces), child: Text(AppLocalizations.of(context)!.viewplaces),
), ),
// Visual section divider
const SizedBox(height: 20), const SizedBox(height: 20),
const Divider(), const Divider(),
const SizedBox(height: 20), const SizedBox(height: 20),
// * Excursion Management Section
// Button to start new excursion
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(250, 40), minimumSize: const Size(250, 40),
), ),
onPressed: () => Navigator.pushNamed(context, '/excursion'), onPressed: () => Navigator.pushNamed(context, '/excursion'),
child: Text(AppLocalizations.of(context)!.excursion), child: Text(AppLocalizations.of(context)!.excursion),
), // Excursion ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Button to view excursions
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(250, 40), minimumSize: const Size(250, 40),
@@ -94,9 +112,13 @@ class HomePage extends StatelessWidget {
child: Text(AppLocalizations.of(context)!.viewExcursionen), child: Text(AppLocalizations.of(context)!.viewExcursionen),
), ),
// Visual section divider
const SizedBox(height: 20), const SizedBox(height: 20),
const Divider(), const Divider(),
const SizedBox(height: 20), const SizedBox(height: 20),
// * File Operations Section
// Button to send data files
ElevatedButton( ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(250, 40), minimumSize: const Size(250, 40),

View File

@@ -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'; import 'package:sqflite/sqflite.dart';
/// Interface for database operations
/// Implements the Repository pattern for database access
abstract interface class IDb { abstract interface class IDb {
/// Get the database instance
Future<Database> get dB; Future<Database> get dB;
/// Initialize the database and create necessary tables
initDatabases(); initDatabases();
/// Create database schema
/// @param excursionDB Database instance
/// @param version Schema version number
onCreateDatabases(Database excursionDB, int version); 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); 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); 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); 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); 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); 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(); Future<List<Map<String, dynamic>>> getAllMainEntries();
/// Get all templates from the database
/// @return List of all templates
Future<List<Map<String, dynamic>>> getAllTemplates(); Future<List<Map<String, dynamic>>> getAllTemplates();
/// Delete all main entries from the database
Future<void> deleteAllMainEntries(); Future<void> deleteAllMainEntries();
/// Delete all templates from the database
Future<void> deleteAllTemplates(); Future<void> deleteAllTemplates();
/// Delete a specific template
/// @param id ID of the template to delete
Future<void> deleteTemplateById(String id); Future<void> deleteTemplateById(String id);
/// Delete a specific main entry
/// @param id ID of the entry to delete
Future<void> deleteMainEntryById(String id); Future<void> deleteMainEntryById(String id);
} }

View File

@@ -13,22 +13,36 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'home.dart'; import 'home.dart';
import 'l10n/l10n.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 { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Set default values (Propably there is a better way to do this) // Initialize SharedPreferences for app settings
SharedPreferences prefs = await SharedPreferences.getInstance(); 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; 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("kTage1")?.isEmpty ?? true) await prefs.setString('kTage1', "28");
if (prefs.getString("kTage2")?.isEmpty ?? true) await prefs.setString('kTage2', "48"); 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("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'); // 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)); runApp(MyApp(isFirstLaunch: isFirstLaunch));
} }
/// Main app widget that configures the app's theme and routing
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
final bool isFirstLaunch; final bool isFirstLaunch;
const MyApp({super.key, required this.isFirstLaunch}); const MyApp({super.key, required this.isFirstLaunch});
@@ -36,13 +50,16 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'LUPUS', title: 'LUPUS',
// Configure light theme with gold color scheme
theme: FlexThemeData.light(scheme: FlexScheme.gold, useMaterial3: true), theme: FlexThemeData.light(scheme: FlexScheme.gold, useMaterial3: true),
darkTheme: // Configure dark theme with green M3 color scheme
FlexThemeData.dark(scheme: FlexScheme.greenM3, useMaterial3: true), darkTheme: FlexThemeData.dark(scheme: FlexScheme.greenM3, useMaterial3: true),
themeMode: ThemeMode.system, themeMode: ThemeMode.system, // Use system theme preference
// here the isFirstLaunch comes into play
// Show intro screen on first launch, otherwise go to home
initialRoute: isFirstLaunch ? '/introScreen' : '/home', initialRoute: isFirstLaunch ? '/introScreen' : '/home',
// Localization settings
// Configure localization support
supportedLocales: L10n.all, supportedLocales: L10n.all,
localizationsDelegates: const [ localizationsDelegates: const [
AppLocalizations.delegate, AppLocalizations.delegate,
@@ -50,6 +67,8 @@ class MyApp extends StatelessWidget {
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
], ],
// Define app navigation routes
routes: { routes: {
'/home': (context) => const HomePage(), '/home': (context) => const HomePage(),
'/addCamMain': (context) => const AddCamMain(), '/addCamMain': (context) => const AddCamMain(),

View File

@@ -4,13 +4,20 @@ import 'package:sqflite/sqflite.dart';
import 'dart:io' as io; import 'dart:io' as io;
import 'package:path/path.dart'; import 'package:path/path.dart';
// * Gives the complete functionality for the databases // * Database helper for managing excursions
// ! functions may not be named complete correctly // * 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 { class ExcursionDBHelper implements IDb {
// Database instance
static Database? _excursionDB; 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 @override
Future<Database> get dB async { Future<Database> get dB async {
if (_excursionDB != null) { if (_excursionDB != null) {
@@ -20,7 +27,8 @@ class ExcursionDBHelper implements IDb {
return _excursionDB!; 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 @override
initDatabases() async { initDatabases() async {
io.Directory documentsDirectory = await getApplicationCacheDirectory(); io.Directory documentsDirectory = await getApplicationCacheDirectory();
@@ -33,21 +41,28 @@ class ExcursionDBHelper implements IDb {
return excursionDB; return excursionDB;
} }
// The function that helps /// Create database tables
/// Sets up schema for both main entries and templates
@override @override
onCreateDatabases(Database excursionDB, int version) async { onCreateDatabases(Database excursionDB, int version) async {
// Create main excursions table with tracking data fields
await excursionDB.execute( 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 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( 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)', '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 @override
Future<int> addMainEntry(Map<String, String> excursion) async { Future<int> addMainEntry(Map<String, String> excursion) async {
var excursionDBClient = await dB; var excursionDBClient = await dB;
// Commented out code for handling existing entries
// final existingID = await excursionDBClient.query( // final existingID = await excursionDBClient.query(
// 'excursion', // 'excursion',
// where: 'ID = ?', // where: 'ID = ?',
@@ -56,7 +71,7 @@ class ExcursionDBHelper implements IDb {
// if (existingID.isNotEmpty) { // if (existingID.isNotEmpty) {
// updateMainEntry(excursion); // updateMainEntry(excursion);
// return existingID.first['ID'] as int; // Return existing ID // return existingID.first['ID'] as int;
// } // }
int id = await excursionDBClient.insert( int id = await excursionDBClient.insert(
@@ -65,13 +80,15 @@ class ExcursionDBHelper implements IDb {
conflictAlgorithm: ConflictAlgorithm.replace, 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 @override
Future<int> updateMainEntry(Map<String, String> excursion) async { Future<int> updateMainEntry(Map<String, String> excursion) async {
var excursionDBClient = await dB; var excursionDBClient = await dB;
return await excursionDBClient.update( return await excursionDBClient.update(
'excursion', 'excursion',
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 @override
Future<void> updateSent(int id) async { Future<void> updateSent(int id) async {
var excursionDBClient = await dB; var excursionDBClient = await dB;
await excursionDBClient.update( await excursionDBClient.update(
'excursion', 'excursion',
{'Sent': 1}, {'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 @override
Future<int> addTemplate(Map<String, String> templates) async { Future<int> addTemplate(Map<String, String> templates) async {
var excursionDBClient = await dB; var excursionDBClient = await dB;
// Commented out code for handling existing templates
// final existingCID = await excursionDBClient.query( // final existingCID = await excursionDBClient.query(
// 'excursionTemplates', // 'excursionTemplates',
// where: 'ID = ?', // where: 'ID = ?',
@@ -110,16 +129,15 @@ class ExcursionDBHelper implements IDb {
int id = await excursionDBClient.insert( int id = await excursionDBClient.insert(
'excursionTemplates', 'excursionTemplates',
templates, templates,
// conflictAlgorithm: ConflictAlgorithm.replace,
); );
return id; return id;
} }
// Updates a existing template /// Update an existing template
/// @param template Map containing the updated template data
@override @override
Future<void> updateTemplate(Map<String, String> template) async { Future<void> updateTemplate(Map<String, String> template) async {
var excursionDBClient = await dB; var excursionDBClient = await dB;
await excursionDBClient.update( await excursionDBClient.update(
'excursionTemplates', 'excursionTemplates',
template, 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 @override
Future<List<Map<String, dynamic>>> getAllMainEntries() async { Future<List<Map<String, dynamic>>> getAllMainEntries() async {
var excursionDBClient = await dB; 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 @override
Future<List<Map<String, dynamic>>> getAllTemplates() async { Future<List<Map<String, dynamic>>> getAllTemplates() async {
var excursionDBClient = await dB; 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 @override
Future<void> deleteAllMainEntries() async { Future<void> deleteAllMainEntries() async {
var excursionDBClient = await dB; var excursionDBClient = await dB;
await excursionDBClient.delete('excursion'); 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 @override
Future<void> deleteAllTemplates() async { Future<void> deleteAllTemplates() async {
var excursionDBClient = await dB; var excursionDBClient = await dB;
await excursionDBClient.delete('excursionTemplates'); await excursionDBClient.delete('excursionTemplates');
} }
// delete specific template /// Delete a specific template by ID
/// @param id ID of the template to delete
@override @override
Future<void> deleteTemplateById(String id) async { Future<void> deleteTemplateById(String id) async {
var excursionDBClient = await dB; 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 @override
Future<void> deleteMainEntryById(String id) async { Future<void> deleteMainEntryById(String id) async {
var excursionDBClient = await dB; var excursionDBClient = await dB;

View File

@@ -4,13 +4,20 @@ import 'package:sqflite/sqflite.dart';
import 'dart:io' as io; import 'dart:io' as io;
import 'package:path/path.dart'; import 'package:path/path.dart';
// * Gives the complete functionality for the databases // * Database helper for managing camera trap locations (places)
// ! functions may not be named complete correctly // * 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; 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 @override
Future<Database> get dB async { Future<Database> get dB async {
if (_dB != null) { if (_dB != null) {
@@ -20,7 +27,8 @@ class PlaceDBHelper implements IDb{
return _dB!; 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 @override
initDatabases() async { initDatabases() async {
io.Directory documentsDirectory = await getApplicationCacheDirectory(); io.Directory documentsDirectory = await getApplicationCacheDirectory();
@@ -30,19 +38,26 @@ class PlaceDBHelper implements IDb{
return placeDB; return placeDB;
} }
// The function that helps /// Create database tables
/// Sets up schema for both main entries and templates
@override @override
onCreateDatabases(Database placeDB, int version) async { onCreateDatabases(Database placeDB, int version) async {
// Create main places table
await placeDB.execute( 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 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( 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))'); '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 @override
Future<int> addMainEntry(Map<String, String> place) async { Future<int> addMainEntry(Map<String, String> place) async {
var placeDBClient = await dB; var placeDBClient = await dB;
// Commented out code for handling existing entries
// final existingID = await placeDBClient.query( // final existingID = await placeDBClient.query(
// 'place', // 'place',
// where: 'ID = ?', // where: 'ID = ?',
@@ -51,7 +66,7 @@ class PlaceDBHelper implements IDb{
// //
// if (existingID.isNotEmpty) { // if (existingID.isNotEmpty) {
// updateMainEntry(place); // updateMainEntry(place);
// return existingID.first['ID'] as int; // Return existing ID // return existingID.first['ID'] as int;
// } // }
int id = await placeDBClient.insert( int id = await placeDBClient.insert(
@@ -60,31 +75,35 @@ class PlaceDBHelper implements IDb{
//conflictAlgorithm: ConflictAlgorithm.replace, //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 @override
Future<int> updateMainEntry(Map<String, String> place) async { Future<int> updateMainEntry(Map<String, String> place) async {
var placeDBClient = await dB; var placeDBClient = await dB;
return await placeDBClient return await placeDBClient
.update('place', place, where: "ID = ?", whereArgs: [place['ID']]); .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 @override
Future<void> updateSent(int id) async { Future<void> updateSent(int id) async {
var placeDBClient = await dB; var placeDBClient = await dB;
await placeDBClient.update('place', {'Sent': 1}, await placeDBClient.update('place', {'Sent': 1},
where: 'ID = ?', whereArgs: [id]); 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 @override
Future<int> addTemplate(Map<String, String> templates) async { Future<int> addTemplate(Map<String, String> templates) async {
var placeDBClient = await dB; var placeDBClient = await dB;
// Commented out code for handling existing templates
// final existingCID = await placeDBClient.query( // final existingCID = await placeDBClient.query(
// 'placeTemplates', // 'placeTemplates',
// where: 'ID = ?', // where: 'ID = ?',
@@ -97,16 +116,15 @@ class PlaceDBHelper implements IDb{
int id = await placeDBClient.insert( int id = await placeDBClient.insert(
'placeTemplates', 'placeTemplates',
templates, templates,
// conflictAlgorithm: ConflictAlgorithm.replace,
); );
return id; return id;
} }
// Updates a existing template /// Update an existing template
/// @param template Map containing the updated template data
@override @override
Future<void> updateTemplate(Map<String, String> template) async { Future<void> updateTemplate(Map<String, String> template) async {
var placeDBClient = await dB; var placeDBClient = await dB;
await placeDBClient.update( await placeDBClient.update(
'placeTemplates', 'placeTemplates',
template, 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 @override
Future<List<Map<String, dynamic>>> getAllMainEntries() async { Future<List<Map<String, dynamic>>> getAllMainEntries() async {
var placeDBClient = await dB; 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 @override
Future<List<Map<String, dynamic>>> getAllTemplates() async { Future<List<Map<String, dynamic>>> getAllTemplates() async {
var placeDBClient = await dB; 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 @override
Future<void> deleteAllMainEntries() async { Future<void> deleteAllMainEntries() async {
var placeDBClient = await dB; var placeDBClient = await dB;
await placeDBClient.delete('place'); 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 @override
Future<void> deleteAllTemplates() async { Future<void> deleteAllTemplates() async {
var placeDBClient = await dB; var placeDBClient = await dB;
await placeDBClient.delete('placeTemplates'); await placeDBClient.delete('placeTemplates');
} }
// delete specific template /// Delete a specific template by ID
/// @param id ID of the template to delete
@override @override
Future<void> deleteTemplateById(String id) async { Future<void> deleteTemplateById(String id) async {
var placeDBClient = await dB; 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 @override
Future<void> deleteMainEntryById(String id) async { Future<void> deleteMainEntryById(String id) async {
var placeDBClient = await dB; var placeDBClient = await dB;

View File

@@ -22,9 +22,14 @@ import 'widgets/mez.dart';
import 'widgets/platzung.dart'; import 'widgets/platzung.dart';
import 'widgets/status.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 { class AddCamMain extends StatefulWidget {
/// Whether this form is being used to create a template
final bool isTemplate; final bool isTemplate;
/// Whether the entry has been sent to the server
final bool isSent; final bool isSent;
/// Existing data to populate the form with (for editing)
final Map<String, dynamic>? existingData; final Map<String, dynamic>? existingData;
const AddCamMain({ const AddCamMain({
@@ -38,14 +43,17 @@ class AddCamMain extends StatefulWidget {
State<AddCamMain> createState() => _AddCamMainState(); State<AddCamMain> createState() => _AddCamMainState();
} }
/// State class for the camera trap location form
class _AddCamMainState extends State<AddCamMain> { class _AddCamMainState extends State<AddCamMain> {
// var declaration /// Current step in the multi-step form
int currentStep = 0; int currentStep = 0;
/// Whether this form is being used as a template
late bool isTemplate; late bool isTemplate;
/// Current GPS position, initialized with default values for Germany
Position currentPosition = Position( Position currentPosition = Position(
longitude: 10.0, longitude: 10.0, // Default longitude (roughly center of Germany)
latitude: 51.0, latitude: 51.0, // Default latitude (roughly center of Germany)
timestamp: DateTime.now(), timestamp: DateTime.now(),
accuracy: 0.0, accuracy: 0.0,
altitude: 0.0, altitude: 0.0,
@@ -56,6 +64,12 @@ class _AddCamMainState extends State<AddCamMain> {
headingAccuracy: 0.0, 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 = { Map<String, Map<String, dynamic>> rmap = {
"ID": {"controller": TextEditingController(), "required": false}, "ID": {"controller": TextEditingController(), "required": false},
// Step 1 // Step 1
@@ -102,6 +116,8 @@ class _AddCamMainState extends State<AddCamMain> {
"Sent": {"controller": TextEditingController(), "required": false}, "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> getFieldsText() {
Map<String, String> puff = {}; Map<String, String> puff = {};
@@ -112,8 +128,12 @@ class _AddCamMainState extends State<AddCamMain> {
return puff; return puff;
} }
/// Flag indicating whether position is currently being loaded
bool isLoadingPosition = false; bool isLoadingPosition = false;
/// Initializes the GPS position
/// Handles location permissions and device settings
/// @return Future<Position> The determined position
Future<Position> _initializePosition() async { Future<Position> _initializePosition() async {
try { try {
final position = await GeolocatorService.deteterminePosition(); final position = await GeolocatorService.deteterminePosition();

View File

@@ -1,25 +0,0 @@
// Karte
// ! completely new page
// Status
// STTyp
// platzung
// FotoFilm
// MEZ
// KontDat
// AbbauDat

View File

@@ -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_disabled_exception.dart';
import 'package:fforte/screens/addCam/exceptions/location_forbidden_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:fforte/screens/excursion/exceptions/need_always_location_exception.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
/// Service class for handling all GPS location related functionality
class GeolocatorService { 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 { static Future<Position> deteterminePosition({bool alwaysOnNeeded = false}) async {
bool locationEnabled; bool locationEnabled;
LocationPermission permissionGiven; LocationPermission permissionGiven;
// Check if location services are enabled
locationEnabled = await Geolocator.isLocationServiceEnabled(); locationEnabled = await Geolocator.isLocationServiceEnabled();
if (!locationEnabled) { if (!locationEnabled) {
throw LocationDisabledException(); throw LocationDisabledException();
} }
// Check and request location permissions if needed
permissionGiven = await Geolocator.checkPermission(); permissionGiven = await Geolocator.checkPermission();
if (permissionGiven == LocationPermission.denied) { if (permissionGiven == LocationPermission.denied) {
permissionGiven = await Geolocator.requestPermission(); permissionGiven = await Geolocator.requestPermission();
@@ -22,13 +37,16 @@ class GeolocatorService {
} }
} }
// Check for always-on permission if required
if (alwaysOnNeeded && permissionGiven != LocationPermission.always) { if (alwaysOnNeeded && permissionGiven != LocationPermission.always) {
throw NeedAlwaysLocation(); 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 { static Future<bool> alwaysPositionEnabled() async {
LocationPermission permissionGiven = await Geolocator.checkPermission(); LocationPermission permissionGiven = await Geolocator.checkPermission();
bool locationEnabled = await Geolocator.isLocationServiceEnabled(); bool locationEnabled = await Geolocator.isLocationServiceEnabled();

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class AbbauDat extends StatefulWidget {
/// Initial dismantling date, can be null if not set
final DateTime? initAbbauDat; final DateTime? initAbbauDat;
/// Callback function when date is changed
final Function(DateTime) onDateChanged; final Function(DateTime) onDateChanged;
const AbbauDat({super.key, required this.initAbbauDat, required this.onDateChanged}); const AbbauDat({super.key, required this.initAbbauDat, required this.onDateChanged});
@@ -11,7 +21,9 @@ class AbbauDat extends StatefulWidget {
State<AbbauDat> createState() => _AbbauDatState(); State<AbbauDat> createState() => _AbbauDatState();
} }
/// State class for the dismantling date widget
class _AbbauDatState extends State<AbbauDat> { class _AbbauDatState extends State<AbbauDat> {
/// Currently selected dismantling date
DateTime? abbauDat; DateTime? abbauDat;
@override @override
@@ -25,6 +37,7 @@ class _AbbauDatState extends State<AbbauDat> {
return Row( return Row(
children: [ children: [
Column(children: [ Column(children: [
// Date picker button
SizedBox( SizedBox(
width: 140, width: 140,
child: ElevatedButton( child: ElevatedButton(
@@ -38,34 +51,34 @@ class _AbbauDatState extends State<AbbauDat> {
), ),
Row( Row(
children: [ children: [
const SizedBox( const SizedBox(width: 10),
width: 10, // Display selected date or "nothing" text
), Builder(builder: (context) {
Builder(builder: (context) { if (abbauDat != null) {
if (abbauDat != null) { return Text(
return Text( '${abbauDat?.day}. ${abbauDat?.month}. ${abbauDat?.year}');
'${abbauDat?.day}. ${abbauDat?.month}. ${abbauDat?.year}'); } else {
} else { return Text(AppLocalizations.of(context)!.nichts);
return Text(AppLocalizations.of(context)!.nichts); }
} }),
}), const SizedBox(width: 10),
const SizedBox( // Clear date button
width: 10, ElevatedButton(
), onPressed: () {
ElevatedButton( setState(() {
onPressed: () { abbauDat = null;
setState(() { });
abbauDat = null; },
}); child: const Text("X"))
}, ]),
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 { Future<DateTime?> pickDate() async {
final date = await showDatePicker( final date = await showDatePicker(
context: context, context: context,

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class FotoFilm extends StatefulWidget {
/// Callback function when media type selection changes
final Function(String) onFotoFilmChanged; final Function(String) onFotoFilmChanged;
/// Initial media type selection ('foto' by default)
final String initialFotoFilm; final String initialFotoFilm;
const FotoFilm( const FotoFilm(
@@ -14,7 +24,9 @@ class FotoFilm extends StatefulWidget {
State<FotoFilm> createState() => _FotoFilmState(); State<FotoFilm> createState() => _FotoFilmState();
} }
/// State class for the photo/film selection widget
class _FotoFilmState extends State<FotoFilm> { class _FotoFilmState extends State<FotoFilm> {
/// Currently selected media type
String? _selectedFotoFilm; String? _selectedFotoFilm;
@override @override
@@ -27,6 +39,7 @@ class _FotoFilmState extends State<FotoFilm> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Photo mode radio button
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.foto), title: Text(AppLocalizations.of(context)!.foto),
@@ -41,6 +54,7 @@ class _FotoFilmState extends State<FotoFilm> {
}, },
), ),
), ),
// Film/Video mode radio button
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.film), title: Text(AppLocalizations.of(context)!.film),

View File

@@ -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/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
@@ -6,11 +12,18 @@ import 'package:latlong2/latlong.dart';
// import 'package:geocoding/geocoding.dart'; // import 'package:geocoding/geocoding.dart';
import 'package:fforte/l10n/app_localizations.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 { class Karte extends StatefulWidget {
/// Controller for nearby location name
final TextEditingController beiOrtC; final TextEditingController beiOrtC;
/// Controller for location details
final TextEditingController ortInfoC; final TextEditingController ortInfoC;
/// Controller for longitude coordinate
final TextEditingController decLngC; final TextEditingController decLngC;
/// Controller for latitude coordinate
final TextEditingController decLatC; final TextEditingController decLatC;
/// Current GPS position
final Position currentPosition; final Position currentPosition;
const Karte( const Karte(
@@ -25,15 +38,20 @@ class Karte extends StatefulWidget {
KarteState createState() => KarteState(); KarteState createState() => KarteState();
} }
/// State class for the map widget
class KarteState extends State<Karte> { class KarteState extends State<Karte> {
/// Current marker on the map
Marker? currentMarker; Marker? currentMarker;
/// Selected position coordinates
LatLng? selectedPosition; LatLng? selectedPosition;
/// Whether the save button should be visible
bool saveVisible = false; bool saveVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Initialize marker at current position
currentMarker = Marker( currentMarker = Marker(
point: LatLng( point: LatLng(
widget.currentPosition.latitude, widget.currentPosition.longitude), widget.currentPosition.latitude, widget.currentPosition.longitude),
@@ -50,6 +68,7 @@ class KarteState extends State<Karte> {
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.map), title: Text(AppLocalizations.of(context)!.map),
actions: [ actions: [
// Save location button
Visibility( Visibility(
visible: saveVisible, visible: saveVisible,
child: Padding( child: Padding(
@@ -76,6 +95,7 @@ class KarteState extends State<Karte> {
), ),
], ],
), ),
// Map display with OpenStreetMap tiles
body: FlutterMap( body: FlutterMap(
mapController: MapController(), mapController: MapController(),
options: MapOptions( options: MapOptions(
@@ -89,15 +109,21 @@ class KarteState extends State<Karte> {
onTap: _handleTap, onTap: _handleTap,
), ),
children: [ children: [
// OpenStreetMap tile layer
TileLayer( TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'de.lupus.apps', userAgentPackageName: 'de.lupus.apps',
), ),
// Marker layer
MarkerLayer(markers: [currentMarker!]), 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) { _handleTap(TapPosition position, LatLng latlng) {
setState(() { setState(() {
currentMarker = Marker( currentMarker = Marker(
@@ -111,7 +137,7 @@ class KarteState extends State<Karte> {
); );
// selectedPosition = latlng; // selectedPosition = latlng;
saveVisible = true; saveVisible = true;
}); });
// ScaffoldMessenger.of(context).showSnackBar(SnackBar( // ScaffoldMessenger.of(context).showSnackBar(SnackBar(
// content: Text( // content: Text(
// "${AppLocalizations.of(context)!.markerSet}\n${selectedPosition!.latitude}\n${selectedPosition!.longitude}"))); // "${AppLocalizations.of(context)!.markerSet}\n${selectedPosition!.latitude}\n${selectedPosition!.longitude}")));

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class KontDat extends StatefulWidget {
/// Initial control date, if any
final DateTime? initKontDat; final DateTime? initKontDat;
/// Callback function when date is changed
final Function(DateTime) onDateChanged; final Function(DateTime) onDateChanged;
const KontDat( const KontDat(
@@ -12,7 +22,9 @@ class KontDat extends StatefulWidget {
State<KontDat> createState() => _KontDatState(); State<KontDat> createState() => _KontDatState();
} }
/// State class for the control date widget
class _KontDatState extends State<KontDat> { class _KontDatState extends State<KontDat> {
/// Currently selected control date
DateTime? kontDat; DateTime? kontDat;
@override @override
@@ -26,6 +38,7 @@ class _KontDatState extends State<KontDat> {
return Row( return Row(
children: [ children: [
Row(children: [ Row(children: [
// Date picker button
SizedBox( SizedBox(
width: 140, width: 140,
child: ElevatedButton( child: ElevatedButton(
@@ -39,9 +52,8 @@ class _KontDatState extends State<KontDat> {
}, },
child: Text(AppLocalizations.of(context)!.pickkontdat)), child: Text(AppLocalizations.of(context)!.pickkontdat)),
), ),
const SizedBox( const SizedBox(width: 10),
width: 10, // Display selected date in DD.MM.YYYY format
),
Text( Text(
'${kontDat?.day}. ${kontDat?.month}. ${kontDat?.year}', '${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 { Future<DateTime?> pickDate() async {
final date = await showDatePicker( final date = await showDatePicker(
context: context, context: context,

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class MEZ extends StatefulWidget {
/// Callback function when time zone selection changes
final Function(String) onMEZChanged; final Function(String) onMEZChanged;
/// Initial time zone selection ('sommerzeit' by default)
final String initialMEZ; final String initialMEZ;
const MEZ( const MEZ(
@@ -12,7 +22,9 @@ class MEZ extends StatefulWidget {
State<MEZ> createState() => _MEZState(); State<MEZ> createState() => _MEZState();
} }
/// State class for the time zone selection widget
class _MEZState extends State<MEZ> { class _MEZState extends State<MEZ> {
/// Currently selected time zone
String? _selectedMEZ; String? _selectedMEZ;
@override @override
@@ -25,6 +37,7 @@ class _MEZState extends State<MEZ> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Summer time (MESZ) radio button
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.sommerzeit), title: Text(AppLocalizations.of(context)!.sommerzeit),
@@ -39,6 +52,7 @@ class _MEZState extends State<MEZ> {
}, },
), ),
), ),
// Winter time (MEZ) radio button
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.winterzeit), title: Text(AppLocalizations.of(context)!.winterzeit),

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class Platzung extends StatefulWidget {
/// Callback function when placement type selection changes
final Function(String) onPlatzungChanged; final Function(String) onPlatzungChanged;
/// Initial placement type selection
final String? initialPlatzung; final String? initialPlatzung;
const Platzung({ const Platzung({
@@ -15,7 +33,9 @@ class Platzung extends StatefulWidget {
State<Platzung> createState() => _PlatzungState(); State<Platzung> createState() => _PlatzungState();
} }
/// State class for the placement type selection widget
class _PlatzungState extends State<Platzung> { class _PlatzungState extends State<Platzung> {
/// Currently selected placement type
String? _selectedPlatzung; String? _selectedPlatzung;
@override @override
@@ -30,6 +50,7 @@ class _PlatzungState extends State<Platzung> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Bait station placement option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.kirrung), title: Text(AppLocalizations.of(context)!.kirrung),
@@ -44,6 +65,7 @@ class _PlatzungState extends State<Platzung> {
}, },
), ),
), ),
// Water source placement option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.wasserstelle), title: Text(AppLocalizations.of(context)!.wasserstelle),
@@ -58,6 +80,7 @@ class _PlatzungState extends State<Platzung> {
}, },
), ),
), ),
// Forest placement option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.wald), title: Text(AppLocalizations.of(context)!.wald),
@@ -72,6 +95,7 @@ class _PlatzungState extends State<Platzung> {
}, },
), ),
), ),
// Game pass placement option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.wildwechsel), title: Text(AppLocalizations.of(context)!.wildwechsel),
@@ -86,6 +110,7 @@ class _PlatzungState extends State<Platzung> {
}, },
), ),
), ),
// Path/Road placement option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.wegstrasse), title: Text(AppLocalizations.of(context)!.wegstrasse),
@@ -100,6 +125,7 @@ class _PlatzungState extends State<Platzung> {
}, },
), ),
), ),
// Farm/Garden placement option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.hofgarten), title: Text(AppLocalizations.of(context)!.hofgarten),
@@ -114,6 +140,7 @@ class _PlatzungState extends State<Platzung> {
}, },
), ),
), ),
// Meadow/Field placement option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.wiesefeld), title: Text(AppLocalizations.of(context)!.wiesefeld),

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class Status extends StatefulWidget {
/// Callback function when status selection changes
final Function(String) onStatusChanged; final Function(String) onStatusChanged;
/// Initial status selection ('Aktiv' by default)
final String initialStatus; final String initialStatus;
const Status( const Status(
@@ -12,7 +22,9 @@ class Status extends StatefulWidget {
State<Status> createState() => _StatusState(); State<Status> createState() => _StatusState();
} }
/// State class for the status selection widget
class _StatusState extends State<Status> { class _StatusState extends State<Status> {
/// Currently selected status
String? _selectedStatus; String? _selectedStatus;
@override @override
@@ -25,6 +37,7 @@ class _StatusState extends State<Status> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Active status option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.aktiv), title: Text(AppLocalizations.of(context)!.aktiv),
@@ -39,6 +52,7 @@ class _StatusState extends State<Status> {
}, },
), ),
), ),
// Inactive status option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.inaktiv), title: Text(AppLocalizations.of(context)!.inaktiv),

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class STTyp extends StatefulWidget {
/// Callback function when sampling type changes
final Function(String) onSTTypChanged; final Function(String) onSTTypChanged;
/// Initial sampling type value
final String initialSTTyp; final String initialSTTyp;
const STTyp( const STTyp(
@@ -14,7 +25,9 @@ class STTyp extends StatefulWidget {
State<STTyp> createState() => _STTypState(); State<STTyp> createState() => _STTypState();
} }
/// State class for the sampling type selection widget
class _STTypState extends State<STTyp> { class _STTypState extends State<STTyp> {
/// Currently selected sampling type
String? _selectedSTTyp; String? _selectedSTTyp;
@override @override
@@ -27,6 +40,7 @@ class _STTypState extends State<STTyp> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Opportunistic sampling option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.opportunistisch), title: Text(AppLocalizations.of(context)!.opportunistisch),
@@ -41,6 +55,7 @@ class _STTypState extends State<STTyp> {
}, },
), ),
), ),
// Systematic sampling option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.systematisch), title: Text(AppLocalizations.of(context)!.systematisch),

View File

@@ -24,9 +24,14 @@ import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
import 'package:shared_preferences/shared_preferences.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 { class ExcursionMain extends StatefulWidget {
/// Whether this is a template excursion
final bool isTemplate; final bool isTemplate;
/// Whether the excursion data has been sent to the server
final bool isSent; final bool isSent;
/// Existing excursion data for editing
final Map<String, dynamic>? existingData; final Map<String, dynamic>? existingData;
const ExcursionMain({ const ExcursionMain({
@@ -40,9 +45,13 @@ class ExcursionMain extends StatefulWidget {
State<ExcursionMain> createState() => _ExcursionMainState(); State<ExcursionMain> createState() => _ExcursionMainState();
} }
/// State class for the main excursion screen
class _ExcursionMainState extends State<ExcursionMain> { class _ExcursionMainState extends State<ExcursionMain> {
/// Current step in the form
int currentStep = 0; int currentStep = 0;
/// Whether this is a template excursion
late bool isTemplate; late bool isTemplate;
/// Current GPS position
Position currentPosition = Position( Position currentPosition = Position(
longitude: 10.0, longitude: 10.0,
latitude: 51.0, latitude: 51.0,
@@ -56,12 +65,14 @@ class _ExcursionMainState extends State<ExcursionMain> {
headingAccuracy: 0.0, headingAccuracy: 0.0,
); );
/// Whether to show extended BImA information
bool bimaExtended = false; 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 = { Map<String, Map<String, dynamic>> rmap = {
"ID": {"controller": TextEditingController(), "required": false}, "ID": {"controller": TextEditingController(), "required": false},
// Step 1 // Step 1 - Basic Information
"Datum": {"controller": TextEditingController(), "required": false}, "Datum": {"controller": TextEditingController(), "required": false},
"Rudel": {"controller": TextEditingController(), "required": false}, "Rudel": {"controller": TextEditingController(), "required": false},
"Teilnehmer": {"controller": TextEditingController(), "required": false}, "Teilnehmer": {"controller": TextEditingController(), "required": false},
@@ -76,7 +87,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
"BimaNutzer": {"controller": TextEditingController(), "required": false}, "BimaNutzer": {"controller": TextEditingController(), "required": false},
"BimaAGV": {"controller": TextEditingController(), "required": false}, "BimaAGV": {"controller": TextEditingController(), "required": false},
// Step 2 // Step 2 - Environmental Conditions and Observations
"Weg": {"controller": TextEditingController(), "required": false}, "Weg": {"controller": TextEditingController(), "required": false},
"Wetter": {"controller": TextEditingController(), "required": false}, "Wetter": {"controller": TextEditingController(), "required": false},
"Temperat": {"controller": TextEditingController(), "required": false}, "Temperat": {"controller": TextEditingController(), "required": false},
@@ -89,7 +100,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
"KmFuProz": {"controller": TextEditingController(), "required": false}, "KmFuProz": {"controller": TextEditingController(), "required": false},
"KmRaProz": {"controller": TextEditingController(), "required": false}, "KmRaProz": {"controller": TextEditingController(), "required": false},
// Spur maybe own step? // Track Findings
"SpGut": {"controller": TextEditingController(), "required": false}, "SpGut": {"controller": TextEditingController(), "required": false},
"SpMittel": {"controller": TextEditingController(), "required": false}, "SpMittel": {"controller": TextEditingController(), "required": false},
"SpSchlecht": {"controller": TextEditingController(), "required": false}, "SpSchlecht": {"controller": TextEditingController(), "required": false},
@@ -101,6 +112,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
"WelpenAnz": {"controller": TextEditingController(), "required": false}, "WelpenAnz": {"controller": TextEditingController(), "required": false},
"WpSicher": {"controller": TextEditingController(), "required": false}, "WpSicher": {"controller": TextEditingController(), "required": false},
// Sample Counts
"LosungGes": {"controller": TextEditingController(), "required": false}, "LosungGes": {"controller": TextEditingController(), "required": false},
"LosungAnz": {"controller": TextEditingController(), "required": false}, "LosungAnz": {"controller": TextEditingController(), "required": false},
"LosungGen": {"controller": TextEditingController(), "required": false}, "LosungGen": {"controller": TextEditingController(), "required": false},
@@ -114,7 +126,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
"GenetiKm": {"controller": TextEditingController(), "required": false}, "GenetiKm": {"controller": TextEditingController(), "required": false},
"Hinweise": {"controller": TextEditingController(), "required": false}, "Hinweise": {"controller": TextEditingController(), "required": false},
// Step 3 // Step 3 - Notes and Communication
"Bemerk": {"controller": TextEditingController(), "required": false}, "Bemerk": {"controller": TextEditingController(), "required": false},
"IntKomm": {"controller": TextEditingController(), "required": false}, "IntKomm": {"controller": TextEditingController(), "required": false},
"FallNum": {"controller": TextEditingController(), "required": false}, "FallNum": {"controller": TextEditingController(), "required": false},
@@ -123,6 +135,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
@override @override
void initState() { void initState() {
// Initialize location services
GeolocatorService.deteterminePosition( GeolocatorService.deteterminePosition(
alwaysOnNeeded: false, alwaysOnNeeded: false,
).then((result) => currentPosition = result).catchError((error) async { ).then((result) => currentPosition = result).catchError((error) async {
@@ -156,13 +169,14 @@ class _ExcursionMainState extends State<ExcursionMain> {
return currentPosition; return currentPosition;
}); });
// Load existing data or set defaults
if (widget.existingData?.isNotEmpty ?? false) { if (widget.existingData?.isNotEmpty ?? false) {
for (var key in widget.existingData!.keys) { for (var key in widget.existingData!.keys) {
rmap[key]!["controller"]!.text = rmap[key]!["controller"]!.text =
widget.existingData?[key].toString() ?? ""; widget.existingData?[key].toString() ?? "";
} }
} else { } else {
// Set BLand and default values if there is no existing data // Set default state and date
SharedPreferences.getInstance().then((prefs) { SharedPreferences.getInstance().then((prefs) {
rmap["BLand"]!["controller"]!.text = prefs.getString('bLand') ?? ""; rmap["BLand"]!["controller"]!.text = prefs.getString('bLand') ?? "";
}); });
@@ -178,12 +192,15 @@ class _ExcursionMainState extends State<ExcursionMain> {
@override @override
void dispose() { void dispose() {
// Dispose all controllers
for (String key in rmap.keys) { for (String key in rmap.keys) {
rmap[key]!["controller"].dispose(); rmap[key]!["controller"].dispose();
} }
super.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> getFieldsText() {
Map<String, String> puff = {}; Map<String, String> puff = {};
@@ -196,12 +213,14 @@ class _ExcursionMainState extends State<ExcursionMain> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
/// Build the steps for the form
/// @return List<Step> List of form steps
List<Step> getSteps() => [ List<Step> getSteps() => [
Step( Step(
title: Text(AppLocalizations.of(context)!.dateandtime), title: Text(AppLocalizations.of(context)!.dateandtime),
content: Column( content: Column(
children: [ children: [
// ---------- Date // Date picker
Datum( Datum(
initDatum: DateTime.now(), initDatum: DateTime.now(),
onDateChanged: (date) { onDateChanged: (date) {
@@ -210,7 +229,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
name: AppLocalizations.of(context)!.date, name: AppLocalizations.of(context)!.date,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Pack // Pack/Group field
VarTextField( VarTextField(
textController: rmap["Rudel"]!["controller"]!, textController: rmap["Rudel"]!["controller"]!,
localization: AppLocalizations.of(context)!.rudel, localization: AppLocalizations.of(context)!.rudel,
@@ -219,7 +238,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Participants // Participants field
VarTextField( VarTextField(
textController: rmap["Teilnehmer"]!["controller"]!, textController: rmap["Teilnehmer"]!["controller"]!,
localization: AppLocalizations.of(context)!.teilnehmer, localization: AppLocalizations.of(context)!.teilnehmer,
@@ -228,7 +247,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Duration // Duration field
VarTextField( VarTextField(
textController: rmap["Dauer"]!["controller"]!, textController: rmap["Dauer"]!["controller"]!,
localization: AppLocalizations.of(context)!.dauer, localization: AppLocalizations.of(context)!.dauer,
@@ -237,13 +256,13 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// ---------- Dog(leash) // Dog and leash selection
HundULeine( HundULeine(
mHund: rmap["MHund"]!["controller"]!, mHund: rmap["MHund"]!["controller"]!,
mLeine: rmap["MLeine"]!["controller"]!, mLeine: rmap["MLeine"]!["controller"]!,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- State // State field
VarTextField( VarTextField(
textController: rmap["BLand"]!["controller"]!, textController: rmap["BLand"]!["controller"]!,
localization: AppLocalizations.of(context)!.bland, localization: AppLocalizations.of(context)!.bland,
@@ -252,7 +271,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Country // County field
VarTextField( VarTextField(
textController: rmap["Lkr"]!["controller"]!, textController: rmap["Lkr"]!["controller"]!,
localization: AppLocalizations.of(context)!.lkr, localization: AppLocalizations.of(context)!.lkr,
@@ -261,7 +280,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- By State // Nearby location field
VarTextField( VarTextField(
textController: rmap["BeiOrt"]!["controller"]!, textController: rmap["BeiOrt"]!["controller"]!,
localization: AppLocalizations.of(context)!.beiort, localization: AppLocalizations.of(context)!.beiort,
@@ -270,7 +289,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Bima number // BImA information section
const Divider(), const Divider(),
const SizedBox(height: 10), const SizedBox(height: 10),
ClipRRect( ClipRRect(
@@ -307,7 +326,6 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Bima name
VarTextField( VarTextField(
textController: textController:
rmap["BimaName"]!["controller"]!, rmap["BimaName"]!["controller"]!,
@@ -318,7 +336,6 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Bima user
BimaNutzer( BimaNutzer(
onBimaNutzerChanged: (value) { onBimaNutzerChanged: (value) {
setState(() { setState(() {
@@ -328,7 +345,6 @@ class _ExcursionMainState extends State<ExcursionMain> {
}, },
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Bima AGV
VarTextField( VarTextField(
textController: rmap["BimaAGV"]!["controller"]!, textController: rmap["BimaAGV"]!["controller"]!,
localization: localization:
@@ -351,7 +367,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
title: Text(AppLocalizations.of(context)!.umstaendeUndAktionen), title: Text(AppLocalizations.of(context)!.umstaendeUndAktionen),
content: Column( content: Column(
children: [ children: [
// ---------- Tracking // GPS tracking button
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
// Check for always permission before starting tracking // Check for always permission before starting tracking
@@ -426,7 +442,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Weather // Weather field
VarTextField( VarTextField(
textController: rmap["Wetter"]!["controller"]!, textController: rmap["Wetter"]!["controller"]!,
localization: AppLocalizations.of(context)!.wetter, localization: AppLocalizations.of(context)!.wetter,
@@ -435,7 +451,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Temperature // Temperature field
VarTextField( VarTextField(
textController: rmap["Temperat"]!["controller"]!, textController: rmap["Temperat"]!["controller"]!,
localization: AppLocalizations.of(context)!.temperatur, localization: AppLocalizations.of(context)!.temperatur,
@@ -444,11 +460,11 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// ---------- Last precipitation // Last precipitation selection
LetzterNiederschlag( LetzterNiederschlag(
controller: rmap["RegenVor"]!["controller"]!), controller: rmap["RegenVor"]!["controller"]!),
const SizedBox(height: 20), const SizedBox(height: 20),
// ---------- Track conditions // Distance and track conditions
StreckeUSpurbedingungen( StreckeUSpurbedingungen(
kmAutoController: rmap["KmAuto"]!["controller"]!, kmAutoController: rmap["KmAuto"]!["controller"]!,
kmFussController: rmap["KmFuss"]!["controller"]!, kmFussController: rmap["KmFuss"]!["controller"]!,
@@ -459,7 +475,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
const Divider(), const Divider(),
// ---------- Track found // Track findings
SpurGefunden( SpurGefunden(
spurFund: rmap["SpurFund"]!["controller"]!, spurFund: rmap["SpurFund"]!["controller"]!,
spurLang: rmap["SpurLang"]!["controller"]!, spurLang: rmap["SpurLang"]!["controller"]!,
@@ -471,7 +487,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
), ),
const Divider(), const Divider(),
const SizedBox(height: 20), const SizedBox(height: 20),
// ---------- Counts // Sample counts
Anzahlen( Anzahlen(
losungAnz: rmap["LosungAnz"]!["controller"]!, losungAnz: rmap["LosungAnz"]!["controller"]!,
losungGes: rmap["LosungGes"]!["controller"]!, losungGes: rmap["LosungGes"]!["controller"]!,
@@ -486,7 +502,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
const SizedBox(height: 20), const SizedBox(height: 20),
const Divider(), const Divider(),
const SizedBox(height: 20), const SizedBox(height: 20),
// ---------- Clues // Observations section
Align( Align(
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text( child: Text(
@@ -502,7 +518,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
title: Text(AppLocalizations.of(context)!.intkomm), title: Text(AppLocalizations.of(context)!.intkomm),
content: Column( content: Column(
children: [ children: [
// ---------- Remarks // Remarks field
VarTextField( VarTextField(
textController: rmap["Bemerk"]!["controller"]!, textController: rmap["Bemerk"]!["controller"]!,
localization: AppLocalizations.of(context)!.sonstbemerkungen, localization: AppLocalizations.of(context)!.sonstbemerkungen,
@@ -511,7 +527,7 @@ class _ExcursionMainState extends State<ExcursionMain> {
dbDesignation: DatabasesEnum.excursion, dbDesignation: DatabasesEnum.excursion,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// ---------- Internal communication // Internal communication field
VarTextField( VarTextField(
textController: rmap["IntKomm"]!["controller"]!, textController: rmap["IntKomm"]!["controller"]!,
localization: AppLocalizations.of(context)!.intkomm, localization: AppLocalizations.of(context)!.intkomm,
@@ -583,7 +599,6 @@ class _ExcursionMainState extends State<ExcursionMain> {
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context)!.excursion), title: Text(AppLocalizations.of(context)!.excursion),
actions: [ actions: [
// Text(TrackingService().isTracking ? "Tracking" : "Not tracking")
Image.asset( Image.asset(
TrackingService().isTracking TrackingService().isTracking
? "assets/icons/tracking_on.png" ? "assets/icons/tracking_on.png"

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class Anzahlen extends StatefulWidget {
/// Controller for number of droppings found
final TextEditingController losungAnz; final TextEditingController losungAnz;
/// Controller for number of droppings collected
final TextEditingController losungGes; final TextEditingController losungGes;
/// Controller for number of genetic samples from droppings
final TextEditingController losungGen; final TextEditingController losungGen;
/// Controller for number of urine marking spots
final TextEditingController urinAnz; final TextEditingController urinAnz;
/// Controller for number of genetic samples from urine
final TextEditingController urinGen; final TextEditingController urinGen;
/// Controller for number of estrus blood spots
final TextEditingController oestrAnz; final TextEditingController oestrAnz;
/// Controller for number of genetic samples from estrus blood
final TextEditingController oestrGen; final TextEditingController oestrGen;
/// Controller for number of hair samples
final TextEditingController haarAnz; final TextEditingController haarAnz;
/// Controller for number of genetic samples from hair
final TextEditingController haarGen; final TextEditingController haarGen;
const Anzahlen( const Anzahlen(
@@ -28,6 +47,7 @@ class Anzahlen extends StatefulWidget {
AnzahlenState createState() => AnzahlenState(); AnzahlenState createState() => AnzahlenState();
} }
/// State class for the quantity tracking widget
class AnzahlenState extends State<Anzahlen> { class AnzahlenState extends State<Anzahlen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -37,6 +57,7 @@ class AnzahlenState extends State<Anzahlen> {
children: [ children: [
Column( Column(
children: [ children: [
// Droppings count section
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -46,9 +67,7 @@ class AnzahlenState extends State<Anzahlen> {
child: Text( child: Text(
AppLocalizations.of(context)!.anzahlLosungen)), AppLocalizations.of(context)!.anzahlLosungen)),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, child: TextField( 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), onTap: () => widget.losungAnz.selection = TextSelection(baseOffset: 0, extentOffset: widget.losungAnz.value.text.length),
)), )),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
flex: 2, flex: 2,
child: Align( child: Align(
@@ -67,9 +84,7 @@ class AnzahlenState extends State<Anzahlen> {
child: Text( child: Text(
AppLocalizations.of(context)!.davonEingesammelt)), AppLocalizations.of(context)!.davonEingesammelt)),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, child: TextField( 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), onTap: () => widget.losungGes.selection = TextSelection(baseOffset: 0, extentOffset: widget.losungGes.value.text.length),
)), )),
), ),
const SizedBox( const SizedBox(height: 20),
height: 20,
),
], ],
), ),
// Genetic samples from droppings
Row( Row(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
@@ -94,9 +108,7 @@ class AnzahlenState extends State<Anzahlen> {
AppLocalizations.of(context)!.davonGenetikproben), AppLocalizations.of(context)!.davonGenetikproben),
), ),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, child: TextField( alignment: Alignment.centerLeft, child: TextField(
@@ -107,9 +119,8 @@ class AnzahlenState extends State<Anzahlen> {
), ),
], ],
), ),
const Divider( const Divider(height: 40),
height: 40, // Urine marking spots section
),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -119,9 +130,7 @@ class AnzahlenState extends State<Anzahlen> {
child: Text(AppLocalizations.of(context)! child: Text(AppLocalizations.of(context)!
.anzahlUrinMakierstellen)), .anzahlUrinMakierstellen)),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, child: TextField( 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), onTap: () => widget.urinAnz.selection = TextSelection(baseOffset: 0, extentOffset: widget.urinAnz.value.text.length),
)), )),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
flex: 2, flex: 2,
child: Align( child: Align(
@@ -140,9 +147,7 @@ class AnzahlenState extends State<Anzahlen> {
child: Text(AppLocalizations.of(context)! child: Text(AppLocalizations.of(context)!
.davonGenetikproben)), .davonGenetikproben)),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, child: TextField( 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), onTap: () => widget.urinGen.selection = TextSelection(baseOffset: 0, extentOffset: widget.urinGen.value.text.length),
)), )),
), ),
const SizedBox( const SizedBox(height: 20),
height: 20,
),
], ],
), ),
const Divider( const Divider(height: 40),
height: 40, // Estrus blood section
),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -168,9 +170,7 @@ class AnzahlenState extends State<Anzahlen> {
child: Text( child: Text(
AppLocalizations.of(context)!.anzahlOestrusblut)), AppLocalizations.of(context)!.anzahlOestrusblut)),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, child: TextField( 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), onTap: () => widget.oestrAnz.selection = TextSelection(baseOffset: 0, extentOffset: widget.oestrAnz.value.text.length),
)), )),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
flex: 2, flex: 2,
child: Align( child: Align(
@@ -189,9 +187,7 @@ class AnzahlenState extends State<Anzahlen> {
child: Text(AppLocalizations.of(context)! child: Text(AppLocalizations.of(context)!
.davonGenetikproben)), .davonGenetikproben)),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, child: TextField( 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), onTap: () => widget.oestrGen.selection = TextSelection(baseOffset: 0, extentOffset: widget.oestrGen.value.text.length),
)), )),
), ),
const SizedBox( const SizedBox(height: 20),
height: 20,
),
], ],
), ),
const Divider( const Divider(height: 40),
height: 40, // Hair samples section
),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -217,9 +210,7 @@ class AnzahlenState extends State<Anzahlen> {
child: Text( child: Text(
AppLocalizations.of(context)!.anzahlHaarproben)), AppLocalizations.of(context)!.anzahlHaarproben)),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, child: TextField( 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), onTap: () => widget.haarAnz.selection = TextSelection(baseOffset: 0, extentOffset: widget.haarAnz.value.text.length),
)), )),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
flex: 2, flex: 2,
child: Align( child: Align(
@@ -238,9 +227,7 @@ class AnzahlenState extends State<Anzahlen> {
child: Text(AppLocalizations.of(context)! child: Text(AppLocalizations.of(context)!
.davonGenetikproben)), .davonGenetikproben)),
), ),
const SizedBox( const SizedBox(width: 20),
width: 20,
),
Expanded( Expanded(
child: Align( child: Align(
alignment: Alignment.centerLeft, child: TextField( 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), onTap: () => widget.haarGen.selection = TextSelection(baseOffset: 0, extentOffset: widget.haarGen.value.text.length),
)), )),
), ),
const SizedBox( const SizedBox(height: 20),
height: 20,
),
], ],
), ),
], ],

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/l10n/app_localizations.dart';
// Bundeswehr /// Widget for selecting the type of BImA property user
// Gastreitkraefte /// Used to categorize the property where monitoring takes place
// NNE Bund
// Geschaeftsliegenschaft/AGV
// kein
class BimaNutzer extends StatefulWidget { class BimaNutzer extends StatefulWidget {
/// Callback function when user type selection changes
final Function(String) onBimaNutzerChanged; final Function(String) onBimaNutzerChanged;
/// Initial user type selection ('Bundeswehr' by default)
final String initialStatus; final String initialStatus;
const BimaNutzer( const BimaNutzer(
@@ -18,7 +27,9 @@ class BimaNutzer extends StatefulWidget {
State<BimaNutzer> createState() => _StatusState(); State<BimaNutzer> createState() => _StatusState();
} }
/// State class for the BImA user selection widget
class _StatusState extends State<BimaNutzer> { class _StatusState extends State<BimaNutzer> {
/// Currently selected user type
String? _selectedStatus; String? _selectedStatus;
@override @override
@@ -31,6 +42,7 @@ class _StatusState extends State<BimaNutzer> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// German Armed Forces option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.bundeswehr), title: Text(AppLocalizations.of(context)!.bundeswehr),
@@ -45,6 +57,7 @@ class _StatusState extends State<BimaNutzer> {
}, },
), ),
), ),
// Foreign Armed Forces option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.gaststreitkraefte), title: Text(AppLocalizations.of(context)!.gaststreitkraefte),
@@ -59,6 +72,7 @@ class _StatusState extends State<BimaNutzer> {
}, },
), ),
), ),
// Federal non-civil use option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.nneBund), title: Text(AppLocalizations.of(context)!.nneBund),
@@ -73,6 +87,7 @@ class _StatusState extends State<BimaNutzer> {
}, },
), ),
), ),
// Commercial property option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.geschaeftsliegenschaftAGV), title: Text(AppLocalizations.of(context)!.geschaeftsliegenschaftAGV),
@@ -87,6 +102,7 @@ class _StatusState extends State<BimaNutzer> {
}, },
), ),
), ),
// No user option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.kein), title: Text(AppLocalizations.of(context)!.kein),

View File

@@ -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/enums/databases.dart';
import 'package:fforte/screens/sharedWidgets/var_text_field.dart'; import 'package:fforte/screens/sharedWidgets/var_text_field.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class Hinweise extends StatefulWidget {
/// Controller for the combined observation text
final TextEditingController hinweise; final TextEditingController hinweise;
const Hinweise({super.key, required this.hinweise}); const Hinweise({super.key, required this.hinweise});
@@ -11,21 +26,29 @@ class Hinweise extends StatefulWidget {
@override @override
State<Hinweise> createState() => _HinweiseState(); 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(); TextEditingController sonstigesController = TextEditingController();
@override @override
void initState() { void initState() {
sonstigesController.addListener(updateController); sonstigesController.addListener(updateController);
// Initialize checkboxes based on existing text
liegestelleChecked = widget.hinweise.text.contains("Liegestelle") ? true : false; liegestelleChecked = widget.hinweise.text.contains("Liegestelle") ? true : false;
kadaverChecked = widget.hinweise.text.contains("Wildtierkadaver") ? true : false; kadaverChecked = widget.hinweise.text.contains("Wildtierkadaver") ? true : false;
sichtungChecked = widget.hinweise.text.contains("Sichtung") ? true : false; sichtungChecked = widget.hinweise.text.contains("Sichtung") ? true : false;
@@ -33,6 +56,7 @@ class _HinweiseState extends State<Hinweise> {
bool firstRun = true; bool firstRun = true;
// Parse existing other observations
for (String val in widget.hinweise.text.split(",")) { for (String val in widget.hinweise.text.split(",")) {
if (val != "Liegestelle" && val != "Wildtierkadaver" && val != "Sichtung" && val != "Heulen" && val != "") { if (val != "Liegestelle" && val != "Wildtierkadaver" && val != "Sichtung" && val != "Heulen" && val != "") {
sonstigesChecked = true; sonstigesChecked = true;
@@ -51,6 +75,8 @@ class _HinweiseState extends State<Hinweise> {
super.dispose(); 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() { void updateController() {
Map<String, bool> props = { Map<String, bool> props = {
"Liegestelle": liegestelleChecked, "Liegestelle": liegestelleChecked,
@@ -63,6 +89,7 @@ class _HinweiseState extends State<Hinweise> {
widget.hinweise.text = ""; widget.hinweise.text = "";
// Build combined text from selected options
for (String key in props.keys) { for (String key in props.keys) {
if (!firstRun && props[key]!) { if (!firstRun && props[key]!) {
widget.hinweise.text += ","; widget.hinweise.text += ",";
@@ -74,7 +101,6 @@ class _HinweiseState extends State<Hinweise> {
} else if (props[key]!){ } else if (props[key]!){
widget.hinweise.text += key; widget.hinweise.text += key;
} }
} }
debugPrint(widget.hinweise.text); debugPrint(widget.hinweise.text);
} }
@@ -83,6 +109,7 @@ class _HinweiseState extends State<Hinweise> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Resting place checkbox
CheckboxListTile( CheckboxListTile(
title: Text(AppLocalizations.of(context)!.liegestelle), title: Text(AppLocalizations.of(context)!.liegestelle),
value: liegestelleChecked, value: liegestelleChecked,
@@ -90,6 +117,7 @@ class _HinweiseState extends State<Hinweise> {
setState(() => liegestelleChecked = value ?? false); setState(() => liegestelleChecked = value ?? false);
updateController(); updateController();
}), }),
// Animal carcass checkbox
CheckboxListTile( CheckboxListTile(
title: Text(AppLocalizations.of(context)!.wildtierKadaver), title: Text(AppLocalizations.of(context)!.wildtierKadaver),
value: kadaverChecked, value: kadaverChecked,
@@ -97,6 +125,7 @@ class _HinweiseState extends State<Hinweise> {
setState(() => kadaverChecked = value ?? false); setState(() => kadaverChecked = value ?? false);
updateController(); updateController();
}), }),
// Direct sighting checkbox
CheckboxListTile( CheckboxListTile(
title: Text(AppLocalizations.of(context)!.sichtung), title: Text(AppLocalizations.of(context)!.sichtung),
value: sichtungChecked, value: sichtungChecked,
@@ -104,6 +133,7 @@ class _HinweiseState extends State<Hinweise> {
setState(() => sichtungChecked = value ?? false); setState(() => sichtungChecked = value ?? false);
updateController(); updateController();
}), }),
// Howling checkbox
CheckboxListTile( CheckboxListTile(
title: Text(AppLocalizations.of(context)!.heulen), title: Text(AppLocalizations.of(context)!.heulen),
value: heulenChecked, value: heulenChecked,
@@ -111,6 +141,7 @@ class _HinweiseState extends State<Hinweise> {
setState(() => heulenChecked = value ?? false); setState(() => heulenChecked = value ?? false);
updateController(); updateController();
}), }),
// Other observations checkbox and input field
CheckboxListTile( CheckboxListTile(
title: Text(AppLocalizations.of(context)!.sonstiges), title: Text(AppLocalizations.of(context)!.sonstiges),
value: sonstigesChecked, value: sonstigesChecked,

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class HundULeine extends StatefulWidget {
// 1. with dog (ja, bzw name oder nein) 2. with leash /// Controller for dog presence status (yes/name or no)
// if nothing selected null
final TextEditingController mHund; final TextEditingController mHund;
/// Controller for leash status
final TextEditingController mLeine; final TextEditingController mLeine;
const HundULeine({super.key, required this.mHund, required this.mLeine}); const HundULeine({super.key, required this.mHund, required this.mLeine});
@@ -13,13 +22,18 @@ class HundULeine extends StatefulWidget {
HundULeineState createState() => HundULeineState(); HundULeineState createState() => HundULeineState();
} }
/// State class for the dog and leash selection widget
class HundULeineState extends State<HundULeine> { class HundULeineState extends State<HundULeine> {
/// Currently selected dog presence value
late String _selectedMHundValue; late String _selectedMHundValue;
/// Currently selected leash status value
late String _selectedMLeineValue; late String _selectedMLeineValue;
/// Whether to show leash selection options
bool visible = false; bool visible = false;
@override @override
void initState() { void initState() {
// Initialize dog presence selection
if (widget.mHund.text == "") { if (widget.mHund.text == "") {
_selectedMHundValue = "nein"; _selectedMHundValue = "nein";
} else { } else {
@@ -27,6 +41,7 @@ class HundULeineState extends State<HundULeine> {
visible = true; visible = true;
} }
// Initialize leash status selection
if (widget.mLeine.text == "") { if (widget.mLeine.text == "") {
_selectedMLeineValue = "nein"; _selectedMLeineValue = "nein";
} else { } else {
@@ -36,6 +51,9 @@ class HundULeineState extends State<HundULeine> {
super.initState(); 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) { void onValueChanged(String mHund, String mLeine) {
setState(() { setState(() {
visible = mHund == "ja" ? true : false; visible = mHund == "ja" ? true : false;
@@ -50,10 +68,12 @@ class HundULeineState extends State<HundULeine> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Dog presence section header
Align( Align(
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text(AppLocalizations.of(context)!.mHund), child: Text(AppLocalizations.of(context)!.mHund),
), ),
// Dog presence - Yes option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.ja), title: Text(AppLocalizations.of(context)!.ja),
@@ -65,6 +85,7 @@ class HundULeineState extends State<HundULeine> {
}, },
), ),
), ),
// Dog presence - No option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.nein), title: Text(AppLocalizations.of(context)!.nein),
@@ -76,19 +97,14 @@ class HundULeineState extends State<HundULeine> {
}, },
), ),
), ),
// Conditional leash status section
if (visible) ...[ if (visible) ...[
// TextField( // Leash status section header
// controller: controller,
// onChanged: (value) {
// onValueChanged("ja", _selectedMLeineValue);
// },
// decoration:
// InputDecoration(hintText: AppLocalizations.of(context)!.name),
// ),
Align( Align(
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text(AppLocalizations.of(context)!.mLeine), child: Text(AppLocalizations.of(context)!.mLeine),
), ),
// Leash status - Yes option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.ja), title: Text(AppLocalizations.of(context)!.ja),
@@ -100,6 +116,7 @@ class HundULeineState extends State<HundULeine> {
}, },
), ),
), ),
// Leash status - No option
ListTile( ListTile(
visualDensity: const VisualDensity(vertical: -4), visualDensity: const VisualDensity(vertical: -4),
title: Text(AppLocalizations.of(context)!.nein), title: Text(AppLocalizations.of(context)!.nein),

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class LetzterNiederschlag extends StatefulWidget {
/// Controller for storing the selected precipitation timing
final TextEditingController controller; final TextEditingController controller;
const LetzterNiederschlag({super.key, required this.controller}); const LetzterNiederschlag({super.key, required this.controller});
@@ -10,11 +27,14 @@ class LetzterNiederschlag extends StatefulWidget {
LetzterNiederschlagState createState() => LetzterNiederschlagState(); LetzterNiederschlagState createState() => LetzterNiederschlagState();
} }
/// State class for the last precipitation selection widget
class LetzterNiederschlagState extends State<LetzterNiederschlag> { class LetzterNiederschlagState extends State<LetzterNiederschlag> {
late String? selectedValue; // Variable für den ausgewählten Wert /// Currently selected precipitation timing value
late String? selectedValue;
@override @override
void initState() { void initState() {
// Initialize selection from controller
if (widget.controller.text == "") { if (widget.controller.text == "") {
selectedValue = null; selectedValue = null;
} else { } else {
@@ -36,30 +56,37 @@ class LetzterNiederschlagState extends State<LetzterNiederschlag> {
}); });
}, },
items: [ items: [
// Currently raining option
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: "aktuell", value: "aktuell",
child: Text(AppLocalizations.of(context)!.aktuell), child: Text(AppLocalizations.of(context)!.aktuell),
), ),
// Same morning option
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: "am selben Morgen", value: "am selben Morgen",
child: Text(AppLocalizations.of(context)!.selberMorgen), child: Text(AppLocalizations.of(context)!.selberMorgen),
), ),
// Last night option
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: "in der Nacht", value: "in der Nacht",
child: Text(AppLocalizations.of(context)!.letzteNacht), child: Text(AppLocalizations.of(context)!.letzteNacht),
), ),
// Previous day/evening option
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: "am Tag oder Abend zuvor", value: "am Tag oder Abend zuvor",
child: Text(AppLocalizations.of(context)!.vortag), child: Text(AppLocalizations.of(context)!.vortag),
), ),
// 2-3 days ago option
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: "vor 2 bis 3 Tagen", value: "vor 2 bis 3 Tagen",
child: Text(AppLocalizations.of(context)!.vor23Tagen), child: Text(AppLocalizations.of(context)!.vor23Tagen),
), ),
// 4-6 days ago option
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: "vor 4 bis 6 Tagen", value: "vor 4 bis 6 Tagen",
child: Text(AppLocalizations.of(context)!.vor46Tagen), child: Text(AppLocalizations.of(context)!.vor46Tagen),
), ),
// 1 week or more option
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: ">=1 Woche", value: ">=1 Woche",
child: Text(AppLocalizations.of(context)!.vor1Woche), child: Text(AppLocalizations.of(context)!.vor1Woche),

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class SpurGefunden extends StatefulWidget {
/// Controller for track presence status
final TextEditingController spurFund; final TextEditingController spurFund;
/// Controller for total track length
final TextEditingController spurLang; final TextEditingController spurLang;
/// Controller for estimated number of animals
final TextEditingController spurTiere; final TextEditingController spurTiere;
/// Controller for track identification confidence
final TextEditingController spSicher; final TextEditingController spSicher;
/// Controller for cub/pup track length
final TextEditingController welpenSp; final TextEditingController welpenSp;
/// Controller for estimated number of cubs/pups
final TextEditingController welpenAnz; final TextEditingController welpenAnz;
/// Controller for cub/pup track identification confidence
final TextEditingController wpSicher; final TextEditingController wpSicher;
const SpurGefunden({ const SpurGefunden({
@@ -25,14 +43,20 @@ class SpurGefunden extends StatefulWidget {
State<SpurGefunden> createState() => _SpurGefundenState(); State<SpurGefunden> createState() => _SpurGefundenState();
} }
/// State class for the track findings widget
class _SpurGefundenState extends State<SpurGefunden> { class _SpurGefundenState extends State<SpurGefunden> {
/// Whether any tracks were found
late bool _spurFundChecked; late bool _spurFundChecked;
/// Whether adult track identification is confident
bool _spSicher = false; bool _spSicher = false;
/// Whether cub/pup track identification is confident
bool _wpSicher = false; bool _wpSicher = false;
/// Whether cub/pup tracks were found
late bool _welpenSpFundChecked; late bool _welpenSpFundChecked;
@override @override
void initState() { void initState() {
// Initialize track finding states
if (widget.spurFund.text == "") { if (widget.spurFund.text == "") {
_spurFundChecked = false; _spurFundChecked = false;
_welpenSpFundChecked = false; _welpenSpFundChecked = false;
@@ -53,6 +77,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Track presence checkbox
Row( Row(
children: [ children: [
Text(AppLocalizations.of(context)!.spurGefunden), 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( Visibility(
visible: _spurFundChecked, visible: _spurFundChecked,
child: Column( child: Column(
children: [ children: [
// Total track length input
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -101,7 +118,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
), ),
], ],
), ),
// const SizedBox(height: 10), // Estimated animal count input
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -124,6 +141,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
), ),
], ],
), ),
// Track identification confidence
Row( Row(
children: [ children: [
Text(AppLocalizations.of(context)!.sicher), Text(AppLocalizations.of(context)!.sicher),
@@ -139,7 +157,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
), ),
], ],
), ),
// const SizedBox(height: 10), // Cub/pup track presence checkbox
Row( Row(
children: [ children: [
Text(AppLocalizations.of(context)!.welpenSpurGefunden), Text(AppLocalizations.of(context)!.welpenSpurGefunden),
@@ -153,10 +171,12 @@ class _SpurGefundenState extends State<SpurGefunden> {
), ),
], ],
), ),
// Cub/pup track details section
Visibility( Visibility(
visible: _welpenSpFundChecked, visible: _welpenSpFundChecked,
child: Column( child: Column(
children: [ children: [
// Cub/pup track length input
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -179,9 +199,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
), ),
], ],
), ),
// Estimated cub/pup count input
// const SizedBox(height: 10),
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -204,6 +222,7 @@ class _SpurGefundenState extends State<SpurGefunden> {
), ),
], ],
), ),
// Cub/pup track identification confidence
Row( Row(
children: [ children: [
Text(AppLocalizations.of(context)!.sicher), Text(AppLocalizations.of(context)!.sicher),

View File

@@ -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:fforte/screens/helper/snack_bar_helper.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.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 { class StreckeUSpurbedingungen extends StatefulWidget {
/// Controller for distance traveled by car
final TextEditingController kmAutoController; final TextEditingController kmAutoController;
/// Controller for distance traveled on foot
final TextEditingController kmFussController; final TextEditingController kmFussController;
/// Controller for distance traveled by bicycle
final TextEditingController kmRadController; final TextEditingController kmRadController;
/// Controller for distance with good track conditions
final TextEditingController spGutController; final TextEditingController spGutController;
/// Controller for distance with medium track conditions
final TextEditingController spMittelController; final TextEditingController spMittelController;
/// Controller for distance with poor track conditions
final TextEditingController spSchlechtController; final TextEditingController spSchlechtController;
const StreckeUSpurbedingungen({ const StreckeUSpurbedingungen({
@@ -24,6 +39,7 @@ class StreckeUSpurbedingungen extends StatefulWidget {
StreckeUSpurbedingungenState createState() => StreckeUSpurbedingungenState(); StreckeUSpurbedingungenState createState() => StreckeUSpurbedingungenState();
} }
/// State class for the distance and track conditions widget
class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> { class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
// vars for percent text fields // vars for percent text fields
// String carPercent = "0"; // String carPercent = "0";
@@ -44,7 +60,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
// widget.kmFussController.addListener(onDistanceTravledUpdated); // widget.kmFussController.addListener(onDistanceTravledUpdated);
// widget.kmRadController.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 == "") { if (widget.kmAutoController.text == "") {
widget.kmAutoController.text = "0"; widget.kmAutoController.text = "0";
widget.kmFussController.text = "0"; widget.kmFussController.text = "0";
@@ -56,7 +72,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
widget.spMittelController.addListener(onTrackConditionsUpdated); widget.spMittelController.addListener(onTrackConditionsUpdated);
widget.spSchlechtController.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 == "") { if (widget.spGutController.text == "") {
widget.spGutController.text = "0"; widget.spGutController.text = "0";
widget.spMittelController.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() { void onTrackConditionsUpdated() {
try { try {
// Parse track condition distances
double kmGood = double.parse(widget.spGutController.text); double kmGood = double.parse(widget.spGutController.text);
double kmMiddle = double.parse(widget.spMittelController.text); double kmMiddle = double.parse(widget.spMittelController.text);
double kmBad = double.parse(widget.spSchlechtController.text); double kmBad = double.parse(widget.spSchlechtController.text);
// Parse travel distances
double kmAuto = double.parse(widget.kmAutoController.text); double kmAuto = double.parse(widget.kmAutoController.text);
double kmFuss = double.parse(widget.kmFussController.text); double kmFuss = double.parse(widget.kmFussController.text);
double kmRad = double.parse(widget.kmRadController.text); double kmRad = double.parse(widget.kmRadController.text);
// Calculate totals
double gesConditionsKm = (kmGood + kmMiddle + kmBad); double gesConditionsKm = (kmGood + kmMiddle + kmBad);
double gesDistanceKm = (kmAuto + kmFuss + kmRad); double gesDistanceKm = (kmAuto + kmFuss + kmRad);
// Show warning if track conditions exceed distance
if (gesConditionsKm > gesDistanceKm) { if (gesConditionsKm > gesDistanceKm) {
SnackBarHelper.showSnackBarMessage(context, AppLocalizations.of(context)!.bedingungenGroesserAlsStrecke); SnackBarHelper.showSnackBarMessage(context, AppLocalizations.of(context)!.bedingungenGroesserAlsStrecke);
} }
@@ -115,6 +136,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
children: [ children: [
// Travel distance section header
Align( Align(
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text( child: Text(
@@ -124,8 +146,10 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
// Travel distance inputs
Row( Row(
children: [ children: [
// Car distance input
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@@ -145,6 +169,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
), ),
), ),
// Foot distance input
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@@ -164,6 +189,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
), ),
), ),
// Bicycle distance input
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@@ -189,6 +215,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
const SizedBox(height: 20), const SizedBox(height: 20),
// Track conditions section header
Align( Align(
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text( child: Text(
@@ -198,8 +225,10 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
), ),
const SizedBox(height: 10,), const SizedBox(height: 10,),
// Track condition inputs
Row( Row(
children: [ children: [
// Good conditions input
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
@@ -215,6 +244,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
), ),
), ),
), ),
// Medium conditions input
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
@@ -230,6 +260,7 @@ class StreckeUSpurbedingungenState extends State<StreckeUSpurbedingungen> {
), ),
), ),
), ),
// Poor conditions input
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),

View File

@@ -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 'dart:async';
import 'package:fforte/l10n/app_localizations.dart'; 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:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
/// Widget for managing GPS tracking functionality
/// Provides map visualization and tracking controls
class Tracking extends StatefulWidget { class Tracking extends StatefulWidget {
/// Initial position for the tracking session
final Position startPosition; final Position startPosition;
/// Controller for storing the tracked path
final TextEditingController weg; final TextEditingController weg;
const Tracking({super.key, required this.startPosition, required this.weg}); const Tracking({super.key, required this.startPosition, required this.weg});
@override @override
State<Tracking> createState() => _TrackingState(); State<Tracking> createState() => _TrackingState();
} }
/// State class for the tracking widget
class _TrackingState extends State<Tracking> { class _TrackingState extends State<Tracking> {
/// Service for managing tracking functionality
final TrackingService _trackingService = TrackingService(); final TrackingService _trackingService = TrackingService();
/// Current GPS position
Position? currentPosition; Position? currentPosition;
/// Controller for the map widget
MapController mapController = MapController(); MapController mapController = MapController();
/// Subscription for position updates
StreamSubscription? _positionSubscription; StreamSubscription? _positionSubscription;
/// Subscription for tracking statistics updates
StreamSubscription? _statsSubscription; StreamSubscription? _statsSubscription;
/// Current tracking statistics
TrackingStats? _currentStats; TrackingStats? _currentStats;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Load existing track if available
if (widget.weg.text.isNotEmpty) { if (widget.weg.text.isNotEmpty) {
for (var element in widget.weg.text.split(";")) { for (var element in widget.weg.text.split(";")) {
List<String> posSplit = element.split(","); List<String> posSplit = element.split(",");
@@ -50,24 +72,27 @@ class _TrackingState extends State<Tracking> {
currentPosition = widget.startPosition; currentPosition = widget.startPosition;
// Initialisiere die Statistiken sofort // Initialize tracking statistics
setState(() { setState(() {
_currentStats = _trackingService.currentStats; _currentStats = _trackingService.currentStats;
}); });
_trackingService.requestStatsUpdate(); _trackingService.requestStatsUpdate();
// Subscribe to position updates
_positionSubscription = _trackingService.positionStream$.listen((position) { _positionSubscription = _trackingService.positionStream$.listen((position) {
setState(() { setState(() {
currentPosition = position; currentPosition = position;
}); });
}); });
// Subscribe to statistics updates
_statsSubscription = _trackingService.statsStream$.listen((stats) { _statsSubscription = _trackingService.statsStream$.listen((stats) {
setState(() { setState(() {
_currentStats = stats; _currentStats = stats;
}); });
}); });
// Check location permissions
GeolocatorService.alwaysPositionEnabled().then((value) { GeolocatorService.alwaysPositionEnabled().then((value) {
if (!value && mounted) { if (!value && mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -84,6 +109,9 @@ class _TrackingState extends State<Tracking> {
super.dispose(); super.dispose();
} }
/// Format distance for display
/// @param meters Distance in meters
/// @return Formatted distance string with appropriate unit
String _formatDistance(double meters) { String _formatDistance(double meters) {
if (meters >= 1000) { if (meters >= 1000) {
return '${(meters / 1000).toStringAsFixed(2)} km'; return '${(meters / 1000).toStringAsFixed(2)} km';
@@ -99,6 +127,7 @@ class _TrackingState extends State<Tracking> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(AppLocalizations.of(context)!.tracking), Text(AppLocalizations.of(context)!.tracking),
// Display tracking statistics if available
if (_currentStats != null) if (_currentStats != null)
DefaultTextStyle( DefaultTextStyle(
style: Theme.of(context).textTheme.bodySmall!, style: Theme.of(context).textTheme.bodySmall!,
@@ -125,6 +154,7 @@ class _TrackingState extends State<Tracking> {
icon: Icon(Icons.arrow_back_rounded) icon: Icon(Icons.arrow_back_rounded)
), ),
actions: [ actions: [
// Delete track button (only when not tracking)
if (!_trackingService.isTracking) if (!_trackingService.isTracking)
IconButton( IconButton(
onPressed: () async { onPressed: () async {
@@ -140,6 +170,7 @@ class _TrackingState extends State<Tracking> {
color: Theme.of(context).colorScheme.errorContainer, color: Theme.of(context).colorScheme.errorContainer,
), ),
), ),
// Stop tracking button (only when tracking)
if (_trackingService.isTracking) if (_trackingService.isTracking)
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -149,6 +180,7 @@ class _TrackingState extends State<Tracking> {
}, },
child: Text(AppLocalizations.of(context)!.trackingStop), child: Text(AppLocalizations.of(context)!.trackingStop),
), ),
// Start/Pause tracking button
TextButton( TextButton(
onPressed: () { onPressed: () {
setState(() { setState(() {
@@ -165,6 +197,7 @@ class _TrackingState extends State<Tracking> {
), ),
], ],
), ),
// Center on current location button
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
mapController.move( mapController.move(
@@ -177,6 +210,7 @@ class _TrackingState extends State<Tracking> {
}, },
child: Icon(Icons.my_location), child: Icon(Icons.my_location),
), ),
// Map display
body: FlutterMap( body: FlutterMap(
mapController: mapController, mapController: mapController,
options: MapOptions( options: MapOptions(
@@ -192,10 +226,12 @@ class _TrackingState extends State<Tracking> {
initialZoom: 16.0, initialZoom: 16.0,
), ),
children: [ children: [
// Base map layer
TileLayer( TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'de.lupus.apps', userAgentPackageName: 'de.lupus.apps',
), ),
// Track path layer
if (_trackingService.pathList.isNotEmpty) if (_trackingService.pathList.isNotEmpty)
PolylineLayer( PolylineLayer(
polylines: [ polylines: [
@@ -206,6 +242,7 @@ class _TrackingState extends State<Tracking> {
), ),
], ],
), ),
// Current position accuracy circle
if (currentPosition != null) if (currentPosition != null)
CircleLayer( CircleLayer(
circles: [ circles: [
@@ -231,6 +268,7 @@ class _TrackingState extends State<Tracking> {
), ),
], ],
), ),
// Current location marker
CurrentLocationLayer(), CurrentLocationLayer(),
], ],
), ),

View File

@@ -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/enums/databases.dart';
import 'package:fforte/screens/helper/snack_bar_helper.dart'; import 'package:fforte/screens/helper/snack_bar_helper.dart';
import 'package:fforte/screens/sharedMethods/http_request.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:fforte/l10n/app_localizations.dart';
import 'package:geolocator/geolocator.dart'; import 'package:geolocator/geolocator.dart';
/// Helper class for managing various dialogs related to adding and saving entries
class AddEntriesDialogHelper { 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( static Future<void> showTemplateDialog(
BuildContext context, BuildContext context,
Map<String, String> saveData, Map<String, String> saveData,
@@ -17,17 +29,19 @@ class AddEntriesDialogHelper {
) async { ) async {
return showDialog( return showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false, // User must make a choice
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context)!.fieldEmpty), title: Text(AppLocalizations.of(context)!.fieldEmpty),
actions: <Widget>[ actions: <Widget>[
// Cancel button
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text(AppLocalizations.of(context)!.cancel), child: Text(AppLocalizations.of(context)!.cancel),
), ),
// Save as template button
TextButton( TextButton(
onPressed: () { onPressed: () {
saveTemplate(saveData, dbType); 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( static Future<dynamic> showServerErrorDialog(
BuildContext context, BuildContext context,
Map<String, String> saveData, Map<String, String> saveData,
@@ -68,6 +88,7 @@ class AddEntriesDialogHelper {
) )
: null, : null,
actions: [ actions: [
// Retry button
if (!isLoading) if (!isLoading)
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@@ -79,19 +100,18 @@ class AddEntriesDialogHelper {
if (errorCode == 200 && context.mounted) { if (errorCode == 200 && context.mounted) {
Navigator.pop(context); Navigator.pop(context);
// saveData(true);
SaveMainEntryMethod.saveEntry( SaveMainEntryMethod.saveEntry(
entryData: saveData, entryData: saveData,
isTemplate: isTemplate, isTemplate: isTemplate,
dbType: dbType, dbType: dbType,
sent: true sent: true
); );
showSuccessDialog(context); showSuccessDialog(context);
} }
}, },
child: Text(AppLocalizations.of(context)!.sendagain), child: Text(AppLocalizations.of(context)!.sendagain),
), ),
// Cancel button
if (!isLoading) if (!isLoading)
TextButton( TextButton(
onPressed: () { 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( static Future<bool> showSaveOptionsDialog(
BuildContext context, BuildContext context,
Map<String, String> saveData, Map<String, String> saveData,
@@ -118,7 +149,7 @@ class AddEntriesDialogHelper {
await showDialog( await showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false, // User must make a choice
builder: (BuildContext context) { builder: (BuildContext context) {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setState) { builder: (context, setState) {
@@ -135,6 +166,7 @@ class AddEntriesDialogHelper {
) )
: null, : null,
actions: [ actions: [
// Save as template button
if (!isLoading) if (!isLoading)
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@@ -156,6 +188,7 @@ class AddEntriesDialogHelper {
}, },
child: Text(AppLocalizations.of(context)!.template), child: Text(AppLocalizations.of(context)!.template),
), ),
// Send to server button
if (!isLoading) if (!isLoading)
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@@ -192,6 +225,7 @@ class AddEntriesDialogHelper {
}, },
child: Text(AppLocalizations.of(context)!.sendtoserver), child: Text(AppLocalizations.of(context)!.sendtoserver),
), ),
// Save as file button
if (!isLoading) if (!isLoading)
TextButton( TextButton(
onPressed: () async { onPressed: () async {
@@ -223,7 +257,7 @@ class AddEntriesDialogHelper {
pop = true; pop = true;
} }
} catch (_) { } catch (_) {
// User cancelled the dialog // User cancelled the file save dialog
} }
setState(() => isLoading = false); setState(() => isLoading = false);
} catch (e) { } catch (e) {
@@ -238,6 +272,7 @@ class AddEntriesDialogHelper {
}, },
child: Text(AppLocalizations.of(context)!.saveasfile), child: Text(AppLocalizations.of(context)!.saveasfile),
), ),
// Save locally only button
if (!isLoading) if (!isLoading)
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -265,6 +300,7 @@ class AddEntriesDialogHelper {
}, },
child: Text(AppLocalizations.of(context)!.justsave), child: Text(AppLocalizations.of(context)!.justsave),
), ),
// Cancel button
if (!isLoading) if (!isLoading)
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -282,6 +318,8 @@ class AddEntriesDialogHelper {
return pop; 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 { static Future<void> showSuccessDialog(BuildContext context) async {
return showDialog( return showDialog(
context: context, 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 { static Future<bool> locationSettingsDialog(BuildContext context) async {
bool reload = false; bool reload = false;
await showDialog( await showDialog(
@@ -316,6 +356,7 @@ class AddEntriesDialogHelper {
return AlertDialog( return AlertDialog(
content: Text(AppLocalizations.of(context)!.needsAlwaysLocation), content: Text(AppLocalizations.of(context)!.needsAlwaysLocation),
actions: [ actions: [
// Open settings button
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await Geolocator.openAppSettings(); await Geolocator.openAppSettings();
@@ -324,6 +365,7 @@ class AddEntriesDialogHelper {
}, },
child: Text("Ok"), child: Text("Ok"),
), ),
// Cancel button
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -337,6 +379,9 @@ class AddEntriesDialogHelper {
return reload; 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 { static Future<bool> deleteCompleteRouteDialog(BuildContext context) async {
bool confirmed = false; bool confirmed = false;
@@ -354,6 +399,7 @@ class AddEntriesDialogHelper {
), ),
), ),
actions: [ actions: [
// Confirm delete button
TextButton( TextButton(
onPressed: () { onPressed: () {
confirmed = true; confirmed = true;
@@ -361,6 +407,7 @@ class AddEntriesDialogHelper {
}, },
child: Text("Ok"), child: Text("Ok"),
), ),
// Cancel button
TextButton( TextButton(
onPressed: () => {Navigator.of(context).pop()}, onPressed: () => {Navigator.of(context).pop()},
child: Text(AppLocalizations.of(context)!.cancel), child: Text(AppLocalizations.of(context)!.cancel),

View File

@@ -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'; import 'package:flutter/material.dart';
/// Utility class for showing snackbar messages
/// Contains static methods to display notifications
class SnackBarHelper { 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) { static void showSnackBarMessage(BuildContext context, String message) {
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message))); .showSnackBar(SnackBar(content: Text(message)));

View File

@@ -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/enums/databases.dart';
import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/l10n/app_localizations.dart';
import 'package:fforte/screens/sharedMethods/delete_main_entries.dart'; import 'package:fforte/screens/sharedMethods/delete_main_entries.dart';
import 'package:fforte/screens/sharedMethods/delete_templates.dart'; import 'package:fforte/screens/sharedMethods/delete_templates.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Helper class for managing confirmation dialogs
/// Contains static methods for showing delete confirmation dialogs
class ViewEntriesDialogHelper { 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( static Future<void> deleteAllMainEntries(
BuildContext context, BuildContext context,
DatabasesEnum dbType, DatabasesEnum dbType,
) async { ) async {
return showDialog( return showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false, // User must make a choice
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteEverything), title: Text(AppLocalizations.of(context)!.deleteEverything),
@@ -23,14 +32,15 @@ class ViewEntriesDialogHelper {
), ),
), ),
actions: <Widget>[ actions: <Widget>[
// Delete confirmation button
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await DeleteMainEntries.deleteAll(dbType); await DeleteMainEntries.deleteAll(dbType);
if (context.mounted) Navigator.of(context).pop(); if (context.mounted) Navigator.of(context).pop();
}, },
child: Text(AppLocalizations.of(context)!.deleteEverything), child: Text(AppLocalizations.of(context)!.deleteEverything),
), ),
// Cancel button
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); 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( static Future<void> deleteAllTemplates(
BuildContext context, BuildContext context,
DatabasesEnum dbType, DatabasesEnum dbType,
) async { ) async {
return showDialog( return showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false, // User must make a choice
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context)!.deleteEverything), title: Text(AppLocalizations.of(context)!.deleteEverything),
@@ -61,6 +74,7 @@ class ViewEntriesDialogHelper {
), ),
), ),
actions: <Widget>[ actions: <Widget>[
// Delete confirmation button
TextButton( TextButton(
onPressed: () async { onPressed: () async {
await DeleteTemplates.deleteAll(dbType); await DeleteTemplates.deleteAll(dbType);
@@ -68,6 +82,7 @@ class ViewEntriesDialogHelper {
}, },
child: Text(AppLocalizations.of(context)!.deleteEverything), child: Text(AppLocalizations.of(context)!.deleteEverything),
), ),
// Cancel button
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
@@ -79,5 +94,4 @@ class ViewEntriesDialogHelper {
}, },
); );
} }
} }

View File

@@ -1,7 +1,15 @@
import 'package:flutter/material.dart'; // * Initial setup screen shown on first app launch
import 'package:shared_preferences/shared_preferences.dart'; // * Allows users to configure:
import 'package:fforte/l10n/app_localizations.dart'; // * - 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 { class IntroScreen extends StatefulWidget {
const IntroScreen({super.key}); const IntroScreen({super.key});
@@ -9,65 +17,52 @@ class IntroScreen extends StatefulWidget {
State<IntroScreen> createState() => _IntroScreenState(); State<IntroScreen> createState() => _IntroScreenState();
} }
/// State class for the intro screen
class _IntroScreenState extends State<IntroScreen> { class _IntroScreenState extends State<IntroScreen> {
TextEditingController addresse1C = TextEditingController(); // Text controllers for input fields
TextEditingController bLandC = TextEditingController(); final TextEditingController addresse1C = TextEditingController();
TextEditingController ffApiAddress = TextEditingController(); final TextEditingController bLandC = TextEditingController();
TextEditingController exApiAddress = TextEditingController(); final TextEditingController ffApiAddress = TextEditingController();
final 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";
/// Save configuration data to SharedPreferences
Future<void> _saveData() async { 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('addresse1', addresse1C.text);
await prefs.setString('bLand', bLandC.text); await prefs.setString('bLand', bLandC.text);
await prefs.setBool('isFirstLaunch', false);
await prefs.setString('fotofallenApiAddress', ffApiAddress.text); await prefs.setString('fotofallenApiAddress', ffApiAddress.text);
await prefs.setString('exkursionenApiAddress', exApiAddress.text); await prefs.setString('exkursionenApiAddress', exApiAddress.text);
}
@override // Mark app as initialized
void initState() { await prefs.setBool('isFirstLaunch', false);
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";
});
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('LUPUS'), title: const Text('LUPUS'),
), ),
body: Center( body: Center(
child: Container( child: Container(
padding: const EdgeInsets.all(31), padding: const EdgeInsets.all(31),
child: Column( child: Column(
children: [ children: [
// Username/Address input
TextField( TextField(
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.benutzername), hintText: AppLocalizations.of(context)!.benutzername
),
controller: addresse1C, controller: addresse1C,
onChanged: (value) => setState(() { onChanged: (value) => setState(() {
addresse1C.text = value; addresse1C.text = value;
}), }),
), ),
const SizedBox( const SizedBox(height: 15),
height: 15,
), // Region settings
Column( Column(
children: [ children: [
Row( Row(
@@ -79,193 +74,58 @@ class _IntroScreenState extends State<IntroScreen> {
controller: bLandC, 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( const SizedBox(height: 35),
height: 35,
), // Camera trap API endpoint
Align( Align(
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text(AppLocalizations.of(context)!.ffApiAddress)), child: Text(AppLocalizations.of(context)!.ffApiAddress)
),
Row( Row(
children: [ children: [
// Expanded(
// flex: 4,
// child: TextField(
// decoration: InputDecoration(
// hintText:
// AppLocalizations.of(context)!.ffApiAddress),
// controller: ffApiAddress,
// ),
// ),
Expanded( Expanded(
flex: 1, flex: 1,
child: TextField( child: TextField(
controller: ffApiAddress, 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( const SizedBox(height: 10),
height: 10,
), // Excursion API endpoint
Align( Align(
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
child: Text(AppLocalizations.of(context)!.exApiAddress)), child: Text(AppLocalizations.of(context)!.exApiAddress)
),
Row( Row(
children: [ children: [
// Expanded(
// flex: 4,
// child: TextField(
// decoration: InputDecoration(
// hintText:
// AppLocalizations.of(context)!.exApiAddress),
// controller: exApiAddress,
// ),
// ),
Expanded( Expanded(
flex: 1, flex: 1,
child: TextField( child: TextField(
controller: exApiAddress, 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( const SizedBox(height: 15),
height: 15,
), // Continue button - saves settings and goes to home
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
_saveData(); _saveData();
Navigator.pushNamedAndRemoveUntil( Navigator.pushNamedAndRemoveUntil(
context, '/home', (route) => false); context,
}, '/home',
child: Text(AppLocalizations.of(context)!.continueB)) (route) => false,
);
},
child: Text(AppLocalizations.of(context)!.continueB)
)
], ],
), ),
), ),

View File

@@ -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:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
// * Widget for the settings screen
class Settings extends StatefulWidget { class Settings extends StatefulWidget {
const Settings({super.key}); const Settings({super.key});
@@ -9,22 +16,28 @@ class Settings extends StatefulWidget {
State<Settings> createState() => _SettingsState(); State<Settings> createState() => _SettingsState();
} }
// * State class for the settings screen
class _SettingsState extends State<Settings> { class _SettingsState extends State<Settings> {
// Default tracking interval in seconds
int _trackingInterval = 60; int _trackingInterval = 60;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadSettings(); _loadSettings(); // Load saved settings on start
} }
// * Load settings from SharedPreferences
Future<void> _loadSettings() async { Future<void> _loadSettings() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() { setState(() {
// Load tracking interval or use default (60 seconds)
_trackingInterval = prefs.getInt('trackingInterval') ?? 60; _trackingInterval = prefs.getInt('trackingInterval') ?? 60;
}); });
} }
// * Save new tracking interval
// * @param value The new interval in seconds
Future<void> _saveTrackingInterval(int value) async { Future<void> _saveTrackingInterval(int value) async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt('trackingInterval', value); 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 { Future<String> _getSaveDir() async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
final String saveDir = prefs.getString('saveDir') ?? ""; final String saveDir = prefs.getString('saveDir') ?? "";
@@ -42,13 +57,20 @@ class _SettingsState extends State<Settings> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context)!.settings),), appBar: AppBar(
title: Text(AppLocalizations.of(context)!.settings),
),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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( FutureBuilder(
future: _getSaveDir(), future: _getSaveDir(),
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -59,15 +81,22 @@ class _SettingsState extends State<Settings> {
} }
} }
), ),
// Button to open directory selection
ElevatedButton( ElevatedButton(
onPressed: () {}, onPressed: () {},
child: Text(AppLocalizations.of(context)!.open) child: Text(AppLocalizations.of(context)!.open)
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// * Tracking interval section
const Text( const Text(
'Tracking Interval (Sekunden)', 'Tracking Interval (Sekunden)',
style: TextStyle(fontSize: 20), style: TextStyle(fontSize: 20),
), ),
// Slider for interval adjustment
// - Minimum: 10 seconds
// - Maximum: 300 seconds (5 minutes)
// - 29 divisions for precise control
Slider( Slider(
value: _trackingInterval.toDouble(), value: _trackingInterval.toDouble(),
min: 10, min: 10,
@@ -78,6 +107,7 @@ class _SettingsState extends State<Settings> {
_saveTrackingInterval(value.round()); _saveTrackingInterval(value.round());
}, },
), ),
// Display current interval
Text('Aktuelles Intervall: $_trackingInterval Sekunden'), Text('Aktuelles Intervall: $_trackingInterval Sekunden'),
], ],
), ),

View File

@@ -1,12 +1,22 @@
class CheckRequired { // * Utility class for validating required form fields
static bool checkRequired(Map<String, Map<String, dynamic>> fieldsList) { // * Used to check if all required fields have been filled out
for (String key in fieldsList.keys) { // * before saving or submitting form data
if (fieldsList[key]!["required"]! && fieldsList[key]!["controller"]!.text.isEmpty) {
return true;
}
}
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
} }
} }

View File

@@ -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/enums/databases.dart';
import 'package:fforte/interfaces/i_db.dart'; import 'package:fforte/interfaces/i_db.dart';
import 'package:fforte/methods/excursion_db_helper.dart'; import 'package:fforte/methods/excursion_db_helper.dart';
import 'package:fforte/methods/place_db_helper.dart'; import 'package:fforte/methods/place_db_helper.dart';
/// Helper class for deleting main entries from the database
class DeleteMainEntries { 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 { static Future<void> deleteAll(DatabasesEnum dbType) async {
// Select appropriate database helper
IDb? db; IDb? db;
if (dbType == DatabasesEnum.place) { if (dbType == DatabasesEnum.place) {
@@ -15,7 +24,11 @@ class DeleteMainEntries {
await db!.deleteAllMainEntries(); 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 { static Future<void> deleteSingle(DatabasesEnum dbType, int id) async {
// Select appropriate database helper
IDb? db; IDb? db;
if (dbType == DatabasesEnum.place) { if (dbType == DatabasesEnum.place) {

View File

@@ -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/enums/databases.dart';
import 'package:fforte/interfaces/i_db.dart'; import 'package:fforte/interfaces/i_db.dart';
import 'package:fforte/methods/excursion_db_helper.dart'; import 'package:fforte/methods/excursion_db_helper.dart';
import 'package:fforte/methods/place_db_helper.dart'; import 'package:fforte/methods/place_db_helper.dart';
/// Helper class for deleting templates from the database
class DeleteTemplates { 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 { static Future<void> deleteSingle(DatabasesEnum dbType, String id) async {
// Select appropriate database helper
IDb? db; IDb? db;
if (dbType == DatabasesEnum.place) { if (dbType == DatabasesEnum.place) {
@@ -17,7 +26,10 @@ class DeleteTemplates {
await db!.deleteTemplateById(id); 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 { static Future<void> deleteAll(DatabasesEnum dbType) async {
// Select appropriate database helper
IDb? db; IDb? db;
if (dbType == DatabasesEnum.place) { if (dbType == DatabasesEnum.place) {

View File

@@ -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 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.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 { 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 { static Future<int> httpRequest({Map<String, String>? saveDataMap, String? saveDataString}) async {
// print(jsonEncode(place)); // print(jsonEncode(place));
@@ -18,6 +31,7 @@ class HttpRequestService {
Response(requestOptions: RequestOptions(path: ''), statusCode: 400); Response(requestOptions: RequestOptions(path: ''), statusCode: 400);
try { try {
// Choose endpoint based on data type (camera trap vs excursion)
if (saveDataMap != null && saveDataMap.containsKey("CID") || saveDataString != null && saveDataString.contains("CID")) { if (saveDataMap != null && saveDataMap.containsKey("CID") || saveDataString != null && saveDataString.contains("CID")) {
response = await dio.post(prefs.getString('fotofallenApiAddress') ?? "", response = await dio.post(prefs.getString('fotofallenApiAddress') ?? "",
data: saveDataMap == null ? saveDataString : jsonEncode(saveDataMap)); data: saveDataMap == null ? saveDataString : jsonEncode(saveDataMap));

View File

@@ -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:convert';
import 'dart:io'; import 'dart:io';
@@ -7,7 +13,14 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
/// Helper class for saving entries to files
class SaveFileMethod { 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( static Future<void> saveFile(
Map<String, String> place, Map<String, String> place,
int id, int id,
@@ -15,24 +28,32 @@ class SaveFileMethod {
DatabasesEnum dbType, DatabasesEnum dbType,
) async { ) async {
try { try {
// Let user select save directory
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
if (selectedDirectory == null) { if (selectedDirectory == null) {
throw FileDialogCancelled(); throw FileDialogCancelled();
} }
// Save entry as JSON
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
String jsonPlace = jsonEncode(place); String jsonPlace = jsonEncode(place);
// Remember selected directory for future use
await prefs.setString('saveDir', selectedDirectory); await prefs.setString('saveDir', selectedDirectory);
// Create file with format: prefix-id-identifier.txt
// For places: identifier = CID
// For excursions: identifier = date
File file = File( File file = File(
'$selectedDirectory/$fileNameLocalization-$id-${dbType == DatabasesEnum.place ? place["CID"] : place["Datum"]!.split(" ").first}.txt', '$selectedDirectory/$fileNameLocalization-$id-${dbType == DatabasesEnum.place ? place["CID"] : place["Datum"]!.split(" ").first}.txt',
); );
// Write JSON data to file
await file.writeAsString(jsonPlace); await file.writeAsString(jsonPlace);
} catch (e) { } catch (e) {
debugPrint(e.toString()); debugPrint(e.toString());
rethrow; // Re-throw to allow proper error handling by caller
} }
} }
} }

View File

@@ -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/enums/databases.dart';
import 'package:fforte/interfaces/i_db.dart'; import 'package:fforte/interfaces/i_db.dart';
import 'package:fforte/methods/excursion_db_helper.dart'; import 'package:fforte/methods/excursion_db_helper.dart';
import 'package:fforte/methods/place_db_helper.dart'; import 'package:fforte/methods/place_db_helper.dart';
/// Helper class for saving main entries to the database
class SaveMainEntryMethod { 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({ static Future<int> saveEntry({
required Map<String, String> entryData, required Map<String, String> entryData,
required bool isTemplate, required bool isTemplate,
required DatabasesEnum dbType, required DatabasesEnum dbType,
bool sent = false, bool sent = false,
}) async { }) async {
// Select appropriate database helper
IDb? placeDB; IDb? placeDB;
if (dbType == DatabasesEnum.place) { if (dbType == DatabasesEnum.place) {
placeDB = PlaceDBHelper(); placeDB = PlaceDBHelper();
} else if (dbType == DatabasesEnum.excursion) { } else if (dbType == DatabasesEnum.excursion) {
placeDB = ExcursionDBHelper(); placeDB = ExcursionDBHelper();
} }
// If converting from template, delete the template first
if (isTemplate) await placeDB!.deleteTemplateById(entryData["ID"]!); if (isTemplate) await placeDB!.deleteTemplateById(entryData["ID"]!);
// Handle new entry creation vs update
int entryId; int entryId;
if (entryData["ID"] == "" || isTemplate) { if (entryData["ID"] == "" || isTemplate) {
// Create new entry
entryData.remove("ID"); entryData.remove("ID");
entryId = await placeDB!.addMainEntry(entryData); entryId = await placeDB!.addMainEntry(entryData);
// Commented out template deletion by CID
// await placeDB.deleteTemplateById(entryData["CID"]!); // await placeDB.deleteTemplateById(entryData["CID"]!);
} else { } else {
// Update existing entry
entryId = await placeDB!.updateMainEntry(entryData); entryId = await placeDB!.updateMainEntry(entryData);
} }
// Update sent status if entry was sent to server
if (sent == true) { if (sent == true) {
placeDB.updateSent(entryId); // Update 'Sent' using the correct ID placeDB.updateSent(entryId);
} }
return entryId; return entryId;
} }
} }

View File

@@ -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/enums/databases.dart';
import 'package:fforte/interfaces/i_db.dart'; import 'package:fforte/interfaces/i_db.dart';
import 'package:fforte/methods/excursion_db_helper.dart'; import 'package:fforte/methods/excursion_db_helper.dart';
import 'package:fforte/methods/place_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 { Future<int> saveTemplate(Map<String, String> templateData, DatabasesEnum dbType,) async {
// Select appropriate database helper
IDb dbHelper; IDb dbHelper;
int id =templateData["ID"]! != "" ? int.parse(templateData["ID"]!) : -1; int id = templateData["ID"]! != "" ? int.parse(templateData["ID"]!) : -1;
if (dbType == DatabasesEnum.place) { if (dbType == DatabasesEnum.place) {
dbHelper = PlaceDBHelper(); dbHelper = PlaceDBHelper();
} else if (dbType == DatabasesEnum.excursion) { } else if (dbType == DatabasesEnum.excursion) {
dbHelper = ExcursionDBHelper(); dbHelper = ExcursionDBHelper();
} else { } else {
return -1; return -1; // Invalid database type
} }
// Remove sent status as it's not needed for templates
templateData.remove("Sent"); templateData.remove("Sent");
// Handle new template creation vs update
if (templateData["ID"]! == "" || templateData["ID"]! == "-1") { if (templateData["ID"]! == "" || templateData["ID"]! == "-1") {
// Create new template
templateData.remove("ID"); templateData.remove("ID");
id = await dbHelper.addTemplate(templateData); id = await dbHelper.addTemplate(templateData);
} else { } else {
// Update existing template
await dbHelper.updateTemplate(templateData); await dbHelper.updateTemplate(templateData);
} }

View File

@@ -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:fforte/screens/sharedMethods/http_request.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'dart:io'; import 'dart:io';
/// Helper class for sending files to the server
class SendFile { 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 { static Future<void> sendFile() async {
File? pickedFile; File? pickedFile;
// Open file picker dialog
FilePickerResult? result = await FilePicker.platform.pickFiles(); FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) { if (result != null) {
// Read and send file contents
pickedFile = File(result.files.single.path!); pickedFile = File(result.files.single.path!);
String fileContent = await pickedFile.readAsString(); String fileContent = await pickedFile.readAsString();
await HttpRequestService.httpRequest(saveDataString: fileContent); await HttpRequestService.httpRequest(saveDataString: fileContent);
@@ -17,64 +28,67 @@ class SendFile {
} }
} }
// class SendFile extends StatefulWidget { // * Legacy widget implementation kept for reference
// const SendFile({super.key}); // * This was a stateful widget version of the file sender
// // * with additional UI elements and error handling
// @override /*
// State<SendFile> createState() => _SendFileState(); class SendFile extends StatefulWidget {
// } const SendFile({super.key});
//
// class _SendFileState extends State<SendFile> { @override
// File? pickedFile; State<SendFile> createState() => _SendFileState();
// }
// @override
// Widget build(BuildContext context) { class _SendFileState extends State<SendFile> {
// return Scaffold( File? pickedFile;
// appBar: AppBar(),
// body: Column( @override
// children: [ Widget build(BuildContext context) {
// ElevatedButton( return Scaffold(
// onPressed: () async { appBar: AppBar(),
// FilePickerResult? result = body: Column(
// await FilePicker.platform.pickFiles(); children: [
// ElevatedButton(
// if (result != null) { onPressed: () async {
// pickedFile = File(result.files.single.path!); FilePickerResult? result =
// } else { await FilePicker.platform.pickFiles();
// pickedFile = File("");
// } if (result != null) {
// }, pickedFile = File(result.files.single.path!);
// child: Text(AppLocalizations.of(context)!.pickfile)), } else {
// Text(pickedFile.toString()), pickedFile = File("");
// ElevatedButton( }
// onPressed: () async { },
// final dio = Dio(); child: Text(AppLocalizations.of(context)!.pickfile)),
// final SharedPreferences prefs = await SharedPreferences.getInstance(); Text(pickedFile.toString()),
// String? fileContent = await pickedFile?.readAsString(); ElevatedButton(
// onPressed: () async {
// dio.options.responseType = ResponseType.plain; final dio = Dio();
// Response response = Response( final SharedPreferences prefs = await SharedPreferences.getInstance();
// requestOptions: RequestOptions(path: ''), statusCode: 400); String? fileContent = await pickedFile?.readAsString();
//
// try { dio.options.responseType = ResponseType.plain;
// response = await dio.post(prefs.getString('apiAddress') ?? "", Response response = Response(
// data: jsonEncode(fileContent)); requestOptions: RequestOptions(path: ''), statusCode: 400);
// } on DioException catch (e) {
// if (e.response?.statusCode == 500) { try {
// /* print('-------------------------'); response = await dio.post(prefs.getString('apiAddress') ?? "",
// print('code 500'); */ data: jsonEncode(fileContent));
// return; } on DioException catch (e) {
// } if (e.response?.statusCode == 500) {
// } return;
// if (response.statusCode == 201) { }
// // print(response.statusCode); }
// } else { if (response.statusCode == 201) {
// //print(response.statusCode); // Success handling was here
// } } else {
// }, // Error handling was here
// child: Text(AppLocalizations.of(context)!.sendtoserver)) }
// ], },
// ), child: Text(AppLocalizations.of(context)!.sendtoserver))
// ); ],
// } ),
// } );
}
}
*/

View File

@@ -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'; 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 { class Datum extends StatefulWidget {
/// Initial date value
final DateTime? initDatum; final DateTime? initDatum;
/// Callback function when date changes
final Function(DateTime) onDateChanged; final Function(DateTime) onDateChanged;
/// Label for the date picker button
final String name; final String name;
const Datum( const Datum(
@@ -12,7 +25,9 @@ class Datum extends StatefulWidget {
State<Datum> createState() => _DatumState(); State<Datum> createState() => _DatumState();
} }
/// State class for the date selection widget
class _DatumState extends State<Datum> { class _DatumState extends State<Datum> {
/// Currently selected date
DateTime? datum; DateTime? datum;
@override @override
@@ -26,6 +41,7 @@ class _DatumState extends State<Datum> {
return Row( return Row(
children: [ children: [
Row(children: [ Row(children: [
// Date picker button
SizedBox( SizedBox(
width: 140, width: 140,
child: ElevatedButton( child: ElevatedButton(
@@ -40,6 +56,7 @@ class _DatumState extends State<Datum> {
const SizedBox( const SizedBox(
width: 10, width: 10,
), ),
// Formatted date display
Text( Text(
'${datum?.day}. ${datum?.month}. ${datum?.year}', '${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 { Future<DateTime?> pickDate() async {
final date = await showDatePicker( final date = await showDatePicker(
context: context, context: context,

View File

@@ -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/enums/databases.dart';
import 'package:fforte/methods/excursion_db_helper.dart'; import 'package:fforte/methods/excursion_db_helper.dart';
import 'package:fforte/methods/place_db_helper.dart'; import 'package:fforte/methods/place_db_helper.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.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 { class VarTextField extends StatefulWidget {
/// Controller for the text input
final TextEditingController textController; final TextEditingController textController;
/// Localized label/hint text
final String localization; final String localization;
/// Database type (place or excursion)
final DatabasesEnum dbDesignation; final DatabasesEnum dbDesignation;
/// Database field name
final String dbName; final String dbName;
/// Default value key for preferences
final String? defaultValue; final String? defaultValue;
/// Whether the field is required
final bool required; final bool required;
const VarTextField({ const VarTextField({
@@ -26,24 +43,31 @@ class VarTextField extends StatefulWidget {
State<VarTextField> createState() => _VarTextFieldState(); State<VarTextField> createState() => _VarTextFieldState();
} }
/// State class for the variable text field widget
class _VarTextFieldState extends State<VarTextField> { class _VarTextFieldState extends State<VarTextField> {
/// List of previous values from database
List<String> dbVar = []; List<String> dbVar = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Load default value if field is empty
if (widget.textController.text == "" && widget.defaultValue != null) { if (widget.textController.text == "" && widget.defaultValue != null) {
_loadPref(); _loadPref();
} }
// Load previous values from database
_loadData().then((e) => dbVar = e); _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 { Future<List<String>> _loadData() async {
List<Map<String, dynamic>> entries = []; List<Map<String, dynamic>> entries = [];
List<Map<String, dynamic>> templatesEntries = []; List<Map<String, dynamic>> templatesEntries = [];
// Get entries from appropriate database
if (widget.dbDesignation == DatabasesEnum.place) { if (widget.dbDesignation == DatabasesEnum.place) {
entries = await PlaceDBHelper().getAllMainEntries(); entries = await PlaceDBHelper().getAllMainEntries();
templatesEntries = await PlaceDBHelper().getAllTemplates(); templatesEntries = await PlaceDBHelper().getAllTemplates();
@@ -54,6 +78,7 @@ class _VarTextFieldState extends State<VarTextField> {
List<String> erg = []; List<String> erg = [];
// Extract values for this field from entries
for (var element in entries) { for (var element in entries) {
for (var key in element.keys) { for (var key in element.keys) {
if (key == widget.dbName && element[key].toString() != "") { 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 element in templatesEntries) {
for (var key in element.keys) { for (var key in element.keys) {
if (key == widget.dbName && element[key].toString() != "") { if (key == widget.dbName && element[key].toString() != "") {
@@ -73,6 +99,7 @@ class _VarTextFieldState extends State<VarTextField> {
return erg; return erg;
} }
/// Load default value from preferences
void _loadPref() { void _loadPref() {
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
SharedPreferences prefs = await SharedPreferences.getInstance(); SharedPreferences prefs = await SharedPreferences.getInstance();
@@ -87,6 +114,7 @@ class _VarTextFieldState extends State<VarTextField> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
// Text input field
Expanded( Expanded(
flex: 5, flex: 5,
child: TextField( child: TextField(
@@ -100,6 +128,7 @@ class _VarTextFieldState extends State<VarTextField> {
}, },
decoration: InputDecoration( decoration: InputDecoration(
hintText: widget.localization, hintText: widget.localization,
// Border color based on required status and value
enabledBorder: enabledBorder:
widget.required widget.required
? (widget.textController.text.isEmpty ? (widget.textController.text.isEmpty
@@ -128,6 +157,7 @@ class _VarTextFieldState extends State<VarTextField> {
), ),
), ),
const Expanded(child: SizedBox(width: 15)), const Expanded(child: SizedBox(width: 15)),
// Dropdown for previous values
Expanded( Expanded(
flex: 1, flex: 1,
child: Align( child: Align(

View File

@@ -337,65 +337,6 @@ class _ViewEntriesState extends State<ViewEntries> {
MarkerLayer(markers: marker), 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
), ),
], ],
), ),

View File

@@ -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'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
// * Notification service class that handles all notification functionality
class NotificationService { class NotificationService {
// Plugin instance for local notifications
final notifiactionPlugin = FlutterLocalNotificationsPlugin(); final notifiactionPlugin = FlutterLocalNotificationsPlugin();
// Initialization status flag
bool _isInitialized = false; bool _isInitialized = false;
// Getter for initialization status
bool get isInitialized => _isInitialized; bool get isInitialized => _isInitialized;
// * Initialize the notification service
// * - Requests notification permissions
// * - Configures Android-specific settings
// * - Initializes the notification plugin
Future<void> initNotification() async { Future<void> initNotification() async {
// Prevent multiple initializations
if (_isInitialized) return; if (_isInitialized) return;
// Request permissions for Android notifications
await notifiactionPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()!.requestNotificationsPermission(); await notifiactionPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()!.requestNotificationsPermission();
// Android-specific initialization settings
const initSettingsAndroid = AndroidInitializationSettings( const initSettingsAndroid = AndroidInitializationSettings(
'@mipmap/ic_launcher', '@mipmap/ic_launcher', // App icon for notifications
); );
// Overall initialization settings
const initSettings = InitializationSettings(android: initSettingsAndroid); const initSettings = InitializationSettings(android: initSettingsAndroid);
// Initialize plugin
await notifiactionPlugin.initialize(initSettings); await notifiactionPlugin.initialize(initSettings);
_isInitialized = true; _isInitialized = true;
} }
// * Create default notification settings
// * Configures a notification with:
// * - Low importance and priority
// * - Ongoing status
// * - Specific channel for tracking
NotificationDetails notificationDetails() { NotificationDetails notificationDetails() {
return const NotificationDetails( return const NotificationDetails(
android: AndroidNotificationDetails( android: AndroidNotificationDetails(
"tracking0", "tracking0", // Channel ID
"tracking ongoing", "tracking ongoing", // Channel name
importance: Importance.low, importance: Importance.low,
priority: Priority.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 { Future<void> showNotification({int id = 0, String? title}) async {
return notifiactionPlugin.show(id, title, "", notificationDetails()); 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 { Future<void> deleteNotification({id = 0}) async {
await notifiactionPlugin.cancel(id); await notifiactionPlugin.cancel(id);
} }

View File

@@ -8,13 +8,14 @@ import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:shared_preferences/shared_preferences.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. /// Service that runs the geolocator service to track the device's position
/// This is needed for excursions /// 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] /// Start tracking via [startTracking]
/// Manage the position stream via [pauseTracking], [stopTracking] and [resumeTracking] /// Control tracking via [pauseTracking], [stopTracking], and [resumeTracking]
class TrackingService { class TrackingService {
// Singleton stuff // Singleton implementation
static TrackingService? _instance; static TrackingService? _instance;
factory TrackingService() { factory TrackingService() {
@@ -24,7 +25,7 @@ class TrackingService {
TrackingService._internal(); TrackingService._internal();
/// Resets all values, making it possible to start tracking again. /// Reset all tracking values for a fresh start
static void resetInstance() { static void resetInstance() {
if (_instance != null) { if (_instance != null) {
_instance!.dispose(); _instance!.dispose();
@@ -32,36 +33,38 @@ class TrackingService {
} }
} }
// Variables // Core tracking variables
// - Stores the tracked coordinates // Stores tracked GPS coordinates
List<LatLng> pathList = []; List<LatLng> pathList = [];
// - Stores all gotten accuracies // Stores GPS accuracy values
List<double> accuracyList = []; List<double> accuracyList = [];
// - Stores timer so that is responsible vor the periodically tracking // Timer for periodic tracking
Timer? _positionTimer; Timer? _positionTimer;
// Current tracking status
bool isTracking = false; bool isTracking = false;
// - Some more Singleton stuff (i guess. Vibecoded it because of lack of time) // Context for UI interactions
BuildContext? _lastContext; BuildContext? _lastContext;
// Stream controllers for position and stats updates
final _positionController = StreamController<Position>.broadcast(); final _positionController = StreamController<Position>.broadcast();
final _statsController = StreamController<TrackingStats>.broadcast(); final _statsController = StreamController<TrackingStats>.broadcast();
// - Stores the last measured accuracy so that it can be displayed in the excursions view // Stream getters
double? currentAccuracy;
// - Getter
Stream<Position> get positionStream$ => _positionController.stream; Stream<Position> get positionStream$ => _positionController.stream;
Stream<TrackingStats> get statsStream$ => _statsController.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) { 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.isEmpty) return 0;
if (accuracies.length == 1) return accuracies.first; 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(); 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) { if (sorted.length % 2 == 0) {
int midIndex = sorted.length ~/ 2; int midIndex = sorted.length ~/ 2;
return (sorted[midIndex - 1] + sorted[midIndex]) / 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 { Future<void> startTracking(BuildContext context) async {
if (isTracking) return; if (isTracking) return;
// Configure high-accuracy GPS settings
final LocationSettings locationSettings = final LocationSettings locationSettings =
LocationSettings(accuracy: LocationAccuracy.high); LocationSettings(accuracy: LocationAccuracy.high);
_lastContext = context; _lastContext = context;
// Initialize and show tracking notification
await NotificationService().initNotification(); await NotificationService().initNotification();
if (context.mounted) { if (context.mounted) {
NotificationService().showNotification( NotificationService().showNotification(
@@ -84,23 +94,25 @@ class TrackingService {
); );
} }
// Get tracking interval from settings // Load tracking interval from settings
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final intervalSeconds = prefs.getInt('trackingInterval') ?? 60; final intervalSeconds = prefs.getInt('trackingInterval') ?? 60;
// Create a timer that triggers position updates // Set up periodic position updates
_positionTimer = _positionTimer =
Timer.periodic(Duration(seconds: intervalSeconds), (_) async { Timer.periodic(Duration(seconds: intervalSeconds), (_) async {
try { try {
final Position position = await Geolocator.getCurrentPosition( final Position position = await Geolocator.getCurrentPosition(
locationSettings: locationSettings); locationSettings: locationSettings);
// Store position and accuracy data
pathList.add(LatLng(position.latitude, position.longitude)); pathList.add(LatLng(position.latitude, position.longitude));
accuracyList.add(position.accuracy); accuracyList.add(position.accuracy);
currentAccuracy = position.accuracy; currentAccuracy = position.accuracy;
_positionController.add(position); _positionController.add(position);
_updateStats(); _updateStats();
} catch (e) { } catch (e) {
// Handle errors with notification
NotificationService().deleteNotification(); NotificationService().deleteNotification();
NotificationService().showNotification(title: "ERROR: $e"); NotificationService().showNotification(title: "ERROR: $e");
} }
@@ -124,9 +136,15 @@ class TrackingService {
isTracking = true; isTracking = true;
} }
// Last calculated tracking statistics
TrackingStats? _lastStats; TrackingStats? _lastStats;
TrackingStats? get currentStats => _lastStats; TrackingStats? get currentStats => _lastStats;
/// Update tracking statistics
/// Calculates:
/// - Current GPS accuracy
/// - Average accuracy
/// - Total distance traveled
void _updateStats() { void _updateStats() {
if (pathList.isEmpty) { if (pathList.isEmpty) {
_lastStats = TrackingStats( _lastStats = TrackingStats(
@@ -137,6 +155,7 @@ class TrackingService {
return; return;
} }
// Calculate total distance
double totalDistance = 0; double totalDistance = 0;
for (int i = 1; i < pathList.length; i++) { for (int i = 1; i < pathList.length; i++) {
totalDistance += _calculateDistance( totalDistance += _calculateDistance(
@@ -155,19 +174,24 @@ class TrackingService {
_statsController.add(_lastStats!); _statsController.add(_lastStats!);
} }
/// Request a manual stats update
void requestStatsUpdate() { void requestStatsUpdate() {
_updateStats(); _updateStats();
} }
/// Calculate distance between two GPS coordinates using the Haversine formula
/// @return Distance in meters
double _calculateDistance( double _calculateDistance(
double lat1, double lon1, double lat2, double lon2) { 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 lat1Rad = lat1 * math.pi / 180;
double lat2Rad = lat2 * math.pi / 180; double lat2Rad = lat2 * math.pi / 180;
double deltaLat = (lat2 - lat1) * math.pi / 180; double deltaLat = (lat2 - lat1) * math.pi / 180;
double deltaLon = (lon2 - lon1) * math.pi / 180; double deltaLon = (lon2 - lon1) * math.pi / 180;
// Haversine formula calculation
double a = math.sin(deltaLat / 2) * math.sin(deltaLat / 2) + double a = math.sin(deltaLat / 2) * math.sin(deltaLat / 2) +
math.cos(lat1Rad) * math.cos(lat1Rad) *
math.cos(lat2Rad) * math.cos(lat2Rad) *
@@ -178,11 +202,13 @@ class TrackingService {
return earthRadius * c; return earthRadius * c;
} }
/// Temporarily pause tracking
void pauseTracking() { void pauseTracking() {
_positionTimer?.cancel(); _positionTimer?.cancel();
isTracking = false; isTracking = false;
} }
/// Resume paused tracking
void resumeTracking() { void resumeTracking() {
if (!isTracking && _lastContext != null) { if (!isTracking && _lastContext != null) {
startTracking(_lastContext!); startTracking(_lastContext!);
@@ -190,6 +216,7 @@ class TrackingService {
isTracking = true; isTracking = true;
} }
/// Stop tracking completely and clear current state
void stopTracking() { void stopTracking() {
_positionTimer?.cancel(); _positionTimer?.cancel();
NotificationService().deleteNotification(); NotificationService().deleteNotification();
@@ -199,6 +226,7 @@ class TrackingService {
_lastContext = null; _lastContext = null;
} }
/// Clear all recorded position data
void clearPath() { void clearPath() {
pathList.clear(); pathList.clear();
accuracyList.clear(); accuracyList.clear();
@@ -206,10 +234,13 @@ class TrackingService {
_updateStats(); _updateStats();
} }
/// Convert tracked path to string format
/// Format: "latitude,longitude;latitude,longitude;..."
String getPathAsString() { String getPathAsString() {
return pathList.map((pos) => "${pos.latitude},${pos.longitude}").join(";"); return pathList.map((pos) => "${pos.latitude},${pos.longitude}").join(";");
} }
/// Clean up resources
void dispose() { void dispose() {
stopTracking(); stopTracking();
_positionController.close(); _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 { class TrackingStats {
final double currentAccuracy; final double currentAccuracy;
final double averageAccuracy; final double averageAccuracy;