diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index f768ac4..c5476cd 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -52,6 +52,14 @@ jobs: distribution: temurin java-version: ${{ env.JAVA_VERSION }} + - name: Cache Android SDK + uses: actions/cache@v3 + with: + path: ${{ env.ANDROID_SDK_ROOT }} + key: android-sdk-${{ runner.os }}-java${{ env.JAVA_VERSION }}-platform33-buildtools33.0.2 + restore-keys: | + android-sdk-${{ runner.os }}-java${{ env.JAVA_VERSION }}- + - name: Install Android SDK run: | mkdir -p "$ANDROID_SDK_ROOT"/cmdline-tools @@ -74,10 +82,29 @@ jobs: uses: subosito/flutter-action@v2 with: channel: ${{ env.FLUTTER_CHANNEL }} + cache: true - name: Allow all git directories (CI) run: git config --global --add safe.directory '*' + - name: Cache pub packages + uses: actions/cache@v3 + with: + path: ~/.pub-cache + key: flutter-pub-${{ runner.os }}-${{ hashFiles('pubspec.lock') }} + restore-keys: | + flutter-pub-${{ runner.os }}- + + - name: Cache Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('android/**/*.gradle*', 'android/**/*.properties') }} + restore-keys: | + gradle-${{ runner.os }}- + - name: Flutter dependencies run: flutter pub get @@ -160,10 +187,19 @@ jobs: uses: subosito/flutter-action@v2 with: channel: ${{ env.FLUTTER_CHANNEL }} + cache: true - name: Allow all git directories (CI) run: git config --global --add safe.directory '*' + - name: Cache pub packages + uses: actions/cache@v3 + with: + path: ~/.pub-cache + key: flutter-pub-${{ runner.os }}-${{ hashFiles('pubspec.lock') }} + restore-keys: | + flutter-pub-${{ runner.os }}- + - name: Flutter dependencies run: flutter pub get diff --git a/lib/components/pages/dashboard.dart b/lib/components/pages/dashboard.dart index e80d2d9..c946a52 100644 --- a/lib/components/pages/dashboard.dart +++ b/lib/components/pages/dashboard.dart @@ -76,8 +76,9 @@ class _DashboardState extends State { if (isInitialLoading) Positioned.fill( child: Container( - color: - Theme.of(context).colorScheme.surface.withOpacity(0.7), + color: Theme.of( + context, + ).colorScheme.surface.withOpacity(0.7), child: const Center( child: Column( mainAxisSize: MainAxisSize.min, @@ -183,7 +184,8 @@ class _DashboardState extends State { _buildCard( context, title: 'On this day', - action: data.onThisDay + action: + data.onThisDay .where((leg) => leg.beginTime.year != DateTime.now().year) .length > 5 @@ -328,10 +330,10 @@ class _DashboardState extends State { } Widget _buildTripsCard(BuildContext context, DataService data) { - final trips_unsorted = data.trips; + final tripsUnsorted = data.trips; List trips = []; - if (trips_unsorted.isNotEmpty) { - trips = [...trips_unsorted]..sort((a, b) => b.tripId.compareTo(a.tripId)); + if (tripsUnsorted.isNotEmpty) { + trips = [...tripsUnsorted]..sort((a, b) => b.tripId.compareTo(a.tripId)); } return _buildCard( context, diff --git a/lib/components/pages/new_entry.dart b/lib/components/pages/new_entry.dart index bf24499..b8e6cb5 100644 --- a/lib/components/pages/new_entry.dart +++ b/lib/components/pages/new_entry.dart @@ -86,8 +86,8 @@ class _NewEntryPageState extends State { final tripIds = sorted.map((t) => t.tripId).toSet(); final selectedValue = (_selectedTripId != null && tripIds.contains(_selectedTripId)) - ? _selectedTripId - : null; + ? _selectedTripId + : null; return Row( children: [ Expanded( @@ -100,8 +100,10 @@ class _NewEntryPageState extends State { items: [ const DropdownMenuItem(value: null, child: Text('No trip')), ...sorted.map( - (t) => - DropdownMenuItem(value: t.tripId, child: Text(t.tripName)), + (t) => DropdownMenuItem( + value: t.tripId, + child: Text(t.tripName), + ), ), ], onChanged: (val) { @@ -304,7 +306,7 @@ class _NewEntryPageState extends State { if (_useManualMileage) { final body = { - "leg_trip": _selectedTripId ?? null, + "leg_trip": _selectedTripId, "leg_start": startVal, "leg_end": endVal, "leg_begin_time": _legDateTime.toIso8601String(), @@ -318,7 +320,7 @@ class _NewEntryPageState extends State { await api.post('/add/manual', body); } else { final body = { - "leg_trip": _selectedTripId ?? null, + "leg_trip": _selectedTripId, "leg_begin_time": _legDateTime.toIso8601String(), "leg_route": routeStations, "leg_notes": _notesController.text.trim(), @@ -429,14 +431,15 @@ class _NewEntryPageState extends State { _useManualMileage = data['useManualMileage'] ?? _useManualMileage; _selectedTripId = data['selectedTripId']; if (data['routeResult'] is Map) { - _routeResult = - RouteResult.fromJson(Map.from(data['routeResult'])); + _routeResult = RouteResult.fromJson( + Map.from(data['routeResult']), + ); _mileageController.text = _routeResult!.distance.toStringAsFixed(2); } if (data['tractionItems'] is List) { - _restoreTractionItems(List>.from( - data['tractionItems'].cast(), - )); + _restoreTractionItems( + List>.from(data['tractionItems'].cast()), + ); } }); _startController.text = data['start'] ?? ''; @@ -609,34 +612,34 @@ class _NewEntryPageState extends State { final mileagePanel = _section( 'Mileage', [ - if (_useManualMileage) - TextFormField( - controller: _mileageController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, + if (_useManualMileage) + TextFormField( + controller: _mileageController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + decoration: const InputDecoration( + labelText: 'Mileage (mi)', + border: OutlineInputBorder(), + ), + ) + else if (_routeResult != null) + ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Calculated mileage'), + subtitle: Text( + '${_routeResult!.distance.toStringAsFixed(2)} mi', + ), ), - decoration: const InputDecoration( - labelText: 'Mileage (mi)', - border: OutlineInputBorder(), + if (!_useManualMileage) + Align( + alignment: Alignment.centerLeft, + child: ElevatedButton.icon( + onPressed: _openCalculator, + icon: const Icon(Icons.calculate), + label: const Text('Open mileage calculator'), + ), ), - ) - else if (_routeResult != null) - ListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Calculated mileage'), - 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'), - ), - ), ], trailing: FilterChip( label: Text(_useManualMileage ? 'Manual' : 'Automatic'), @@ -793,9 +796,9 @@ class _NewEntryPageState extends State { children: [ Text( title, - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), ), if (trailing != null) trailing, ], diff --git a/lib/components/pages/traction.dart b/lib/components/pages/traction.dart index 79fe6e9..1a87c38 100644 --- a/lib/components/pages/traction.dart +++ b/lib/components/pages/traction.dart @@ -68,17 +68,20 @@ class _TractionPageState extends State { } bool get _hasFilters { - final dynamicFieldsUsed = _dynamicControllers.values - .any((controller) => controller.text.trim().isNotEmpty) || - _enumSelections.values - .any((value) => (value ?? '').toString().trim().isNotEmpty); + final dynamicFieldsUsed = + _dynamicControllers.values.any( + (controller) => controller.text.trim().isNotEmpty, + ) || + _enumSelections.values.any( + (value) => (value ?? '').toString().trim().isNotEmpty, + ); return [ - _selectedClass, - _classController.text, - _numberController.text, - _nameController.text, - ].any((value) => (value ?? '').toString().trim().isNotEmpty) || + _selectedClass, + _classController.text, + _numberController.text, + _nameController.text, + ].any((value) => (value ?? '').toString().trim().isNotEmpty) || dynamicFieldsUsed; } @@ -109,7 +112,11 @@ class _TractionPageState extends State { } void _clearFilters() { - for (final controller in [_classController, _numberController, _nameController]) { + for (final controller in [ + _classController, + _numberController, + _nameController, + ]) { controller.clear(); } for (final controller in _dynamicControllers.values) { @@ -135,9 +142,13 @@ class _TractionPageState extends State { List _activeEventFields(List fields) { return fields .where( - (field) => - !['class', 'number', 'name', 'build date', 'build_date'] - .contains(field.name.toLowerCase()), + (field) => ![ + 'class', + 'number', + 'name', + 'build date', + 'build_date', + ].contains(field.name.toLowerCase()), ) .toList(); } @@ -147,7 +158,10 @@ class _TractionPageState extends State { if (field.enumValues != null) { _enumSelections.putIfAbsent(field.name, () => null); } else { - _dynamicControllers.putIfAbsent(field.name, () => TextEditingController()); + _dynamicControllers.putIfAbsent( + field.name, + () => TextEditingController(), + ); } } } @@ -228,17 +242,22 @@ class _TractionPageState extends State { ); }, fieldViewBuilder: - (context, controller, focusNode, onFieldSubmitted) { - return TextField( - controller: controller, - focusNode: focusNode, - decoration: const InputDecoration( - labelText: 'Class', - border: OutlineInputBorder(), - ), - onSubmitted: (_) => _refreshTraction(), - ); - }, + ( + context, + controller, + focusNode, + onFieldSubmitted, + ) { + return TextField( + controller: controller, + focusNode: focusNode, + decoration: const InputDecoration( + labelText: 'Class', + border: OutlineInputBorder(), + ), + onSubmitted: (_) => _refreshTraction(), + ); + }, optionsViewBuilder: (context, onSelected, options) { final optionList = options.toList(); if (optionList.isEmpty) { @@ -323,7 +342,9 @@ class _TractionPageState extends State { : Icons.expand_more, ), label: Text( - _showAdvancedFilters ? 'Hide filters' : 'More filters', + _showAdvancedFilters + ? 'Hide filters' + : 'More filters', ), ), ElevatedButton.icon( @@ -344,24 +365,26 @@ class _TractionPageState extends State { ? const Center( child: Padding( padding: EdgeInsets.all(8.0), - child: CircularProgressIndicator(strokeWidth: 2), + child: CircularProgressIndicator( + strokeWidth: 2, + ), ), ) : extraFields.isEmpty - ? const Text('No extra filters available right now.') - : Wrap( - spacing: 12, - runSpacing: 12, - children: extraFields - .map( - (field) => _buildFilterInput( - context, - field, - isMobile, - ), - ) - .toList(), - ), + ? const Text('No extra filters available right now.') + : Wrap( + spacing: 12, + runSpacing: 12, + children: extraFields + .map( + (field) => _buildFilterInput( + context, + field, + isMobile, + ), + ) + .toList(), + ), ), secondChild: const SizedBox.shrink(), ), @@ -404,8 +427,9 @@ class _TractionPageState extends State { Padding( padding: const EdgeInsets.only(top: 8.0), child: OutlinedButton.icon( - onPressed: - data.isTractionLoading ? null : () => _refreshTraction(append: true), + onPressed: data.isTractionLoading + ? null + : () => _refreshTraction(append: true), icon: data.isTractionLoading ? const SizedBox( height: 14, @@ -580,7 +604,11 @@ class _TractionPageState extends State { (Color, Color) _statusChipColors(BuildContext context, String status) { final scheme = Theme.of(context).colorScheme; final isDark = scheme.brightness == Brightness.dark; - Color blend(Color base, {double bgOpacity = 0.18, double fgOpacity = 0.82}) { + Color blend( + Color base, { + double bgOpacity = 0.18, + double fgOpacity = 0.82, + }) { final bg = Color.alphaBlend( base.withOpacity(isDark ? bgOpacity + 0.07 : bgOpacity), scheme.surface, @@ -719,7 +747,10 @@ class _TractionPageState extends State { ) { final width = isMobile ? double.infinity : 220.0; if (field.enumValues != null && field.enumValues!.isNotEmpty) { - final options = field.enumValues!.map((e) => e.toString()).toSet().toList(); + final options = field.enumValues! + .map((e) => e.toString()) + .toSet() + .toList(); final currentValue = _enumSelections[field.name]; final safeValue = options.contains(currentValue) ? currentValue : null; return SizedBox( @@ -732,14 +763,9 @@ class _TractionPageState extends State { ), items: [ const DropdownMenuItem(value: null, child: Text('Any')), - ...options - .map( - (value) => DropdownMenuItem( - value: value, - child: Text(value), - ), - ) - .toList(), + ...options.map( + (value) => DropdownMenuItem(value: value, child: Text(value)), + ), ], onChanged: (val) { setState(() { @@ -757,7 +783,9 @@ class _TractionPageState extends State { TextInputType? inputType; if (field.type != null) { final type = field.type!.toLowerCase(); - if (type.contains('int') || type.contains('num') || type.contains('double')) { + if (type.contains('int') || + type.contains('num') || + type.contains('double')) { inputType = const TextInputType.numberWithOptions(decimal: true); } } diff --git a/lib/services/dataService.dart b/lib/services/dataService.dart index cc5fc8e..0dfefe5 100644 --- a/lib/services/dataService.dart +++ b/lib/services/dataService.dart @@ -261,8 +261,8 @@ class DataService extends ChangeNotifier { try { final json = await api.get('/trips/legs-and-stats'); if (json is List) { - final trip_map = json.map((e) => TripDetail.fromJson(e)).toList(); - _tripDetails = [...trip_map]..sort((a, b) => b.id.compareTo(a.id)); + final tripMap = json.map((e) => TripDetail.fromJson(e)).toList(); + _tripDetails = [...tripMap]..sort((a, b) => b.id.compareTo(a.id)); } else { _tripDetails = []; } @@ -360,12 +360,12 @@ class DataService extends ChangeNotifier { } } if (raw != null) { - final trip_map = raw + final tripMap = raw .whereType>() .map((e) => TripSummary.fromJson(e)) .toList(); - _tripList = [...trip_map]..sort((a, b) => b.tripId.compareTo(a.tripId)); + _tripList = [...tripMap]..sort((a, b) => b.tripId.compareTo(a.tripId)); } else { debugPrint('Unexpected trip list response: $json'); _tripList = [];