mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-05-31 08:08:19 +08:00
opt opus rich text
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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!,
|
||||||
|
|||||||
Reference in New Issue
Block a user