mirror of
https://github.com/Livinglist/Hacki.git
synced 2025-08-06 09:40:59 +08:00
422 lines
12 KiB
Dart
422 lines
12 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:hacki/cubits/cubits.dart';
|
|
import 'package:hacki/models/models.dart';
|
|
import 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
|
import 'package:hacki/utils/utils.dart';
|
|
import 'package:linkify/linkify.dart' hide UrlLinkifier;
|
|
|
|
export 'package:hacki/screens/widgets/custom_linkify/linkifiers/linkifiers.dart';
|
|
export 'package:linkify/linkify.dart'
|
|
show
|
|
EmailElement,
|
|
EmailLinkifier,
|
|
LinkableElement,
|
|
Linkifier,
|
|
LinkifyElement,
|
|
LinkifyOptions,
|
|
TextElement;
|
|
|
|
/// Callback clicked link
|
|
typedef LinkCallback = void Function(LinkableElement link);
|
|
|
|
/// Turns URLs into links
|
|
class Linkify extends StatelessWidget {
|
|
const Linkify({
|
|
required this.text,
|
|
super.key,
|
|
this.linkifiers = defaultLinkifiers,
|
|
this.onOpen,
|
|
this.options = LinkifierUtil.linkifyOptions,
|
|
// TextSpan
|
|
this.style,
|
|
this.linkStyle,
|
|
// RichText
|
|
this.textAlign = TextAlign.start,
|
|
this.textDirection,
|
|
this.maxLines,
|
|
this.overflow = TextOverflow.clip,
|
|
this.textScaler,
|
|
this.softWrap = true,
|
|
this.strutStyle,
|
|
this.locale,
|
|
this.textWidthBasis = TextWidthBasis.parent,
|
|
this.textHeightBehavior,
|
|
});
|
|
|
|
/// Text to be linkified
|
|
final String text;
|
|
|
|
/// Linkifiers to be used for linkify
|
|
final List<Linkifier> linkifiers;
|
|
|
|
/// Callback for tapping a link
|
|
final LinkCallback? onOpen;
|
|
|
|
/// linkify's options.
|
|
final LinkifyOptions options;
|
|
|
|
// TextSpan
|
|
|
|
/// Style for non-link text
|
|
final TextStyle? style;
|
|
|
|
/// Style of link text
|
|
final TextStyle? linkStyle;
|
|
|
|
// Text.rich
|
|
|
|
/// How the text should be aligned horizontally.
|
|
final TextAlign textAlign;
|
|
|
|
/// Text direction of the text
|
|
final TextDirection? textDirection;
|
|
|
|
/// The maximum number of lines for the text to span, wrapping if necessary
|
|
final int? maxLines;
|
|
|
|
/// How visual overflow should be handled.
|
|
final TextOverflow overflow;
|
|
|
|
/// The number of font pixels for each logical pixel
|
|
final TextScaler? textScaler;
|
|
|
|
/// Whether the text should break at soft line breaks.
|
|
final bool softWrap;
|
|
|
|
/// The strut style used for the vertical layout
|
|
final StrutStyle? strutStyle;
|
|
|
|
/// Used to select a font when the same Unicode character can
|
|
/// be rendered differently, depending on the locale
|
|
final Locale? locale;
|
|
|
|
/// Defines how to measure the width of the rendered text.
|
|
final TextWidthBasis textWidthBasis;
|
|
|
|
/// Defines how the paragraph will apply TextStyle.height to the ascent of
|
|
/// the first line and descent of the last line.
|
|
final TextHeightBehavior? textHeightBehavior;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final List<LinkifyElement> elements = linkify(
|
|
text,
|
|
options: options,
|
|
linkifiers: linkifiers,
|
|
);
|
|
|
|
return Text.rich(
|
|
buildTextSpan(
|
|
elements,
|
|
primaryColor: context.read<PreferenceCubit>().state.appColor,
|
|
style: Theme.of(context).textTheme.bodyMedium?.merge(style),
|
|
onOpen: onOpen,
|
|
useMouseRegion: true,
|
|
linkStyle: Theme.of(context)
|
|
.textTheme
|
|
.bodyMedium
|
|
?.merge(style)
|
|
.copyWith(
|
|
color: Colors.blueAccent,
|
|
decoration: TextDecoration.underline,
|
|
)
|
|
.merge(linkStyle),
|
|
),
|
|
textAlign: textAlign,
|
|
textDirection: textDirection,
|
|
maxLines: maxLines,
|
|
overflow: overflow,
|
|
textScaler: textScaler,
|
|
softWrap: softWrap,
|
|
strutStyle: strutStyle,
|
|
locale: locale,
|
|
textWidthBasis: textWidthBasis,
|
|
textHeightBehavior: textHeightBehavior,
|
|
);
|
|
}
|
|
}
|
|
|
|
const UrlLinkifier _urlLinkifier = UrlLinkifier();
|
|
const EmailLinkifier _emailLinkifier = EmailLinkifier();
|
|
const QuoteLinkifier _quoteLinkifier = QuoteLinkifier();
|
|
const EmphasisLinkifier _emphasisLinkifier = EmphasisLinkifier();
|
|
const CodeLinkifier _codeLinkifier = CodeLinkifier();
|
|
const List<Linkifier> defaultLinkifiers = <Linkifier>[
|
|
_codeLinkifier,
|
|
_urlLinkifier,
|
|
_emailLinkifier,
|
|
_quoteLinkifier,
|
|
_emphasisLinkifier,
|
|
];
|
|
|
|
/// Turns URLs into links
|
|
class SelectableLinkify extends StatelessWidget {
|
|
const SelectableLinkify({
|
|
required this.text,
|
|
super.key,
|
|
this.semanticsLabel,
|
|
this.linkifiers = defaultLinkifiers,
|
|
this.onOpen,
|
|
this.options = LinkifierUtil.linkifyOptions,
|
|
// TextSpan
|
|
this.style,
|
|
this.linkStyle,
|
|
// RichText
|
|
this.textAlign,
|
|
this.textDirection,
|
|
this.minLines,
|
|
this.maxLines,
|
|
// SelectableText
|
|
this.focusNode,
|
|
this.textScaler,
|
|
this.strutStyle,
|
|
this.showCursor = false,
|
|
this.autofocus = false,
|
|
this.cursorWidth = 2.0,
|
|
this.cursorRadius,
|
|
this.cursorColor,
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
this.enableInteractiveSelection = true,
|
|
this.onTap,
|
|
this.scrollPhysics,
|
|
this.textWidthBasis,
|
|
this.textHeightBehavior,
|
|
this.cursorHeight,
|
|
this.selectionControls,
|
|
this.onSelectionChanged,
|
|
this.contextMenuBuilder = _defaultContextMenuBuilder,
|
|
});
|
|
|
|
/// Text to be linkified
|
|
final String text;
|
|
|
|
final String? semanticsLabel;
|
|
|
|
/// The number of font pixels for each logical pixel
|
|
final TextScaler? textScaler;
|
|
|
|
/// Linkifiers to be used for linkify
|
|
final List<Linkifier> linkifiers;
|
|
|
|
/// Callback for tapping a link
|
|
final LinkCallback? onOpen;
|
|
|
|
/// linkify's options.
|
|
final LinkifyOptions options;
|
|
|
|
// TextSpan
|
|
|
|
/// Style for non-link text
|
|
final TextStyle? style;
|
|
|
|
/// Style of link text
|
|
final TextStyle? linkStyle;
|
|
|
|
// Text.rich
|
|
|
|
/// How the text should be aligned horizontally.
|
|
final TextAlign? textAlign;
|
|
|
|
/// Text direction of the text
|
|
final TextDirection? textDirection;
|
|
|
|
/// The minimum number of lines to occupy when the content spans fewer lines.
|
|
final int? minLines;
|
|
|
|
/// The maximum number of lines for the text to span, wrapping if necessary
|
|
final int? maxLines;
|
|
|
|
/// The strut style used for the vertical layout
|
|
final StrutStyle? strutStyle;
|
|
|
|
/// Defines how to measure the width of the rendered text.
|
|
final TextWidthBasis? textWidthBasis;
|
|
|
|
// SelectableText.rich
|
|
|
|
/// Defines the focus for this widget.
|
|
final FocusNode? focusNode;
|
|
|
|
/// Whether to show cursor
|
|
final bool showCursor;
|
|
|
|
/// Whether this text field should focus itself if
|
|
/// nothing else is already focused.
|
|
final bool autofocus;
|
|
|
|
/// How thick the cursor will be
|
|
final double cursorWidth;
|
|
|
|
/// How rounded the corners of the cursor should be
|
|
final Radius? cursorRadius;
|
|
|
|
/// The color to use when painting the cursor
|
|
final Color? cursorColor;
|
|
|
|
/// Determines the way that drag start behavior is handled
|
|
final DragStartBehavior dragStartBehavior;
|
|
|
|
/// If true, then long-pressing this TextField will select text and show the cut/copy/paste menu,
|
|
/// and tapping will move the text caret
|
|
final bool enableInteractiveSelection;
|
|
|
|
/// Called when the user taps on this selectable text (not link)
|
|
final GestureTapCallback? onTap;
|
|
|
|
final ScrollPhysics? scrollPhysics;
|
|
|
|
/// Defines how the paragraph will apply TextStyle.height to the ascent of
|
|
/// the first line and descent of the last line.
|
|
final TextHeightBehavior? textHeightBehavior;
|
|
|
|
/// How tall the cursor will be.
|
|
final double? cursorHeight;
|
|
|
|
/// Optional delegate for building the text selection handles and toolbar.
|
|
final TextSelectionControls? selectionControls;
|
|
|
|
/// Called when the user changes the selection of text (including the
|
|
/// cursor location).
|
|
final SelectionChangedCallback? onSelectionChanged;
|
|
|
|
final EditableTextContextMenuBuilder? contextMenuBuilder;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final List<LinkifyElement> elements = LinkifierUtil.linkify(text);
|
|
return SelectableText.rich(
|
|
buildTextSpan(
|
|
elements,
|
|
primaryColor: context.read<PreferenceCubit>().state.appColor,
|
|
style: Theme.of(context).textTheme.bodyMedium?.merge(style),
|
|
onOpen: onOpen,
|
|
linkStyle: Theme.of(context)
|
|
.textTheme
|
|
.bodyMedium
|
|
?.merge(style)
|
|
.copyWith(
|
|
color: Colors.blueAccent,
|
|
decoration: TextDecoration.underline,
|
|
)
|
|
.merge(linkStyle),
|
|
),
|
|
textAlign: textAlign,
|
|
textDirection: textDirection,
|
|
minLines: minLines,
|
|
maxLines: maxLines,
|
|
focusNode: focusNode,
|
|
strutStyle: strutStyle,
|
|
showCursor: showCursor,
|
|
textScaler: textScaler,
|
|
autofocus: autofocus,
|
|
cursorWidth: cursorWidth,
|
|
cursorRadius: cursorRadius,
|
|
cursorColor: cursorColor,
|
|
dragStartBehavior: dragStartBehavior,
|
|
enableInteractiveSelection: enableInteractiveSelection,
|
|
onTap: onTap,
|
|
scrollPhysics: scrollPhysics,
|
|
textWidthBasis: textWidthBasis,
|
|
textHeightBehavior: textHeightBehavior,
|
|
cursorHeight: cursorHeight,
|
|
selectionControls: selectionControls,
|
|
onSelectionChanged: onSelectionChanged,
|
|
contextMenuBuilder: contextMenuBuilder,
|
|
semanticsLabel: semanticsLabel,
|
|
);
|
|
}
|
|
|
|
static Widget _defaultContextMenuBuilder(
|
|
BuildContext context,
|
|
EditableTextState editableTextState,
|
|
) {
|
|
return AdaptiveTextSelectionToolbar.editableText(
|
|
editableTextState: editableTextState,
|
|
);
|
|
}
|
|
}
|
|
|
|
class LinkableSpan extends WidgetSpan {
|
|
LinkableSpan({
|
|
required MouseCursor mouseCursor,
|
|
required InlineSpan inlineSpan,
|
|
}) : super(
|
|
child: MouseRegion(
|
|
cursor: mouseCursor,
|
|
child: Text.rich(
|
|
inlineSpan,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Raw TextSpan builder for more control on the RichText
|
|
TextSpan buildTextSpan(
|
|
List<LinkifyElement> elements, {
|
|
required MaterialColor primaryColor,
|
|
TextStyle? style,
|
|
TextStyle? linkStyle,
|
|
LinkCallback? onOpen,
|
|
bool useMouseRegion = false,
|
|
}) {
|
|
return TextSpan(
|
|
children: elements.map<InlineSpan>(
|
|
(LinkifyElement element) {
|
|
if (element is LinkableElement) {
|
|
if (useMouseRegion) {
|
|
return LinkableSpan(
|
|
mouseCursor: SystemMouseCursors.click,
|
|
inlineSpan: TextSpan(
|
|
text: element.text,
|
|
style: linkStyle,
|
|
recognizer: onOpen != null
|
|
? (TapGestureRecognizer()..onTap = () => onOpen(element))
|
|
: null,
|
|
),
|
|
);
|
|
} else {
|
|
return TextSpan(
|
|
text: element.text,
|
|
style: linkStyle,
|
|
recognizer: onOpen != null
|
|
? (TapGestureRecognizer()..onTap = () => onOpen(element))
|
|
: null,
|
|
);
|
|
}
|
|
} else {
|
|
if (element is QuoteElement) {
|
|
return TextSpan(
|
|
text: element.text,
|
|
style: style?.copyWith(
|
|
backgroundColor: primaryColor.withValues(alpha: 0.3),
|
|
),
|
|
);
|
|
} else if (element is EmphasisElement) {
|
|
return TextSpan(
|
|
text: element.text,
|
|
style: style?.copyWith(
|
|
fontStyle: FontStyle.italic,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
);
|
|
} else if (element is CodeElement) {
|
|
return TextSpan(
|
|
text: element.text,
|
|
style: style?.copyWith(
|
|
fontFamily: Font.ubuntuMono.name,
|
|
),
|
|
);
|
|
}
|
|
|
|
return TextSpan(
|
|
text: element.text,
|
|
style: style,
|
|
);
|
|
}
|
|
},
|
|
).toList(),
|
|
);
|
|
}
|