refa: article (#757)

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
dom
2025-04-26 14:54:22 +08:00
committed by GitHub
parent 64f7ba2a1a
commit 40fb93f036
87 changed files with 2628 additions and 993 deletions

View File

@@ -1,94 +0,0 @@
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
show SourceModel;
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/models/dynamics/article_content_model.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
Widget articleContent({
required BuildContext context,
required List<ArticleContentModel> list,
Function(List<String>, int)? callback,
required double maxWidth,
}) {
debugPrint('articleContent');
List<String>? imgList = list
.where((item) => item.pic != null)
.toList()
.map((item) => item.pic?.pics?.first.url ?? '')
.toList();
return SliverList.separated(
itemCount: list.length,
itemBuilder: (context, index) {
ArticleContentModel item = list[index];
if (item.text != null) {
List<InlineSpan> spanList = [];
item.text?.nodes?.forEach((item) {
spanList.add(TextSpan(
text: item.word?.words,
style: TextStyle(
letterSpacing: 0.3,
fontSize: 17,
height: LineHeight.percent(125).size,
fontStyle:
item.word?.style?.italic == true ? FontStyle.italic : null,
color: item.word?.color != null
? Color(int.parse(
item.word!.color!.replaceFirst('#', 'FF'),
radix: 16,
))
: null,
decoration: item.word?.style?.strikethrough == true
? TextDecoration.lineThrough
: null,
fontWeight:
item.word?.style?.bold == true ? FontWeight.bold : null,
),
));
});
return SelectableText.rich(TextSpan(children: spanList));
} else if (item.line != null) {
return Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 10),
child: CachedNetworkImage(
imageUrl: item.line?.pic?.url?.http2https ?? '',
height: item.line?.pic?.height?.toDouble(),
),
);
} else if (item.pic != null) {
return Hero(
tag: item.pic!.pics!.first.url!,
child: GestureDetector(
onTap: () {
if (callback != null) {
callback(
imgList,
imgList.indexOf(item.pic!.pics!.first.url!),
);
} else {
context.imageView(
initialPage: imgList.indexOf(item.pic!.pics!.first.url!),
imgList: imgList.map((url) => SourceModel(url: url)).toList(),
);
}
},
child: NetworkImgLayer(
width: maxWidth,
height: maxWidth *
item.pic!.pics!.first.height! /
item.pic!.pics!.first.width!,
src: item.pic!.pics!.first.url,
),
),
);
} else {
return const SizedBox.shrink();
// return Text('unsupported content');
}
},
separatorBuilder: (context, index) => const SizedBox(height: 10),
);
}

View File

@@ -1,129 +0,0 @@
import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart'
show SourceModel;
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'network_img_layer.dart';
import 'package:html/dom.dart' as dom;
Widget htmlRender({
required BuildContext context,
required dom.Element element,
int? imgCount,
List<String>? imgList,
required double maxWidth,
Function(List<String>, int)? callback,
}) {
debugPrint('htmlRender');
return SelectionArea(
child: Html.fromElement(
documentElement: element,
onLinkTap: (String? url, Map<String, String> buildContext, attributes) {},
extensions: [
TagExtension(
tagsToExtend: <String>{'img'},
builder: (ExtensionContext extensionContext) {
try {
final Map<String, dynamic> attributes = extensionContext.attributes;
final List<dynamic> key = attributes.keys.toList();
String imgUrl = key.contains('src')
? attributes['src'] as String
: attributes['data-src'] as String;
imgUrl = imgUrl.contains('@') ? imgUrl.split('@').first : imgUrl;
final bool isEmote = imgUrl.contains('/emote/');
final bool isMall = imgUrl.contains('/mall/');
if (isMall) {
return const SizedBox.shrink();
}
String? clazz = attributes['class'];
String? height = RegExp(r'max-height:(\d+)px')
.firstMatch('${attributes['style']}')
?.group(1);
if (clazz?.contains('cut-off') == true || height != null) {
return CachedNetworkImage(
width: maxWidth,
height: height != null ? double.parse(height) : null,
imageUrl: Utils.thumbnailImgUrl(imgUrl),
fit: BoxFit.contain,
);
}
return Hero(
tag: imgUrl,
child: GestureDetector(
onTap: () {
if (callback != null) {
callback([imgUrl], 0);
} else {
context.imageView(
imgList: [SourceModel(url: imgUrl)],
);
}
},
child: NetworkImgLayer(
width: isEmote ? 22 : maxWidth,
height: isEmote ? 22 : 200,
src: imgUrl,
ignoreHeight: !isEmote,
),
),
);
} catch (err) {
return const SizedBox.shrink();
}
},
),
],
style: {
'html': Style(
fontSize: FontSize(16),
lineHeight: LineHeight.percent(160),
letterSpacing: 0.3,
),
'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
'a': Style(
color: Theme.of(context).colorScheme.primary,
textDecoration: TextDecoration.none,
),
'br': Style(
lineHeight: LineHeight.percent(-1),
),
'p': Style(
margin: Margins.only(bottom: 4),
),
'span': Style(
fontSize: FontSize.large,
height: Height(1.8),
),
'div': Style(height: Height.auto()),
'li > p': Style(
display: Display.inline,
),
'li': Style(
padding: HtmlPaddings.only(bottom: 4),
textAlign: TextAlign.justify,
),
'img': Style(margin: Margins.only(top: 4, bottom: 4)),
'h1,h2': Style(
fontSize: FontSize.xLarge,
fontWeight: FontWeight.bold,
margin: Margins.only(bottom: 8),
),
'h3,h4,h5': Style(
fontSize: FontSize(16),
fontWeight: FontWeight.bold,
margin: Margins.only(bottom: 4),
),
'figcaption': Style(
fontSize: FontSize.large,
textAlign: TextAlign.center,
),
'strong': Style(fontWeight: FontWeight.bold),
'figure': Style(
margin: Margins.zero,
),
},
));
}

