diff --git a/lib/widgets/album_description.dart b/lib/widgets/album_description.dart index 569d1ebc..6171854c 100644 --- a/lib/widgets/album_description.dart +++ b/lib/widgets/album_description.dart @@ -46,7 +46,25 @@ class AlbumDescription extends StatelessWidget { const TextSpan(text: '\n\n'), ], ]; - return TextSpan(children: spans(fragment.nodes)); + TextSpan trimEnd(TextSpan span) { + final children = List.of(span.children ?? const []); + while (children.isNotEmpty) { + final last = trimEnd(children.last as TextSpan); + if (last.toPlainText().isNotEmpty) { + children[children.length - 1] = last; + return TextSpan( + text: span.text, + style: span.style, + children: children, + ); + } + children.removeLast(); + } + return TextSpan(text: span.text?.trimRight(), style: span.style); + } + + // Paragraph separators at the end are not hidden editorial content. + return trimEnd(TextSpan(children: spans(fragment.nodes))); } void _showFullDescription(BuildContext context, TextSpan text) { @@ -121,43 +139,71 @@ class AlbumDescription extends StatelessWidget { if (text.toPlainText().trim().isEmpty) return const SizedBox.shrink(); final theme = Theme.of(context); final more = MaterialLocalizations.of(context).moreButtonTooltip; + final previewStyle = theme.textTheme.bodyLarge?.copyWith( + fontSize: context.isMornye ? 17 : 16, + height: 1.4, + color: theme.colorScheme.onSurfaceVariant, + ); return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: Column( - children: [ - InkWell( - onTap: () => _showFullDescription(context, text), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Text.rich( - text, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodyLarge?.copyWith( - fontSize: context.isMornye ? 17 : 16, - height: 1.4, - color: theme.colorScheme.onSurfaceVariant, + child: LayoutBuilder( + builder: (context, constraints) { + var effectiveStyle = DefaultTextStyle.of( + context, + ).style.merge(previewStyle); + if (MediaQuery.boldTextOf(context)) { + effectiveStyle = effectiveStyle.merge( + const TextStyle(fontWeight: FontWeight.bold), + ); + } + final painter = TextPainter( + text: TextSpan(style: effectiveStyle, children: [text]), + maxLines: 2, + textDirection: Directionality.of(context), + textScaler: MediaQuery.textScalerOf(context), + locale: Localizations.maybeLocaleOf(context), + )..layout(maxWidth: constraints.maxWidth); + final hasMore = painter.didExceedMaxLines; + painter.dispose(); + return Column( + children: [ + InkWell( + onTap: hasMore + ? () => _showFullDescription(context, text) + : null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Text.rich( + text, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: previewStyle, + ), ), - ), + if (hasMore) ...[ + const SizedBox(width: 8), + Text( + context.isMornye ? more.toUpperCase() : more, + style: theme.textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface, + ), + ), + ], + ], ), - const SizedBox(width: 8), - Text( - context.isMornye ? more.toUpperCase() : more, - style: theme.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, - color: theme.colorScheme.onSurface, - ), - ), - ], + ), ), - ), - ), - Divider(color: theme.colorScheme.onSurface.withValues(alpha: 0.15)), - ], + Divider( + color: theme.colorScheme.onSurface.withValues(alpha: 0.15), + ), + ], + ); + }, ), ); } diff --git a/test/album_description_test.dart b/test/album_description_test.dart index 26068bfc..8e238148 100644 --- a/test/album_description_test.dart +++ b/test/album_description_test.dart @@ -44,6 +44,88 @@ void main() { ); for (final style in ['mornye-light', 'mornye-dark', 'material']) { + testWidgets('short album notes have no More action ($style)', ( + tester, + ) async { + final mornye = style.startsWith('mornye'); + tester.view.physicalSize = const Size(393, 700); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + for (final description in [ + 'A short description.', + '

A bold sound.

\n\n', + '

First line.
Second line.

', + ]) { + await tester.pumpWidget( + MaterialApp( + theme: mornye + ? MornyeTheme.build( + style.endsWith('light') + ? Brightness.light + : Brightness.dark, + ) + : ThemeData(), + home: Scaffold( + body: AlbumDescription(title: 'Album', description: description), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.text(mornye ? 'MORE' : 'More'), findsNothing); + final tap = tester.widget(find.byType(InkWell)); + expect(tap.onTap, isNull); + expect(tester.takeException(), isNull); + } + }); + + testWidgets('More responds to album preview width and text size ($style)', ( + tester, + ) async { + final mornye = style.startsWith('mornye'); + final scale = ValueNotifier(1.0); + addTearDown(scale.dispose); + tester.view.physicalSize = const Size(393, 700); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + await tester.pumpWidget( + MaterialApp( + theme: mornye + ? MornyeTheme.build( + style.endsWith('light') ? Brightness.light : Brightness.dark, + ) + : ThemeData(), + home: ValueListenableBuilder( + valueListenable: scale, + builder: (context, value, child) => MediaQuery( + data: MediaQuery.of( + context, + ).copyWith(textScaler: TextScaler.linear(value)), + child: child!, + ), + child: const Scaffold( + body: AlbumDescription( + title: 'Album', + description: 'New sounds and new stories.', + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final more = find.text(mornye ? 'MORE' : 'More'); + expect(more, findsNothing); + tester.view.physicalSize = const Size(180, 700); + await tester.pumpAndSettle(); + expect(more, findsOneWidget); + tester.view.physicalSize = const Size(393, 700); + await tester.pumpAndSettle(); + expect(more, findsNothing); + scale.value = 2.5; + await tester.pumpAndSettle(); + expect(more, findsOneWidget); + expect(tester.takeException(), isNull); + }); + testWidgets('album description opens a formatted reading sheet ($style)', ( tester, ) async {