21 Commits
Stable ... main

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
c7233daec7 mac OS compatibility 2024-11-08 23:20:09 +01:00
c03e9556b7 UI cleanup 2024-11-08 23:05:01 +01:00
bcd10ed512 simbrief own card in ui 2024-11-08 23:02:28 +01:00
4ab8b70524 simrbrief integration 2024-11-08 22:59:47 +01:00
10377a2a0a removed squawk from expected clearance page 2024-11-08 22:38:29 +01:00
69f0600d21 s 2024-10-21 04:13:22 +02:00
0ff3b71ef4 Merge pull request 'Easy pocketbase tracker' (#1) from usage-counter into main
Reviewed-on: #1
2024-10-21 01:43:35 +00:00
49e6806250 Easy pocketbase tracker 2024-10-21 03:42:22 +02:00
20e1c07519 Update pubspec.lock 2024-10-20 18:36:28 +02:00
db6e489de6 changed button name 2024-10-20 18:13:38 +02:00
51dd38ca9a fixed order from CRATF to CRAFT on main page (lol) 2024-10-20 18:06:37 +02:00
5ee6b33097 Icons 2024-10-20 02:18:41 +02:00
4aa5bf784b Update README.md 2024-10-19 12:42:57 +00:00
ec76a028cb Up2Date 2024-10-19 14:17:37 +02:00
9f98189983 LICENSE 2024-10-18 18:07:26 +02:00
c344f57698 Merge branch 'main' of https://git.degnedict.de/bene/ifrbuddy 2024-10-18 18:06:29 +02:00
7d905a84a6 License 2024-10-18 18:06:25 +02:00
2e3a1993e0 Update README.md 2024-10-18 16:04:35 +00:00
15 changed files with 530 additions and 474 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"cmake.ignoreCMakeListsMissing": true
}

7
LICENSE Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2024 Degnedict
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,16 +1,3 @@
# ifrbuddy # ifrbuddy
A new Flutter project. A simple way to write down IFR clearances using the CRAFT method.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

1
buildandpush.sh Normal file
View File

@@ -0,0 +1 @@
flutter build web --release && docker buildx build --platform linux/amd64 -t git.degnedict.de/bene/ifrbuddy --push .

View File

@@ -12,7 +12,8 @@ class ComparisonPage extends StatefulWidget {
final bool isDarkMode; final bool isDarkMode;
final Function toggleDarkMode; final Function toggleDarkMode;
const ComparisonPage({super.key, const ComparisonPage({
super.key,
required this.expectedClearanceLimit, required this.expectedClearanceLimit,
required this.expectedRoute, required this.expectedRoute,
required this.expectedAltitude, required this.expectedAltitude,
@@ -183,7 +184,7 @@ Widget build(BuildContext context) {
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: Text( child: Text(
'Use this Page while receiving your clearance. The arrow button copies the excpected value. Same if left empty.', 'Use this Page while receiving your clearance.\nThe arrow button copies the expected value. If left empty, expected values are still copied. the arrow buttons are a help for the Brain :)',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -212,8 +213,8 @@ Widget build(BuildContext context) {
buildExpectedClearanceField('Clearance Limit', widget.expectedClearanceLimit), buildExpectedClearanceField('Clearance Limit', widget.expectedClearanceLimit),
buildExpectedClearanceField('Route', widget.expectedRoute), buildExpectedClearanceField('Route', widget.expectedRoute),
buildExpectedClearanceField('Altitude', widget.expectedAltitude), buildExpectedClearanceField('Altitude', widget.expectedAltitude),
buildExpectedClearanceField('Transponder (Squawk)', widget.expectedSquawk),
buildExpectedClearanceField('Frequency', widget.expectedFrequency), buildExpectedClearanceField('Frequency', widget.expectedFrequency),
// buildExpectedClearanceField('Transponder (Squawk)', widget.expectedSquawk),
], ],
), ),
), ),
@@ -251,6 +252,12 @@ Widget build(BuildContext context) {
widget.expectedAltitude, widget.expectedAltitude,
focusNode: _actualAltitudeFocusNode, focusNode: _actualAltitudeFocusNode,
), ),
buildActualClearanceField(
'Frequency',
_actualFrequencyController,
widget.expectedFrequency,
formatterList: [FrequencyInputFormatter()],
),
buildActualClearanceField( buildActualClearanceField(
'Transponder (Squawk)', 'Transponder (Squawk)',
_actualSquawkController, _actualSquawkController,
@@ -260,12 +267,6 @@ Widget build(BuildContext context) {
LengthLimitingTextInputFormatter(4), LengthLimitingTextInputFormatter(4),
], ],
), ),
buildActualClearanceField(
'Frequency',
_actualFrequencyController,
widget.expectedFrequency,
formatterList: [FrequencyInputFormatter()],
),
], ],
), ),
), ),
@@ -278,7 +279,7 @@ Widget build(BuildContext context) {
// Proceed Button // Proceed Button
ElevatedButton( ElevatedButton(
onPressed: _navigateToFinalClearance, onPressed: _navigateToFinalClearance,
child: const Text('Proceed to Clearance Display'), child: const Text('Proceed to Readback page'),
), ),
], ],
), ),

View File

@@ -1,9 +1,13 @@
// ignore_for_file: prefer_const_constructors
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:keyboard_actions/keyboard_actions.dart'; // Correct package import
import 'comparison_page.dart'; import 'comparison_page.dart';
import 'final_clearance_page.dart'; // Import final clearance page import 'final_clearance_page.dart'; // Import final clearance page
import '../utils/frequency_input_formatter.dart'; import '../utils/frequency_input_formatter.dart'; // Ensure this path is correct
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class ExpectationInputPage extends StatefulWidget { class ExpectationInputPage extends StatefulWidget {
final bool isDarkMode; final bool isDarkMode;
@@ -31,13 +35,43 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
TextEditingController(); TextEditingController();
final TextEditingController _expectedFrequencyController = final TextEditingController _expectedFrequencyController =
TextEditingController(); TextEditingController();
final TextEditingController _simbriefIdController = TextEditingController();
// Reintroduce FocusNodes // FocusNodes for each field
final FocusNode _clearanceLimitFocusNode = FocusNode(); final FocusNode _clearanceLimitFocusNode = FocusNode();
final FocusNode _routeFocusNode = FocusNode(); final FocusNode _routeFocusNode = FocusNode();
final FocusNode _altitudeFocusNode = FocusNode(); final FocusNode _altitudeFocusNode = FocusNode();
final FocusNode _squawkFocusNode = FocusNode();
final FocusNode _frequencyFocusNode = FocusNode(); final FocusNode _frequencyFocusNode = FocusNode();
final FocusNode _squawkFocusNode = FocusNode();
bool _saveSimbriefId = false;
static const String _simbriefIdKey = 'simbrief_id';
@override
void initState() {
super.initState();
_loadSavedSimbriefId();
}
Future<void> _loadSavedSimbriefId() async {
final prefs = await SharedPreferences.getInstance();
final savedId = prefs.getString(_simbriefIdKey);
if (savedId != null) {
setState(() {
_simbriefIdController.text = savedId;
_saveSimbriefId = true;
});
}
}
Future<void> _saveSimbriefIdToPrefs() async {
final prefs = await SharedPreferences.getInstance();
if (_saveSimbriefId) {
await prefs.setString(_simbriefIdKey, _simbriefIdController.text);
} else {
await prefs.remove(_simbriefIdKey);
}
}
@override @override
void dispose() { void dispose() {
@@ -49,11 +83,48 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
_clearanceLimitFocusNode.dispose(); _clearanceLimitFocusNode.dispose();
_routeFocusNode.dispose(); _routeFocusNode.dispose();
_altitudeFocusNode.dispose(); _altitudeFocusNode.dispose();
_squawkFocusNode.dispose();
_frequencyFocusNode.dispose(); _frequencyFocusNode.dispose();
_squawkFocusNode.dispose();
super.dispose(); super.dispose();
} }
Future<void> _fetchSimbriefData() async {
try {
if (_saveSimbriefId) {
await _saveSimbriefIdToPrefs();
}
final response = await http.get(
Uri.parse('https://www.simbrief.com/api/xml.fetcher.php?userid=${_simbriefIdController.text}&json=1'),
);
if (!mounted) return;
if (response.statusCode == 200) {
final data = json.decode(response.body);
String fullRoute = data['general']['route'] ?? '';
String sid = fullRoute.split(' ').first;
setState(() {
_expectedClearanceLimitController.text = data['destination']['icao_code'] ?? '';
_expectedRouteController.text = sid;
_expectedAltitudeController.text = '';
_expectedFrequencyController.text = '';
});
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('SimBrief data successfully loaded')),
);
}
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error loading SimBrief data: $e')),
);
}
}
void _navigateToComparisonPage() { void _navigateToComparisonPage() {
if (_formKey.currentState?.validate() ?? false) { if (_formKey.currentState?.validate() ?? false) {
Navigator.push( Navigator.push(
@@ -93,7 +164,7 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
} }
} }
// Updated method to build a text field with optional input // Method to build a text field with optional input and standard keyboard
Widget buildTextField({ Widget buildTextField({
required String label, required String label,
required TextEditingController controller, required TextEditingController controller,
@@ -120,20 +191,15 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
onFieldSubmitted: (_) { onFieldSubmitted: (_) {
if (isLastField) { if (isLastField) {
// If it's the last field, unfocus to close the keyboard // If it's the last field, unfocus to close the keyboard and submit
currentFocus.unfocus(); currentFocus.unfocus();
// Optionally, you can trigger form submission here _navigateToComparisonPage();
// _navigateToComparisonPage();
} else if (nextFocus != null) { } else if (nextFocus != null) {
FocusScope.of(context).requestFocus(nextFocus); FocusScope.of(context).requestFocus(nextFocus);
} }
}, },
// Updated validator to allow empty inputs // Validator allows empty inputs but validates if input is present
validator: (value) { validator: (value) {
// If you want to perform validation only when input is not empty,
// you can add conditional checks here.
// Example: Validate numerical fields if they are not empty
if (value != null && value.isNotEmpty) { if (value != null && value.isNotEmpty) {
if (keyboardType == TextInputType.number || if (keyboardType == TextInputType.number ||
keyboardType == const TextInputType.numberWithOptions(decimal: true)) { keyboardType == const TextInputType.numberWithOptions(decimal: true)) {
@@ -144,7 +210,7 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
// Add more specific validations if needed // Add more specific validations if needed
} }
// Example: Validate Transponder (Squawk) field // Validate Transponder (Squawk) field
if (label == 'Transponder (Squawk)') { if (label == 'Transponder (Squawk)') {
if (!RegExp(r'^[0-7]{1,4}$').hasMatch(value)) { if (!RegExp(r'^[0-7]{1,4}$').hasMatch(value)) {
return 'Squawk must be 1-4 digits between 0 and 7'; return 'Squawk must be 1-4 digits between 0 and 7';
@@ -165,105 +231,6 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
); );
} }
// Configure KeyboardActions
KeyboardActionsConfig _buildConfig(BuildContext context) {
return KeyboardActionsConfig(
keyboardSeparatorColor: Colors.grey, // Optional: Customize the separator color
nextFocus: true, // Automatically handle next focus
actions: [
// Define actions for each FocusNode
KeyboardActionsItem(
focusNode: _clearanceLimitFocusNode,
toolbarButtons: [
(node) {
return GestureDetector(
onTap: () => node.nextFocus(),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"Next",
style: TextStyle(color: Colors.blue, fontSize: 16),
),
),
);
}
],
),
KeyboardActionsItem(
focusNode: _routeFocusNode,
toolbarButtons: [
(node) {
return GestureDetector(
onTap: () => node.nextFocus(),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"Next",
style: TextStyle(color: Colors.blue, fontSize: 16),
),
),
);
}
],
),
KeyboardActionsItem(
focusNode: _altitudeFocusNode,
toolbarButtons: [
(node) {
return GestureDetector(
onTap: () => node.nextFocus(),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"Next",
style: TextStyle(color: Colors.blue, fontSize: 16),
),
),
);
}
],
),
KeyboardActionsItem(
focusNode: _squawkFocusNode,
toolbarButtons: [
(node) {
return GestureDetector(
onTap: () => node.nextFocus(),
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"Next",
style: TextStyle(color: Colors.blue, fontSize: 16),
),
),
);
}
],
),
KeyboardActionsItem(
focusNode: _frequencyFocusNode,
toolbarButtons: [
(node) {
return GestureDetector(
onTap: () {
node.unfocus(); // Dismiss the keyboard
_navigateToComparisonPage(); // Optionally, submit the form
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"Done",
style: TextStyle(color: Colors.blue, fontSize: 16),
),
),
);
}
],
),
],
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -279,40 +246,131 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
), ),
], ],
), ),
body: GestureDetector( body: LayoutBuilder(
// Dismiss the keyboard when tapping outside builder: (context, constraints) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), onTap: () => FocusScope.of(context).unfocus(),
child: KeyboardActions(
config: _buildConfig(context), // Apply KeyboardActionsConfig
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
// ConstrainedBox hinzufügen
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight - 100, // AppBar-Höhe abziehen
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
const Card( if (constraints.maxWidth > 600)
// Desktop Layout
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: _buildWelcomeCard(),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: _buildSimbriefCard(),
),
],
),
)
else
// Mobile Layout
Column(
children: [
_buildWelcomeCard(),
const SizedBox(height: 16),
_buildSimbriefCard(),
],
),
_buildMainContent(),
],
),
),
),
);
},
),
);
}
// Hilfsmethoden zum Erstellen der Cards
Widget _buildWelcomeCard() {
return Card(
elevation: 2, elevation: 2,
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: const [
Text( Text(
'Welcome to IFR Buddy!', 'Welcome to IFR Buddy!',
style: TextStyle( style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.left,
), ),
SizedBox(height: 10), SizedBox(height: 10),
Text( Text(
'Just an easy tool for writing down IFR clearances without a pen.', 'Just an easy tool for writing down IFR clearances without a pen.',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
textAlign: TextAlign.left,
), ),
], ],
), ),
), ),
);
}
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: 16), const SizedBox(height: 10),
Card( 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, elevation: 2,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@@ -326,12 +384,15 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
const Text( const Text(
'Enter your expected clearance information below. ' 'Enter your expected clearance information below (Or use the SimBrief import). '
'Use the listening page or skip to the readback page.', 'Use the listening page or skip to the readback page.',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.all(8.0),
),
Form( Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
@@ -341,6 +402,12 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
controller: _expectedClearanceLimitController, controller: _expectedClearanceLimitController,
currentFocus: _clearanceLimitFocusNode, currentFocus: _clearanceLimitFocusNode,
nextFocus: _routeFocusNode, 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 enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning:
@@ -351,6 +418,12 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
controller: _expectedRouteController, controller: _expectedRouteController,
currentFocus: _routeFocusNode, currentFocus: _routeFocusNode,
nextFocus: _altitudeFocusNode, 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 enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning:
@@ -360,54 +433,54 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
label: 'Altitude', label: 'Altitude',
controller: _expectedAltitudeController, controller: _expectedAltitudeController,
currentFocus: _altitudeFocusNode, currentFocus: _altitudeFocusNode,
nextFocus: _squawkFocusNode, nextFocus: _frequencyFocusNode, // Updated next focus
keyboardType: TextInputType.number, keyboardType: TextInputType.text, // Standard keyboard
inputFormatters: [ inputFormatters: null, // No restrictions
FilteringTextInputFormatter.digitsOnly
],
enableAutocorrect: false, // Disable autocorrect enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning:
false, // iOS-specific false, // iOS-specific
), ),
buildTextField( buildTextField(
label: 'Transponder (Squawk)', label: 'Departure Frequency', // Changed label
controller: _expectedSquawkController,
currentFocus: _squawkFocusNode,
nextFocus: _frequencyFocusNode,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(
RegExp(r'[0-7]')),
LengthLimitingTextInputFormatter(4),
],
enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning:
false, // iOS-specific
),
buildTextField(
label: 'Departure Frequency',
controller: _expectedFrequencyController, controller: _expectedFrequencyController,
currentFocus: _frequencyFocusNode, currentFocus: _frequencyFocusNode,
isLastField: true, nextFocus: _squawkFocusNode, // Next focus to Squawk
keyboardType: keyboardType: TextInputType.text, // Standard keyboard
const TextInputType.numberWithOptions(decimal: true), inputFormatters: [
inputFormatters: [FrequencyInputFormatter()], FrequencyInputFormatter(), // Handles decimal inputs
],
enableAutocorrect: false, // Disable autocorrect enableAutocorrect: false, // Disable autocorrect
enableSuggestions: false, // Disable suggestions enableSuggestions: false, // Disable suggestions
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning:
false, // iOS-specific 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), const SizedBox(height: 20),
ElevatedButton( ElevatedButton(
onPressed: _skipReadback, onPressed: _navigateToComparisonPage,
child: const Text('Skip to Readback'), child: const Text('Next'),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
ElevatedButton( TextButton(
onPressed: _navigateToComparisonPage, onPressed: _skipReadback,
child: const Text('Navigate to Comparison Page'), child: const Text('Skip to end'),
), ),
], ],
), ),
@@ -415,12 +488,6 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
], ],
), ),
), ),
),
],
),
),
),
),
); );
} }
} }

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'expectation_input_page.dart'; import 'expectation_input_page.dart';
import 'edit_clearance_page.dart'; // Import the edit clearance page
import '../widgets/clearance_field.dart'; import '../widgets/clearance_field.dart';
class FinalClearanceDisplay extends StatefulWidget { class FinalClearanceDisplay extends StatefulWidget {
@@ -12,7 +11,8 @@ class FinalClearanceDisplay extends StatefulWidget {
final bool isDarkMode; final bool isDarkMode;
final Function toggleDarkMode; final Function toggleDarkMode;
const FinalClearanceDisplay({super.key, const FinalClearanceDisplay({
super.key,
required this.clearanceLimit, required this.clearanceLimit,
required this.route, required this.route,
required this.altitude, required this.altitude,
@@ -43,35 +43,6 @@ class FinalClearanceDisplayState extends State<FinalClearanceDisplay> {
frequency = widget.frequency; frequency = widget.frequency;
} }
// Navigate to Edit Clearance Page and handle returned data
Future<void> _navigateToEditClearance() async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => EditClearancePage(
clearanceLimit: clearanceLimit,
route: route,
altitude: altitude,
squawk: squawk,
frequency: frequency,
isDarkMode: widget.isDarkMode,
toggleDarkMode: widget.toggleDarkMode,
),
),
);
// If the result contains updated data, update the state with the new values
if (result != null && mounted) {
setState(() {
clearanceLimit = result['clearanceLimit'];
route = result['route'];
altitude = result['altitude'];
squawk = result['squawk'];
frequency = result['frequency'];
});
}
}
// 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(
@@ -97,18 +68,7 @@ class FinalClearanceDisplayState extends State<FinalClearanceDisplay> {
_navigateHome(context); _navigateHome(context);
}, },
), ),
actions: [ actions: const [],
IconButton(
icon: const Icon(Icons.edit), // Edit button
onPressed: _navigateToEditClearance,
),
IconButton(
icon: Icon(widget.isDarkMode ? Icons.dark_mode : Icons.light_mode),
onPressed: () {
widget.toggleDarkMode();
},
),
],
), ),
body: Center( body: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -129,8 +89,8 @@ class FinalClearanceDisplayState extends State<FinalClearanceDisplay> {
buildClearanceField('Clearance Limit', clearanceLimit), buildClearanceField('Clearance Limit', clearanceLimit),
buildClearanceField('Route', route), buildClearanceField('Route', route),
buildClearanceField('Altitude', altitude), buildClearanceField('Altitude', altitude),
buildClearanceField('Transponder (Squawk)', squawk),
buildClearanceField('Frequency', frequency), buildClearanceField('Frequency', frequency),
buildClearanceField('Transponder (Squawk)', squawk),
], ],
), ),
), ),

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];
// Limit to three decimal places
if (fraction.length > 3) {
fraction = fraction.substring(0, 3);
} }
if (formatted.endsWith('.') && digitsOnly.length <= 3) { // Reconstruct the text
formatted = formatted.substring(0, formatted.length - 1); 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

@@ -8,5 +8,7 @@
<true/> <true/>
<key>com.apple.security.network.server</key> <key>com.apple.security.network.server</key>
<true/> <true/>
<key>com.apple.security.network.client</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -4,5 +4,8 @@
<dict> <dict>
<key>com.apple.security.app-sandbox</key> <key>com.apple.security.app-sandbox</key>
<true/> <true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>w
</dict> </dict>
</plist> </plist>

View File

@@ -176,30 +176,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.9.0" version: "4.9.0"
keyboard_actions:
dependency: "direct main"
description:
name: keyboard_actions
sha256: "31e0ab2a706ac8f58887efa60efc1f19aecdf37d8ab0f665a0f156d1fbeab650"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.4" version: "10.0.5"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.3" version: "3.0.5"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
@@ -228,18 +220,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.8.0" version: "0.11.1"
meta: meta:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.12.0" version: "1.15.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@@ -401,10 +393,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.0" version: "0.7.2"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -425,10 +417,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.1" version: "14.2.5"
web: web:
dependency: transitive dependency: transitive
description: description:

View File

@@ -36,9 +36,7 @@ dependencies:
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
http: ^1.2.2 http: ^1.2.2
shared_preferences: ^2.3.2 shared_preferences: ^2.3.2
keyboard_actions: ^4.2.0
flutter_launcher_icons: ^0.14.1 flutter_launcher_icons: ^0.14.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
@@ -50,6 +48,12 @@ dev_dependencies:
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^3.0.0 flutter_lints: ^3.0.0
flutter_icons:
android: true
ios: true
macos: true # Enable macOS icon generation
image_path: "assets/icon-512.png" # Path to your icon image
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 B

View File

@@ -18,7 +18,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project."> <meta name="description" content="Write down IFR Clearances easily">
<!-- No cache tag --> <!-- No cache tag -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
@@ -29,7 +29,7 @@
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="ifrbuddy"> <meta name="apple-mobile-web-app-title" content="IFR Buddy">
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon --> <!-- Favicon -->
@@ -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>

View File

@@ -1,11 +1,11 @@
{ {
"name": "ifrbuddy", "name": "IFR Buddy",
"short_name": "ifrbuddy", "short_name": "IFR Buddy",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#hexcode", "background_color": "#hexcode",
"theme_color": "#hexcode", "theme_color": "#hexcode",
"description": "A new Flutter project.", "description": "Write down IFR Clearances easily",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"prefer_related_applications": false, "prefer_related_applications": false,
"icons": [ "icons": [