View File

@@ -9,14 +9,13 @@ class NetworkImgLayer extends StatelessWidget {
super.key,
this.src,
required this.width,
required this.height,
this.height,
this.type,
this.fadeOutDuration,
this.fadeInDuration,
// 图片质量 默认1%
this.quality,
this.semanticsLabel,
this.ignoreHeight,
this.radius,
this.imageBuilder,
this.isLongPic,
@@ -27,13 +26,12 @@ class NetworkImgLayer extends StatelessWidget {
final String? src;
final double width;
final double height;
final double? height;
final String? type;
final Duration? fadeOutDuration;
final Duration? fadeInDuration;
final int? quality;
final String? semanticsLabel;
final bool? ignoreHeight;
final double? radius;
final ImageWidgetBuilder? imageBuilder;
final Function? isLongPic;
@@ -59,7 +57,7 @@ class NetworkImgLayer extends StatelessWidget {
Widget _buildImage(context) {
int? memCacheWidth, memCacheHeight;
if (ignoreHeight == true || callback?.call() == true || width <= height) {
if (height == null || callback?.call() == true || width <= height!) {
memCacheWidth = width.cacheSize(context);
} else {
memCacheHeight = height.cacheSize(context);
@@ -67,7 +65,7 @@ class NetworkImgLayer extends StatelessWidget {
return CachedNetworkImage(
imageUrl: Utils.thumbnailImgUrl(src, quality),
width: width,
height: ignoreHeight == null || ignoreHeight == false ? height : null,
height: height,
memCacheWidth: memCacheWidth,
memCacheHeight: memCacheHeight,
fit: boxFit ?? BoxFit.cover,

View File

@@ -138,7 +138,7 @@ class _SavePanelState extends State<SavePanel> {
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri',
};
} catch (_) {}
} else if (currentRoute.startsWith('/htmlRender')) {
} else if (currentRoute.startsWith('/articlePage')) {
try {
final type = _item.type.toInt();
late final oid = _item.oid;

View File

@@ -53,16 +53,16 @@ class VideoCardV extends StatelessWidget {
// 动态
case 'picture':
try {
String dynamicType = 'picture';
String type = 'picture';
String uri = videoItem.uri!;
String id = '';
if (uri.startsWith('bilibili://article/')) {
dynamicType = 'read';
type = 'read';
RegExp regex = RegExp(r'\d+');
Match match = regex.firstMatch(uri)!;
String matchedNumber = match.group(0)!;
videoItem.param = int.parse(matchedNumber);
id = 'cv${videoItem.param}';
id = '${videoItem.param}';
}
if (uri.startsWith('http')) {
String id = Uri.parse(uri).path.split('/')[1];
@@ -71,12 +71,13 @@ class VideoCardV extends StatelessWidget {
return;
}
}
Get.toNamed('/htmlRender', parameters: {
'url': uri,
'title': videoItem.title,
'id': id,
'dynamicType': dynamicType
});
Get.toNamed(
'/articlePage',
parameters: {
'id': id,
'type': type,
},
);
} catch (err) {
SmartDialog.showToast(err.toString());
}