Fix/garage layout batch4 (#86)
* fix(garage): improve landscape layout (#62) * fix(garage): make service dialog responsive (#63) * fix(garage): truncate service type dropdown (#64) * fix(garage): avoid compact landscape card overflow (#62) --------- Co-authored-by: Developer <dev@example.com>
This commit is contained in:
parent
3f64083bdd
commit
74943a8420
|
|
@ -112,14 +112,17 @@ class GarageScreen extends ConsumerWidget {
|
||||||
if (isMultiColumn) {
|
if (isMultiColumn) {
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: 550,
|
maxCrossAxisExtent: responsive.isShortViewport ? 600 : 550,
|
||||||
mainAxisExtent: 420,
|
mainAxisExtent: responsive.isShortViewport ? 300 : 420,
|
||||||
crossAxisSpacing: 16,
|
crossAxisSpacing: 16,
|
||||||
mainAxisSpacing: 16,
|
mainAxisSpacing: 16,
|
||||||
),
|
),
|
||||||
itemCount: bikes.length,
|
itemCount: bikes.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
if (index == bikes.length) {
|
||||||
|
return _buildAddMotorcycleCard(context, ref, bikes);
|
||||||
|
}
|
||||||
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: true);
|
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -127,8 +130,11 @@ class GarageScreen extends ConsumerWidget {
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
itemCount: bikes.length,
|
itemCount: bikes.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
if (index == bikes.length) {
|
||||||
|
return _buildAddMotorcycleCard(context, ref, bikes);
|
||||||
|
}
|
||||||
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: false);
|
return _buildMotorcycleCard(context, ref, bikes[index], colors, isGrid: false);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -136,19 +142,6 @@ class GarageScreen extends ConsumerWidget {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
floatingActionButton: bikesAsync.value != null && bikesAsync.value!.isNotEmpty
|
|
||||||
? FloatingActionButton.extended(
|
|
||||||
key: const ValueKey('garage_add_motorcycle_fab'),
|
|
||||||
backgroundColor: colors.cyberCyan,
|
|
||||||
foregroundColor: Colors.black,
|
|
||||||
icon: const Icon(Icons.two_wheeler),
|
|
||||||
label: const Text(
|
|
||||||
'Add Motorcycle',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.w600, letterSpacing: 0.2),
|
|
||||||
),
|
|
||||||
onPressed: () => _onAddMotorcycle(context, ref, bikesAsync.value),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,6 +166,27 @@ class GarageScreen extends ConsumerWidget {
|
||||||
context.push('/garage/add');
|
context.push('/garage/add');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildAddMotorcycleCard(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
List<GarageMotorcycle> bikes,
|
||||||
|
) {
|
||||||
|
final colors = context.motorColors;
|
||||||
|
return Card(
|
||||||
|
child: Center(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Add Motorcycle'),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: colors.cyberCyan,
|
||||||
|
side: BorderSide(color: colors.cyberCyan.withValues(alpha: 0.6)),
|
||||||
|
),
|
||||||
|
onPressed: () => _onAddMotorcycle(context, ref, bikes),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildEmptyState(
|
Widget _buildEmptyState(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
MotorSignalThemeColors colors,
|
MotorSignalThemeColors colors,
|
||||||
|
|
@ -306,7 +320,7 @@ class GarageScreen extends ConsumerWidget {
|
||||||
year: bike.year,
|
year: bike.year,
|
||||||
make: bike.makeName,
|
make: bike.makeName,
|
||||||
model: bike.modelName,
|
model: bike.modelName,
|
||||||
height: 150,
|
height: isGrid ? 100 : 150,
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
|
||||||
|
|
@ -1207,17 +1207,29 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return StatefulBuilder(
|
return StatefulBuilder(
|
||||||
builder: (context, setState) {
|
builder: (context, setState) {
|
||||||
|
final size = MediaQuery.sizeOf(context);
|
||||||
|
final viewInsets = MediaQuery.viewInsetsOf(context);
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
|
scrollable: true,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: 560,
|
||||||
|
maxHeight: size.height - viewInsets.bottom - 48,
|
||||||
|
),
|
||||||
title: const Text('Log New Service'),
|
title: const Text('Log New Service'),
|
||||||
content: SingleChildScrollView(
|
content: Column(
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: selectedType,
|
initialValue: selectedType,
|
||||||
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(labelText: 'Service Type'),
|
decoration: const InputDecoration(labelText: 'Service Type'),
|
||||||
items: serviceTypes.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
|
items: serviceTypes
|
||||||
|
.map((t) => DropdownMenuItem(
|
||||||
|
value: t,
|
||||||
|
child: Text(t, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
if (val != null) setState(() => selectedType = val);
|
if (val != null) setState(() => selectedType = val);
|
||||||
},
|
},
|
||||||
|
|
@ -1268,7 +1280,6 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
|
|
@ -1340,8 +1351,14 @@ class MotorcycleDetailScreen extends ConsumerWidget {
|
||||||
children: [
|
children: [
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: selectedType,
|
initialValue: selectedType,
|
||||||
|
isExpanded: true,
|
||||||
decoration: const InputDecoration(labelText: 'Service Type'),
|
decoration: const InputDecoration(labelText: 'Service Type'),
|
||||||
items: serviceTypes.map((t) => DropdownMenuItem(value: t, child: Text(t))).toList(),
|
items: serviceTypes
|
||||||
|
.map((t) => DropdownMenuItem(
|
||||||
|
value: t,
|
||||||
|
child: Text(t, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
if (val != null) setState(() => selectedType = val);
|
if (val != null) setState(() => selectedType = val);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import 'package:motorcycle_dash/features/records/records_service.dart';
|
||||||
import 'package:motorcycle_dash/features/garage/garage_screen.dart';
|
import 'package:motorcycle_dash/features/garage/garage_screen.dart';
|
||||||
import 'package:motorcycle_dash/features/garage/motorcycle_detail_screen.dart';
|
import 'package:motorcycle_dash/features/garage/motorcycle_detail_screen.dart';
|
||||||
import 'package:motorcycle_dash/features/garage/add_motorcycle_screen.dart';
|
import 'package:motorcycle_dash/features/garage/add_motorcycle_screen.dart';
|
||||||
|
import 'package:motorcycle_dash/features/garage/widgets/motorcycle_photo.dart';
|
||||||
|
|
||||||
class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService {
|
class _FakeGitHubSyncService extends ChangeNotifier implements GitHubSyncService {
|
||||||
@override
|
@override
|
||||||
|
|
@ -340,5 +341,86 @@ void main() {
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Garage responsive regression coverage', () {
|
||||||
|
testWidgets('BUG-015: landscape garage card and Add Motorcycle remain reachable', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(800, 360);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(() => tester.view.resetPhysicalSize());
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildTestApp(const GarageScreen()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
final grid = tester.widget<GridView>(find.byType(GridView));
|
||||||
|
final delegate = grid.gridDelegate as SliverGridDelegateWithMaxCrossAxisExtent;
|
||||||
|
expect(delegate.mainAxisExtent, lessThanOrEqualTo(320));
|
||||||
|
expect(tester.getSize(find.byType(MotorcyclePhoto).first).height, lessThanOrEqualTo(120));
|
||||||
|
final addMotorcycle = find.text('Add Motorcycle');
|
||||||
|
await tester.ensureVisible(addMotorcycle);
|
||||||
|
expect(addMotorcycle.hitTestable(), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildTestApp(const MotorcycleDetailScreen(motorcycleId: 1)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.ensureVisible(find.text('SPECIFICATIONS'));
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(tester.getTopLeft(find.text('SPECIFICATIONS')).dy, lessThan(360));
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final keyboardInset in [0.0, 180.0]) {
|
||||||
|
testWidgets('BUG-016: landscape service dialog remains usable with keyboard inset $keyboardInset', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(800, 360);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
tester.view.viewInsets = FakeViewPadding(bottom: keyboardInset);
|
||||||
|
addTearDown(() {
|
||||||
|
tester.view.resetPhysicalSize();
|
||||||
|
tester.view.resetViewInsets();
|
||||||
|
});
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildTestApp(const MotorcycleDetailScreen(motorcycleId: 1)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.ensureVisible(find.text('Log Service'));
|
||||||
|
await tester.tap(find.text('Log Service'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(find.text('Log New Service'), findsOneWidget);
|
||||||
|
final notesLabel = find.text('Notes (e.g. Brand, weight, shop)');
|
||||||
|
await tester.ensureVisible(notesLabel);
|
||||||
|
expect(tester.getBottomRight(notesLabel).dy, lessThan(360 - keyboardInset));
|
||||||
|
expect(find.text('Log New Service').hitTestable(), findsNothing);
|
||||||
|
expect(find.text('Cancel').hitTestable(), findsOneWidget);
|
||||||
|
expect(find.text('Save Record').hitTestable(), findsOneWidget);
|
||||||
|
await tester.tap(find.text('Cancel'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Log New Service'), findsNothing);
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('BUG-017: narrow portrait service type dropdown truncates label without overflow', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(360, 640);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(() => tester.view.resetPhysicalSize());
|
||||||
|
|
||||||
|
await tester.pumpWidget(buildTestApp(const MotorcycleDetailScreen(motorcycleId: 1)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.ensureVisible(find.text('Log Service'));
|
||||||
|
await tester.tap(find.text('Log Service'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
final dropdown = tester.widget<DropdownButton<String>>(find.byType(DropdownButton<String>).first);
|
||||||
|
expect(dropdown.isExpanded, isTrue);
|
||||||
|
final label = tester.widget<Text>(find.text('Engine Oil & Filter').first);
|
||||||
|
expect(label.maxLines, 1);
|
||||||
|
expect(label.overflow, TextOverflow.ellipsis);
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue