opt opus rich text

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-05-19 11:56:24 +08:00
parent a360212dc7
commit 8fd62cf2f3
7 changed files with 173 additions and 99 deletions

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/models/dynamics/vote_model.dart'; import 'package:PiliPlus/models/dynamics/vote_model.dart';
class ArticleContentModel { class ArticleContentModel {
@@ -33,6 +34,7 @@ class Pic {
num? height; num? height;
num? size; num? size;
String? liveUrl; String? liveUrl;
bool? isLongPic;
Pic.fromJson(Map<String, dynamic> json) { Pic.fromJson(Map<String, dynamic> json) {
url = json['url']; url = json['url'];
@@ -42,6 +44,9 @@ class Pic {
pics = (json['pics'] as List?)?.map((item) => Pic.fromJson(item)).toList(); pics = (json['pics'] as List?)?.map((item) => Pic.fromJson(item)).toList();
style = json['style']; style = json['style'];
liveUrl = json['live_url']; liveUrl = json['live_url'];
if (width != null && height != null) {
isLongPic = (height! / width!) > 22 / 9;
}
} }
} }
@@ -71,26 +76,28 @@ class Text {
Text({ Text({
this.nodes, this.nodes,
}); });
List<Nodes>? nodes; List<Node>? nodes;
Text.fromJson(Map<String, dynamic> json) { Text.fromJson(Map<String, dynamic> json) {
nodes = nodes =
(json['nodes'] as List?)?.map((item) => Nodes.fromJson(item)).toList(); (json['nodes'] as List?)?.map((item) => Node.fromJson(item)).toList();
} }
} }
class Nodes { class Node {
int? nodeType; int? nodeType;
Word? word; Word? word;
Rich? rich; Rich? rich;
Formula? formula; Formula? formula;
String? type;
Nodes.fromJson(Map<String, dynamic> json) { Node.fromJson(Map<String, dynamic> json) {
nodeType = json['node_type']; nodeType = json['node_type'];
word = json['word'] == null ? null : Word.fromJson(json['word']); word = json['word'] == null ? null : Word.fromJson(json['word']);
rich = json['rich'] == null ? null : Rich.fromJson(json['rich']); rich = json['rich'] == null ? null : Rich.fromJson(json['rich']);
formula = formula =
json['formula'] == null ? null : Formula.fromJson(json['formula']); json['formula'] == null ? null : Formula.fromJson(json['formula']);
type = json['type'];
} }
} }
@@ -143,12 +150,18 @@ class Rich {
String? jumpUrl; String? jumpUrl;
String? origText; String? origText;
String? text; String? text;
String? type;
String? rid;
Emoji? emoji;
Rich.fromJson(Map<String, dynamic> json) { Rich.fromJson(Map<String, dynamic> json) {
style = json['style'] == null ? null : Style.fromJson(json['style']); style = json['style'] == null ? null : Style.fromJson(json['style']);
jumpUrl = json['jump_url']; jumpUrl = json['jump_url'];
origText = json['orig_text']; origText = json['orig_text'];
text = json['text']; text = json['text'];
type = json['type'];
rid = json['rid'];
emoji = json['emoji'] == null ? null : Emoji.fromJson(json['emoji']);
} }
} }
@@ -368,12 +381,12 @@ class L1st {
class Item { class Item {
int? level; int? level;
int? order; int? order;
List<Nodes>? nodes; List<Node>? nodes;
Item.fromJson(Map<String, dynamic> json) { Item.fromJson(Map<String, dynamic> json) {
level = json['level']; level = json['level'];
order = json['order']; order = json['order'];
nodes = (json['nodes'] as List?)?.map((e) => Nodes.fromJson(e)).toList(); nodes = (json['nodes'] as List?)?.map((e) => Node.fromJson(e)).toList();
} }
} }

View File

@@ -825,24 +825,20 @@ class RichTextNodeItem {
} }
class Emoji { class Emoji {
Emoji({ // String? iconUrl;
this.iconUrl, // String? webpUrl;
this.size, // String? gifUrl;
this.text, String? url;
this.type, late num size;
});
String? iconUrl;
String? webpUrl;
String? gifUrl;
double? size;
String? text; String? text;
int? type; num? type;
Emoji.fromJson(Map<String, dynamic> json) { Emoji.fromJson(Map<String, dynamic> json) {
iconUrl = json['icon_url']; // iconUrl = json['icon_url'];
webpUrl = json['webp_url']; // webpUrl = json['webp_url'];
gifUrl = json['gif_url']; // gifUrl = json['gif_url'];
size = json['size'].toDouble(); url = json['webp_url'] ?? json['gif_url'] ?? json['icon_url'];
size = json['size'] ?? 1;
text = json['text']; text = json['text'];
type = json['type']; type = json['type'];
} }

View File

@@ -444,8 +444,9 @@ class _ArticlePageState extends State<ArticlePage>
}, },
itemCount: length, itemCount: length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final url = pics[0].url!; final pic = pics[index];
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
context.imageView( context.imageView(
imgList: pics imgList: pics
@@ -456,10 +457,28 @@ class _ArticlePageState extends State<ArticlePage>
); );
}, },
child: Hero( child: Hero(
tag: url, tag: pic.url!,
child: CachedNetworkImage( child: Stack(
imageUrl: clipBehavior: Clip.none,
Utils.thumbnailImgUrl(url, 60), alignment: Alignment.center,
children: [
Positioned.fill(
child: CachedNetworkImage(
fit: pic.isLongPic == true
? BoxFit.cover
: null,
imageUrl: Utils.thumbnailImgUrl(
pic.url, 60),
),
),
if (pic.isLongPic == true)
PBadge(
text: '长图',
type: PBadgeType.primary,
right: paddingRight,
bottom: 12,
),
],
), ),
), ),
); );

