diff --git a/lib/common/widgets/html_render.dart b/lib/common/widgets/html_render.dart
index bf58d78c..0a728f86 100644
--- a/lib/common/widgets/html_render.dart
+++ b/lib/common/widgets/html_render.dart
@@ -1,6 +1,9 @@
+import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:get/get.dart';
+import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
+import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
import 'network_img_layer.dart';
// ignore: must_be_immutable
@@ -44,20 +47,52 @@ class HtmlRender extends StatelessWidget {
if (isMall) {
return const 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,
- // );
- return NetworkImgLayer(
- width: isEmote ? 22 : Get.size.width - 24,
- height: isEmote ? 22 : 200,
- src: imgUrl,
+ return InkWell(
+ onTap: () {
+ Navigator.of(context).push(
+ HeroDialogRoute(
+ builder: (BuildContext context) =>
+ InteractiveviewerGallery(
+ sources: imgList ?? [imgUrl],
+ initIndex: imgList?.indexOf(imgUrl) ?? 0,
+ itemBuilder: (
+ BuildContext context,
+ int index,
+ bool isFocus,
+ bool enablePageView,
+ ) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () {
+ if (enablePageView) {
+ Navigator.of(context).pop();
+ }
+ },
+ child: Center(
+ child: Hero(
+ tag: imgUrl,
+ child: CachedNetworkImage(
+ fadeInDuration:
+ const Duration(milliseconds: 0),
+ imageUrl: imgUrl,
+ fit: BoxFit.contain,
+ ),
+ ),
+ ),
+ );
+ },
+ onPageChanged: (int pageIndex) {},
+ ),
+ ),
+ );
+ },
+ child: CachedNetworkImage(imageUrl: imgUrl),
);
+ // return NetworkImgLayer(
+ // width: isEmote ? 22 : Get.size.width - 24,
+ // height: isEmote ? 22 : 200,
+ // src: imgUrl,
+ // );
} catch (err) {
return const SizedBox();
}
@@ -66,7 +101,7 @@ class HtmlRender extends StatelessWidget {
],
style: {
'html': Style(
- fontSize: FontSize.medium,
+ fontSize: FontSize.large,
lineHeight: LineHeight.percent(140),
),
'body': Style(margin: Margins.zero, padding: HtmlPaddings.zero),
@@ -78,7 +113,7 @@ class HtmlRender extends StatelessWidget {
margin: Margins.only(bottom: 10),
),
'span': Style(
- fontSize: FontSize.medium,
+ fontSize: FontSize.large,
height: Height(1.65),
),
'div': Style(height: Height.auto()),
diff --git a/lib/common/widgets/video_card_v.dart b/lib/common/widgets/video_card_v.dart
index 7a9ef39c..378c9f75 100644
--- a/lib/common/widgets/video_card_v.dart
+++ b/lib/common/widgets/video_card_v.dart
@@ -60,17 +60,13 @@ class VideoCardV extends StatelessWidget {
// 动态
case 'picture':
try {
- String dynamicType = 'picture';
String uri = videoItem.uri;
- String id = '';
if (videoItem.uri.startsWith('bilibili://article/')) {
// https://www.bilibili.com/read/cv27063554
- dynamicType = 'read';
RegExp regex = RegExp(r'\d+');
Match match = regex.firstMatch(videoItem.uri)!;
String matchedNumber = match.group(0)!;
videoItem.param = int.parse(matchedNumber);
- id = 'cv${videoItem.param}';
}
if (uri.startsWith('http')) {
String path = Uri.parse(uri).path;
@@ -88,11 +84,10 @@ class VideoCardV extends StatelessWidget {
return;
}
}
- Get.toNamed('/htmlRender', parameters: {
- 'url': uri,
+ Get.toNamed('/read', parameters: {
'title': videoItem.title,
- 'id': id,
- 'dynamicType': dynamicType
+ 'id': videoItem.param,
+ 'articleType': 'read'
});
} catch (err) {
SmartDialog.showToast(err.toString());
diff --git a/lib/http/read.dart b/lib/http/read.dart
index 4fff4547..22ca3503 100644
--- a/lib/http/read.dart
+++ b/lib/http/read.dart
@@ -1,6 +1,8 @@
import 'dart:convert';
+import 'dart:developer';
import 'package:html/parser.dart';
import 'package:pilipala/models/read/opus.dart';
+import 'package:pilipala/models/read/read.dart';
import 'init.dart';
class ReadHttp {
@@ -32,4 +34,23 @@ class ReadHttp {
'data': OpusDataModel.fromJson(jsonData),
};
}
+
+ // 解析专栏 cv格式
+ static Future parseArticleCv({required String id}) async {
+ var res = await Request().get(
+ 'https://www.bilibili.com/read/cv$id',
+ extra: {'ua': 'pc'},
+ );
+ String scriptContent =
+ extractScriptContents(parse(res.data).body!.outerHtml)[0];
+ int startIndex = scriptContent.indexOf('{');
+ int endIndex = scriptContent.lastIndexOf('};');
+ String jsonContent = scriptContent.substring(startIndex, endIndex + 1);
+ // 解析JSON字符串为Map
+ Map jsonData = json.decode(jsonContent);
+ return {
+ 'status': true,
+ 'data': ReadDataModel.fromJson(jsonData),
+ };
+ }
}
diff --git a/lib/models/read/opus.dart b/lib/models/read/opus.dart
index 3e233766..f2cf3d9c 100644
--- a/lib/models/read/opus.dart
+++ b/lib/models/read/opus.dart
@@ -302,14 +302,17 @@ class ModuleParagraphText {
class ModuleParagraphTextNode {
ModuleParagraphTextNode({
this.type,
+ this.nodeType,
this.word,
});
String? type;
+ int? nodeType;
ModuleParagraphTextNodeWord? word;
ModuleParagraphTextNode.fromJson(Map json) {
type = json['type'];
+ nodeType = json['node_type'];
word = json['word'] != null
? ModuleParagraphTextNodeWord.fromJson(json['word'])
: null;
diff --git a/lib/models/read/read.dart b/lib/models/read/read.dart
new file mode 100644
index 00000000..fe351da4
--- /dev/null
+++ b/lib/models/read/read.dart
@@ -0,0 +1,286 @@
+import 'package:pilipala/models/member/info.dart';
+
+import 'opus.dart';
+
+class ReadDataModel {
+ ReadDataModel({
+ this.cvid,
+ this.readInfo,
+ this.readViewInfo,
+ this.upInfo,
+ this.catalogList,
+ this.recommendInfoList,
+ this.hiddenInteraction,
+ this.isModern,
+ });
+
+ int? cvid;
+ ReadInfo? readInfo;
+ Map? readViewInfo;
+ Map? upInfo;
+ List? catalogList;
+ List? recommendInfoList;
+ bool? hiddenInteraction;
+ bool? isModern;
+
+ ReadDataModel.fromJson(Map json) {
+ cvid = json['cvid'];
+ readInfo =
+ json['readInfo'] != null ? ReadInfo.fromJson(json['readInfo']) : null;
+ readViewInfo = json['readViewInfo'];
+ upInfo = json['upInfo'];
+ if (json['catalogList'] != null) {
+ catalogList = [];
+ json['catalogList'].forEach((v) {
+ catalogList!.add(v);
+ });
+ }
+ if (json['recommendInfoList'] != null) {
+ recommendInfoList = [];
+ json['recommendInfoList'].forEach((v) {
+ recommendInfoList!.add(v);
+ });
+ }
+ hiddenInteraction = json['hiddenInteraction'];
+ isModern = json['isModern'];
+ }
+}
+
+class ReadInfo {
+ ReadInfo({
+ this.id,
+ this.category,
+ this.title,
+ this.summary,
+ this.bannerUrl,
+ this.author,
+ this.publishTime,
+ this.ctime,
+ this.mtime,
+ this.stats,
+ this.attributes,
+ this.words,
+ this.originImageUrls,
+ this.content,
+ this.opus,
+ this.dynIdStr,
+ this.totalArtNum,
+ });
+
+ int? id;
+ Map? category;
+ String? title;
+ String? summary;
+ String? bannerUrl;
+ Author? author;
+ int? publishTime;
+ int? ctime;
+ int? mtime;
+ Map? stats;
+ int? attributes;
+ int? words;
+ List? originImageUrls;
+ String? content;
+ Opus? opus;
+ String? dynIdStr;
+ int? totalArtNum;
+
+ ReadInfo.fromJson(Map json) {
+ id = json['id'];
+ category = json['category'];
+ title = json['title'];
+ summary = json['summary'];
+ bannerUrl = json['banner_url'];
+ author = Author.fromJson(json['author']);
+ publishTime = json['publish_time'];
+ ctime = json['ctime'];
+ mtime = json['mtime'];
+ stats = json['stats'];
+ attributes = json['attributes'];
+ words = json['words'];
+ if (json['origin_image_urls'] != null) {
+ originImageUrls = [];
+ json['origin_image_urls'].forEach((v) {
+ originImageUrls!.add(v);
+ });
+ }
+ content = json['content'];
+ opus = json['opus'] != null ? Opus.fromJson(json['opus']) : null;
+ dynIdStr = json['dyn_id_str'];
+ totalArtNum = json['total_art_num'];
+ }
+}
+
+class Author {
+ Author({
+ this.mid,
+ this.name,
+ this.face,
+ this.vip,
+ this.fans,
+ this.level,
+ });
+
+ int? mid;
+ String? name;
+ String? face;
+ Vip? vip;
+ int? fans;
+ int? level;
+
+ Author.fromJson(Map json) {
+ mid = json['mid'];
+ name = json['name'];
+ face = json['face'];
+ vip = json['vip'] != null ? Vip.fromJson(json['vip']) : null;
+ fans = json['fans'];
+ level = json['level'];
+ }
+}
+
+class Opus {
+ // "opus_id": 976625853207150600,
+ // "opus_source": 2,
+ // "title": "真的很想骂人 但又没什么好骂的",
+ // "content": {
+ // "paragraphs": [{
+ // "para_type": 1,
+ // "text": {
+ // "nodes": [{
+ // "node_type": 1,
+ // "word": {
+ // "words": "21年玩到今年4月的号没了 ow1的时候45的号 玩了三年 后面第9赛季一个英杰5的号(虽然是偷的 但我任何违规行为都没有还是给我封了) 最近玩的号叫velleity 只和队友打天梯以及训练赛 又没了 连带着我一个一把没玩过只玩过一场训练赛的小号也没了 实在是无话可说了...",
+ // "font_size": 17,
+ // "style": {},
+ // "font_level": "regular"
+ // }
+ // }]
+ // }
+ // }, {
+ // "para_type": 2,
+ // "pic": {
+ // "pics": [{
+ // "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Fba4e57459451fe74dcb70fd20bde9823316082117.jpg",
+ // "width": 1600,
+ // "height": 1000,
+ // "size": 588.482421875
+ // }],
+ // "style": 1
+ // }
+ // }, {
+ // "para_type": 1,
+ // "text": {
+ // "nodes": [{
+ // "node_type": 1,
+ // "word": {
+ // "words": "\n",
+ // "font_size": 17,
+ // "style": {},
+ // "font_level": "regular"
+ // }
+ // }]
+ // }
+ // }, {
+ // "para_type": 2,
+ // "pic": {
+ // "pics": [{
+ // "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002F0945be6b621091ddb8189482a87a36fb316082117.jpg",
+ // "width": 1600,
+ // "height": 1002,
+ // "size": 665.7861328125
+ // }],
+ // "style": 1
+ // }
+ // }, {
+ // "para_type": 1,
+ // "text": {
+ // "nodes": [{
+ // "node_type": 1,
+ // "word": {
+ // "words": "\n",
+ // "font_size": 17,
+ // "style": {},
+ // "font_level": "regular"
+ // }
+ // }]
+ // }
+ // }, {
+ // "para_type": 2,
+ // "pic": {
+ // "pics": [{
+ // "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Ffa60649f8786578a764a1e68a2c5d23f316082117.jpg",
+ // "width": 1600,
+ // "height": 999,
+ // "size": 332.970703125
+ // }],
+ // "style": 1
+ // }
+ // }, {
+ // "para_type": 1,
+ // "text": {
+ // "nodes": [{
+ // "node_type": 1,
+ // "word": {
+ // "words": "\n",
+ // "font_size": 17,
+ // "style": {},
+ // "font_level": "regular"
+ // }
+ // }]
+ // }
+ // }]
+ // },
+ // "pub_info": {
+ // "uid": 316082117,
+ // "pub_time": 1726226826
+ // },
+ // "article": {
+ // "category_id": 15,
+ // "cover": [{
+ // "url": "https:\u002F\u002Fi0.hdslb.com\u002Fbfs\u002Fnew_dyn\u002Fbanner\u002Feb10074186a62f98c18e1b5b9deb38be316082117.png",
+ // "width": 1071,
+ // "height": 315,
+ // "size": 225.625
+ // }]
+ // },
+ // "version": {
+ // "cvid": 38660379,
+ // "version_id": 101683514411343360
+ // }
+ Opus({
+ this.opusId,
+ this.opusSource,
+ this.title,
+ this.content,
+ });
+
+ int? opusId;
+ int? opusSource;
+ String? title;
+ Content? content;
+
+ Opus.fromJson(Map json) {
+ opusId = json['opus_id'];
+ opusSource = json['opus_source'];
+ title = json['title'];
+ content =
+ json['content'] != null ? Content.fromJson(json['content']) : null;
+ }
+}
+
+class Content {
+ Content({
+ this.paragraphs,
+ });
+
+ List? paragraphs;
+
+ Content.fromJson(Map json) {
+ if (json['paragraphs'] != null) {
+ paragraphs = [];
+ json['paragraphs'].forEach((v) {
+ paragraphs!.add(ModuleParagraph.fromJson(v));
+ });
+ }
+ }
+}
diff --git a/lib/pages/dynamics/controller.dart b/lib/pages/dynamics/controller.dart
index d9932659..df5b154e 100644
--- a/lib/pages/dynamics/controller.dart
+++ b/lib/pages/dynamics/controller.dart
@@ -154,12 +154,10 @@ class DynamicsController extends GetxController {
Iterable matches = digitRegExp.allMatches(jumpUrl);
String number = matches.first.group(0)!;
if (jumpUrl.contains('read')) {
- number = 'cv$number';
- Get.toNamed('/htmlRender', parameters: {
- 'url': url,
+ Get.toNamed('/read', parameters: {
'title': title,
'id': number,
- 'dynamicType': url.split('/')[1]
+ 'articleType': url.split('/')[1]
});
} else {
Get.toNamed('/opus', parameters: {
diff --git a/lib/pages/opus/text_helper.dart b/lib/pages/opus/text_helper.dart
index b22e7db5..0ec8b088 100644
--- a/lib/pages/opus/text_helper.dart
+++ b/lib/pages/opus/text_helper.dart
@@ -17,26 +17,46 @@ class TextHelper {
static TextSpan buildTextSpan(
ModuleParagraphTextNode node, int? align, BuildContext context) {
- switch (node.type) {
- case 'TEXT_NODE_TYPE_WORD':
- return TextSpan(
- text: node.word?.words ?? '',
- style: TextStyle(
- fontSize:
- node.word?.fontSize != null ? node.word!.fontSize! * 0.95 : 14,
- fontWeight: node.word?.style?.bold != null
- ? FontWeight.bold
- : FontWeight.normal,
- height: align == 1 ? 2 : 1.5,
- color: node.word?.color != null
- ? Color(
- int.parse(node.word!.color!.substring(1, 7), radix: 16) +
- 0xFF000000)
- : Theme.of(context).colorScheme.onBackground,
- ),
- );
- default:
- return const TextSpan(text: '');
+ // 获取node的所有key
+ if (node.nodeType != null) {
+ return TextSpan(
+ text: node.word?.words ?? '',
+ style: TextStyle(
+ fontSize:
+ node.word?.fontSize != null ? node.word!.fontSize! * 0.95 : 14,
+ fontWeight: node.word?.style?.bold != null
+ ? FontWeight.bold
+ : FontWeight.normal,
+ height: align == 1 ? 2 : 1.5,
+ color: node.word?.color != null
+ ? Color(int.parse(node.word!.color!.substring(1, 7), radix: 16) +
+ 0xFF000000)
+ : Theme.of(context).colorScheme.onBackground,
+ ),
+ );
+ } else {
+ switch (node.type) {
+ case 'TEXT_NODE_TYPE_WORD':
+ return TextSpan(
+ text: node.word?.words ?? '',
+ style: TextStyle(
+ fontSize: node.word?.fontSize != null
+ ? node.word!.fontSize! * 0.95
+ : 14,
+ fontWeight: node.word?.style?.bold != null
+ ? FontWeight.bold
+ : FontWeight.normal,
+ height: align == 1 ? 2 : 1.5,
+ color: node.word?.color != null
+ ? Color(
+ int.parse(node.word!.color!.substring(1, 7), radix: 16) +
+ 0xFF000000)
+ : Theme.of(context).colorScheme.onBackground,
+ ),
+ );
+ default:
+ return const TextSpan(text: '');
+ }
}
}
}
diff --git a/lib/pages/read/controller.dart b/lib/pages/read/controller.dart
new file mode 100644
index 00000000..b30fe85f
--- /dev/null
+++ b/lib/pages/read/controller.dart
@@ -0,0 +1,89 @@
+import 'dart:async';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:get/get.dart';
+import 'package:pilipala/http/read.dart';
+import 'package:pilipala/models/read/read.dart';
+import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart';
+import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart';
+
+class ReadPageController extends GetxController {
+ late String url;
+ RxString title = ''.obs;
+ late String id;
+ late String articleType;
+ Rx cvData = ReadDataModel().obs;
+ final ScrollController scrollController = ScrollController();
+ final StreamController appbarStream = StreamController();
+
+ @override
+ void onInit() {
+ super.onInit();
+ title.value = Get.parameters['title'] ?? '';
+ id = Get.parameters['id']!;
+ articleType = Get.parameters['articleType']!;
+ scrollController.addListener(_scrollListener);
+ }
+
+ Future fetchCvData() async {
+ var res = await ReadHttp.parseArticleCv(id: id);
+ if (res['status']) {
+ cvData.value = res['data'];
+ title.value = cvData.value.readInfo!.title!;
+ }
+ return res;
+ }
+
+ void _scrollListener() {
+ final double offset = scrollController.position.pixels;
+ if (offset > 100) {
+ appbarStream.add(true);
+ } else {
+ appbarStream.add(false);
+ }
+ }
+
+ void onPreviewImg(picList, initIndex, context) {
+ Navigator.of(context).push(
+ HeroDialogRoute(
+ builder: (BuildContext context) => InteractiveviewerGallery(
+ sources: picList,
+ initIndex: initIndex,
+ itemBuilder: (
+ BuildContext context,
+ int index,
+ bool isFocus,
+ bool enablePageView,
+ ) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: () {
+ if (enablePageView) {
+ Navigator.of(context).pop();
+ }
+ },
+ child: Center(
+ child: Hero(
+ tag: picList[index],
+ child: CachedNetworkImage(
+ fadeInDuration: const Duration(milliseconds: 0),
+ imageUrl: picList[index],
+ fit: BoxFit.contain,
+ ),
+ ),
+ ),
+ );
+ },
+ onPageChanged: (int pageIndex) {},
+ ),
+ ),
+ );
+ }
+
+ @override
+ void onClose() {
+ scrollController.removeListener(_scrollListener);
+ appbarStream.close();
+ super.onClose();
+ }
+}
diff --git a/lib/pages/read/index.dart b/lib/pages/read/index.dart
new file mode 100644
index 00000000..3603c994
--- /dev/null
+++ b/lib/pages/read/index.dart
@@ -0,0 +1,4 @@
+library read;
+
+export 'controller.dart';
+export 'view.dart';
diff --git a/lib/pages/read/view.dart b/lib/pages/read/view.dart
new file mode 100644
index 00000000..7c1e0601
--- /dev/null
+++ b/lib/pages/read/view.dart
@@ -0,0 +1,342 @@
+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 'package:pilipala/models/read/opus.dart';
+import 'package:pilipala/models/read/read.dart';
+import 'package:pilipala/pages/opus/text_helper.dart';
+import 'package:pilipala/utils/utils.dart';
+import 'controller.dart';
+
+class ReadPage extends StatefulWidget {
+ const ReadPage({super.key});
+
+ @override
+ State createState() => _ReadPageState();
+}
+
+class _ReadPageState extends State {
+ final ReadPageController controller = Get.put(ReadPageController());
+ late Future _futureBuilderFuture;
+
+ @override
+ void initState() {
+ super.initState();
+ _futureBuilderFuture = controller.fetchCvData();
+ }
+
+ List extractDataSrc(String input) {
+ final regex = RegExp(r'data-src="([^"]*)"');
+ final matches = regex.allMatches(input);
+ return matches.map((match) {
+ final dataSrc = match.group(1)!;
+ return dataSrc.startsWith('//') ? 'https:$dataSrc' : dataSrc;
+ }).toList();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: _buildAppBar(),
+ body: SingleChildScrollView(
+ controller: controller.scrollController,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildTitle(),
+ _buildFutureContent(),
+ ],
+ ),
+ ),
+ );
+ }
+
+ AppBar _buildAppBar() {
+ return AppBar(
+ title: StreamBuilder(
+ stream: controller.appbarStream.stream.distinct(),
+ initialData: false,
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ return AnimatedOpacity(
+ opacity: snapshot.data ? 1 : 0,
+ curve: Curves.easeOut,
+ duration: const Duration(milliseconds: 500),
+ child: Obx(
+ () => Text(
+ controller.title.value,
+ style: const TextStyle(fontSize: 16),
+ ),
+ ),
+ );
+ },
+ ),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.more_vert_rounded),
+ onPressed: () {},
+ ),
+ const SizedBox(width: 16),
+ ],
+ );
+ }
+
+ Widget _buildTitle() {
+ return Padding(
+ padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
+ child: Obx(
+ () => Text(
+ controller.title.value,
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ letterSpacing: 1,
+ height: 1.5,
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFutureContent() {
+ return FutureBuilder(
+ future: _futureBuilderFuture,
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ if (snapshot.data == null) {
+ return const SizedBox();
+ }
+ if (snapshot.data['status']) {
+ return _buildContent(snapshot.data['data']);
+ } else {
+ return _buildError(snapshot.data['message']);
+ }
+ } else {
+ return _buildLoading();
+ }
+ },
+ );
+ }
+
+ Widget _buildContent(ReadDataModel cvData) {
+ final List picList = _extractPicList(cvData);
+ final List imgList = extractDataSrc(cvData.readInfo!.content!);
+
+ return Padding(
+ padding: EdgeInsets.fromLTRB(
+ 16, 0, 16, MediaQuery.of(context).padding.bottom + 40),
+ child: cvData.readInfo!.opus == null
+ ? _buildNonOpusContent(cvData, imgList)
+ : _buildOpusContent(cvData, picList),
+ );
+ }
+
+ List _extractPicList(ReadDataModel cvData) {
+ final List picList = [];
+ if (cvData.readInfo!.opus != null) {
+ final List paragraphs =
+ cvData.readInfo!.opus!.content!.paragraphs!;
+ for (var paragraph in paragraphs) {
+ if (paragraph.paraType == 2) {
+ for (var pic in paragraph.pic!.pics!) {
+ picList.add(pic.url!);
+ }
+ }
+ }
+ }
+ return picList;
+ }
+
+ Widget _buildNonOpusContent(ReadDataModel cvData, List imgList) {
+ return Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(bottom: 30),
+ child: _buildStatsWidget(cvData),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 20),
+ child: _buildAuthorWidget(cvData),
+ ),
+ HtmlRender(
+ htmlContent: cvData.readInfo!.content!,
+ imgList: imgList,
+ ),
+ ],
+ );
+ }
+
+ Widget _buildOpusContent(ReadDataModel cvData, List picList) {
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(bottom: 30),
+ child: _buildStatsWidget(cvData),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 20),
+ child: _buildAuthorWidget(cvData),
+ ),
+ ...cvData.readInfo!.opus!.content!.paragraphs!.map(
+ (ModuleParagraph paragraph) {
+ return Column(
+ children: [
+ if (paragraph.paraType == 1)
+ _buildTextParagraph(paragraph)
+ else if (paragraph.paraType == 2)
+ ..._buildPics(paragraph, picList)
+ else
+ const SizedBox(),
+ ],
+ );
+ },
+ ),
+ ],
+ );
+ }
+
+ Widget _buildTextParagraph(ModuleParagraph paragraph) {
+ return Container(
+ alignment: TextHelper.getAlignment(paragraph.align),
+ margin: const EdgeInsets.only(bottom: 10),
+ child: Text.rich(
+ TextSpan(
+ children: paragraph.text?.nodes?.map((node) {
+ return TextHelper.buildTextSpan(node, paragraph.align, context);
+ }).toList() ??
+ [],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildError(String message) {
+ return SizedBox(
+ height: 100,
+ child: Center(
+ child: Text(message),
+ ),
+ );
+ }
+
+ Widget _buildLoading() {
+ return const SizedBox(
+ height: 100,
+ child: Center(
+ child: CircularProgressIndicator(),
+ ),
+ );
+ }
+
+ Widget _buildStatsWidget(ReadDataModel cvData) {
+ return Row(
+ children: [
+ StyledText(Utils.CustomStamp_str(
+ timestamp: cvData.readInfo!.publishTime!,
+ date: 'YY-MM-DD hh:mm',
+ toInt: false,
+ )),
+ const SizedBox(width: 10),
+ StyledText('${Utils.numFormat(cvData.readInfo!.stats!['view'])}浏览'),
+ const StyledText(' · '),
+ StyledText('${cvData.readInfo!.stats!['like']}点赞'),
+ // const StyledText(' · '),
+ // StyledText('${cvData.readInfo!.stats!['reply']}评论'),
+ ],
+ );
+ }
+
+ Widget _buildAuthorWidget(ReadDataModel cvData) {
+ final Author author = cvData.readInfo!.author!;
+ return Row(
+ children: [
+ NetworkImgLayer(
+ width: 48,
+ height: 48,
+ type: 'avatar',
+ src: author.face,
+ ),
+ const SizedBox(width: 10),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Text(
+ author.name!,
+ style: TextStyle(
+ color: author.vip!.nicknameColor != null
+ ? Color(author.vip!.nicknameColor!)
+ : null,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ const SizedBox(width: 6),
+ Image.asset(
+ 'assets/images/lv/lv${author.level}.png',
+ height: 11,
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ StyledText('粉丝: ${Utils.numFormat(author.fans)}'),
+ const SizedBox(width: 10),
+ StyledText(
+ '文章: ${Utils.numFormat(cvData.readInfo!.totalArtNum)}'),
+ ],
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+
+ List _buildPics(ModuleParagraph paragraph, List picList) {
+ return paragraph.pic?.pics
+ ?.map(
+ (Pic pic) => Center(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 10, bottom: 10),
+ child: InkWell(
+ onTap: () {
+ controller.onPreviewImg(
+ picList,
+ picList.indexOf(pic.url!),
+ context,
+ );
+ },
+ child: NetworkImgLayer(
+ src: pic.url,
+ width: (Get.size.width - 32) * pic.scale!,
+ height:
+ (Get.size.width - 32) * pic.scale! / pic.aspectRatio!,
+ type: 'emote',
+ ),
+ ),
+ ),
+ ),
+ )
+ .toList() ??
+ [];
+ }
+}
+
+class StyledText extends StatelessWidget {
+ final String text;
+
+ const StyledText(this.text, {Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Text(
+ text,
+ style: TextStyle(
+ fontSize: 13,
+ color: Theme.of(context).colorScheme.outline,
+ ),
+ );
+ }
+}
diff --git a/lib/pages/search_panel/widgets/article_panel.dart b/lib/pages/search_panel/widgets/article_panel.dart
index c7074229..dd53de66 100644
--- a/lib/pages/search_panel/widgets/article_panel.dart
+++ b/lib/pages/search_panel/widgets/article_panel.dart
@@ -14,11 +14,10 @@ Widget searchArticlePanel(BuildContext context, ctr, list) {
itemBuilder: (context, index) {
return InkWell(
onTap: () {
- Get.toNamed('/htmlRender', parameters: {
- 'url': 'www.bilibili.com/read/cv${list[index].id}',
+ Get.toNamed('/read', parameters: {
'title': list[index].subTitle,
- 'id': 'cv${list[index].id}',
- 'dynamicType': 'read'
+ 'id': list[index].id.toString(),
+ 'articleType': 'read'
});
},
child: Padding(
diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart
index a7fda733..b104c5bc 100644
--- a/lib/router/app_pages.dart
+++ b/lib/router/app_pages.dart
@@ -10,6 +10,7 @@ import 'package:pilipala/pages/message/like/index.dart';
import 'package:pilipala/pages/message/reply/index.dart';
import 'package:pilipala/pages/message/system/index.dart';
import 'package:pilipala/pages/opus/index.dart';
+import 'package:pilipala/pages/read/index.dart';
import 'package:pilipala/pages/setting/pages/logs.dart';
import '../pages/about/index.dart';
@@ -190,6 +191,7 @@ class Routes {
// 专栏
CustomGetPage(name: '/opus', page: () => const OpusPage()),
+ CustomGetPage(name: '/read', page: () => const ReadPage()),
];
}
diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart
index 675300bb..67b8a5d5 100644
--- a/lib/utils/app_scheme.dart
+++ b/lib/utils/app_scheme.dart
@@ -94,12 +94,14 @@ class PiliSchame {
break;
case 'article':
final String id = path.split('/').last.split('?').first;
- Get.toNamed('/htmlRender', parameters: {
- 'url': 'https://www.bilibili.com/read/cv$id',
- 'title': 'cv$id',
- 'id': 'cv$id',
- 'dynamicType': 'read'
- });
+ Get.toNamed(
+ '/read',
+ parameters: {
+ 'title': 'cv$id',
+ 'id': id,
+ 'dynamicType': 'read',
+ },
+ );
break;
case 'pgc':
if (path.contains('ep')) {
@@ -240,12 +242,12 @@ class PiliSchame {
break;
case 'read':
print('专栏');
- String id = 'cv${Utils.matchNum(query!['id']!).first}';
- Get.toNamed('/htmlRender', parameters: {
+ String id = Utils.matchNum(query!['id']!).first.toString();
+ Get.toNamed('/read', parameters: {
'url': value.dataString!,
'title': '',
'id': id,
- 'dynamicType': 'read'
+ 'articleType': 'read'
});
break;
case 'space':