From 451a84e696d50690152e65e0a8705020713a65aa Mon Sep 17 00:00:00 2001 From: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:57:30 +0800 Subject: [PATCH] opt: opus quote (#771) --- lib/pages/article/view.dart | 3 +- lib/pages/article/widgets/opus_content.dart | 494 ++++++++++---------- 2 files changed, 247 insertions(+), 250 deletions(-) diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index 38343e5a..c1ecbf8e 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -357,8 +357,7 @@ class _ArticlePageState extends State ); } else { debugPrint('json page'); - content = opusContent( - context: context, + content = OpusContent( opus: _articleCtr.opus!, callback: _getImageCallback, maxWidth: maxWidth, diff --git a/lib/pages/article/widgets/opus_content.dart b/lib/pages/article/widgets/opus_content.dart index 931695af..82d464c2 100644 --- a/lib/pages/article/widgets/opus_content.dart +++ b/lib/pages/article/widgets/opus_content.dart @@ -3,7 +3,7 @@ import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactivevie show SourceModel; import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/models/dynamics/article_content_model.dart' - show ArticleContentModel; + show ArticleContentModel, Style, Word; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -15,259 +15,257 @@ import 'package:re_highlight/languages/all.dart'; import 'package:re_highlight/re_highlight.dart'; import 'package:re_highlight/styles/all.dart'; -Widget opusContent({ - required BuildContext context, - required List opus, - Function(List, int)? callback, - required double maxWidth, -}) { - debugPrint('opusContent'); +class OpusContent extends StatelessWidget { + final List opus; + final void Function(List, int)? callback; + final double maxWidth; - if (opus.isEmpty) { - return const SliverToBoxAdapter(); - } - final colorScheme = Theme.of(context).colorScheme; - return SliverList.separated( - itemCount: opus.length, - itemBuilder: (context, index) { - final element = opus[index]; - try { - switch (element.paraType) { - case 1 || 4: - return SelectableText.rich( - textAlign: element.align == 1 ? TextAlign.center : null, - TextSpan( - children: element.text?.nodes?.map((item) { - if (item.rich != null) { - return TextSpan( - text: '\u{1F517}${item.rich?.text}', - style: TextStyle( - decoration: item.rich?.style?.strikethrough == true - ? TextDecoration.lineThrough - : null, - fontStyle: item.rich?.style?.italic == true - ? FontStyle.italic - : null, - fontWeight: item.rich?.style?.bold == true - ? FontWeight.bold - : null, - color: colorScheme.primary, - ), - recognizer: TapGestureRecognizer() - ..onTap = () { - if (item.rich?.jumpUrl != null) { - PiliScheme.routePushFromUrl(item.rich!.jumpUrl!); - } - }, - ); - } - return TextSpan( - text: item.word?.words, - style: TextStyle( - decoration: item.word?.style?.strikethrough == true - ? TextDecoration.lineThrough - : null, - fontStyle: item.word?.style?.italic == true - ? FontStyle.italic - : null, - fontWeight: - item.word?.style?.bold == true ? FontWeight.bold : null, - color: item.word?.color != null - ? Color(item.word!.color!) - : null, - fontSize: item.word?.fontSize, - ), - ); - }).toList()), - ); - case 2 when (element.pic != null): - element.pic!.pics!.first.onCalHeight(maxWidth); - return Hero( - tag: element.pic!.pics!.first.url!, - child: GestureDetector( - onTap: () { - if (callback != null) { - callback([element.pic!.pics!.first.url!], 0); - } else { - context.imageView( - initialPage: 0, - imgList: [ - SourceModel(url: element.pic!.pics!.first.url!) - ], - ); - } - }, - child: NetworkImgLayer( - width: maxWidth, - height: element.pic!.pics!.first.calHeight, - src: element.pic!.pics!.first.url!, - quality: 60, - ), - ), - ); - case 3 when (element.line != null): - return CachedNetworkImage( - width: maxWidth, - fit: BoxFit.contain, - height: element.line?.pic?.height?.toDouble(), - imageUrl: Utils.thumbnailImgUrl(element.line!.pic!.url!), - ); - case 5 when (element.list != null): - return SelectableText.rich( - TextSpan( - children: element.list!.items?.asMap().entries.map((entry) { - return TextSpan( - children: [ - WidgetSpan( - child: Icon(MdiIcons.circleMedium), - alignment: PlaceholderAlignment.middle, - ), - ...entry.value.nodes!.map((item) { - return TextSpan( - children: [ - TextSpan( - text: item.word?.words, - style: TextStyle( - decoration: - item.word?.style?.strikethrough == true - ? TextDecoration.lineThrough - : null, - fontStyle: item.word?.style?.italic == true - ? FontStyle.italic - : null, - fontWeight: item.word?.style?.bold == true - ? FontWeight.bold - : null, - color: item.word?.color != null - ? Color(item.word!.color!) - : null, - fontSize: item.word?.fontSize, - ), - ), - ], - ); - }), - if (entry.key < element.list!.items!.length - 1) - const TextSpan(text: '\n'), - ], - ); - }).toList(), - ), - ); - case 6 when (element.linkCard?.card?.ugc != null): - return Material( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), - ), - color: colorScheme.onInverseSurface, - child: InkWell( - onTap: () { - try { - PiliScheme.videoPush( - int.parse(element.linkCard!.card!.oid!), - null, - ); - } catch (_) {} - }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - NetworkImgLayer( - radius: 6, - width: 65 * StyleString.aspectRatio, - height: 65, - src: element.linkCard!.card!.ugc!.cover, - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(element.linkCard!.card!.ugc!.title!), - Text( - element.linkCard!.card!.ugc!.descSecond!, - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ); - case 7 when (element.code != null): - final Highlight highlight = Highlight() - ..registerLanguages(builtinAllLanguages); - final HighlightResult result = highlight.highlightAuto( - element.code!.content!, - element.code!.lang == 'language-clike' - ? const ['c', 'java'] - : [ - element.code!.lang! - .replaceAll('language-', '') - .replaceAll('like', ''), - ]); - final TextSpanRenderer renderer = TextSpanRenderer( - const TextStyle(), builtinAllThemes['github']!); - result.render(renderer); - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8)), - color: colorScheme.onInverseSurface, - ), - width: double.infinity, - child: SelectableText.rich(renderer.span!), - ); - default: - debugPrint('unknown type ${element.paraType}'); - if (element.text?.nodes?.isNotEmpty == true) { + const OpusContent({ + super.key, + required this.opus, + this.callback, + required this.maxWidth, + }); + + static TextStyle _getStyle(Style? style, [Color? color, double? fontSize]) => + TextStyle( + decoration: + style?.strikethrough == true ? TextDecoration.lineThrough : null, + fontStyle: style?.italic == true ? FontStyle.italic : null, + fontWeight: style?.bold == true ? FontWeight.bold : null, + color: color, + fontSize: fontSize, + ); + + static TextSpan _getSpan(Word? word) => TextSpan( + text: word?.words, + style: _getStyle( + word?.style, + word?.color != null ? Color(word!.color!) : null, + word?.fontSize, + )); + + @override + Widget build(BuildContext context) { + debugPrint('opusContent'); + + if (opus.isEmpty) { + return const SliverToBoxAdapter(); + } + final colorScheme = Theme.of(context).colorScheme; + return SliverList.separated( + itemCount: opus.length, + itemBuilder: (context, index) { + final element = opus[index]; + try { + switch (element.paraType) { + case 1: return SelectableText.rich( textAlign: element.align == 1 ? TextAlign.center : null, TextSpan( - children: element.text!.nodes!.map((item) { - return TextSpan( - text: item.word?.words, - style: TextStyle( - decoration: item.word?.style?.strikethrough == true - ? TextDecoration.lineThrough - : null, - fontStyle: item.word?.style?.italic == true - ? FontStyle.italic - : null, - fontWeight: item.word?.style?.bold == true - ? FontWeight.bold - : null, - color: item.word?.color != null - ? Color(item.word!.color!) - : null, - fontSize: item.word?.fontSize, - ), - ); + children: element.text?.nodes?.map((item) { + if (item.rich != null) { + return TextSpan( + text: '\u{1F517}${item.rich?.text}', + style: _getStyle(item.rich?.style, colorScheme.primary), + recognizer: item.rich?.jumpUrl == null + ? null + : (TapGestureRecognizer() + ..onTap = () { + PiliScheme.routePushFromUrl(item.rich!.jumpUrl!); + }), + ); + } + return _getSpan(item.word); }).toList()), ); - } + case 4: + return Container( + padding: const EdgeInsets.only(left: 8), + decoration: const BoxDecoration( + border: Border( + left: BorderSide(color: Color(0xFFE0E0E0), width: 4), + ), + ), + child: SelectableText.rich( + textAlign: element.align == 1 ? TextAlign.center : null, + TextSpan( + children: element.text?.nodes?.map((item) { + if (item.rich != null) { + return TextSpan( + text: '\u{1F517}${item.rich?.text}', + style: _getStyle(item.rich?.style, colorScheme.primary), + recognizer: item.rich?.jumpUrl == null + ? null + : (TapGestureRecognizer() + ..onTap = () { + PiliScheme.routePushFromUrl( + item.rich!.jumpUrl!); + }), + ); + } + return TextSpan( + text: item.word?.words, + style: _getStyle( + item.word?.style, + item.word?.color != null + ? Color(item.word!.color!).withOpacity(0.7) + : colorScheme.onSurface.withOpacity(0.7), + item.word?.fontSize, + )); + }).toList()), + ), + ); + case 2 when (element.pic != null): + element.pic!.pics!.first.onCalHeight(maxWidth); + return Hero( + tag: element.pic!.pics!.first.url!, + child: GestureDetector( + onTap: () { + if (callback != null) { + callback!([element.pic!.pics!.first.url!], 0); + } else { + context.imageView( + initialPage: 0, + imgList: [ + SourceModel(url: element.pic!.pics!.first.url!) + ], + ); + } + }, + child: NetworkImgLayer( + width: maxWidth, + height: element.pic!.pics!.first.calHeight, + src: element.pic!.pics!.first.url!, + quality: 60, + ), + ), + ); + case 3 when (element.line != null): + return CachedNetworkImage( + width: maxWidth, + fit: BoxFit.contain, + height: element.line!.pic!.height?.toDouble(), + imageUrl: Utils.thumbnailImgUrl(element.line!.pic!.url!), + ); + case 5 when (element.list != null): + return SelectableText.rich( + TextSpan( + children: element.list!.items?.indexed.map((entry) { + return TextSpan( + children: [ + WidgetSpan( + child: Icon(MdiIcons.circleMedium), + alignment: PlaceholderAlignment.middle, + ), + ...entry.$2.nodes!.map((item) { + return _getSpan(item.word); + }), + if (entry.$1 < element.list!.items!.length - 1) + const TextSpan(text: '\n'), + ], + ); + }).toList(), + ), + ); + case 6 when (element.linkCard?.card?.ugc != null): + return Material( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + color: colorScheme.onInverseSurface, + child: InkWell( + onTap: () { + try { + PiliScheme.videoPush( + int.parse(element.linkCard!.card!.oid!), + null, + ); + } catch (_) {} + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + NetworkImgLayer( + radius: 6, + width: 65 * StyleString.aspectRatio, + height: 65, + src: element.linkCard!.card!.ugc!.cover, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(element.linkCard!.card!.ugc!.title!), + Text( + element.linkCard!.card!.ugc!.descSecond!, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + case 7 when (element.code != null): + final Highlight highlight = Highlight() + ..registerLanguages(builtinAllLanguages); + final HighlightResult result = highlight.highlightAuto( + element.code!.content!, + element.code!.lang == 'language-clike' + ? const ['c', 'java'] + : [ + element.code!.lang! + .replaceAll('language-', '') + .replaceAll('like', ''), + ]); + final TextSpanRenderer renderer = TextSpanRenderer( + const TextStyle(), builtinAllThemes['github']!); + result.render(renderer); + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(8)), + color: colorScheme.onInverseSurface, + ), + width: double.infinity, + child: SelectableText.rich(renderer.span!), + ); + default: + debugPrint('unknown type ${element.paraType}'); + if (element.text?.nodes?.isNotEmpty == true) { + return SelectableText.rich( + textAlign: element.align == 1 ? TextAlign.center : null, + TextSpan( + children: element.text!.nodes! + .map((item) => _getSpan(item.word)) + .toList()), + ); + } - return SelectableText('不支持的类型 (${element.paraType})', - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.red, - )); + return SelectableText('不支持的类型 (${element.paraType})', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.red, + )); + } + } catch (e) { + return SelectableText('错误的类型 $e', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.red, + )); } - } catch (e) { - return SelectableText('错误的类型 $e', - style: const TextStyle( - fontWeight: FontWeight.bold, - color: Colors.red, - )); - } - }, - separatorBuilder: (context, index) => const SizedBox(height: 10), - ); + }, + separatorBuilder: (context, index) => const SizedBox(height: 10), + ); + } }