View File

@@ -4,8 +4,9 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/image/image_view.dart'; import 'package:PiliPlus/common/widgets/image/image_view.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models/dynamics/article_content_model.dart' import 'package:PiliPlus/models/dynamics/article_content_model.dart'
show ArticleContentModel, Style, Word; show ArticleContentModel, Rich, Style, Word;
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/widgets/vote.dart'; import 'package:PiliPlus/pages/dynamics/widgets/vote.dart';
import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/app_scheme.dart';
@@ -68,46 +69,78 @@ class OpusContent extends StatelessWidget {
switch (element.paraType) { switch (element.paraType) {
case 1 || 4: case 1 || 4:
final isQuote = element.paraType == 4; final isQuote = element.paraType == 4;
Widget widget = SelectableText.rich( Widget widget = SelectionArea(
textAlign: element.align == 1 ? TextAlign.center : null, child: Text.rich(
TextSpan( textAlign: element.align == 1 ? TextAlign.center : null,
children: element.text?.nodes?.map<TextSpan>((item) { TextSpan(
if (item.rich != null) { children: element.text?.nodes?.map((item) {
return TextSpan( switch (item.type) {
text: '\u{1F517}${item.rich!.text}', case 'TEXT_NODE_TYPE_RICH' when (item.rich != null):
style: _getStyle(item.rich!.style, colorScheme.primary), Rich rich = item.rich!;
recognizer: item.rich!.jumpUrl == null switch (rich.type) {
? null case 'RICH_TEXT_NODE_TYPE_EMOJI':
: (TapGestureRecognizer() Emoji emoji = rich.emoji!;
..onTap = () { final size = 20.0 * emoji.size;
PiliScheme.routePushFromUrl(item.rich!.jumpUrl!); return WidgetSpan(
}), child: NetworkImgLayer(
); width: size,
} else if (item.formula != null) { height: size,
// TEXT_NODE_TYPE_FORMULA src: emoji.url,
return TextSpan( type: ImageType.emote,
children: [ ),
WidgetSpan( );
child: SizedBox( default:
height: 65, return TextSpan(
child: CachedNetworkSVGImage( text:
'https://api.bilibili.com/x/web-frontend/mathjax/tex?formula=${Uri.encodeComponent(item.formula!.latexContent!)}', '${rich.type == 'RICH_TEXT_NODE_TYPE_WEB' ? '\u{1F517}' : ''}${item.rich!.text}',
colorFilter: ColorFilter.mode( style: _getStyle(
colorScheme.onSurfaceVariant, rich.style,
BlendMode.srcIn, rich.type == 'RICH_TEXT_NODE_TYPE_TEXT'
? null
: colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
switch (rich.type) {
case 'RICH_TEXT_NODE_TYPE_AT':
Get.toNamed('/member?mid=${rich.rid}');
// case 'RICH_TEXT_NODE_TYPE_TOPIC':
default:
if (rich.jumpUrl != null) {
PiliScheme.routePushFromUrl(
rich.jumpUrl!,
);
}
}
},
);
}
case 'TEXT_NODE_TYPE_FORMULA' when (item.formula != null):
return TextSpan(
children: [
WidgetSpan(
child: CachedNetworkSVGImage(
height: 65,
'https://api.bilibili.com/x/web-frontend/mathjax/tex?formula=${Uri.encodeComponent(item.formula!.latexContent!)}',
colorFilter: ColorFilter.mode(
colorScheme.onSurfaceVariant,
BlendMode.srcIn,
),
alignment: Alignment.centerLeft,
placeholderBuilder: (_) =>
const SizedBox.shrink(),
), ),
alignment: Alignment.centerLeft,
placeholderBuilder: (context) =>
const SizedBox.shrink(),
), ),
), ],
), );
], default:
); return _getSpan(
} item.word,
return _getSpan( isQuote ? colorScheme.onSurfaceVariant : null,
item.word, isQuote ? colorScheme.onSurfaceVariant : null); );
}).toList()), }
}).toList()),
),
); );
if (isQuote) { if (isQuote) {
widget = Container( widget = Container(
@@ -175,23 +208,25 @@ class OpusContent extends StatelessWidget {
imageUrl: Utils.thumbnailImgUrl(element.line!.pic!.url!), imageUrl: Utils.thumbnailImgUrl(element.line!.pic!.url!),
); );
case 5 when (element.list != null): case 5 when (element.list != null):
return SelectableText.rich( return SelectionArea(
TextSpan( child: Text.rich(
children: element.list!.items?.indexed.map((entry) { TextSpan(
return TextSpan( children: element.list!.items?.indexed.map((entry) {
children: [ return TextSpan(
WidgetSpan( children: [
child: Icon(MdiIcons.circleMedium), WidgetSpan(
alignment: PlaceholderAlignment.middle, child: Icon(MdiIcons.circleMedium),
), alignment: PlaceholderAlignment.middle,
...entry.$2.nodes!.map((item) { ),
return _getSpan(item.word); ...entry.$2.nodes!.map((item) {
}), return _getSpan(item.word);
if (entry.$1 < element.list!.items!.length - 1) }),
const TextSpan(text: '\n'), if (entry.$1 < element.list!.items!.length - 1)
], const TextSpan(text: '\n'),
); ],
}).toList(), );
}).toList(),
),
), ),
); );
case 6: case 6:
@@ -513,32 +548,42 @@ class OpusContent extends StatelessWidget {
color: colorScheme.onInverseSurface, color: colorScheme.onInverseSurface,
), ),
width: double.infinity, width: double.infinity,
child: SelectableText.rich(renderer.span!), child: SelectionArea(child: Text.rich(renderer.span!)),
); );
default: default:
debugPrint('unknown type ${element.paraType}'); debugPrint('unknown type ${element.paraType}');
if (element.text?.nodes?.isNotEmpty == true) { if (element.text?.nodes?.isNotEmpty == true) {
return SelectableText.rich( return SelectionArea(
textAlign: element.align == 1 ? TextAlign.center : null, child: Text.rich(
TextSpan( textAlign: element.align == 1 ? TextAlign.center : null,
children: element.text!.nodes! TextSpan(
.map<TextSpan>((item) => _getSpan(item.word)) children: element.text!.nodes!
.toList()), .map<TextSpan>((item) => _getSpan(item.word))
.toList()),
),
); );
} }
return SelectableText('不支持的类型 (${element.paraType})', return SelectionArea(
child: Text(
'不支持的类型 (${element.paraType})',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.red, color: Colors.red,
)); ),
),
);
} }
} catch (e) { } catch (e) {
return SelectableText('错误的类型 $e', return SelectionArea(
child: Text(
'错误的类型 $e',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.red, color: Colors.red,
)); ),
),
);
} }
}, },
separatorBuilder: (context, index) => const SizedBox(height: 10), separatorBuilder: (context, index) => const SizedBox(height: 10),

