From e34c689ed9b7c10ecaf222ca6eb520c090b464e6 Mon Sep 17 00:00:00 2001 From: Pete Gregory Date: Thu, 11 Dec 2025 01:24:44 +0000 Subject: [PATCH] new pipeline --- .gitea/workflows/release.yml | 145 +++++++++++++++-------- lib/components/pages/new_entry.dart | 32 ++--- lib/components/pages/traction.dart | 173 ++++++++++++++-------------- lib/services/dataService.dart | 37 ++++++ 4 files changed, 232 insertions(+), 155 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index ff6e63a..7fa7ed6 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -10,10 +10,10 @@ env: JAVA_VERSION: "17" ANDROID_SDK_ROOT: "${{ github.workspace }}/android-sdk" FLUTTER_CHANNEL: "stable" - BUILD_WINDOWS: "false" # set to "true" when you have a Windows runner available + BUILD_WINDOWS: "false" # set to "true" when you actually want Windows builds jobs: - build: + meta: runs-on: ubuntu-latest outputs: base_version: ${{ steps.meta.outputs.base }} @@ -21,7 +21,21 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Install OS deps (Android + Linux desktop) + - name: Determine version + id: meta + run: | + RAW_VERSION=$(awk '/^version:/{print $2}' pubspec.yaml) + BASE_VERSION=${RAW_VERSION%%+*} + echo "base=${BASE_VERSION}" >> "$GITHUB_OUTPUT" + + android-build: + runs-on: ubuntu-latest + needs: meta + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install OS deps (Android) run: | if command -v sudo >/dev/null 2>&1; then SUDO="sudo" @@ -29,7 +43,7 @@ jobs: SUDO="" fi $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 liblzma-dev curl + $SUDO apt-get install -y unzip xz-utils zip libstdc++6 liblzma-dev curl - name: Setup Java uses: actions/setup-java@v4 @@ -43,8 +57,11 @@ jobs: curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o /tmp/cli-tools.zip unzip -q /tmp/cli-tools.zip -d "$ANDROID_SDK_ROOT"/cmdline-tools mv "$ANDROID_SDK_ROOT"/cmdline-tools/cmdline-tools "$ANDROID_SDK_ROOT"/cmdline-tools/latest - yes | "$ANDROID_SDK_ROOT"/cmdline-tools/latest/bin/sdkmanager --licenses - yes | "$ANDROID_SDK_ROOT"/cmdline-tools/latest/bin/sdkmanager "platform-tools" "platforms;android-33" "build-tools;33.0.2" + + yes | "$ANDROID_SDK_ROOT"/cmdline-tools/latest/bin/sdkmanager --sdk_root="$ANDROID_SDK_ROOT" --licenses + yes | "$ANDROID_SDK_ROOT"/cmdline-tools/latest/bin/sdkmanager --sdk_root="$ANDROID_SDK_ROOT" \ + "platform-tools" "platforms;android-33" "build-tools;33.0.2" + echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV" echo "$ANDROID_SDK_ROOT/platform-tools" >> "$GITHUB_PATH" echo "$ANDROID_SDK_ROOT/build-tools/33.0.2" >> "$GITHUB_PATH" @@ -57,46 +74,34 @@ jobs: - name: Flutter dependencies run: flutter pub get - - name: Enable Linux desktop - run: flutter config --enable-linux-desktop - - - name: Determine version - id: meta - run: | - RAW_VERSION=$(awk '/^version:/{print $2}' pubspec.yaml) - BASE_VERSION=${RAW_VERSION%%+*} - echo "base=${BASE_VERSION}" >> "$GITHUB_OUTPUT" - - name: Build APK (release) run: | flutter build apk --release cp build/app/outputs/flutter-apk/app-release.apk app-release.apk - - name: Build Linux binary (release) - run: | - flutter build linux --release - tar -C build/linux/x64/release/bundle -czf app-linux-x64.tar.gz . - - - name: Upload APK artifact + - name: Upload Android APK artifact uses: actions/upload-artifact@v4 with: - name: apk + name: android-apk path: app-release.apk - - name: Upload Linux artifact - uses: actions/upload-artifact@v4 - with: - name: linux - path: app-linux-x64.tar.gz - - windows-build: - if: env.BUILD_WINDOWS == 'true' - runs-on: windows-latest - needs: build + linux-build: + runs-on: ubuntu-latest + needs: meta steps: - name: Checkout uses: actions/checkout@v4 + - name: Install OS deps (Linux desktop) + run: | + if command -v sudo >/dev/null 2>&1; then + SUDO="sudo" + else + SUDO="" + fi + $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 liblzma-dev curl + - name: Setup Flutter uses: subosito/flutter-action@v2 with: @@ -105,48 +110,88 @@ jobs: - name: Flutter dependencies run: flutter pub get + - name: Enable Linux desktop + run: flutter config --enable-linux-desktop + + - name: Build Linux binary (release) + run: | + flutter build linux --release + tar -C build/linux/x64/release/bundle -czf app-linux-x64.tar.gz . + + - name: Upload Linux artifact + uses: actions/upload-artifact@v4 + with: + name: linux-bundle + path: app-linux-x64.tar.gz + + windows-build: + runs-on: windows-latest + needs: meta + # Job always runs; individual steps are gated so release jobs can still depend on it. + steps: + - name: Checkout + if: env.BUILD_WINDOWS == 'true' + uses: actions/checkout@v4 + + - name: Setup Flutter + if: env.BUILD_WINDOWS == 'true' + uses: subosito/flutter-action@v2 + with: + channel: ${{ env.FLUTTER_CHANNEL }} + + - name: Flutter dependencies + if: env.BUILD_WINDOWS == 'true' + run: flutter pub get + - name: Enable Windows desktop + if: env.BUILD_WINDOWS == 'true' run: flutter config --enable-windows-desktop - name: Build Windows binary (release) + if: env.BUILD_WINDOWS == 'true' run: | flutter build windows --release powershell -Command "Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath app-windows-x64.zip" - name: Upload Windows artifact + if: env.BUILD_WINDOWS == 'true' uses: actions/upload-artifact@v4 with: - name: windows + name: windows-zip path: app-windows-x64.zip release-dev: if: github.ref_name == 'dev' runs-on: ubuntu-latest - needs: [build] + needs: + - meta + - android-build + - linux-build + - windows-build steps: - - name: Download APK + - name: Download Android APK uses: actions/download-artifact@v4 with: - name: apk + name: android-apk path: artifacts - name: Download Linux bundle uses: actions/download-artifact@v4 with: - name: linux + name: linux-bundle path: artifacts - name: Download Windows bundle (optional) uses: actions/download-artifact@v4 with: - name: windows + name: windows-zip path: artifacts if-no-files-found: ignore - - name: Prepare artifacts and tag + - name: Prepare artefacts and tag id: bundle run: | - BASE="${{ needs.build.outputs.base_version }}" + BASE="${{ needs.meta.outputs.base_version }}" TAG="v${BASE}-dev" mv artifacts/app-release.apk "artifacts/app-${BASE}-dev.apk" @@ -180,31 +225,35 @@ jobs: release-master: if: github.ref_name == 'master' runs-on: ubuntu-latest - needs: [build] + needs: + - meta + - android-build + - linux-build + - windows-build steps: - - name: Download APK + - name: Download Android APK uses: actions/download-artifact@v4 with: - name: apk + name: android-apk path: artifacts - name: Download Linux bundle uses: actions/download-artifact@v4 with: - name: linux + name: linux-bundle path: artifacts - name: Download Windows bundle (optional) uses: actions/download-artifact@v4 with: - name: windows + name: windows-zip path: artifacts if-no-files-found: ignore - - name: Prepare artifacts and tag + - name: Prepare artefacts and tag id: bundle run: | - BASE="${{ needs.build.outputs.base_version }}" + BASE="${{ needs.meta.outputs.base_version }}" TAG="v${BASE}" mv artifacts/app-release.apk "artifacts/app-${BASE}.apk" diff --git a/lib/components/pages/new_entry.dart b/lib/components/pages/new_entry.dart index b4c8f44..4eb6003 100644 --- a/lib/components/pages/new_entry.dart +++ b/lib/components/pages/new_entry.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:mileograph_flutter/components/calculator/calculator.dart'; @@ -25,21 +27,19 @@ class _NewEntryPageState extends State { final _mileageController = TextEditingController(); final _networkController = TextEditingController(); bool _submitting = false; - bool _initialised = false; bool _useManualMileage = false; RouteResult? _routeResult; final List<_TractionItem> _tractionItems = [_TractionItem.marker()]; int? _selectedTripId; - bool _tripsRequested = false; @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (!_tripsRequested) { - _tripsRequested = true; - context.read().fetchTrips(); - } + Future.microtask(() { + if (!mounted) return; + final data = context.read(); + data.fetchClassList(); + data.fetchTrips(); }); } @@ -140,21 +140,6 @@ class _NewEntryPageState extends State { } } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (!_initialised) { - _initialised = true; - WidgetsBinding.instance.addPostFrameCallback((_) { - context.read().fetchClassList(); - if (!_tripsRequested) { - _tripsRequested = true; - context.read().fetchTrips(); - } - }); - } - } - Future _openCalculator() async { final result = await Navigator.of(context).push( MaterialPageRoute( @@ -304,6 +289,9 @@ class _NewEntryPageState extends State { }; await api.post('/add', body); } + if (mounted) { + context.read().refreshLegs(); + } try { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/components/pages/traction.dart b/lib/components/pages/traction.dart index 9146d4a..441a4ec 100644 --- a/lib/components/pages/traction.dart +++ b/lib/components/pages/traction.dart @@ -150,11 +150,12 @@ class _TractionPageState extends State { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Fleet', - style: Theme.of(context).textTheme.labelMedium), + Text('Fleet', style: Theme.of(context).textTheme.labelMedium), const SizedBox(height: 2), - Text('Traction', - style: Theme.of(context).textTheme.headlineSmall), + Text( + 'Traction', + style: Theme.of(context).textTheme.headlineSmall, + ), ], ), IconButton( @@ -174,8 +175,10 @@ class _TractionPageState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('Filters', - style: Theme.of(context).textTheme.titleMedium), + Text( + 'Filters', + style: Theme.of(context).textTheme.titleMedium, + ), TextButton( onPressed: _clearFilters, child: const Text('Clear'), @@ -199,24 +202,30 @@ class _TractionPageState extends State { (c) => c.toLowerCase().contains(query), ); }, - initialValue: - TextEditingValue(text: _classController.text), - fieldViewBuilder: (context, controller, focusNode, - onFieldSubmitted) { - controller.value = _classController.value; - return TextField( - controller: controller, - focusNode: focusNode, - decoration: const InputDecoration( - labelText: 'Class', - border: OutlineInputBorder(), - ), - onChanged: (val) { - _classController.text = val; + initialValue: TextEditingValue( + text: _classController.text, + ), + fieldViewBuilder: + ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + controller.value = _classController.value; + return TextField( + controller: controller, + focusNode: focusNode, + decoration: const InputDecoration( + labelText: 'Class', + border: OutlineInputBorder(), + ), + onChanged: (val) { + _classController.text = val; + }, + onSubmitted: (_) => _refreshTraction(), + ); }, - onSubmitted: (_) => _refreshTraction(), - ); - }, onSelected: (String selection) { setState(() { _selectedClass = selection; @@ -249,7 +258,7 @@ class _TractionPageState extends State { ), ), FilterChip( - label: const Text('Had only'), + label: const Text('Had first'), selected: _hadOnly, onSelected: (v) { setState(() => _hadOnly = v); @@ -258,13 +267,18 @@ class _TractionPageState extends State { ), TextButton.icon( onPressed: () => setState( - () => _showAdvancedFilters = !_showAdvancedFilters), - icon: Icon(_showAdvancedFilters - ? Icons.expand_less - : Icons.expand_more), - label: Text(_showAdvancedFilters - ? 'Hide filters' - : 'More filters'), + () => _showAdvancedFilters = !_showAdvancedFilters, + ), + icon: Icon( + _showAdvancedFilters + ? Icons.expand_less + : Icons.expand_more, + ), + label: Text( + _showAdvancedFilters + ? 'Hide filters' + : 'More filters', + ), ), ElevatedButton.icon( onPressed: _refreshTraction, @@ -398,10 +412,9 @@ class _TractionPageState extends State { children: [ Text( 'No traction found', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), ), const SizedBox(height: 8), const Text('Try relaxing the filters or sync again.'), @@ -449,8 +462,10 @@ class _TractionPageState extends State { icon: const Icon(Icons.arrow_back), label: const Text('Back'), style: TextButton.styleFrom( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), foregroundColor: Theme.of(context).colorScheme.onSurface, ), ), @@ -484,25 +499,24 @@ class _TractionPageState extends State { children: [ Text( '${loco.locoClass} ${loco.number}', - style: Theme.of(context) - .textTheme - .titleMedium - ?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), ), if ((loco.name ?? '').isNotEmpty) Text( loco.name ?? '', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(fontStyle: FontStyle.italic), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontStyle: FontStyle.italic, + ), ), ], ), Chip( label: Text(status), - backgroundColor: - Theme.of(context).colorScheme.surfaceContainerHighest, + backgroundColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, ), ], ), @@ -529,9 +543,11 @@ class _TractionPageState extends State { } }); }, - icon: Icon(isSelected - ? Icons.remove_circle_outline - : Icons.add_circle_outline), + icon: Icon( + isSelected + ? Icons.remove_circle_outline + : Icons.add_circle_outline, + ), label: Text(isSelected ? 'Remove' : 'Add to entry'), ), ], @@ -551,17 +567,9 @@ class _TractionPageState extends State { value: (loco.trips ?? loco.journeys ?? 0).toString(), ), if (operatorName.isNotEmpty) - _statPill( - context, - label: 'Operator', - value: operatorName, - ), + _statPill(context, label: 'Operator', value: operatorName), if (domain.isNotEmpty) - _statPill( - context, - label: 'Domain', - value: domain, - ), + _statPill(context, label: 'Domain', value: domain), ], ), ], @@ -570,8 +578,11 @@ class _TractionPageState extends State { ); } - Widget _statPill(BuildContext context, - {required String label, required String value}) { + Widget _statPill( + BuildContext context, { + required String label, + required String value, + }) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( @@ -581,16 +592,12 @@ class _TractionPageState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text( - '$label: ', - style: Theme.of(context).textTheme.labelSmall, - ), + Text('$label: ', style: Theme.of(context).textTheme.labelSmall), Text( value, - style: Theme.of(context) - .textTheme - .labelSmall - ?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700), ), ], ), @@ -621,10 +628,9 @@ class _TractionPageState extends State { const SizedBox(width: 8), Text( '${loco.locoClass} ${loco.number}', - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith(fontWeight: FontWeight.w700), + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), ), ], ), @@ -647,10 +653,11 @@ class _TractionPageState extends State { _detailRow('Owner', loco.owner ?? ''), _detailRow('Livery', loco.livery ?? ''), _detailRow('Location', loco.location ?? ''), + _detailRow('Mileage', _formatNumber(loco.mileage ?? 0)), _detailRow( - 'Mileage', _formatNumber(loco.mileage ?? 0)), - _detailRow('Trips', - (loco.trips ?? loco.journeys ?? 0).toString()), + 'Trips', + (loco.trips ?? loco.journeys ?? 0).toString(), + ), _detailRow('EVN', loco.evn ?? ''), if (loco.notes != null && loco.notes!.isNotEmpty) _detailRow('Notes', loco.notes!), @@ -676,17 +683,13 @@ class _TractionPageState extends State { width: 110, child: Text( label, - style: Theme.of(context) - .textTheme - .labelMedium - ?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of( + context, + ).textTheme.labelMedium?.copyWith(fontWeight: FontWeight.w600), ), ), Expanded( - child: Text( - value, - style: Theme.of(context).textTheme.bodyMedium, - ), + child: Text(value, style: Theme.of(context).textTheme.bodyMedium), ), ], ), diff --git a/lib/services/dataService.dart b/lib/services/dataService.dart index b314413..31724ec 100644 --- a/lib/services/dataService.dart +++ b/lib/services/dataService.dart @@ -5,11 +5,29 @@ import 'package:flutter/scheduler.dart'; import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/services/apiService.dart'; // assumes you've moved HomepageStats + submodels to a separate file +class _LegFetchOptions { + final int limit; + final String sortBy; + final int sortDirection; + final String? dateRangeStart; + final String? dateRangeEnd; + + const _LegFetchOptions({ + this.limit = 100, + this.sortBy = 'date', + this.sortDirection = 0, + this.dateRangeStart, + this.dateRangeEnd, + }); +} + class DataService extends ChangeNotifier { final ApiService api; DataService({required this.api}); + _LegFetchOptions _lastLegsFetch = const _LegFetchOptions(); + // Homepage Data HomepageStats? _homepageStats; HomepageStats? get homepageStats => _homepageStats; @@ -89,6 +107,15 @@ class DataService extends ChangeNotifier { bool append = false, }) async { _isLegsLoading = true; + if (!append) { + _lastLegsFetch = _LegFetchOptions( + limit: limit, + sortBy: sortBy, + sortDirection: sortDirection, + dateRangeStart: dateRangeStart, + dateRangeEnd: dateRangeEnd, + ); + } final buffer = StringBuffer( '?sort_direction=$sortDirection&sort_by=$sortBy&offset=$offset&limit=$limit'); if (dateRangeStart != null && dateRangeStart.isNotEmpty) { @@ -117,6 +144,16 @@ class DataService extends ChangeNotifier { } } + Future refreshLegs() { + return fetchLegs( + limit: _lastLegsFetch.limit, + sortBy: _lastLegsFetch.sortBy, + sortDirection: _lastLegsFetch.sortDirection, + dateRangeStart: _lastLegsFetch.dateRangeStart, + dateRangeEnd: _lastLegsFetch.dateRangeEnd, + ); + } + Future fetchHadTraction({int offset = 0, int limit = 100}) async { await fetchTraction( hadOnly: true,