feat: 动态页自渲染

This commit is contained in:
guozhigq
2023-09-08 16:46:51 +08:00
parent 396f9fbbac
commit 517ca032d2
8 changed files with 356 additions and 0 deletions

View File

@ -0,0 +1,136 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
// ignore: must_be_immutable
class HtmlRender extends StatelessWidget {
String? htmlContent;
final int? imgCount;
final List? imgList;
HtmlRender({
this.htmlContent,
this.imgCount,
this.imgList,
super.key,
});
@override
Widget build(BuildContext context) {
return Html(
data: htmlContent,
// tagsList: Html.tags..addAll(["form", "label", "input"]),
onLinkTap: (url, buildContext, attributes) => {},
extensions: [
TagExtension(
tagsToExtend: {"img"},
builder: (extensionContext) {
String? imgUrl = extensionContext.attributes['src'];
if (imgUrl!.startsWith('//')) {
imgUrl = 'https:$imgUrl';
}
if (imgUrl.startsWith('http://')) {
imgUrl = imgUrl.replaceAll('http://', 'https://');
}
print(imgUrl);
bool isEmote = imgUrl.contains('/emote/');
bool isMall = imgUrl.contains('/mall/');
if (isMall) {
return SizedBox();
}
// bool inTable =
// extensionContext.element!.previousElementSibling == null ||
// extensionContext.element!.nextElementSibling == null;
// imgUrl = Utils().imageUrl(imgUrl!);
return Image.network(
imgUrl,
width: isEmote ? 22 : null,
height: isEmote ? 22 : null,
);
},
),
],
style: {
"html": Style(
fontSize: FontSize.medium,
lineHeight: LineHeight.percent(140),
),
"body": Style(margin: Margins.zero, padding: HtmlPaddings.zero),
"a": Style(
color: Theme.of(context).colorScheme.primary,
textDecoration: TextDecoration.none,
),
"p": Style(
margin: Margins.only(bottom: 0),
),
"span": Style(
fontSize: FontSize.medium,
),
"li > p": Style(
display: Display.inline,
),
"li": Style(
padding: HtmlPaddings.only(bottom: 4),
textAlign: TextAlign.justify,
),
"image": Style(margin: Margins.only(top: 4, bottom: 4)),
"p > img": Style(margin: Margins.only(top: 4, bottom: 4)),
"code": Style(
backgroundColor: Theme.of(context).colorScheme.onInverseSurface),
"code > span": Style(textAlign: TextAlign.start),
"hr": Style(
margin: Margins.zero,
padding: HtmlPaddings.zero,
border: Border(
top: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
),
),
'table': Style(
border: Border(
right: BorderSide(
width: 0.5,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
bottom: BorderSide(
width: 0.5,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
),
),
'tr': Style(
border: Border(
top: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
left: BorderSide(
width: 1.0,
color:
Theme.of(context).colorScheme.onBackground.withOpacity(0.3),
),
),
),
'thead': Style(
backgroundColor: Theme.of(context).colorScheme.background,
),
'th': Style(
padding: HtmlPaddings.only(left: 3, right: 3),
),
'td': Style(
padding: HtmlPaddings.all(4.0),
alignment: Alignment.center,
textAlign: TextAlign.center,
),
},
);
}
}

40
lib/http/html.dart Normal file
View File

@ -0,0 +1,40 @@
import 'package:html/dom.dart';
import 'package:html/parser.dart';
import 'package:pilipala/http/index.dart';
class HtmlHttp {
static Future reqHtml(id) async {
var response = await Request().get("https://www.bilibili.com/opus/$id");
Document rootTree = parse(response.data);
Element body = rootTree.body!;
Element appDom = body.querySelector('#app')!;
Element authorHeader = appDom.querySelector('.fixed-author-header')!;
// 头像
String avatar = authorHeader.querySelector('img')!.attributes['src']!;
avatar = 'https:${avatar.split('@')[0]}';
String uname =
authorHeader.querySelector('.fixed-author-header__author__name')!.text;
// 动态详情
Element opusDetail = appDom.querySelector('.opus-detail')!;
// 发布时间
String updateTime =
opusDetail.querySelector('.opus-module-author__pub__text')!.text;
//
String opusContent =
opusDetail.querySelector('.opus-module-content')!.innerHtml;
String commentId = opusDetail
.querySelector('.bili-comment-container')!
.className
.split(' ')[1]
.split('-')[2];
// List imgList = opusDetail.querySelectorAll('bili-album__preview__picture__img');
return {
'status': true,
'avatar': avatar,
'uname': uname,
'updateTime': updateTime,
'content': opusContent,
'commentId': commentId
};
}
}

View File

@ -0,0 +1,19 @@
import 'package:get/get.dart';
import 'package:pilipala/http/html.dart';
class HtmlRenderController extends GetxController {
late String id;
late Map response;
@override
void onInit() {
super.onInit();
id = Get.parameters['id']!;
}
Future reqHtml() async {
var res = await HtmlHttp.reqHtml(id);
response = res;
return res;
}
}

View File

@ -0,0 +1,4 @@
library html_render;
export './controller.dart';
export './view.dart';

117
lib/pages/html/view.dart Normal file
View File

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/html_render.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'controller.dart';
class HtmlRenderPage extends StatefulWidget {
const HtmlRenderPage({super.key});
@override
State<HtmlRenderPage> createState() => _HtmlRenderPageState();
}
class _HtmlRenderPageState extends State<HtmlRenderPage> {
HtmlRenderController htmlRenderCtr = Get.put(HtmlRenderController());
late String title;
late String id;
@override
void initState() {
super.initState();
title = Get.parameters['title']!;
id = Get.parameters['id']!;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text(title),
),
body: SingleChildScrollView(
child: Column(
children: [
FutureBuilder(
future: htmlRenderCtr.reqHtml(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data['status']) {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: Row(
children: [
NetworkImgLayer(
width: 40,
height: 40,
type: 'avatar',
src: htmlRenderCtr.response['avatar']!,
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(htmlRenderCtr.response['uname'],
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.titleSmall!
.fontSize,
)),
Text(
htmlRenderCtr.response['updateTime'],
style: TextStyle(
color:
Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
),
),
],
),
const Spacer(),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: HtmlRender(
htmlContent: htmlRenderCtr.response['content'],
),
),
Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 8,
color: Theme.of(context)
.dividerColor
.withOpacity(0.05),
),
),
),
),
],
);
} else {
return Text('error');
}
} else {
// 骨架屏
return const SizedBox();
}
},
),
],
),
),
);
}
}