View File

@@ -130,13 +130,14 @@ TextSpan? richNode(
break; break;
// 表情 // 表情
case 'RICH_TEXT_NODE_TYPE_EMOJI' when (i.emoji != null): case 'RICH_TEXT_NODE_TYPE_EMOJI' when (i.emoji != null):
final size = i.emoji!.size * 20.0;
spanChildren.add( spanChildren.add(
WidgetSpan( WidgetSpan(
child: NetworkImgLayer( child: NetworkImgLayer(
src: i.emoji!.webpUrl ?? i.emoji!.gifUrl ?? i.emoji!.iconUrl, src: i.emoji!.url,
type: ImageType.emote, type: ImageType.emote,
width: (i.emoji!.size ?? 1) * 20, width: size,
height: (i.emoji!.size ?? 1) * 20, height: size,
), ),
), ),
); );

View File

@@ -59,7 +59,7 @@ class _UpPanelState extends State<UpPanel> {
children: [ children: [
TextSpan( TextSpan(
text: text:
'Live(${widget.dynamicsController.upData.value.liveUsers?.count ?? "0"})', 'Live(${widget.dynamicsController.upData.value.liveUsers?.count})', // checked
), ),
if (!isTop) ...[ if (!isTop) ...[
const TextSpan(text: '\n'), const TextSpan(text: '\n'),

View File

@@ -158,7 +158,7 @@ class _MemberHomeState extends State<MemberHome>
], ],
if (res.article?.item?.isNotEmpty == true) ...[ if (res.article?.item?.isNotEmpty == true) ...[
_videoHeader( _videoHeader(
title: '专栏', title: '图文',
param: 'contribute', param: 'contribute',
param1: 'opus', param1: 'opus',
count: res.article!.count!, count: res.article!.count!,