flutter fixes and pipeline speedup
Some checks failed
Release / meta (push) Successful in 20s
Release / linux-build (push) Successful in 36m32s
Release / release-dev (push) Has been cancelled
Release / release-master (push) Has been cancelled
Release / android-build (push) Has been cancelled

This commit is contained in:
2025-12-14 11:20:39 +00:00
parent eb01cf0e8e
commit a2b38a7aec
5 changed files with 173 additions and 104 deletions

View File

@@ -52,6 +52,14 @@ jobs:
distribution: temurin distribution: temurin
java-version: ${{ env.JAVA_VERSION }} 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 - name: Install Android SDK
run: | run: |
mkdir -p "$ANDROID_SDK_ROOT"/cmdline-tools mkdir -p "$ANDROID_SDK_ROOT"/cmdline-tools
@@ -74,10 +82,29 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: ${{ env.FLUTTER_CHANNEL }} channel: ${{ env.FLUTTER_CHANNEL }}
cache: true
- name: Allow all git directories (CI) - name: Allow all git directories (CI)
run: git config --global --add safe.directory '*' 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 - name: Flutter dependencies
run: flutter pub get run: flutter pub get
@@ -160,10 +187,19 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: ${{ env.FLUTTER_CHANNEL }} channel: ${{ env.FLUTTER_CHANNEL }}
cache: true
- name: Allow all git directories (CI) - name: Allow all git directories (CI)
run: git config --global --add safe.directory '*' 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 - name: Flutter dependencies
run: flutter pub get run: flutter pub get

View File