View File

@ -14,6 +14,7 @@ import 'package:pilipala/pages/follow/index.dart';
import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/html/index.dart';
import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/pages/liveRoom/view.dart';
import 'package:pilipala/pages/member/index.dart';
@ -107,6 +108,8 @@ class Routes {
name: '/displayModeSetting', page: () => const SetDiaplayMode()),
// 关于
CustomGetPage(name: '/about', page: () => const AboutPage()),
//
CustomGetPage(name: '/htmlRender', page: () => const HtmlRenderPage()),
];
}

View File

@ -257,6 +257,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
csslib:
dependency: transitive
description:
name: csslib
sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f"
url: "https://pub.dev"
source: hosted
version: "0.17.3"
cupertino_icons:
dependency: "direct main"
description:
@ -454,6 +462,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.0"
flutter_html:
dependency: "direct main"
description:
name: flutter_html
sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee"
url: "https://pub.dev"
source: hosted
version: "3.0.0-beta.2"
flutter_launcher_icons:
dependency: "direct dev"
description:
@ -581,6 +597,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
html:
dependency: "direct main"
description:
name: html
sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
url: "https://pub.dev"
source: hosted
version: "0.15.4"
http:
dependency: transitive
description:
@ -669,6 +693,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
list_counter:
dependency: transitive
description:
name: list_counter
sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237
url: "https://pub.dev"
source: hosted
version: "1.0.2"
loading_more_list:
dependency: "direct main"
description:

View File

@ -119,6 +119,11 @@ dependencies:
system_proxy: ^0.1.0
# pip
floating: ^2.0.1
# html解析
html: ^0.15.4
# html渲染
flutter_html: ^3.0.0-beta.2
dev_dependencies:
flutter_test: