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

View File

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

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';
/// Interface for database operations
/// Implements the Repository pattern for database access
abstract interface class IDb {
/// Get the database instance
Future<Database> get dB;
/// Initialize the database and create necessary tables
initDatabases();
/// Create database schema
/// @param excursionDB Database instance
/// @param version Schema version number
onCreateDatabases(Database excursionDB, int version);
/// Add a new main entry
/// @param excursion Map of entry data
/// @return ID of the new entry
Future<int> addMainEntry(Map<String, String> excursion);
/// Update an existing main entry
/// @param excursion Map of updated entry data
/// @return Number of rows affected
Future<int> updateMainEntry(Map<String, String> excursion);
/// Mark an entry as sent to the server
/// @param id ID of the entry to update
Future<void> updateSent(int id);
/// Add a new template entry
/// @param templates Map of template data
/// @return ID of the new template
Future<int> addTemplate(Map<String, String> templates);
/// Update an existing template
/// @param template Map of updated template data
Future<void> updateTemplate(Map<String, String> template);
/// Get all main entries from the database
/// @return List of all entries
Future<List<Map<String, dynamic>>> getAllMainEntries();
/// Get all templates from the database
/// @return List of all templates
Future<List<Map<String, dynamic>>> getAllTemplates();
/// Delete all main entries from the database
Future<void> deleteAllMainEntries();
/// Delete all templates from the database
Future<void> deleteAllTemplates();
/// Delete a specific template
/// @param id ID of the template to delete
Future<void> deleteTemplateById(String id);
/// Delete a specific main entry
/// @param id ID of the entry to delete
Future<void> deleteMainEntryById(String id);
}

View File

@@ -13,22 +13,36 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'home.dart';
import 'l10n/l10n.dart';
// * Main entry point of the LUPUS app
// * Configures the app's:
// * - Theme and appearance
// * - Localization
// * - Navigation routes
// * - Initial settings
/// Initialize the app and configure default settings
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Set default values (Propably there is a better way to do this)
// Initialize SharedPreferences for app settings
SharedPreferences prefs = await SharedPreferences.getInstance();
// isFirstLaunch decides whether the intro screen is shown or not
// Check if this is the first app launch
bool isFirstLaunch = prefs.getBool('isFirstLaunch') ?? true;
// Set default control periods if not already configured
if (prefs.getString("kTage1")?.isEmpty ?? true) await prefs.setString('kTage1', "28");
if (prefs.getString("kTage2")?.isEmpty ?? true) await prefs.setString('kTage2', "48");
// Commented out API addresses for testing purposes
// if (prefs.getString("fotofallenApiAddress")?.isEmpty ?? true) await prefs.setString('fotofallenApiAddress', 'http://192.168.1.170/www.dbb-wolf.de/data/app24.php');
// if (prefs.getString("exkursionenApiAddress")?.isEmpty ?? true) await prefs.setString('exkursionenApiAddress', 'http://192.168.1.170/www.dbb-wolf.de/data/api_exkursion.php');
runApp(MyApp(isFirstLaunch: isFirstLaunch));
}
/// Main app widget that configures the app's theme and routing
class MyApp extends StatelessWidget {
final bool isFirstLaunch;
const MyApp({super.key, required this.isFirstLaunch});
@@ -36,13 +50,16 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'LUPUS',
// Configure light theme with gold color scheme
theme: FlexThemeData.light(scheme: FlexScheme.gold, useMaterial3: true),
darkTheme:
FlexThemeData.dark(scheme: FlexScheme.greenM3, useMaterial3: true),
themeMode: ThemeMode.system,
// here the isFirstLaunch comes into play
// Configure dark theme with green M3 color scheme
darkTheme: FlexThemeData.dark(scheme: FlexScheme.greenM3, useMaterial3: true),
themeMode: ThemeMode.system, // Use system theme preference
// Show intro screen on first launch, otherwise go to home
initialRoute: isFirstLaunch ? '/introScreen' : '/home',
// Localization settings
// Configure localization support
supportedLocales: L10n.all,
localizationsDelegates: const [
AppLocalizations.delegate,
@@ -50,6 +67,8 @@ class MyApp extends StatelessWidget {
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
// Define app navigation routes
routes: {
'/home': (context) => const HomePage(),
'/addCamMain': (context) => const AddCamMain(),

View File

@@ -4,13 +4,20 @@ import 'package:sqflite/sqflite.dart';
import 'dart:io' as io;
import 'package:path/path.dart';
// * Gives the complete functionality for the databases
// ! functions may not be named complete correctly
// * Database helper for managing excursions
// * Provides CRUD operations for both main entries and templates
// * Uses SQLite database with two tables:
// * - excursion: Stores finalized excursion records
// * - excursionTemplates: Stores template entries for quick creation
/// Database helper class implementing IDb interface
/// Handles all database operations for excursions and tracking data
class ExcursionDBHelper implements IDb {
// Database instance
static Database? _excursionDB;
// checks if the databses are existing and creates them with the initPlaceDatabase function if not
/// Get database instance, creating it if necessary
/// Returns existing database or initializes a new one
@override
Future<Database> get dB async {
if (_excursionDB != null) {
@@ -20,7 +27,8 @@ class ExcursionDBHelper implements IDb {
return _excursionDB!;
}
// Creates the databases with help from the _onCreateExcursion function
/// Initialize the database
/// Creates database file and tables if they don't exist
@override
initDatabases() async {
io.Directory documentsDirectory = await getApplicationCacheDirectory();
@@ -33,21 +41,28 @@ class ExcursionDBHelper implements IDb {
return excursionDB;
}
// The function that helps
/// Create database tables
/// Sets up schema for both main entries and templates
@override
onCreateDatabases(Database excursionDB, int version) async {
// Create main excursions table with tracking data fields
await excursionDB.execute(
'CREATE TABLE excursion (ID INTEGER PRIMARY KEY AUTOINCREMENT, Rudel TEXT, Teilnehmer TEXT, Datum TEXT, Dauer TEXT, MHund INTEGER, MLeine TEXT, BLand TEXT, Lkr TEXT, BeiOrt TEXT, BimaName TEXT, Wetter TEXT, Temperat TEXT, RegenVor TEXT, KmRad TEXT, KmAuto TEXT, KmFuss TEXT, KmTotal TEXT, KmAuProz TEXT, KmFuProz TEXT, KmRaProz TEXT, SpGut TEXT, SpMittel, SpSchlecht TEXT, SpurFund TEXT, SpurLang TEXT, SpurTiere Text, SpSicher TEXT, WelpenSp TEXT, WelpenAnz TEXT, WpSicher TEXT, LosungGes TEXT, LosungAnz TEXT, LosungGen TEXT, UrinAnz TEXT, UrinGen TEXT, OestrAnz TEXT, OestrGen TEXT, HaarAnz TEXT, HaarGen TEXT, LosungKm TEXT, GenetiKm TEXT, Hinweise TEXT, Bemerk TEXT, IntKomm TEXT, BimaNr TEXT, BimaNutzer TEXT, BimaAGV TEXT, FallNum INTEGER, Weg TEXT, Sent INTEGER DEFAULT 0)',
);
// Create templates table (similar structure but without Sent flag)
await excursionDB.execute(
'CREATE TABLE excursionTemplates (ID INTEGER PRIMARY KEY AUTOINCREMENT, Rudel TEXT, Teilnehmer TEXT, Datum TEXT, Dauer TEXT, MHund INTEGER, MLeine TEXT, BLand TEXT, Lkr TEXT, BeiOrt TEXT, BimaName TEXT, Wetter TEXT, Temperat TEXT, RegenVor TEXT, KmRad TEXT, KmAuto TEXT, KmFuss TEXT, KmTotal TEXT, KmAuProz TEXT, KmFuProz TEXT, KmRaProz TEXT, SpGut TEXT, SpMittel, SpSchlecht TEXT, SpurFund TEXT, SpurLang TEXT, SpurTiere Text, SpSicher TEXT, WelpenSp TEXT, WelpenAnz TEXT, WpSicher TEXT, LosungGes TEXT, LosungAnz TEXT, LosungGen TEXT, UrinAnz TEXT, UrinGen TEXT, OestrAnz TEXT, OestrGen TEXT, HaarAnz TEXT, HaarGen TEXT, LosungKm TEXT, GenetiKm TEXT, Hinweise TEXT, Bemerk TEXT, IntKomm TEXT, BimaNr TEXT, BimaNutzer TEXT, BimaAGV TEXT, FallNum INTEGER, Weg TEXT)',
);
}
// Function to add a finished entry and return its ID
/// Add a new main entry to the database
/// @param excursion Map containing the excursion data
/// @return ID of the newly inserted entry
@override
Future<int> addMainEntry(Map<String, String> excursion) async {
var excursionDBClient = await dB;
// Commented out code for handling existing entries
// final existingID = await excursionDBClient.query(
// 'excursion',
// where: 'ID = ?',
@@ -56,7 +71,7 @@ class ExcursionDBHelper implements IDb {
// if (existingID.isNotEmpty) {
// updateMainEntry(excursion);
// return existingID.first['ID'] as int; // Return existing ID
// return existingID.first['ID'] as int;
// }
int id = await excursionDBClient.insert(
@@ -65,13 +80,15 @@ class ExcursionDBHelper implements IDb {
conflictAlgorithm: ConflictAlgorithm.replace,
);
return id; // Return the ID of the newly inserted entry
return id;
}
/// Update an existing main entry
/// @param excursion Map containing the updated excursion data
/// @return Number of rows affected
@override
Future<int> updateMainEntry(Map<String, String> excursion) async {
var excursionDBClient = await dB;
return await excursionDBClient.update(
'excursion',
excursion,
@@ -80,11 +97,11 @@ class ExcursionDBHelper implements IDb {
);
}
// function to update the sent value
/// Mark an entry as sent to the server
/// @param id ID of the entry to update
@override
Future<void> updateSent(int id) async {
var excursionDBClient = await dB;
await excursionDBClient.update(
'excursion',
{'Sent': 1},
@@ -93,11 +110,13 @@ class ExcursionDBHelper implements IDb {
);
}
// same thing as before but with templatews
/// Add a new template entry
/// @param templates Map containing the template data
/// @return ID of the newly inserted template
@override
Future<int> addTemplate(Map<String, String> templates) async {
var excursionDBClient = await dB;
// Commented out code for handling existing templates
// final existingCID = await excursionDBClient.query(
// 'excursionTemplates',
// where: 'ID = ?',
@@ -110,16 +129,15 @@ class ExcursionDBHelper implements IDb {
int id = await excursionDBClient.insert(
'excursionTemplates',
templates,
// conflictAlgorithm: ConflictAlgorithm.replace,
);
return id;
}
// Updates a existing template
/// Update an existing template
/// @param template Map containing the updated template data
@override
Future<void> updateTemplate(Map<String, String> template) async {
var excursionDBClient = await dB;
await excursionDBClient.update(
'excursionTemplates',
template,
@@ -128,7 +146,8 @@ class ExcursionDBHelper implements IDb {
);
}
// get the finished entries from db
/// Retrieve all main entries from the database
/// @return List of all excursions or empty list if none exist
@override
Future<List<Map<String, dynamic>>> getAllMainEntries() async {
var excursionDBClient = await dB;
@@ -140,7 +159,8 @@ class ExcursionDBHelper implements IDb {
}
}
// get the finished templates from db
/// Retrieve all templates from the database
/// @return List of all templates or empty list if none exist
@override
Future<List<Map<String, dynamic>>> getAllTemplates() async {
var excursionDBClient = await dB;
@@ -152,21 +172,24 @@ class ExcursionDBHelper implements IDb {
}
}
// deletes all finished entries from the db LOCALLY
/// Delete all main entries from the local database
/// Note: This only affects the local database, not the server
@override
Future<void> deleteAllMainEntries() async {
var excursionDBClient = await dB;
await excursionDBClient.delete('excursion');
}
// deletes all templates from the db LOCALLY
/// Delete all templates from the local database
/// Note: This only affects the local database, not the server
@override
Future<void> deleteAllTemplates() async {
var excursionDBClient = await dB;
await excursionDBClient.delete('excursionTemplates');
}
// delete specific template
/// Delete a specific template by ID
/// @param id ID of the template to delete
@override
Future<void> deleteTemplateById(String id) async {
var excursionDBClient = await dB;
@@ -177,7 +200,8 @@ class ExcursionDBHelper implements IDb {
);
}
// delete specific excursion
/// Delete a specific main entry by ID
/// @param id ID of the entry to delete
@override
Future<void> deleteMainEntryById(String id) async {
var excursionDBClient = await dB;

View File

@@ -4,13 +4,20 @@ import 'package:sqflite/sqflite.dart';
import 'dart:io' as io;
import 'package:path/path.dart';
// * Gives the complete functionality for the databases
// ! functions may not be named complete correctly
// * Database helper for managing camera trap locations (places)
// * Provides CRUD operations for both main entries and templates
// * Uses SQLite database with two tables:
// * - place: Stores finalized camera trap locations
// * - placeTemplates: Stores template entries for quick creation
class PlaceDBHelper implements IDb{
/// Database helper class implementing IDb interface
/// Handles all database operations for camera trap locations
class PlaceDBHelper implements IDb {
// Database instance
static Database? _dB;
// checks if the databses are existing and creates them with the initPlaceDatabase function if not
/// Get database instance, creating it if necessary
/// Returns existing database or initializes a new one
@override
Future<Database> get dB async {
if (_dB != null) {
@@ -20,7 +27,8 @@ class PlaceDBHelper implements IDb{
return _dB!;
}
// Creates the databases with help from the _onCreatePlace function
/// Initialize the database
/// Creates database file and tables if they don't exist
@override
initDatabases() async {
io.Directory documentsDirectory = await getApplicationCacheDirectory();
@@ -30,19 +38,26 @@ class PlaceDBHelper implements IDb{
return placeDB;
}
// The function that helps
/// Create database tables
/// Sets up schema for both main entries and templates
@override
onCreateDatabases(Database placeDB, int version) async {
// Create main places table
await placeDB.execute(
'CREATE TABLE place (ID INTEGER PRIMARY KEY AUTOINCREMENT, CID TEXT, Standort TEXT, Rudel TEXT, Datum TEXT, Adresse1 TEXT, Adresse2 TEXT, Adresse3 TEXT, BLand TEXT, Lkr TEXT, BeiOrt TEXT, OrtInfo TEXT, Status TEXT, FFTyp TEXT, FotoFilm TEXT, MEZ TEXT, Platzung TEXT, KSchloNr TEXT, KontDat DATE, Betreuung TEXT, AbbauDat DATE, Auftrag TEXT, KontAbsp TEXT, SonstBem TEXT, FKontakt1 TEXT, FKontakt2 TEXT, FKontakt3 TEXT, KTage1 INTEGER, KTage2 INTEGER, ProtoAm DATE, IntKomm TEXT, DECLNG DECIMALS(4,8), DECLAT DECIMALS(4,8), Sent INTEGER DEFAULT 0)');
// Create templates table (similar structure but without Sent flag)
await placeDB.execute(
'CREATE TABLE placeTemplates (ID INTEGER PRIMARY KEY AUTOINCREMENT, CID TEXT, Standort TEXT, Rudel TEXT, Datum TEXT, Adresse1 TEXT, Adresse2 TEXT, Adresse3 TEXT, BLand TEXT, Lkr TEXT, BeiOrt TEXT, OrtInfo TEXT, Status TEXT, FFTyp TEXT, FotoFilm TEXT, MEZ TEXT, Platzung TEXT, KSchloNr TEXT, KontDat DATE, Betreuung TEXT, AbbauDat DATE, Auftrag TEXT, KontAbsp TEXT, SonstBem TEXT, FKontakt1 TEXT, FKontakt2 TEXT, FKontakt3 TEXT, KTage1 INTEGER, KTage2 INTEGER, ProtoAm DATE, IntKomm TEXT, DECLNG DECIMALS(4,8), DECLAT DECIMALS(4,8))');
}
// Function to add a finished entry and return its ID
/// Add a new main entry to the database
/// @param place Map containing the place data
/// @return ID of the newly inserted entry
@override
Future<int> addMainEntry(Map<String, String> place) async {
var placeDBClient = await dB;
// Commented out code for handling existing entries
// final existingID = await placeDBClient.query(
// 'place',
// where: 'ID = ?',
@@ -51,7 +66,7 @@ class PlaceDBHelper implements IDb{
//
// if (existingID.isNotEmpty) {
// updateMainEntry(place);
// return existingID.first['ID'] as int; // Return existing ID
// return existingID.first['ID'] as int;
// }
int id = await placeDBClient.insert(
@@ -60,31 +75,35 @@ class PlaceDBHelper implements IDb{
//conflictAlgorithm: ConflictAlgorithm.replace,
);
return id; // Return the ID of the newly inserted entry
return id;
}
/// Update an existing main entry
/// @param place Map containing the updated place data
/// @return Number of rows affected
@override
Future<int> updateMainEntry(Map<String, String> place) async {
var placeDBClient = await dB;
return await placeDBClient
.update('place', place, where: "ID = ?", whereArgs: [place['ID']]);
}
// function to update the sent value
/// Mark an entry as sent to the server
/// @param id ID of the entry to update
@override
Future<void> updateSent(int id) async {
var placeDBClient = await dB;
await placeDBClient.update('place', {'Sent': 1},
where: 'ID = ?', whereArgs: [id]);
}
// same thing as before but with templatews
/// Add a new template entry
/// @param templates Map containing the template data
/// @return ID of the newly inserted template
@override
Future<int> addTemplate(Map<String, String> templates) async {
var placeDBClient = await dB;
// Commented out code for handling existing templates
// final existingCID = await placeDBClient.query(
// 'placeTemplates',
// where: 'ID = ?',
@@ -97,16 +116,15 @@ class PlaceDBHelper implements IDb{
int id = await placeDBClient.insert(
'placeTemplates',
templates,
// conflictAlgorithm: ConflictAlgorithm.replace,
);
return id;
}
// Updates a existing template
/// Update an existing template
/// @param template Map containing the updated template data
@override
Future<void> updateTemplate(Map<String, String> template) async {
var placeDBClient = await dB;
await placeDBClient.update(
'placeTemplates',
template,
@@ -115,7 +133,8 @@ class PlaceDBHelper implements IDb{
);
}
// get the finished entries from db
/// Retrieve all main entries from the database
/// @return List of all places or empty list if none exist
@override
Future<List<Map<String, dynamic>>> getAllMainEntries() async {
var placeDBClient = await dB;
@@ -127,7 +146,8 @@ class PlaceDBHelper implements IDb{
}
}
// get the finished templates from db
/// Retrieve all templates from the database
/// @return List of all templates or empty list if none exist
@override
Future<List<Map<String, dynamic>>> getAllTemplates() async {
var placeDBClient = await dB;
@@ -139,21 +159,24 @@ class PlaceDBHelper implements IDb{
}
}
// deletes all finished entries from the db LOCALLY
/// Delete all main entries from the local database
/// Note: This only affects the local database, not the server
@override
Future<void> deleteAllMainEntries() async {
var placeDBClient = await dB;
await placeDBClient.delete('place');
}
// deletes all templates from the db LOCALLY
/// Delete all templates from the local database
/// Note: This only affects the local database, not the server
@override
Future<void> deleteAllTemplates() async {
var placeDBClient = await dB;
await placeDBClient.delete('placeTemplates');
}
// delete specific template
/// Delete a specific template by ID
/// @param id ID of the template to delete
@override
Future<void> deleteTemplateById(String id) async {
var placeDBClient = await dB;
@@ -164,7 +187,8 @@ class PlaceDBHelper implements IDb{
);
}
// delete specific place
/// Delete a specific main entry by ID
/// @param id ID of the entry to delete
@override
Future<void> deleteMainEntryById(String id) async {
var placeDBClient = await dB;

View File

@@ -22,9 +22,14 @@ import 'widgets/mez.dart';
import 'widgets/platzung.dart';
import 'widgets/status.dart';
/// Widget for adding or editing camera trap locations
/// Supports template creation, editing existing entries, and new entries
class AddCamMain extends StatefulWidget {
/// Whether this form is being used to create a template
final bool isTemplate;
/// Whether the entry has been sent to the server
final bool isSent;
/// Existing data to populate the form with (for editing)
final Map<String, dynamic>? existingData;
const AddCamMain({
@@ -38,14 +43,17 @@ class AddCamMain extends StatefulWidget {
State<AddCamMain> createState() => _AddCamMainState();
}
/// State class for the camera trap location form
class _AddCamMainState extends State<AddCamMain> {
// var declaration
/// Current step in the multi-step form
int currentStep = 0;
/// Whether this form is being used as a template
late bool isTemplate;
/// Current GPS position, initialized with default values for Germany
Position currentPosition = Position(
longitude: 10.0,
latitude: 51.0,
longitude: 10.0, // Default longitude (roughly center of Germany)
latitude: 51.0, // Default latitude (roughly center of Germany)
timestamp: DateTime.now(),
accuracy: 0.0,
altitude: 0.0,
@@ -56,6 +64,12 @@ class _AddCamMainState extends State<AddCamMain> {
headingAccuracy: 0.0,
);
/// Map containing all form fields with their controllers and required status
/// Organized by steps:
/// - Step 1: Basic information (ID, Status, etc.)
/// - Step 2: Location information
/// - Step 3: Dates and control periods
/// - Step 4: Contact information
Map<String, Map<String, dynamic>> rmap = {
"ID": {"controller": TextEditingController(), "required": false},
// Step 1
@@ -102,6 +116,8 @@ class _AddCamMainState extends State<AddCamMain> {
"Sent": {"controller": TextEditingController(), "required": false},
};
/// Retrieves all field values as a map
/// @return Map of field names to their current values
Map<String, String> getFieldsText() {
Map<String, String> puff = {};
@@ -112,8 +128,12 @@ class _AddCamMainState extends State<AddCamMain> {
return puff;
}
/// Flag indicating whether position is currently being loaded
bool isLoadingPosition = false;
/// Initializes the GPS position
/// Handles location permissions and device settings
/// @return Future<Position> The determined position
Future<Position> _initializePosition() async {
try {
final position = await GeolocatorService.deteterminePosition();

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_forbidden_exception.dart';
import 'package:fforte/screens/excursion/exceptions/need_always_location_exception.dart';
import 'package:geolocator/geolocator.dart';
/// Service class for handling all GPS location related functionality
class GeolocatorService {
// determine live position with checks for denied permission and turned off location service
/// Determine the current device position with permission checks
/// @param alwaysOnNeeded Whether the app needs always-on location permission
/// @throws LocationDisabledException if location services are disabled
/// @throws LocationForbiddenException if location permission is denied
/// @throws NeedAlwaysLocation if always-on permission is needed but not granted
/// @return Future<Position> The current GPS position
static Future<Position> deteterminePosition({bool alwaysOnNeeded = false}) async {
bool locationEnabled;
LocationPermission permissionGiven;
// Check if location services are enabled
locationEnabled = await Geolocator.isLocationServiceEnabled();
if (!locationEnabled) {
throw LocationDisabledException();
}
// Check and request location permissions if needed
permissionGiven = await Geolocator.checkPermission();
if (permissionGiven == LocationPermission.denied) {
permissionGiven = await Geolocator.requestPermission();
@@ -22,13 +37,16 @@ class GeolocatorService {
}
}
// Check for always-on permission if required
if (alwaysOnNeeded && permissionGiven != LocationPermission.always) {
throw NeedAlwaysLocation();
}
return await Geolocator.getCurrentPosition();
return await Geolocator.getCurrentPosition();
}
/// Check if always-on location permission is enabled
/// @return Future<bool> True if always-on permission is granted or location is disabled
static Future<bool> alwaysPositionEnabled() async {
LocationPermission permissionGiven = await Geolocator.checkPermission();
bool locationEnabled = await Geolocator.isLocationServiceEnabled();

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

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

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

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:fforte/l10n/app_localizations.dart';
/// Widget for selecting and displaying camera trap control dates
/// Used to schedule when the camera trap should be checked
class KontDat extends StatefulWidget {
/// Initial control date, if any
final DateTime? initKontDat;
/// Callback function when date is changed
final Function(DateTime) onDateChanged;
const KontDat(
@@ -12,7 +22,9 @@ class KontDat extends StatefulWidget {
State<KontDat> createState() => _KontDatState();
}
/// State class for the control date widget
class _KontDatState extends State<KontDat> {
/// Currently selected control date
DateTime? kontDat;
@override
@@ -26,6 +38,7 @@ class _KontDatState extends State<KontDat> {
return Row(
children: [
Row(children: [
// Date picker button
SizedBox(
width: 140,
child: ElevatedButton(
@@ -39,9 +52,8 @@ class _KontDatState extends State<KontDat> {
},
child: Text(AppLocalizations.of(context)!.pickkontdat)),
),
const SizedBox(
width: 10,
),
const SizedBox(width: 10),
// Display selected date in DD.MM.YYYY format
Text(
'${kontDat?.day}. ${kontDat?.month}. ${kontDat?.year}',
),
@@ -50,6 +62,8 @@ class _KontDatState extends State<KontDat> {
);
}
/// Show date picker dialog and return selected date
/// @return Future<DateTime?> Selected date or null if cancelled
Future<DateTime?> pickDate() async {
final date = await showDatePicker(
context: context,

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

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

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

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

View File

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

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

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

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/screens/sharedWidgets/var_text_field.dart';
import 'package:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.dart';
/// Widget for recording various types of wildlife observations
/// Combines predefined categories with custom input options
class Hinweise extends StatefulWidget {
/// Controller for the combined observation text
final TextEditingController hinweise;
const Hinweise({super.key, required this.hinweise});
@@ -11,21 +26,29 @@ class Hinweise extends StatefulWidget {
@override
State<Hinweise> createState() => _HinweiseState();
}
class _HinweiseState extends State<Hinweise> {
// Vars for Checkboxes
late bool liegestelleChecked;
late bool kadaverChecked;
late bool sichtungChecked;
late bool heulenChecked;
bool sonstigesChecked = false;
// for sonstiges textfield
/// State class for the observations widget
class _HinweiseState extends State<Hinweise> {
// Checkbox state variables
/// Whether resting place was observed
late bool liegestelleChecked;
/// Whether animal carcass was found
late bool kadaverChecked;
/// Whether direct sighting occurred
late bool sichtungChecked;
/// Whether howling was heard
late bool heulenChecked;
/// Whether other observations exist
bool sonstigesChecked = false;
/// Controller for additional observations text
TextEditingController sonstigesController = TextEditingController();
@override
void initState() {
sonstigesController.addListener(updateController);
// Initialize checkboxes based on existing text
liegestelleChecked = widget.hinweise.text.contains("Liegestelle") ? true : false;
kadaverChecked = widget.hinweise.text.contains("Wildtierkadaver") ? true : false;
sichtungChecked = widget.hinweise.text.contains("Sichtung") ? true : false;
@@ -33,6 +56,7 @@ class _HinweiseState extends State<Hinweise> {
bool firstRun = true;
// Parse existing other observations
for (String val in widget.hinweise.text.split(",")) {
if (val != "Liegestelle" && val != "Wildtierkadaver" && val != "Sichtung" && val != "Heulen" && val != "") {
sonstigesChecked = true;
@@ -51,6 +75,8 @@ class _HinweiseState extends State<Hinweise> {
super.dispose();
}
/// Update the main controller text based on selected options
/// Combines all checked items and additional text into a comma-separated string
void updateController() {
Map<String, bool> props = {
"Liegestelle": liegestelleChecked,
@@ -63,6 +89,7 @@ class _HinweiseState extends State<Hinweise> {
widget.hinweise.text = "";
// Build combined text from selected options
for (String key in props.keys) {
if (!firstRun && props[key]!) {
widget.hinweise.text += ",";
@@ -74,7 +101,6 @@ class _HinweiseState extends State<Hinweise> {
} else if (props[key]!){
widget.hinweise.text += key;
}
}
debugPrint(widget.hinweise.text);
}
@@ -83,6 +109,7 @@ class _HinweiseState extends State<Hinweise> {
Widget build(BuildContext context) {
return Column(
children: [
// Resting place checkbox
CheckboxListTile(
title: Text(AppLocalizations.of(context)!.liegestelle),
value: liegestelleChecked,
@@ -90,6 +117,7 @@ class _HinweiseState extends State<Hinweise> {
setState(() => liegestelleChecked = value ?? false);
updateController();
}),
// Animal carcass checkbox
CheckboxListTile(
title: Text(AppLocalizations.of(context)!.wildtierKadaver),
value: kadaverChecked,
@@ -97,6 +125,7 @@ class _HinweiseState extends State<Hinweise> {
setState(() => kadaverChecked = value ?? false);
updateController();
}),
// Direct sighting checkbox
CheckboxListTile(
title: Text(AppLocalizations.of(context)!.sichtung),
value: sichtungChecked,
@@ -104,6 +133,7 @@ class _HinweiseState extends State<Hinweise> {
setState(() => sichtungChecked = value ?? false);
updateController();
}),
// Howling checkbox
CheckboxListTile(
title: Text(AppLocalizations.of(context)!.heulen),
value: heulenChecked,
@@ -111,6 +141,7 @@ class _HinweiseState extends State<Hinweise> {
setState(() => heulenChecked = value ?? false);
updateController();
}),
// Other observations checkbox and input field
CheckboxListTile(
title: Text(AppLocalizations.of(context)!.sonstiges),
value: sonstigesChecked,

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

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

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

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

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

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/screens/helper/snack_bar_helper.dart';
import 'package:fforte/screens/sharedMethods/http_request.dart';
@@ -8,8 +16,12 @@ import 'package:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.dart';
import 'package:geolocator/geolocator.dart';
/// Helper class for managing various dialogs related to adding and saving entries
class AddEntriesDialogHelper {
// Function to show the dialog where the user has to choose if he want to safe his values as a template
/// Show dialog for saving current data as a template
/// @param context The BuildContext to show the dialog in
/// @param saveData Map containing the data to save
/// @param dbType The type of database (place/excursion)
static Future<void> showTemplateDialog(
BuildContext context,
Map<String, String> saveData,
@@ -17,17 +29,19 @@ class AddEntriesDialogHelper {
) async {
return showDialog(
context: context,
barrierDismissible: false,
barrierDismissible: false, // User must make a choice
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!.fieldEmpty),
actions: <Widget>[
// Cancel button
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(context)!.cancel),
),
// Save as template button
TextButton(
onPressed: () {
saveTemplate(saveData, dbType);
@@ -45,6 +59,12 @@ class AddEntriesDialogHelper {
);
}
/// Show error dialog when server communication fails
/// Offers options to retry or cancel
/// @param context The BuildContext to show the dialog in
/// @param saveData Map containing the data to save
/// @param isTemplate Whether this is a template entry
/// @param dbType The type of database (place/excursion)
static Future<dynamic> showServerErrorDialog(
BuildContext context,
Map<String, String> saveData,
@@ -68,6 +88,7 @@ class AddEntriesDialogHelper {
)
: null,
actions: [
// Retry button
if (!isLoading)
TextButton(
onPressed: () async {
@@ -79,19 +100,18 @@ class AddEntriesDialogHelper {
if (errorCode == 200 && context.mounted) {
Navigator.pop(context);
// saveData(true);
SaveMainEntryMethod.saveEntry(
entryData: saveData,
isTemplate: isTemplate,
dbType: dbType,
sent: true
);
showSuccessDialog(context);
}
},
child: Text(AppLocalizations.of(context)!.sendagain),
),
// Cancel button
if (!isLoading)
TextButton(
onPressed: () {
@@ -107,6 +127,17 @@ class AddEntriesDialogHelper {
);
}
/// Show dialog with various save options
/// Options include:
/// - Save as template
/// - Send to server
/// - Save as file
/// - Save locally only
/// @param context The BuildContext to show the dialog in
/// @param saveData Map containing the data to save
/// @param isTemplate Whether this is a template entry
/// @param dbType The type of database (place/excursion)
/// @return bool Whether the operation was completed successfully
static Future<bool> showSaveOptionsDialog(
BuildContext context,
Map<String, String> saveData,
@@ -118,7 +149,7 @@ class AddEntriesDialogHelper {
await showDialog(
context: context,
barrierDismissible: false,
barrierDismissible: false, // User must make a choice
builder: (BuildContext context) {
return StatefulBuilder(
builder: (context, setState) {
@@ -135,6 +166,7 @@ class AddEntriesDialogHelper {
)
: null,
actions: [
// Save as template button
if (!isLoading)
TextButton(
onPressed: () async {
@@ -156,6 +188,7 @@ class AddEntriesDialogHelper {
},
child: Text(AppLocalizations.of(context)!.template),
),
// Send to server button
if (!isLoading)
TextButton(
onPressed: () async {
@@ -192,6 +225,7 @@ class AddEntriesDialogHelper {
},
child: Text(AppLocalizations.of(context)!.sendtoserver),
),
// Save as file button
if (!isLoading)
TextButton(
onPressed: () async {
@@ -223,7 +257,7 @@ class AddEntriesDialogHelper {
pop = true;
}
} catch (_) {
// User cancelled the dialog
// User cancelled the file save dialog
}
setState(() => isLoading = false);
} catch (e) {
@@ -238,6 +272,7 @@ class AddEntriesDialogHelper {
},
child: Text(AppLocalizations.of(context)!.saveasfile),
),
// Save locally only button
if (!isLoading)
TextButton(
onPressed: () {
@@ -265,6 +300,7 @@ class AddEntriesDialogHelper {
},
child: Text(AppLocalizations.of(context)!.justsave),
),
// Cancel button
if (!isLoading)
TextButton(
onPressed: () {
@@ -282,6 +318,8 @@ class AddEntriesDialogHelper {
return pop;
}
/// Show success dialog after successful save operation
/// @param context The BuildContext to show the dialog in
static Future<void> showSuccessDialog(BuildContext context) async {
return showDialog(
context: context,
@@ -305,8 +343,10 @@ class AddEntriesDialogHelper {
);
}
/// Show dialog requesting location permission settings
/// @param context The BuildContext to show the dialog in
/// @return bool Whether the settings were changed
static Future<bool> locationSettingsDialog(BuildContext context) async {
bool reload = false;
await showDialog(
@@ -316,6 +356,7 @@ class AddEntriesDialogHelper {
return AlertDialog(
content: Text(AppLocalizations.of(context)!.needsAlwaysLocation),
actions: [
// Open settings button
TextButton(
onPressed: () async {
await Geolocator.openAppSettings();
@@ -324,6 +365,7 @@ class AddEntriesDialogHelper {
},
child: Text("Ok"),
),
// Cancel button
TextButton(
onPressed: () {
Navigator.of(context).pop();
@@ -337,6 +379,9 @@ class AddEntriesDialogHelper {
return reload;
}
/// Show confirmation dialog for deleting entire route
/// @param context The BuildContext to show the dialog in
/// @return bool Whether deletion was confirmed
static Future<bool> deleteCompleteRouteDialog(BuildContext context) async {
bool confirmed = false;
@@ -354,6 +399,7 @@ class AddEntriesDialogHelper {
),
),
actions: [
// Confirm delete button
TextButton(
onPressed: () {
confirmed = true;
@@ -361,6 +407,7 @@ class AddEntriesDialogHelper {
},
child: Text("Ok"),
),
// Cancel button
TextButton(
onPressed: () => {Navigator.of(context).pop()},
child: Text(AppLocalizations.of(context)!.cancel),

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';
/// Utility class for showing snackbar messages
/// Contains static methods to display notifications
class SnackBarHelper {
/// Display a snackbar message at the bottom of the screen
/// @param context The BuildContext to show the snackbar in
/// @param message The text message to display
static void showSnackBarMessage(BuildContext context, String message) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)));

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

View File

@@ -1,7 +1,15 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:fforte/l10n/app_localizations.dart';
// * Initial setup screen shown on first app launch
// * Allows users to configure:
// * - Username/Address
// * - Region settings
// * - API endpoints for server communication
// * Settings are persisted using SharedPreferences
import 'package:flutter/material.dart';
import 'package:fforte/l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Widget for initial app configuration
class IntroScreen extends StatefulWidget {
const IntroScreen({super.key});
@@ -9,65 +17,52 @@ class IntroScreen extends StatefulWidget {
State<IntroScreen> createState() => _IntroScreenState();
}
/// State class for the intro screen
class _IntroScreenState extends State<IntroScreen> {
TextEditingController addresse1C = TextEditingController();
TextEditingController bLandC = TextEditingController();
TextEditingController ffApiAddress = TextEditingController();
TextEditingController exApiAddress = TextEditingController();
String selectedFFApiAddress = "https://data.dbb-wolf.de/app24.php";
String selectedEXApiAddress = "https://data.dbb-wolf.de/api_exkursion.php";
String? selectedBLand = "Sachsen";
// Text controllers for input fields
final TextEditingController addresse1C = TextEditingController();
final TextEditingController bLandC = TextEditingController();
final TextEditingController ffApiAddress = TextEditingController();
final TextEditingController exApiAddress = TextEditingController();
/// Save configuration data to SharedPreferences
Future<void> _saveData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final SharedPreferences prefs = await SharedPreferences.getInstance();
// Save user inputs
await prefs.setString('addresse1', addresse1C.text);
await prefs.setString('bLand', bLandC.text);
await prefs.setBool('isFirstLaunch', false);
await prefs.setString('fotofallenApiAddress', ffApiAddress.text);
await prefs.setString('exkursionenApiAddress', exApiAddress.text);
}
@override
void initState() {
super.initState();
_loadPrefs();
}
void _loadPrefs() {
Future.delayed(Duration.zero, () async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
ffApiAddress.text = prefs.getString('fotofallenApiAddress') ?? "https://data.dbb-wolf.de/app24.php";
exApiAddress.text = prefs.getString('exkursionenApiAddress') ?? "https://data.dbb-wolf.de/api_exkursion.php";
addresse1C.text = prefs.getString('addresse1') ?? "";
bLandC.text = prefs.getString('bLand') ?? "Sachsen";
});
});
// Mark app as initialized
await prefs.setBool('isFirstLaunch', false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('LUPUS'),
title: const Text('LUPUS'),
),
body: Center(
child: Container(
padding: const EdgeInsets.all(31),
child: Column(
children: [
// Username/Address input
TextField(
decoration: InputDecoration(
hintText: AppLocalizations.of(context)!.benutzername),
hintText: AppLocalizations.of(context)!.benutzername
),
controller: addresse1C,
onChanged: (value) => setState(() {
addresse1C.text = value;
}),
),
const SizedBox(
height: 15,
),
const SizedBox(height: 15),
// Region settings
Column(
children: [
Row(
@@ -79,193 +74,58 @@ class _IntroScreenState extends State<IntroScreen> {
controller: bLandC,
),
),
// Expanded(
// flex: 1,
// child: PopupMenuButton(
// icon: const Icon(Icons.arrow_drop_down),
// initialValue: selectedBLand,
// onSelected: (value) {
// setState(() {
// selectedBLand = value;
// bLandC.text = value;
// });
// },
// itemBuilder: (context) => const <PopupMenuEntry>[
// PopupMenuItem(
// value: 'Baden-Württemberg',
// child: Text('Baden-Württemberg'),
// ),
// PopupMenuItem(
// value: 'Bayern',
// child: Text('Bayern'),
// ),
// PopupMenuItem(
// value: 'Berlin',
// child: Text('Berlin'),
// ),
// PopupMenuItem(
// value: 'Brandenburg',
// child: Text('Brandenburg'),
// ),
// PopupMenuItem(
// value: 'Bremen',
// child: Text('Bremen'),
// ),
// PopupMenuItem(
// value: 'Hamburg',
// child: Text('Hamburg'),
// ),
// PopupMenuItem(
// value: 'Hessen',
// child: Text('Hessen'),
// ),
// PopupMenuItem(
// value: 'Mecklenburg-Vorpommern',
// child: Text('Mecklenburg-Vorpommern'),
// ),
// PopupMenuItem(
// value: 'Niedersachsen',
// child: Text('Niedersachsen'),
// ),
// PopupMenuItem(
// value: 'Nordrhein-Westfalen',
// child: Text('Nordrhein-Westfalen'),
// ),
// PopupMenuItem(
// value: 'Rheinland-Pfalz',
// child: Text('Rheinland-Pfalz'),
// ),
// PopupMenuItem(
// value: 'Saarland',
// child: Text('Saarland'),
// ),
// PopupMenuItem(
// value: 'Sachsen',
// child: Text('Sachsen'),
// ),
// PopupMenuItem(
// value: 'Sachsen-Anhalt',
// child: Text('Sachsen-Anhalt'),
// ),
// PopupMenuItem(
// value: 'Schleswig-Holstein',
// child: Text('Schleswig-Holstein'),
// ),
// PopupMenuItem(
// value: 'Thüringen',
// child: Text('Thüringen'),
// ),
// ],
// ),
// ),
],
),
const SizedBox(
height: 35,
),
const SizedBox(height: 35),
// Camera trap API endpoint
Align(
alignment: Alignment.bottomLeft,
child: Text(AppLocalizations.of(context)!.ffApiAddress)),
alignment: Alignment.bottomLeft,
child: Text(AppLocalizations.of(context)!.ffApiAddress)
),
Row(
children: [
// Expanded(
// flex: 4,
// child: TextField(
// decoration: InputDecoration(
// hintText:
// AppLocalizations.of(context)!.ffApiAddress),
// controller: ffApiAddress,
// ),
// ),
Expanded(
flex: 1,
child: TextField(
controller: ffApiAddress,
),
// child: PopupMenuButton(
// icon: const Icon(Icons.arrow_drop_down),
// initialValue: selectedFFApiAddress,
// onSelected: (value) {
// setState(() {
// selectedFFApiAddress = value;
// ffApiAddress.text = value;
// });
// },
// itemBuilder: (context) => <PopupMenuEntry>[
// // PopupMenuItem(
// // value:
// // "http://192.168.1.106/www.dbb-wolf.de/data/app24.php",
// // child:
// // Text(AppLocalizations.of(context)!.test)),
// PopupMenuItem(
// value: "https://data.dbb-wolf.de/app24.php",
// child: Text(
// AppLocalizations.of(context)!.notest))
// ],
// ),
//)
)
],
),
const SizedBox(
height: 10,
),
const SizedBox(height: 10),
// Excursion API endpoint
Align(
alignment: Alignment.bottomLeft,
child: Text(AppLocalizations.of(context)!.exApiAddress)),
alignment: Alignment.bottomLeft,
child: Text(AppLocalizations.of(context)!.exApiAddress)
),
Row(
children: [
// Expanded(
// flex: 4,
// child: TextField(
// decoration: InputDecoration(
// hintText:
// AppLocalizations.of(context)!.exApiAddress),
// controller: exApiAddress,
// ),
// ),
Expanded(
flex: 1,
flex: 1,
child: TextField(
controller: exApiAddress,
),
// child: PopupMenuButton(
// icon: const Icon(Icons.arrow_drop_down),
// initialValue: selectedEXApiAddress,
// onSelected: (value) {
// setState(() {
// selectedEXApiAddress = value;
// exApiAddress.text = value;
// });
// },
// itemBuilder: (context) => <PopupMenuEntry>[
// // PopupMenuItem(
// // value:
// // "http://192.168.1.106/www.dbb-wolf.de/data/app24.php",
// // child:
// // Text(AppLocalizations.of(context)!.test)),
// PopupMenuItem(
// value:
// "https://data.dbb-wolf.de/api_exkursion.php",
// child: Text(
// AppLocalizations.of(context)!.notest))
// ],
// )
)
)
],
)
],
),
const SizedBox(
height: 15,
),
const SizedBox(height: 15),
// Continue button - saves settings and goes to home
ElevatedButton(
onPressed: () {
_saveData();
Navigator.pushNamedAndRemoveUntil(
context, '/home', (route) => false);
},
child: Text(AppLocalizations.of(context)!.continueB))
onPressed: () {
_saveData();
Navigator.pushNamedAndRemoveUntil(
context,
'/home',
(route) => false,
);
},
child: Text(AppLocalizations.of(context)!.continueB)
)
],
),
),

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

View File

@@ -1,12 +1,22 @@
class CheckRequired {
static bool checkRequired(Map<String, Map<String, dynamic>> fieldsList) {
for (String key in fieldsList.keys) {
if (fieldsList[key]!["required"]! && fieldsList[key]!["controller"]!.text.isEmpty) {
return true;
}
}
// * Utility class for validating required form fields
// * Used to check if all required fields have been filled out
// * before saving or submitting form data
return false;
/// Helper class for form field validation
class CheckRequired {
/// Check if any required fields are empty
/// @param fieldsList Map of field definitions with their required status and controllers
/// @return true if any required field is empty, false otherwise
static bool checkRequired(Map<String, Map<String, dynamic>> fieldsList) {
// Iterate through all fields
for (String key in fieldsList.keys) {
// Check if field is required and empty
if (fieldsList[key]!["required"]! && fieldsList[key]!["controller"]!.text.isEmpty) {
return true; // Found an empty required field
}
}
return false; // All required fields are filled
}
}

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/interfaces/i_db.dart';
import 'package:fforte/methods/excursion_db_helper.dart';
import 'package:fforte/methods/place_db_helper.dart';
/// Helper class for deleting main entries from the database
class DeleteMainEntries {
/// Delete all main entries of a specific type
/// @param dbType The type of database (place/excursion)
static Future<void> deleteAll(DatabasesEnum dbType) async {
// Select appropriate database helper
IDb? db;
if (dbType == DatabasesEnum.place) {
@@ -15,7 +24,11 @@ class DeleteMainEntries {
await db!.deleteAllMainEntries();
}
/// Delete a single main entry by ID
/// @param dbType The type of database (place/excursion)
/// @param id ID of the entry to delete
static Future<void> deleteSingle(DatabasesEnum dbType, int id) async {
// Select appropriate database helper
IDb? db;
if (dbType == DatabasesEnum.place) {

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/interfaces/i_db.dart';
import 'package:fforte/methods/excursion_db_helper.dart';
import 'package:fforte/methods/place_db_helper.dart';
/// Helper class for deleting templates from the database
class DeleteTemplates {
/// Delete a single template by ID
/// @param dbType The type of database (place/excursion)
/// @param id ID of the template to delete
static Future<void> deleteSingle(DatabasesEnum dbType, String id) async {
// Select appropriate database helper
IDb? db;
if (dbType == DatabasesEnum.place) {
@@ -17,7 +26,10 @@ class DeleteTemplates {
await db!.deleteTemplateById(id);
}
/// Delete all templates of a specific type
/// @param dbType The type of database (place/excursion)
static Future<void> deleteAll(DatabasesEnum dbType) async {
// Select appropriate database helper
IDb? db;
if (dbType == DatabasesEnum.place) {

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 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Service class for making HTTP requests to the backend
/// Handles both camera trap and excursion data submissions
class HttpRequestService {
/// Send data to the appropriate backend endpoint
/// @param saveDataMap Optional map of data to send
/// @param saveDataString Optional string of data to send
/// @return Future<int> HTTP status code of the response
static Future<int> httpRequest({Map<String, String>? saveDataMap, String? saveDataString}) async {
// print(jsonEncode(place));
@@ -18,6 +31,7 @@ class HttpRequestService {
Response(requestOptions: RequestOptions(path: ''), statusCode: 400);
try {
// Choose endpoint based on data type (camera trap vs excursion)
if (saveDataMap != null && saveDataMap.containsKey("CID") || saveDataString != null && saveDataString.contains("CID")) {
response = await dio.post(prefs.getString('fotofallenApiAddress') ?? "",
data: saveDataMap == null ? saveDataString : jsonEncode(saveDataMap));

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

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

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

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

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

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

View File

@@ -337,65 +337,6 @@ class _ViewEntriesState extends State<ViewEntries> {
MarkerLayer(markers: marker),
],
),
// ),
// child: FutureBuilder(
// future: mainEntries,
// builder: (context, snapshot) {
// if (snapshot.connectionState == ConnectionState.waiting) {
// return const CircularProgressIndicator();
// } else if (snapshot.hasError) {
// return Text("Error ${snapshot.error}");
// } else {
// if (snapshot.data != null) {
// markers =
// snapshot.data!.map((e) {
// return Marker(
// width: 80.0,
// height: 80.0,
// point: LatLng(
// double.parse(e['DECLAT'].toString()),
// double.parse(e['DECLNG'].toString()),
// ),
// child: Column(
// children: [
// const Icon(
// Icons.location_on,
// color: Colors.red,
// ),
// Text(
// "ID: ${e['ID'].toString()}",
// style: const TextStyle(color: Colors.black),
// ),
// ],
// ),
// );
// }).toList();
// }
// return FlutterMap(
// options: MapOptions(
// initialCenter:
// markers.isEmpty
// ? const LatLng(50, 10)
// : markers.first.point,
// interactionOptions: const InteractionOptions(
// flags:
// InteractiveFlag.pinchZoom |
// InteractiveFlag.drag |
// InteractiveFlag.pinchMove,
// ),
// ),
// children: [
// TileLayer(
// urlTemplate:
// 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
// userAgentPackageName: 'com.example.app',
// ),
// MarkerLayer(markers: markers),
// ],
// );
// } // REMOVE
// }, // REMOVE
// ), // REMOVE
),
],
),

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

View File

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