@@ -76,8 +76,9 @@ class _DashboardState extends State<Dashboard> {
if (isInitialLoading) if (isInitialLoading)
Positioned.fill( Positioned.fill(
child: Container( child: Container(
color: color: Theme.of(
Theme.of(context).colorScheme.surface.withOpacity(0.7), context,
).colorScheme.surface.withOpacity(0.7),
child: const Center( child: const Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -183,7 +184,8 @@ class _DashboardState extends State<Dashboard> {
_buildCard( _buildCard(
context, context,
title: 'On this day', title: 'On this day',
action: data.onThisDay action:
data.onThisDay
.where((leg) => leg.beginTime.year != DateTime.now().year) .where((leg) => leg.beginTime.year != DateTime.now().year)
.length > .length >
5 5
@@ -328,10 +330,10 @@ class _DashboardState extends State<Dashboard> {
} }
Widget _buildTripsCard(BuildContext context, DataService data) { Widget _buildTripsCard(BuildContext context, DataService data) {
final trips_unsorted = data.trips; final tripsUnsorted = data.trips;
List trips = []; List trips = [];
if (trips_unsorted.isNotEmpty) { if (tripsUnsorted.isNotEmpty) {
trips = [...trips_unsorted]..sort((a, b) => b.tripId.compareTo(a.tripId)); trips = [...tripsUnsorted]..sort((a, b) => b.tripId.compareTo(a.tripId));
} }
return _buildCard( return _buildCard(
context, context,

View File

@@ -86,8 +86,8 @@ class _NewEntryPageState extends State<NewEntryPage> {
final tripIds = sorted.map((t) => t.tripId).toSet(); final tripIds = sorted.map((t) => t.tripId).toSet();
final selectedValue = final selectedValue =
(_selectedTripId != null && tripIds.contains(_selectedTripId)) (_selectedTripId != null && tripIds.contains(_selectedTripId))
? _selectedTripId ? _selectedTripId
: null; : null;
return Row( return Row(
children: [ children: [
Expanded( Expanded(
@@ -100,8 +100,10 @@ class _NewEntryPageState extends State<NewEntryPage> {
items: [ items: [
const DropdownMenuItem(value: null, child: Text('No trip')), const DropdownMenuItem(value: null, child: Text('No trip')),
...sorted.map( ...sorted.map(
(t) => (t) => DropdownMenuItem<int?>(
DropdownMenuItem<int?>(value: t.tripId, child: Text(t.tripName)), value: t.tripId,
child: Text(t.tripName),
),
), ),
], ],
onChanged: (val) { onChanged: (val) {
@@ -304,7 +306,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
if (_useManualMileage) { if (_useManualMileage) {
final body = { final body = {
"leg_trip": _selectedTripId ?? null, "leg_trip": _selectedTripId,
"leg_start": startVal, "leg_start": startVal,
"leg_end": endVal, "leg_end": endVal,
"leg_begin_time": _legDateTime.toIso8601String(), "leg_begin_time": _legDateTime.toIso8601String(),
@@ -318,7 +320,7 @@ class _NewEntryPageState extends State<NewEntryPage> {
await api.post('/add/manual', body); await api.post('/add/manual', body);
} else { } else {
final body = { final body = {
"leg_trip": _selectedTripId ?? null, "leg_trip": _selectedTripId,
"leg_begin_time": _legDateTime.toIso8601String(), "leg_begin_time": _legDateTime.toIso8601String(),
"leg_route": routeStations, "leg_route": routeStations,
"leg_notes": _notesController.text.trim(), "leg_notes": _notesController.text.trim(),
@@ -429,14 +431,15 @@ class _NewEntryPageState extends State<NewEntryPage> {
_useManualMileage = data['useManualMileage'] ?? _useManualMileage; _useManualMileage = data['useManualMileage'] ?? _useManualMileage;
_selectedTripId = data['selectedTripId']; _selectedTripId = data['selectedTripId'];
if (data['routeResult'] is Map<String, dynamic>) { if (data['routeResult'] is Map<String, dynamic>) {
_routeResult = _routeResult = RouteResult.fromJson(
RouteResult.fromJson(Map<String, dynamic>.from(data['routeResult'])); Map<String, dynamic>.from(data['routeResult']),
);
_mileageController.text = _routeResult!.distance.toStringAsFixed(2); _mileageController.text = _routeResult!.distance.toStringAsFixed(2);
} }
if (data['tractionItems'] is List) { if (data['tractionItems'] is List) {
_restoreTractionItems(List<Map<String, dynamic>>.from( _restoreTractionItems(
data['tractionItems'].cast<Map>(), List<Map<String, dynamic>>.from(data['tractionItems'].cast<Map>()),
)); );
} }
}); });
_startController.text = data['start'] ?? ''; _startController.text = data['start'] ?? '';
@@ -609,34 +612,34 @@ class _NewEntryPageState extends State<NewEntryPage> {
final mileagePanel = _section( final mileagePanel = _section(
'Mileage', 'Mileage',
[ [
if (_useManualMileage) if (_useManualMileage)
TextFormField( TextFormField(
controller: _mileageController, controller: _mileageController,
keyboardType: const TextInputType.numberWithOptions( keyboardType: const TextInputType.numberWithOptions(
decimal: true, 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( if (!_useManualMileage)
labelText: 'Mileage (mi)', Align(
border: OutlineInputBorder(), 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( trailing: FilterChip(
label: Text(_useManualMileage ? 'Manual' : 'Automatic'), label: Text(_useManualMileage ? 'Manual' : 'Automatic'),
@@ -793,9 +796,9 @@ class _NewEntryPageState extends State<NewEntryPage> {
children: [ children: [
Text( Text(
title, title,
style: Theme.of( style: Theme.of(context).textTheme.titleMedium?.copyWith(
context, fontWeight: FontWeight.bold,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ),
), ),
if (trailing != null) trailing, if (trailing != null) trailing,
], ],

View File

@@ -68,17 +68,20 @@ class _TractionPageState extends State<TractionPage> {
} }
bool get _hasFilters { bool get _hasFilters {
final dynamicFieldsUsed = _dynamicControllers.values final dynamicFieldsUsed =
.any((controller) => controller.text.trim().isNotEmpty) || _dynamicControllers.values.any(
_enumSelections.values (controller) => controller.text.trim().isNotEmpty,
.any((value) => (value ?? '').toString().trim().isNotEmpty); ) ||
_enumSelections.values.any(
(value) => (value ?? '').toString().trim().isNotEmpty,
);
return [ return [
_selectedClass, _selectedClass,
_classController.text, _classController.text,
_numberController.text, _numberController.text,
_nameController.text, _nameController.text,
].any((value) => (value ?? '').toString().trim().isNotEmpty) || ].any((value) => (value ?? '').toString().trim().isNotEmpty) ||
dynamicFieldsUsed; dynamicFieldsUsed;
} }
@@ -109,7 +112,11 @@ class _TractionPageState extends State<TractionPage> {
} }
void _clearFilters() { void _clearFilters() {
for (final controller in [_classController, _numberController, _nameController]) { for (final controller in [
_classController,
_numberController,
_nameController,
]) {
controller.clear(); controller.clear();
} }
for (final controller in _dynamicControllers.values) { for (final controller in _dynamicControllers.values) {
@@ -135,9 +142,13 @@ class _TractionPageState extends State<TractionPage> {
List<EventField> _activeEventFields(List<EventField> fields) { List<EventField> _activeEventFields(List<EventField> fields) {
return fields return fields
.where( .where(
(field) => (field) => ![
!['class', 'number', 'name', 'build date', 'build_date'] 'class',
.contains(field.name.toLowerCase()), 'number',
'name',
'build date',
'build_date',
].contains(field.name.toLowerCase()),
) )
.toList(); .toList();
} }
@@ -147,7 +158,10 @@ class _TractionPageState extends State<TractionPage> {
if (field.enumValues != null) { if (field.enumValues != null) {
_enumSelections.putIfAbsent(field.name, () => null); _enumSelections.putIfAbsent(field.name, () => null);
} else { } else {
_dynamicControllers.putIfAbsent(field.name, () => TextEditingController()); _dynamicControllers.putIfAbsent(
field.name,
() => TextEditingController(),
);
} }
} }
} }
@@ -228,17 +242,22 @@ class _TractionPageState extends State<TractionPage> {
); );
}, },
fieldViewBuilder: fieldViewBuilder:
(context, controller, focusNode, onFieldSubmitted) { (
return TextField( context,
controller: controller, controller,
focusNode: focusNode, focusNode,
decoration: const InputDecoration( onFieldSubmitted,
labelText: 'Class', ) {
border: OutlineInputBorder(), return TextField(
), controller: controller,
onSubmitted: (_) => _refreshTraction(), focusNode: focusNode,
); decoration: const InputDecoration(
}, labelText: 'Class',
border: OutlineInputBorder(),
),
onSubmitted: (_) => _refreshTraction(),
);
},
optionsViewBuilder: (context, onSelected, options) { optionsViewBuilder: (context, onSelected, options) {
final optionList = options.toList(); final optionList = options.toList();
if (optionList.isEmpty) { if (optionList.isEmpty) {
@@ -323,7 +342,9 @@ class _TractionPageState extends State<TractionPage> {
: Icons.expand_more, : Icons.expand_more,
), ),
label: Text( label: Text(
_showAdvancedFilters ? 'Hide filters' : 'More filters', _showAdvancedFilters
? 'Hide filters'
: 'More filters',
), ),
), ),
ElevatedButton.icon( ElevatedButton.icon(
@@ -344,24 +365,26 @@ class _TractionPageState extends State<TractionPage> {
? const Center( ? const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(8.0), padding: EdgeInsets.all(8.0),
child: CircularProgressIndicator(strokeWidth: 2), child: CircularProgressIndicator(
strokeWidth: 2,
),
), ),
) )
: extraFields.isEmpty : extraFields.isEmpty
? const Text('No extra filters available right now.') ? const Text('No extra filters available right now.')
: Wrap( : Wrap(
spacing: 12, spacing: 12,
runSpacing: 12, runSpacing: 12,
children: extraFields children: extraFields
.map( .map(
(field) => _buildFilterInput( (field) => _buildFilterInput(
context, context,
field, field,
isMobile, isMobile,
), ),
) )
.toList(), .toList(),
), ),
), ),
secondChild: const SizedBox.shrink(), secondChild: const SizedBox.shrink(),
), ),
@@ -404,8 +427,9 @@ class _TractionPageState extends State<TractionPage> {
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: onPressed: data.isTractionLoading
data.isTractionLoading ? null : () => _refreshTraction(append: true), ? null
: () => _refreshTraction(append: true),
icon: data.isTractionLoading icon: data.isTractionLoading
? const SizedBox( ? const SizedBox(
height: 14, height: 14,
@@ -580,7 +604,11 @@ class _TractionPageState extends State<TractionPage> {
(Color, Color) _statusChipColors(BuildContext context, String status) { (Color, Color) _statusChipColors(BuildContext context, String status) {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final isDark = scheme.brightness == Brightness.dark; 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( final bg = Color.alphaBlend(
base.withOpacity(isDark ? bgOpacity + 0.07 : bgOpacity), base.withOpacity(isDark ? bgOpacity + 0.07 : bgOpacity),
scheme.surface, scheme.surface,
@@ -719,7 +747,10 @@ class _TractionPageState extends State<TractionPage> {
) { ) {
final width = isMobile ? double.infinity : 220.0; final width = isMobile ? double.infinity : 220.0;
if (field.enumValues != null && field.enumValues!.isNotEmpty) { 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 currentValue = _enumSelections[field.name];
final safeValue = options.contains(currentValue) ? currentValue : null; final safeValue = options.contains(currentValue) ? currentValue : null;
return SizedBox( return SizedBox(
@@ -732,14 +763,9 @@ class _TractionPageState extends State<TractionPage> {
), ),
items: [ items: [
const DropdownMenuItem(value: null, child: Text('Any')), const DropdownMenuItem(value: null, child: Text('Any')),
...options ...options.map(
.map( (value) => DropdownMenuItem(value: value, child: Text(value)),
(value) => DropdownMenuItem( ),
value: value,
child: Text(value),
),
)
.toList(),
], ],
onChanged: (val) { onChanged: (val) {
setState(() { setState(() {
@@ -757,7 +783,9 @@ class _TractionPageState extends State<TractionPage> {
TextInputType? inputType; TextInputType? inputType;
if (field.type != null) { if (field.type != null) {
final type = field.type!.toLowerCase(); 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); inputType = const TextInputType.numberWithOptions(decimal: true);
} }
} }

View File

@@ -261,8 +261,8 @@ class DataService extends ChangeNotifier {
try { try {
final json = await api.get('/trips/legs-and-stats'); final json = await api.get('/trips/legs-and-stats');
if (json is List) { if (json is List) {
final trip_map = json.map((e) => TripDetail.fromJson(e)).toList(); final tripMap = json.map((e) => TripDetail.fromJson(e)).toList();
_tripDetails = [...trip_map]..sort((a, b) => b.id.compareTo(a.id)); _tripDetails = [...tripMap]..sort((a, b) => b.id.compareTo(a.id));
} else { } else {
_tripDetails = []; _tripDetails = [];
} }
@@ -360,12 +360,12 @@ class DataService extends ChangeNotifier {
} }
} }
if (raw != null) { if (raw != null) {
final trip_map = raw final tripMap = raw
.whereType<Map<String, dynamic>>() .whereType<Map<String, dynamic>>()
.map((e) => TripSummary.fromJson(e)) .map((e) => TripSummary.fromJson(e))
.toList(); .toList();
_tripList = [...trip_map]..sort((a, b) => b.tripId.compareTo(a.tripId)); _tripList = [...tripMap]..sort((a, b) => b.tripId.compareTo(a.tripId));
} else { } else {
debugPrint('Unexpected trip list response: $json'); debugPrint('Unexpected trip list response: $json');
_tripList = []; _tripList = [];