fix(album): show More only for truncated descriptions

This commit is contained in:
zarzet
2026-09-26 23:39:50 +07:00
parent 2cfd2e8a48
commit e0093841bc
2 changed files with 161 additions and 33 deletions
+79 -33
View File
@@ -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<InlineSpan>.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),
),
],
);
},
),
);
}
+82
View File
@@ -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.',
'<p>A <b>bold</b> sound.</p>\n\n',
'<div><p>First line.<br>Second line.</p></div>',
]) {
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<InkWell>(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<double>(
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 {