mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-05-14 03:00:38 +00:00
Refactor ReadMoreWidget to use a new ExpandableText implementation
This commit is contained in:
parent
e8384f51fc
commit
951781f415
4 changed files with 580 additions and 69 deletions
555
lib/modules/manga/detail/widgets/expandable_text.dart
Normal file
555
lib/modules/manga/detail/widgets/expandable_text.dart
Normal file
|
|
@ -0,0 +1,555 @@
|
|||
import 'dart:math';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef StringCallback = void Function(String value);
|
||||
|
||||
class ExpandableText extends StatefulWidget {
|
||||
const ExpandableText(
|
||||
this.text, {
|
||||
super.key,
|
||||
required this.expandText,
|
||||
this.collapseText,
|
||||
this.expanded = false,
|
||||
this.onExpandedChanged,
|
||||
this.onLinkTap,
|
||||
this.linkColor,
|
||||
this.linkEllipsis = true,
|
||||
this.linkStyle,
|
||||
this.prefixText,
|
||||
this.prefixStyle,
|
||||
this.onPrefixTap,
|
||||
this.urlStyle,
|
||||
this.onUrlTap,
|
||||
this.hashtagStyle,
|
||||
this.onHashtagTap,
|
||||
this.mentionStyle,
|
||||
this.onMentionTap,
|
||||
this.expandOnTextTap = false,
|
||||
this.collapseOnTextTap = false,
|
||||
this.style,
|
||||
this.textDirection,
|
||||
this.textAlign,
|
||||
this.textScaler,
|
||||
this.maxLines = 2,
|
||||
this.animation = false,
|
||||
this.animationDuration,
|
||||
this.animationCurve,
|
||||
this.semanticsLabel,
|
||||
this.showGradientOverlay = false,
|
||||
this.gradientOverlayHeight = 30.0,
|
||||
this.showExpandCollapseIcon = false,
|
||||
this.expandIcon,
|
||||
this.collapseIcon,
|
||||
}) : assert(maxLines > 0);
|
||||
|
||||
final String text;
|
||||
final String expandText;
|
||||
final String? collapseText;
|
||||
final bool expanded;
|
||||
final ValueChanged<bool>? onExpandedChanged;
|
||||
final VoidCallback? onLinkTap;
|
||||
final Color? linkColor;
|
||||
final bool linkEllipsis;
|
||||
final TextStyle? linkStyle;
|
||||
final String? prefixText;
|
||||
final TextStyle? prefixStyle;
|
||||
final VoidCallback? onPrefixTap;
|
||||
final TextStyle? urlStyle;
|
||||
final StringCallback? onUrlTap;
|
||||
final TextStyle? hashtagStyle;
|
||||
final StringCallback? onHashtagTap;
|
||||
final TextStyle? mentionStyle;
|
||||
final StringCallback? onMentionTap;
|
||||
final bool expandOnTextTap;
|
||||
final bool collapseOnTextTap;
|
||||
final TextStyle? style;
|
||||
final TextDirection? textDirection;
|
||||
final TextAlign? textAlign;
|
||||
final TextScaler? textScaler;
|
||||
final int maxLines;
|
||||
final bool animation;
|
||||
final Duration? animationDuration;
|
||||
final Curve? animationCurve;
|
||||
final String? semanticsLabel;
|
||||
final bool showGradientOverlay;
|
||||
final double gradientOverlayHeight;
|
||||
final bool showExpandCollapseIcon;
|
||||
final IconData? expandIcon;
|
||||
final IconData? collapseIcon;
|
||||
|
||||
@override
|
||||
ExpandableTextState createState() => ExpandableTextState();
|
||||
}
|
||||
|
||||
class ExpandableTextState extends State<ExpandableText>
|
||||
with TickerProviderStateMixin {
|
||||
bool _expanded = false;
|
||||
late TapGestureRecognizer _linkTapGestureRecognizer;
|
||||
late TapGestureRecognizer _prefixTapGestureRecognizer;
|
||||
|
||||
List<TextSegment> _textSegments = [];
|
||||
final List<TapGestureRecognizer> _textSegmentsTapGestureRecognizers = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_expanded = widget.expanded;
|
||||
_linkTapGestureRecognizer = TapGestureRecognizer()..onTap = _linkTapped;
|
||||
_prefixTapGestureRecognizer = TapGestureRecognizer()..onTap = _prefixTapped;
|
||||
|
||||
_updateText();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ExpandableText oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.text != widget.text ||
|
||||
oldWidget.onUrlTap != widget.onUrlTap ||
|
||||
oldWidget.onHashtagTap != widget.onHashtagTap ||
|
||||
oldWidget.onMentionTap != widget.onMentionTap) {
|
||||
_updateText();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_linkTapGestureRecognizer.dispose();
|
||||
_prefixTapGestureRecognizer.dispose();
|
||||
for (var recognizer in _textSegmentsTapGestureRecognizers) {
|
||||
recognizer.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _linkTapped() {
|
||||
if (widget.onLinkTap != null) {
|
||||
widget.onLinkTap!();
|
||||
return;
|
||||
}
|
||||
|
||||
final toggledExpanded = !_expanded;
|
||||
|
||||
setState(() => _expanded = toggledExpanded);
|
||||
|
||||
widget.onExpandedChanged?.call(toggledExpanded);
|
||||
}
|
||||
|
||||
void _prefixTapped() {
|
||||
widget.onPrefixTap?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
|
||||
var effectiveTextStyle = widget.style;
|
||||
if (widget.style == null || widget.style!.inherit) {
|
||||
effectiveTextStyle = defaultTextStyle.style.merge(widget.style);
|
||||
}
|
||||
|
||||
final linkText =
|
||||
(_expanded ? widget.collapseText : widget.expandText) ?? '';
|
||||
final linkColor =
|
||||
widget.linkColor ??
|
||||
widget.linkStyle?.color ??
|
||||
Theme.of(context).colorScheme.secondary;
|
||||
final linkTextStyle = effectiveTextStyle!
|
||||
.merge(widget.linkStyle)
|
||||
.copyWith(color: linkColor);
|
||||
|
||||
final prefixText =
|
||||
widget.prefixText != null && widget.prefixText!.isNotEmpty
|
||||
? '${widget.prefixText} '
|
||||
: '';
|
||||
|
||||
final link = TextSpan(
|
||||
children: [
|
||||
if (!_expanded)
|
||||
TextSpan(
|
||||
text: '\u2026 ',
|
||||
style: widget.linkEllipsis ? linkTextStyle : effectiveTextStyle,
|
||||
recognizer: widget.linkEllipsis ? _linkTapGestureRecognizer : null,
|
||||
),
|
||||
if (linkText.isNotEmpty)
|
||||
TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
children: <TextSpan>[
|
||||
if (_expanded) const TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: linkText,
|
||||
style: linkTextStyle,
|
||||
recognizer: _linkTapGestureRecognizer,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final prefix = TextSpan(
|
||||
text: prefixText,
|
||||
style: effectiveTextStyle.merge(widget.prefixStyle),
|
||||
recognizer: _prefixTapGestureRecognizer,
|
||||
);
|
||||
|
||||
final text = _textSegments.isNotEmpty
|
||||
? TextSpan(
|
||||
children: _buildTextSpans(_textSegments, effectiveTextStyle, null),
|
||||
)
|
||||
: TextSpan(text: widget.text);
|
||||
|
||||
final content = TextSpan(
|
||||
children: <TextSpan>[prefix, text],
|
||||
style: effectiveTextStyle,
|
||||
);
|
||||
|
||||
Widget result = LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
assert(constraints.hasBoundedWidth);
|
||||
final double maxWidth = constraints.maxWidth;
|
||||
|
||||
final textAlign =
|
||||
widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start;
|
||||
final textDirection =
|
||||
widget.textDirection ?? Directionality.of(context);
|
||||
final textScaler =
|
||||
widget.textScaler ?? MediaQuery.textScalerOf(context);
|
||||
final locale = Localizations.maybeLocaleOf(context);
|
||||
|
||||
TextPainter textPainter = TextPainter(
|
||||
text: link,
|
||||
textAlign: textAlign,
|
||||
textDirection: textDirection,
|
||||
textScaler: textScaler,
|
||||
maxLines: widget.maxLines,
|
||||
locale: locale,
|
||||
);
|
||||
textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth);
|
||||
final linkSize = textPainter.size;
|
||||
|
||||
textPainter.text = content;
|
||||
textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth);
|
||||
final textSize = textPainter.size;
|
||||
|
||||
final bool hasExceededMaxLines = textPainter.didExceedMaxLines;
|
||||
|
||||
TextSpan textSpan;
|
||||
if (hasExceededMaxLines) {
|
||||
final position = textPainter.getPositionForOffset(
|
||||
Offset(textSize.width - linkSize.width, textSize.height),
|
||||
);
|
||||
final endOffset =
|
||||
(textPainter.getOffsetBefore(position.offset) ?? 0) -
|
||||
prefixText.length;
|
||||
|
||||
final recognizer =
|
||||
(_expanded ? widget.collapseOnTextTap : widget.expandOnTextTap)
|
||||
? _linkTapGestureRecognizer
|
||||
: null;
|
||||
|
||||
final text = _textSegments.isNotEmpty
|
||||
? TextSpan(
|
||||
children: _buildTextSpans(
|
||||
_expanded
|
||||
? _textSegments
|
||||
: parseText(
|
||||
widget.text.substring(0, max(endOffset, 0)),
|
||||
),
|
||||
effectiveTextStyle!,
|
||||
recognizer,
|
||||
),
|
||||
)
|
||||
: TextSpan(
|
||||
text: _expanded
|
||||
? widget.text
|
||||
: widget.text.substring(0, max(endOffset, 0)),
|
||||
recognizer: recognizer,
|
||||
);
|
||||
|
||||
textSpan = TextSpan(
|
||||
style: effectiveTextStyle,
|
||||
children: <TextSpan>[prefix, text, link],
|
||||
);
|
||||
} else {
|
||||
textSpan = content;
|
||||
}
|
||||
|
||||
final selectableText = SelectableText.rich(
|
||||
textSpan,
|
||||
textDirection: textDirection,
|
||||
textAlign: textAlign,
|
||||
textScaler: textScaler,
|
||||
);
|
||||
|
||||
Widget textWidget = selectableText;
|
||||
|
||||
if (widget.animation) {
|
||||
textWidget = AnimatedSize(
|
||||
duration:
|
||||
widget.animationDuration ?? const Duration(milliseconds: 200),
|
||||
curve: widget.animationCurve ?? Curves.fastLinearToSlowEaseIn,
|
||||
alignment: Alignment.topLeft,
|
||||
child: textWidget,
|
||||
);
|
||||
}
|
||||
|
||||
// Wrap with Stack to add gradient overlay and icons
|
||||
if (widget.showGradientOverlay || widget.showExpandCollapseIcon) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
textWidget,
|
||||
// Gradient overlay when collapsed and text exceeds max lines
|
||||
if (widget.showGradientOverlay &&
|
||||
!_expanded &&
|
||||
hasExceededMaxLines)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: GestureDetector(
|
||||
onTap: _linkTapped,
|
||||
child: Container(
|
||||
height: widget.gradientOverlayHeight,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Theme.of(context).scaffoldBackgroundColor
|
||||
.withValues(alpha: 0.2),
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
],
|
||||
stops: const [0, 0.9],
|
||||
),
|
||||
),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Icon(
|
||||
widget.expandIcon ??
|
||||
Icons.keyboard_arrow_down_sharp,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Collapse icon when expanded
|
||||
if (widget.showExpandCollapseIcon &&
|
||||
_expanded &&
|
||||
hasExceededMaxLines)
|
||||
GestureDetector(
|
||||
onTap: _linkTapped,
|
||||
child: SizedBox(
|
||||
height: 20,
|
||||
child: Icon(
|
||||
widget.collapseIcon ?? Icons.keyboard_arrow_up_sharp,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return textWidget;
|
||||
},
|
||||
);
|
||||
|
||||
if (widget.semanticsLabel != null) {
|
||||
result = Semantics(
|
||||
textDirection: widget.textDirection,
|
||||
label: widget.semanticsLabel,
|
||||
child: ExcludeSemantics(child: result),
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void _updateText() {
|
||||
for (var recognizer in _textSegmentsTapGestureRecognizers) {
|
||||
recognizer.dispose();
|
||||
}
|
||||
_textSegmentsTapGestureRecognizers.clear();
|
||||
|
||||
if (widget.onUrlTap == null &&
|
||||
widget.onHashtagTap == null &&
|
||||
widget.onMentionTap == null) {
|
||||
_textSegments.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
_textSegments = parseText(widget.text);
|
||||
|
||||
for (var element in _textSegments) {
|
||||
if (element.isUrl && widget.onUrlTap != null) {
|
||||
final recognizer = TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
widget.onUrlTap!(element.name!);
|
||||
};
|
||||
|
||||
_textSegmentsTapGestureRecognizers.add(recognizer);
|
||||
} else if (element.isHashtag && widget.onHashtagTap != null) {
|
||||
final recognizer = TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
widget.onHashtagTap!(element.name!);
|
||||
};
|
||||
|
||||
_textSegmentsTapGestureRecognizers.add(recognizer);
|
||||
} else if (element.isMention && widget.onMentionTap != null) {
|
||||
final recognizer = TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
widget.onMentionTap!(element.name!);
|
||||
};
|
||||
|
||||
_textSegmentsTapGestureRecognizers.add(recognizer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<TextSpan> _buildTextSpans(
|
||||
List<TextSegment> segments,
|
||||
TextStyle textStyle,
|
||||
TapGestureRecognizer? textTapRecognizer,
|
||||
) {
|
||||
final spans = <TextSpan>[];
|
||||
|
||||
var index = 0;
|
||||
for (var segment in segments) {
|
||||
TextStyle? style;
|
||||
TapGestureRecognizer? recognizer;
|
||||
|
||||
if (segment.isUrl && widget.onUrlTap != null) {
|
||||
style = textStyle.merge(widget.urlStyle);
|
||||
recognizer = _textSegmentsTapGestureRecognizers[index++];
|
||||
} else if (segment.isMention && widget.onMentionTap != null) {
|
||||
style = textStyle.merge(widget.mentionStyle);
|
||||
recognizer = _textSegmentsTapGestureRecognizers[index++];
|
||||
} else if (segment.isHashtag && widget.onHashtagTap != null) {
|
||||
style = textStyle.merge(widget.hashtagStyle);
|
||||
recognizer = _textSegmentsTapGestureRecognizers[index++];
|
||||
}
|
||||
|
||||
final span = TextSpan(
|
||||
text: segment.text,
|
||||
style: style,
|
||||
recognizer: recognizer ?? textTapRecognizer,
|
||||
);
|
||||
|
||||
spans.add(span);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
}
|
||||
|
||||
class TextSegment {
|
||||
String text;
|
||||
|
||||
final String? name;
|
||||
final bool isHashtag;
|
||||
final bool isMention;
|
||||
final bool isUrl;
|
||||
|
||||
bool get isText => !isHashtag && !isMention && !isUrl;
|
||||
|
||||
TextSegment(
|
||||
this.text, [
|
||||
this.name,
|
||||
this.isHashtag = false,
|
||||
this.isMention = false,
|
||||
this.isUrl = false,
|
||||
]);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is TextSegment &&
|
||||
runtimeType == other.runtimeType &&
|
||||
text == other.text &&
|
||||
name == other.name &&
|
||||
isHashtag == other.isHashtag &&
|
||||
isMention == other.isMention &&
|
||||
isUrl == other.isUrl;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
text.hashCode ^
|
||||
name.hashCode ^
|
||||
isHashtag.hashCode ^
|
||||
isMention.hashCode ^
|
||||
isUrl.hashCode;
|
||||
}
|
||||
|
||||
/// Split the string into multiple instances of [TextSegment] for mentions, hashtags, URLs and regular text.
|
||||
///
|
||||
/// Mentions are all words that start with @, e.g. @mention.
|
||||
/// Hashtags are all words that start with #, e.g. #hashtag.
|
||||
List<TextSegment> parseText(String? text) {
|
||||
final segments = <TextSegment>[];
|
||||
|
||||
if (text == null || text.isEmpty) {
|
||||
return segments;
|
||||
}
|
||||
|
||||
// parse urls and words starting with @ (mention) or # (hashtag)
|
||||
RegExp exp = RegExp(
|
||||
r'(?<keyword>(#|@)([\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]+)|(?<url>(?:(?:https?|ftp):\/\/)?[-a-z0-9@:%._\+~#=]{1,256}\.[a-z0-9]{1,6}(\/[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)?))',
|
||||
unicode: true,
|
||||
);
|
||||
final matches = exp.allMatches(text);
|
||||
|
||||
var start = 0;
|
||||
for (var match in matches) {
|
||||
// text before the keyword
|
||||
if (match.start > start) {
|
||||
if (segments.isNotEmpty && segments.last.isText) {
|
||||
segments.last.text += text.substring(start, match.start);
|
||||
} else {
|
||||
segments.add(TextSegment(text.substring(start, match.start)));
|
||||
}
|
||||
start = match.start;
|
||||
}
|
||||
|
||||
final url = match.namedGroup('url');
|
||||
final keyword = match.namedGroup('keyword');
|
||||
|
||||
if (url != null) {
|
||||
segments.add(TextSegment(url, url, false, false, true));
|
||||
} else if (keyword != null) {
|
||||
final isWord =
|
||||
match.start == 0 ||
|
||||
[' ', '\n'].contains(text.substring(match.start - 1, start));
|
||||
if (!isWord) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final isHashtag = keyword.startsWith('#');
|
||||
final isMention = keyword.startsWith('@');
|
||||
|
||||
segments.add(
|
||||
TextSegment(keyword, keyword.substring(1), isHashtag, isMention),
|
||||
);
|
||||
}
|
||||
|
||||
start = match.end;
|
||||
}
|
||||
|
||||
// text after the last keyword or the whole text if it does not contain any keywords
|
||||
if (start < text.length) {
|
||||
if (segments.isNotEmpty && segments.last.isText) {
|
||||
segments.last.text += text.substring(start);
|
||||
} else {
|
||||
segments.add(TextSegment(text.substring(start)));
|
||||
}
|
||||
}
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
import 'package:expandable_text/expandable_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/widgets/expandable_text.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
|
||||
class ReadMoreWidget extends StatefulWidget {
|
||||
const ReadMoreWidget({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onChanged,
|
||||
this.initExpanded = true,
|
||||
this.initExpanded = false,
|
||||
});
|
||||
final Function(bool) onChanged;
|
||||
final String text;
|
||||
|
|
@ -29,63 +28,29 @@ class ReadMoreWidgetState extends State<ReadMoreWidget>
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [Text(l10n.no_description)],
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: ExpandableText(
|
||||
animationDuration: const Duration(milliseconds: 500),
|
||||
onExpandedChanged: (value) {
|
||||
setState(() => expanded = value);
|
||||
widget.onChanged(value);
|
||||
},
|
||||
expandOnTextTap: true,
|
||||
widget.text.trim(),
|
||||
expandText: '',
|
||||
maxLines: 3,
|
||||
expanded: expanded,
|
||||
linkColor: Theme.of(context).scaffoldBackgroundColor,
|
||||
animation: true,
|
||||
collapseOnTextTap: true,
|
||||
prefixText: '',
|
||||
),
|
||||
),
|
||||
if (!expanded)
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
child: Container(
|
||||
width: context.width(1),
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Theme.of(
|
||||
context,
|
||||
).scaffoldBackgroundColor.withValues(alpha: 0.2),
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
],
|
||||
stops: const [0, .9],
|
||||
),
|
||||
),
|
||||
child: const Icon(Icons.keyboard_arrow_down_sharp),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (expanded)
|
||||
SizedBox(
|
||||
width: context.width(1),
|
||||
height: 20,
|
||||
child: const Icon(Icons.keyboard_arrow_up_sharp),
|
||||
),
|
||||
],
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
child: ExpandableText(
|
||||
widget.text.trim(),
|
||||
expandText: '',
|
||||
maxLines: 3,
|
||||
expanded: expanded,
|
||||
linkColor: Colors.transparent,
|
||||
animation: true,
|
||||
animationDuration: const Duration(milliseconds: 500),
|
||||
expandOnTextTap: true,
|
||||
collapseOnTextTap: true,
|
||||
prefixText: '',
|
||||
showGradientOverlay: true,
|
||||
gradientOverlayHeight: 30,
|
||||
showExpandCollapseIcon: true,
|
||||
expandIcon: Icons.keyboard_arrow_down_sharp,
|
||||
collapseIcon: Icons.keyboard_arrow_up_sharp,
|
||||
onExpandedChanged: (value) {
|
||||
setState(() => expanded = value);
|
||||
widget.onChanged(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -451,14 +451,6 @@ packages:
|
|||
url: "https://github.com/kodjodevf/epubx.dart.git"
|
||||
source: git
|
||||
version: "4.0.3"
|
||||
expandable_text:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: expandable_text
|
||||
sha256: "7d03ea48af6987b20ece232678b744862aa3250d4a71e2aaf1e4af90015d76b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
expressions:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ dependencies:
|
|||
riverpod_annotation: ^3.0.3
|
||||
html: ^0.15.5
|
||||
font_awesome_flutter: ^10.12.0
|
||||
expandable_text: ^2.3.0
|
||||
flex_color_scheme: ^8.3.1
|
||||
extended_image: ^10.0.0
|
||||
photo_view: ^0.15.0
|
||||
|
|
|
|||
Loading…
Reference in a new issue