From 13cd3cdf147e035ebe5890dd760c44036ecdd4ab Mon Sep 17 00:00:00 2001 From: Pete Gregory Date: Sun, 14 Dec 2025 12:51:20 +0000 Subject: [PATCH] new entry panel fixes --- .gitea/workflows/release.yml | 34 +++++--- lib/components/pages/new_entry.dart | 130 ++++++++++++++++++++++------ 2 files changed, 128 insertions(+), 36 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 1f600b1..2b62983 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -9,7 +9,7 @@ on: env: JAVA_VERSION: "17" ANDROID_SDK_ROOT: "${{ github.workspace }}/android-sdk" - FLUTTER_CHANNEL: "stable" + FLUTTER_VERSION: "3.22.2" BUILD_WINDOWS: "false" # set to "true" when you actually want Windows builds GITEA_BASE_URL: https://git.tgj.services @@ -70,11 +70,17 @@ jobs: echo "$ANDROID_SDK_ROOT/platform-tools" >> "$GITHUB_PATH" echo "$ANDROID_SDK_ROOT/build-tools/33.0.2" >> "$GITHUB_PATH" - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: ${{ env.FLUTTER_CHANNEL }} - cache: true + - name: Install Flutter SDK + run: | + set -euo pipefail + FLUTTER_HOME="$HOME/flutter" + if [ ! -x "$FLUTTER_HOME/bin/flutter" ]; then + rm -rf "$FLUTTER_HOME" + curl -fsSL -o /tmp/flutter.tar.xz "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz" + tar -C "$HOME" -xf /tmp/flutter.tar.xz + fi + echo "$FLUTTER_HOME/bin" >> "$GITHUB_PATH" + "$FLUTTER_HOME/bin/flutter" --version - name: Allow all git directories (CI) run: git config --global --add safe.directory '*' @@ -175,11 +181,17 @@ jobs: $SUDO apt-get update $SUDO apt-get install -y unzip xz-utils zip libstdc++6 libglu1-mesa clang cmake ninja-build pkg-config libgtk-3-dev libsecret-1-dev liblzma-dev curl jq - - name: Setup Flutter - uses: subosito/flutter-action@v2 - with: - channel: ${{ env.FLUTTER_CHANNEL }} - cache: true + - name: Install Flutter SDK + run: | + set -euo pipefail + FLUTTER_HOME="$HOME/flutter" + if [ ! -x "$FLUTTER_HOME/bin/flutter" ]; then + rm -rf "$FLUTTER_HOME" + curl -fsSL -o /tmp/flutter.tar.xz "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${FLUTTER_VERSION}-stable.tar.xz" + tar -C "$HOME" -xf /tmp/flutter.tar.xz + fi + echo "$FLUTTER_HOME/bin" >> "$GITHUB_PATH" + "$FLUTTER_HOME/bin/flutter" --version - name: Allow all git directories (CI) run: git config --global --add safe.directory '*' diff --git a/lib/components/pages/new_entry.dart b/lib/components/pages/new_entry.dart index ed4bfb9..913716c 100644 --- a/lib/components/pages/new_entry.dart +++ b/lib/components/pages/new_entry.dart @@ -283,14 +283,53 @@ class _NewEntryPageState extends State { return payload; } + Future _validateRequiredFields() async { + final missing = []; + + if (_useManualMileage) { + if (_startController.text.trim().isEmpty) missing.add('From'); + if (_endController.text.trim().isEmpty) missing.add('To'); + final mileageText = _mileageController.text.trim(); + if (double.tryParse(mileageText) == null) { + missing.add('Mileage'); + } + } else { + if (_routeResult == null || _routeResult!.calculatedRoute.isEmpty) { + missing.add('Route'); + } + } + + if (_networkController.text.trim().isEmpty) { + missing.add('Network'); + } + + if (missing.isEmpty) return true; + if (!mounted) return false; + + final fieldList = missing.join(', '); + await showDialog( + context: context, + builder: (_) => AlertDialog( + title: const Text('Required field missing'), + content: Text( + missing.length == 1 + ? 'Please fill the following field: $fieldList.' + : 'Please fill the following fields: $fieldList.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + return false; + } + Future _submit() async { if (!_formKey.currentState!.validate()) return; - if (!_useManualMileage && _routeResult == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Please calculate mileage first')), - ); - return; - } + if (!await _validateRequiredFields()) return; setState(() => _submitting = true); final api = context.read(); final routeStations = _routeResult?.calculatedRoute ?? []; @@ -524,6 +563,10 @@ class _NewEntryPageState extends State { child: LayoutBuilder( builder: (context, constraints) { final twoCol = !isMobile && constraints.maxWidth > 1000; + final tractionEmpty = _tractionItems.length == 1; + final mileageEmpty = !_useManualMileage && _routeResult == null; + final balancePanels = twoCol && tractionEmpty && mileageEmpty; + final balancedHeight = balancePanels ? 165.0 : null; final detailPanel = _section('Details', [ _buildTripSelector(context), @@ -614,11 +657,27 @@ class _NewEntryPageState extends State { ), ), _buildTractionList(), - ]); + ], minHeight: balancedHeight); final mileagePanel = _section( 'Mileage', [ + if (!_useManualMileage) + Align( + alignment: Alignment.centerLeft, + child: TextButton.icon( + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + minimumSize: const Size(0, 32), + ), + onPressed: _openCalculator, + icon: const Icon(Icons.calculate, size: 18), + label: const Text('Open mileage calculator'), + ), + ), if (_useManualMileage) TextFormField( controller: _mileageController, @@ -637,14 +696,12 @@ class _NewEntryPageState extends State { subtitle: Text( '${_routeResult!.distance.toStringAsFixed(2)} mi', ), - ), - if (!_useManualMileage) - Align( - alignment: Alignment.centerLeft, - child: ElevatedButton.icon( - onPressed: _openCalculator, - icon: const Icon(Icons.calculate), - label: const Text('Open mileage calculator'), + ) + else + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'No route selected. Use the calculator to add a route.', ), ), ], @@ -656,6 +713,7 @@ class _NewEntryPageState extends State { _saveDraft(); }, ), + minHeight: balancedHeight, ); return SingleChildScrollView( @@ -663,6 +721,22 @@ class _NewEntryPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(0, 36), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: _submitting + ? null + : () => _resetFormState(clearDraft: true), + icon: const Icon(Icons.clear, size: 16), + label: const Text('Clear form'), + ), + ), + const SizedBox(height: 8), detailPanel, const SizedBox(height: 16), twoCol @@ -682,14 +756,6 @@ class _NewEntryPageState extends State { ], ), const SizedBox(height: 12), - OutlinedButton.icon( - onPressed: _submitting - ? null - : () => _resetFormState(clearDraft: true), - icon: const Icon(Icons.clear), - label: const Text('Clear form'), - ), - const SizedBox(height: 8), ElevatedButton.icon( onPressed: _submitting ? null : _submit, icon: _submitting @@ -791,8 +857,13 @@ class _NewEntryPageState extends State { ); } - Widget _section(String title, List children, {Widget? trailing}) { - return Card( + Widget _section( + String title, + List children, { + Widget? trailing, + double? minHeight, + }) { + Widget card = Card( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( @@ -821,6 +892,15 @@ class _NewEntryPageState extends State { ), ), ); + + if (minHeight != null) { + card = ConstrainedBox( + constraints: BoxConstraints(minHeight: minHeight), + child: card, + ); + } + + return card; } }