diff --git a/app/changelog/unreleased/20261014-apps-category-row-text-fit.json b/app/changelog/unreleased/20261014-apps-category-row-text-fit.json new file mode 100644 index 00000000000..f626521235c --- /dev/null +++ b/app/changelog/unreleased/20261014-apps-category-row-text-fit.json @@ -0,0 +1,3 @@ +{ + "change": "App category rows keep the name and rating visible at larger text sizes" +} diff --git a/app/lib/pages/apps/widgets/category_section.dart b/app/lib/pages/apps/widgets/category_section.dart index dc69d3fca51..b542f91b089 100644 --- a/app/lib/pages/apps/widgets/category_section.dart +++ b/app/lib/pages/apps/widgets/category_section.dart @@ -40,24 +40,44 @@ class CategorySection extends StatelessWidget { final displayedApps = apps.take(9).toList(); // --- Configuration Constants --- - // Heights follow the reader's text size: fixed 85 / 60 pt rows cut the app's name, category - // and rating (and the section title) at a larger text size (layout lint, 1.3x). + // Heights follow the reader's text size. OmiType leaves `height` unset, so painted text + // inherits Material's line height (~1.43). Measuring the raw style misses that and the + // name/category/rating column overflows (13px at 1.3x on a 390pt-wide phone). final scaler = MediaQuery.textScalerOf(context); final direction = Directionality.of(context); + final inherited = DefaultTextStyle.of(context).style; double lineOf(TextStyle style) { - final painter = - TextPainter(text: TextSpan(text: 'Ag', style: style), textScaler: scaler, textDirection: direction)..layout(); + final painter = TextPainter( + text: TextSpan(text: 'Ag', style: inherited.merge(style)), + textScaler: scaler, + textDirection: direction, + maxLines: 1, + )..layout(); final height = painter.height; painter.dispose(); return height; } - final rowText = lineOf(OmiType.body) + 2 + lineOf(OmiType.footnote) + 2 + lineOf(OmiType.caption); - final double targetItemHeight = max(85.0, 16 + max(52.0, rowText) + 1); + final rowText = lineOf(OmiType.body.copyWith(fontWeight: FontWeight.w500)) + + 2 + + lineOf(OmiType.footnote) + + 2 + + lineOf(OmiType.caption); + const double cardVerticalPadding = 16; + const double artworkExtent = 60; + // Compact actions paint at 36pt and pad their tap target out to 48pt. + const double actionExtent = 48; + final double targetItemHeight = max( + 85.0, + cardVerticalPadding + max(artworkExtent, max(actionExtent, rowText)) + 1, + ); const double crossAxisSpacing = 0.0; const double mainAxisSpacing = 14.0; const int maxRows = 3; - final double titleSectionHeight = max(60.0, 24 + 16 + lineOf(OmiType.title2)); + final titleLine = lineOf(OmiType.title3); + final allChip = lineOf(OmiType.caption.copyWith(fontWeight: FontWeight.w600)) + 4; + final headerRow = max(titleLine, max(allChip, 16.0)); + final double titleSectionHeight = max(60.0, 24 + 16 + headerRow); // --- Dynamic Calculation --- final int numRows = min(maxRows, displayedApps.length); diff --git a/app/test/services/device_service_connection_failure_test.dart b/app/test/services/device_service_connection_failure_test.dart index 98851a74c20..42806446e8b 100644 --- a/app/test/services/device_service_connection_failure_test.dart +++ b/app/test/services/device_service_connection_failure_test.dart @@ -121,8 +121,12 @@ void main() { expect(service.connectionFor(device.id), same(connection)); expect(connection.status, DeviceConnectionState.disconnected); - // Later native event brings the device back in range + // Later native event brings the device back in range. Production transports + // (and this fake) use an asynchronous broadcast stream, so the connection + // applies the event on a later microtask — the same delivery ensureConnection + // sees after the event loop turns. transport.emitConnected(); + await pumpEventQueue(); expect(connection.status, DeviceConnectionState.connected); // When connection is up, ensureConnection cleanly returns the existing connection diff --git a/app/test/widgets/apps_category_section_large_text_test.dart b/app/test/widgets/apps_category_section_large_text_test.dart index dc037f1a240..7f59c8aa57c 100644 --- a/app/test/widgets/apps_category_section_large_text_test.dart +++ b/app/test/widgets/apps_category_section_large_text_test.dart @@ -10,6 +10,7 @@ import 'package:omi/l10n/app_localizations.dart'; import 'package:omi/pages/apps/providers/add_app_provider.dart'; import 'package:omi/pages/apps/widgets/category_section.dart'; import 'package:omi/providers/app_provider.dart'; +import 'package:omi/ui/ui.dart'; import 'package:omi/utils/platform/platform_manager.dart'; // Regression test for #19238: CategorySection rows overflow at larger text sizes @@ -110,7 +111,13 @@ void main() { isNull, reason: 'CategorySection should not overflow at scale on width ', ); - expect(find.text('Productivity'), findsOneWidget); + expect( + find.byWidgetPredicate( + (widget) => + widget is Text && widget.data == 'Productivity' && widget.style?.fontSize == OmiType.title3.fontSize, + ), + findsOneWidget, + ); expect(find.text('Productivity Booster Pro'), findsOneWidget); } }); diff --git a/app/test/widgets/settings/ai_app_generator_large_text_test.dart b/app/test/widgets/settings/ai_app_generator_large_text_test.dart index 56127326b75..1628989be6f 100644 --- a/app/test/widgets/settings/ai_app_generator_large_text_test.dart +++ b/app/test/widgets/settings/ai_app_generator_large_text_test.dart @@ -104,7 +104,7 @@ void main() { isNull, reason: 'AI app generator suggestion strip should not overflow at scale $scale on width $testWidth', ); - expect(find.text('Try something like'), findsOneWidget); + expect(find.text('Try something like…'), findsOneWidget); expect(find.text('A playful gratitude spinner for daily positivity'), findsOneWidget); } }); diff --git a/app/test/widgets/settings/settings_search_header_stability_test.dart b/app/test/widgets/settings/settings_search_header_stability_test.dart index e6977f2666f..312c5cce9d5 100644 --- a/app/test/widgets/settings/settings_search_header_stability_test.dart +++ b/app/test/widgets/settings/settings_search_header_stability_test.dart @@ -9,6 +9,7 @@ import 'package:omi/l10n/app_localizations.dart'; import 'package:omi/pages/settings/settings_drawer.dart'; import 'package:omi/providers/app_provider.dart'; import 'package:omi/providers/home_provider.dart'; +import 'package:omi/providers/usage_provider.dart'; import 'package:omi/providers/user_provider.dart'; import 'package:omi/ui/ui.dart'; import 'package:omi/utils/platform/platform_manager.dart'; @@ -23,6 +24,7 @@ Widget _buildHarness() { ChangeNotifierProvider.value(value: AppProvider()), ChangeNotifierProvider.value(value: HomeProvider()), ChangeNotifierProvider.value(value: UserProvider()), + ChangeNotifierProvider(create: (_) => UsageProvider()), ], child: const MaterialApp( localizationsDelegates: [