Compare commits

..

11 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
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
9 changed files with 359 additions and 160 deletions

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

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

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

@@ -184,7 +184,7 @@ class ComparisonPageState extends State<ComparisonPage> {
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 expected 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,
), ),
@@ -214,7 +214,7 @@ class ComparisonPageState extends State<ComparisonPage> {
buildExpectedClearanceField('Route', widget.expectedRoute), buildExpectedClearanceField('Route', widget.expectedRoute),
buildExpectedClearanceField('Altitude', widget.expectedAltitude), buildExpectedClearanceField('Altitude', widget.expectedAltitude),
buildExpectedClearanceField('Frequency', widget.expectedFrequency), buildExpectedClearanceField('Frequency', widget.expectedFrequency),
buildExpectedClearanceField('Transponder (Squawk)', widget.expectedSquawk), // buildExpectedClearanceField('Transponder (Squawk)', widget.expectedSquawk),
], ],
), ),
), ),

View File

@@ -1,8 +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 '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'; // Ensure this path is correct 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;
@@ -30,6 +35,7 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
TextEditingController(); TextEditingController();
final TextEditingController _expectedFrequencyController = final TextEditingController _expectedFrequencyController =
TextEditingController(); TextEditingController();
final TextEditingController _simbriefIdController = TextEditingController();
// FocusNodes for each field // FocusNodes for each field
final FocusNode _clearanceLimitFocusNode = FocusNode(); final FocusNode _clearanceLimitFocusNode = FocusNode();
@@ -38,6 +44,35 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
final FocusNode _frequencyFocusNode = FocusNode(); final FocusNode _frequencyFocusNode = FocusNode();
final FocusNode _squawkFocusNode = 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() {
_expectedClearanceLimitController.dispose(); _expectedClearanceLimitController.dispose();
@@ -53,6 +88,43 @@ class ExpectationInputPageState extends State<ExpectationInputPage> {
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(
@@ -174,155 +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
const Card( child: ConstrainedBox(
elevation: 2, constraints: BoxConstraints(
child: Padding( minHeight: constraints.maxHeight - 100, // AppBar-Höhe abziehen
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Welcome to IFR Buddy!',
style: TextStyle(
fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.left,
),
SizedBox(height: 10),
Text(
'Just an easy tool for writing down IFR clearances without a pen.',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.left,
),
],
),
), ),
), 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. '
'Use the listening page or skip to the readback page.',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.left,
),
const SizedBox(height: 16),
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,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'expectation_input_page.dart'; import 'expectation_input_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,8 +43,6 @@ class FinalClearanceDisplayState extends State<FinalClearanceDisplay> {
frequency = widget.frequency; frequency = widget.frequency;
} }
// Navigate to Edit Clearance Page and handle returned data
// 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

@@ -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

@@ -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>