diff --git a/lib/common/widgets/html_render.dart b/lib/common/widgets/html_render.dart new file mode 100644 index 00000000..6733d7cb --- /dev/null +++ b/lib/common/widgets/html_render.dart @@ -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, + ), + }, + ); + } +} diff --git a/lib/http/html.dart b/lib/http/html.dart new file mode 100644 index 00000000..ec92bcfa --- /dev/null +++ b/lib/http/html.dart @@ -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 + }; + } +} diff --git a/lib/pages/html/controller.dart b/lib/pages/html/controller.dart new file mode 100644 index 00000000..2ec55621 --- /dev/null +++ b/lib/pages/html/controller.dart @@ -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; + } +} diff --git a/lib/pages/html/index.dart b/lib/pages/html/index.dart new file mode 100644 index 00000000..c62e60b7 --- /dev/null +++ b/lib/pages/html/index.dart @@ -0,0 +1,4 @@ +library html_render; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/html/view.dart b/lib/pages/html/view.dart new file mode 100644 index 00000000..f8572b14 --- /dev/null +++ b/lib/pages/html/view.dart @@ -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 createState() => _HtmlRenderPageState(); +} + +class _HtmlRenderPageState extends State { + 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(); + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 7edb435b..03088ca4 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -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()), ]; } diff --git a/pubspec.lock b/pubspec.lock index d30fa01b..8d83afb9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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: diff --git a/pubspec.yaml b/pubspec.yaml index e32da85c..a9e48867 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: