Compare commits

..

3 Commits

Author SHA1 Message Date
412556b3f3 Entferne PocketBase-Abhängigkeit und Analytics-Skript hinzugefügt 2025-12-03 03:05:34 +01:00
b58571e52b Third decimal for Frequency 2025-03-06 12:36:57 +01:00
a857648c48 UI resposiveness fix on mobile 2024-11-11 00:07:32 +01:00
6 changed files with 277 additions and 250 deletions

View File

@@ -44,7 +44,6 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
final FocusNode _frequencyFocusNode = FocusNode(); final FocusNode _frequencyFocusNode = FocusNode();
final FocusNode _squawkFocusNode = FocusNode(); final FocusNode _squawkFocusNode = FocusNode();
// Neue Variablen
bool _saveSimbriefId = false; bool _saveSimbriefId = false;
static const String _simbriefIdKey = 'simbrief_id'; static const String _simbriefIdKey = 'simbrief_id';
@@ -54,7 +53,6 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
_loadSavedSimbriefId(); _loadSavedSimbriefId();
} }
// Methode zum Laden der gespeicherten ID
Future<void> _loadSavedSimbriefId() async { Future<void> _loadSavedSimbriefId() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final savedId = prefs.getString(_simbriefIdKey); final savedId = prefs.getString(_simbriefIdKey);
@@ -66,7 +64,6 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
} }
} }
// Methode zum Speichern der ID
Future<void> _saveSimbriefIdToPrefs() async { Future<void> _saveSimbriefIdToPrefs() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
if (_saveSimbriefId) { if (_saveSimbriefId) {
@@ -100,7 +97,6 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
Uri.parse('https://www.simbrief.com/api/xml.fetcher.php?userid=${_simbriefIdController.text}&json=1'), Uri.parse('https://www.simbrief.com/api/xml.fetcher.php?userid=${_simbriefIdController.text}&json=1'),
); );
// Prüfe ob Widget noch mounted ist
if (!mounted) return; if (!mounted) return;
if (response.statusCode == 200) { if (response.statusCode == 200) {
@@ -116,15 +112,15 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
_expectedFrequencyController.text = ''; _expectedFrequencyController.text = '';
}); });
if (!mounted) return; // Zweiter Check vor ScaffoldMessenger if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('SimBrief Daten erfolgreich geladen')), const SnackBar(content: Text('SimBrief data successfully loaded')),
); );
} }
} catch (e) { } catch (e) {
if (!mounted) return; // Check vor Error-SnackBar if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler beim Laden der SimBrief Daten: $e')), SnackBar(content: Text('Error loading SimBrief data: $e')),
); );
} }
} }
@@ -250,217 +246,246 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
), ),
], ],
), ),
body: GestureDetector( body: LayoutBuilder(
// Dismiss the keyboard when tapping outside builder: (context, constraints) {
onTap: () => FocusScope.of(context).unfocus(), return GestureDetector(
child: SingleChildScrollView( onTap: () => FocusScope.of(context).unfocus(),
padding: const EdgeInsets.all(16.0), child: SingleChildScrollView(
child: Column( padding: const EdgeInsets.all(16.0),
children: [ // ConstrainedBox hinzufügen
// IntrinsicHeight Widget hinzufügen child: ConstrainedBox(
IntrinsicHeight( constraints: BoxConstraints(
child: Row( minHeight: constraints.maxHeight - 100, // AppBar-Höhe abziehen
crossAxisAlignment: CrossAxisAlignment.stretch, // Stretch hinzufügen
children: [
// Welcome Card - nimmt 60% der Breite
Expanded(
flex: 3,
child: Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Welcome to IFR Buddy!',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(
'Just an easy tool for writing down IFR clearances without a pen.',
style: TextStyle(fontSize: 16),
),
],
),
),
),
),
const SizedBox(width: 16), // Abstand zwischen den Cards
// SimBrief Card - nimmt 40% der Breite
Expanded(
flex: 2,
child: Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'SimBrief Import',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
TextFormField(
controller: _simbriefIdController,
decoration: const InputDecoration(
labelText: 'SimBrief Pilot ID',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
Row(
children: [
Checkbox(
value: _saveSimbriefId,
onChanged: (bool? value) {
setState(() {
_saveSimbriefId = value ?? false;
});
_saveSimbriefIdToPrefs();
},
),
const Text('Save SimBrief ID'),
],
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _fetchSimbriefData,
child: const Text('Load SimBrief Data'),
),
),
],
),
),
),
),
],
), ),
), child: Column(
const SizedBox(height: 16), mainAxisSize: MainAxisSize.min,
Card( children: [
elevation: 2, if (constraints.maxWidth > 600)
child: Padding( // Desktop Layout
padding: const EdgeInsets.all(16.0), IntrinsicHeight(
child: Column( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Expected Clearance',
style: TextStyle(
fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
const Text(
'Enter your expected clearance information below (Or use the SimBrief import). '
'Use the listening page or skip to the readback page.',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.left,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.all(8.0),
),
Form(
key: _formKey,
child: Column(
children: [ children: [
buildTextField( Expanded(
label: 'Clearance Limit', flex: 3,
controller: _expectedClearanceLimitController, child: _buildWelcomeCard(),
currentFocus: _clearanceLimitFocusNode,
nextFocus: _routeFocusNode,
keyboardType: TextInputType.text, // Standard keyboard
inputFormatters: [
// Allow letters, numbers, spaces, and hyphens
FilteringTextInputFormatter.allow(
RegExp(r'[A-Za-z0-9\s\-]')),
],
enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning:
false, // iOS-specific
), ),
buildTextField( const SizedBox(width: 16),
label: 'Route/Sid', Expanded(
controller: _expectedRouteController, flex: 2,
currentFocus: _routeFocusNode, child: _buildSimbriefCard(),
nextFocus: _altitudeFocusNode,
keyboardType: TextInputType.text, // Standard keyboard
inputFormatters: [
// Allow letters, numbers, spaces, and hyphens
FilteringTextInputFormatter.allow(
RegExp(r'[A-Za-z0-9\s\-]')),
],
enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning:
false, // iOS-specific
),
buildTextField(
label: 'Altitude',
controller: _expectedAltitudeController,
currentFocus: _altitudeFocusNode,
nextFocus: _frequencyFocusNode, // Updated next focus
keyboardType: TextInputType.text, // Standard keyboard
inputFormatters: null, // No restrictions
enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning:
false, // iOS-specific
),
buildTextField(
label: 'Departure Frequency', // Changed label
controller: _expectedFrequencyController,
currentFocus: _frequencyFocusNode,
nextFocus: _squawkFocusNode, // Next focus to Squawk
keyboardType: TextInputType.text, // Standard keyboard
inputFormatters: [
FrequencyInputFormatter(), // Handles decimal inputs
],
enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning:
false, // iOS-specific
),
// buildTextField(
// label: 'Transponder (Squawk)', // Now last field
// controller: _expectedSquawkController,
// currentFocus: _squawkFocusNode,
// isLastField: true, // Mark as last field
// keyboardType: TextInputType.text, // Standard keyboard
// inputFormatters: [
// // Allow only digits 0-7 and limit to 4 characters
// FilteringTextInputFormatter.allow(
// RegExp(r'[0-7]')),
// LengthLimitingTextInputFormatter(4),
// ],
// enableAutocorrect: false, // Disable autocorrect
// enableSuggestions: false, // Disable suggestions
// enableIMEPersonalizedLearning:
// false, // iOS-specific
// ),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _navigateToComparisonPage,
child: const Text('Next'),
),
const SizedBox(height: 10),
TextButton(
onPressed: _skipReadback,
child: const Text('Skip to end'),
), ),
], ],
), ),
)
else
// Mobile Layout
Column(
children: [
_buildWelcomeCard(),
const SizedBox(height: 16),
_buildSimbriefCard(),
],
), ),
], _buildMainContent(),
), ],
), ),
), ),
], ),
), );
},
),
);
}
// Hilfsmethoden zum Erstellen der Cards
Widget _buildWelcomeCard() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Welcome to IFR Buddy!',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 10),
Text(
'Just an easy tool for writing down IFR clearances without a pen.',
style: TextStyle(fontSize: 16),
),
],
),
),
);
}
Widget _buildSimbriefCard() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'SimBrief Import',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
TextFormField(
controller: _simbriefIdController,
decoration: const InputDecoration(
labelText: 'SimBrief Pilot ID',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 10),
Row(
children: [
Checkbox(
value: _saveSimbriefId,
onChanged: (bool? value) {
setState(() {
_saveSimbriefId = value ?? false;
});
_saveSimbriefIdToPrefs();
},
),
const Text('Save SimBrief ID'),
],
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _fetchSimbriefData,
child: const Text('Load SimBrief Data'),
),
),
],
),
),
);
}
Widget _buildMainContent() {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Expected Clearance',
style: TextStyle(
fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
const Text(
'Enter your expected clearance information below (Or use the SimBrief import). '
'Use the listening page or skip to the readback page.',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.left,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.all(8.0),
),
Form(
key: _formKey,
child: Column(
children: [
buildTextField(
label: 'Clearance Limit',
controller: _expectedClearanceLimitController,
currentFocus: _clearanceLimitFocusNode,
nextFocus: _routeFocusNode,
keyboardType: TextInputType.text, // Standard keyboard
inputFormatters: [
// Allow letters, numbers, spaces, and hyphens
FilteringTextInputFormatter.allow(
RegExp(r'[A-Za-z0-9\s\-]')),
],
enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning:
false, // iOS-specific
),
buildTextField(
label: 'Route/Sid',
controller: _expectedRouteController,
currentFocus: _routeFocusNode,
nextFocus: _altitudeFocusNode,
keyboardType: TextInputType.text, // Standard keyboard
inputFormatters: [
// Allow letters, numbers, spaces, and hyphens
FilteringTextInputFormatter.allow(
RegExp(r'[A-Za-z0-9\s\-]')),
],
enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning:
false, // iOS-specific
),
buildTextField(
label: 'Altitude',
controller: _expectedAltitudeController,
currentFocus: _altitudeFocusNode,
nextFocus: _frequencyFocusNode, // Updated next focus
keyboardType: TextInputType.text, // Standard keyboard
inputFormatters: null, // No restrictions
enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning:
false, // iOS-specific
),
buildTextField(
label: 'Departure Frequency', // Changed label
controller: _expectedFrequencyController,
currentFocus: _frequencyFocusNode,
nextFocus: _squawkFocusNode, // Next focus to Squawk
keyboardType: TextInputType.text, // Standard keyboard
inputFormatters: [
FrequencyInputFormatter(), // Handles decimal inputs
],
enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning:
false, // iOS-specific
),
// buildTextField(
// label: 'Transponder (Squawk)', // Now last field
// controller: _expectedSquawkController,
// currentFocus: _squawkFocusNode,
// isLastField: true, // Mark as last field
// keyboardType: TextInputType.text, // Standard keyboard
// inputFormatters: [
// // Allow only digits 0-7 and limit to 4 characters
// FilteringTextInputFormatter.allow(
// RegExp(r'[0-7]')),
// LengthLimitingTextInputFormatter(4),
// ],
// enableAutocorrect: false, // Disable autocorrect
// enableSuggestions: false, // Disable suggestions
// enableIMEPersonalizedLearning:
// false, // iOS-specific
// ),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _navigateToComparisonPage,
child: const Text('Next'),
),
const SizedBox(height: 10),
TextButton(
onPressed: _skipReadback,
child: const Text('Skip to end'),
),
],
),
),
],
), ),
), ),
); );

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:pocketbase/pocketbase.dart'; // Import the PocketBase package
import 'expectation_input_page.dart'; import 'expectation_input_page.dart';
import '../widgets/clearance_field.dart'; import '../widgets/clearance_field.dart';
@@ -33,7 +32,6 @@ class FinalClearanceDisplayState extends State<FinalClearanceDisplay> {
late String altitude; late String altitude;
late String squawk; late String squawk;
late String frequency; late String frequency;
final PocketBase pb = PocketBase('https://backend.degnedict.de'); // Initialize PocketBase
@override @override
void initState() { void initState() {
@@ -43,24 +41,8 @@ class FinalClearanceDisplayState extends State<FinalClearanceDisplay> {
altitude = widget.altitude; altitude = widget.altitude;
squawk = widget.squawk; squawk = widget.squawk;
frequency = widget.frequency; frequency = widget.frequency;
_createPageViewRecord(); // Create a record with the current timestamp
} }
// Function to create a new record with a timestamp in epoch milliseconds in your PocketBase collection
Future<void> _createPageViewRecord() async {
try {
// Get the current time in epoch milliseconds
int currentTimeInMillis = DateTime.now().millisecondsSinceEpoch;
// Create a new record in the collection with the current epoch time
await pb.collection('IFRbuddyUsage').create(body: {
'timestamp': currentTimeInMillis, // Save current timestamp as epoch time (milliseconds)
});
} catch (e) {
}
}
// Navigate back to the first page (ExpectationInputPage) // Navigate back to the first page (ExpectationInputPage)
void _navigateHome(BuildContext context) { void _navigateHome(BuildContext context) {
Navigator.pushAndRemoveUntil( Navigator.pushAndRemoveUntil(

View File

@@ -1,29 +1,55 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
class FrequencyInputFormatter extends TextInputFormatter { class FrequencyInputFormatter extends TextInputFormatter {
static const int maxDigits = 5;
@override @override
TextEditingValue formatEditUpdate( TextEditingValue formatEditUpdate(
TextEditingValue oldValue, TextEditingValue newValue) { TextEditingValue oldValue,
String digitsOnly = newValue.text.replaceAll(RegExp(r'[^0-9]'), ''); TextEditingValue newValue,
) {
// Skip formatting if deleting
if (oldValue.text.length > newValue.text.length) {
return newValue;
}
if (digitsOnly.length > maxDigits) { // Allow only numbers and decimal point
String filteredText = newValue.text.replaceAll(RegExp(r'[^\d.]'), '');
// Only allow one decimal point
if (filteredText.split('.').length > 2) {
return oldValue; return oldValue;
} }
String formatted = digitsOnly; // Format for three decimal places and auto-insert decimal point
if (digitsOnly.length > 3) { if (filteredText.contains('.')) {
formatted = '${digitsOnly.substring(0, 3)}.${digitsOnly.substring(3)}'; List<String> parts = filteredText.split('.');
} String whole = parts[0];
String fraction = parts[1];
if (formatted.endsWith('.') && digitsOnly.length <= 3) { // Limit to three decimal places
formatted = formatted.substring(0, formatted.length - 1); if (fraction.length > 3) {
fraction = fraction.substring(0, 3);
}
// Reconstruct the text
filteredText = "$whole.$fraction";
} else {
// Auto-insert decimal point after the third digit
if (filteredText.length > 3) {
String whole = filteredText.substring(0, 3);
String fraction = filteredText.substring(3);
// Limit fraction to three digits
if (fraction.length > 3) {
fraction = fraction.substring(0, 3);
}
filteredText = "$whole.$fraction";
}
} }
return TextEditingValue( return TextEditingValue(
text: formatted, text: filteredText,
selection: TextSelection.collapsed(offset: formatted.length), selection: TextSelection.collapsed(offset: filteredText.length),
); );
} }
} }

View File

@@ -288,14 +288,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pocketbase:
dependency: "direct main"
description:
name: pocketbase
sha256: "1d2958a3a7cb1e0050f425f179bd6557441fafcf740a79d5b8b80d6954149790"
url: "https://pub.dev"
source: hosted
version: "0.18.1"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -37,7 +37,6 @@ dependencies:
http: ^1.2.2 http: ^1.2.2
shared_preferences: ^2.3.2 shared_preferences: ^2.3.2
flutter_launcher_icons: ^0.14.1 flutter_launcher_icons: ^0.14.1
pocketbase: ^0.18.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View File

@@ -37,6 +37,9 @@
<title>ifrbuddy</title> <title>ifrbuddy</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<!-- Umami Analytics -->
<script defer src="https://umami.degnedict.de/script.js" data-website-id="82ee3dc6-dc80-42d3-8583-d6824aebfed5"></script>
</head> </head>
<body> <body>
<script src="flutter_bootstrap.js" async></script> <script src="flutter_bootstrap.js" async></script>