feat: read专栏内容渲染
This commit is contained in:
@ -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<void>(
|
||||
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()),
|
||||
|
@ -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());
|
||||
|
@ -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<String, dynamic> jsonData = json.decode(jsonContent);
|
||||
return {
|
||||
'status': true,
|
||||
'data': ReadDataModel.fromJson(jsonData),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -302,14 +302,17 @@ class ModuleParagraphText {
|
||||
class ModuleParagraphTextNode {
|
||||
ModuleParagraphTextNode({
|
||||
this.type,
|
||||
this.nodeType,
|
||||
this.word,
|
||||
});
|
||||
|
||||
String? type;
|
||||
int? nodeType;
|
||||
ModuleParagraphTextNodeWord? word;
|
||||
|
||||
ModuleParagraphTextNode.fromJson(Map<String, dynamic> json) {
|
||||
type = json['type'];
|
||||
nodeType = json['node_type'];
|
||||
word = json['word'] != null
|
||||
? ModuleParagraphTextNodeWord.fromJson(json['word'])
|
||||
: null;
|
||||
|
286
lib/models/read/read.dart
Normal file
286
lib/models/read/read.dart
Normal file
@ -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<dynamic>? catalogList;
|
||||
List<dynamic>? recommendInfoList;
|
||||
bool? hiddenInteraction;
|
||||
bool? isModern;
|
||||
|
||||
ReadDataModel.fromJson(Map<String, dynamic> json) {
|
||||
cvid = json['cvid'];
|
||||
readInfo =
|
||||
json['readInfo'] != null ? ReadInfo.fromJson(json['readInfo']) : null;
|
||||
readViewInfo = json['readViewInfo'];
|
||||
upInfo = json['upInfo'];
|
||||
if (json['catalogList'] != null) {
|
||||
catalogList = <dynamic>[];
|
||||
json['catalogList'].forEach((v) {
|
||||
catalogList!.add(v);
|
||||
});
|
||||
}
|
||||
if (json['recommendInfoList'] != null) {
|
||||
recommendInfoList = <dynamic>[];
|
||||
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<String>? originImageUrls;
|
||||
String? content;
|
||||
Opus? opus;
|
||||
String? dynIdStr;
|
||||
int? totalArtNum;
|
||||
|
||||
ReadInfo.fromJson(Map<String, dynamic> 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 = <String>[];
|
||||
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<String, dynamic> 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<String, dynamic> 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<ModuleParagraph>? paragraphs;
|
||||
|
||||
Content.fromJson(Map<String, dynamic> json) {
|
||||
if (json['paragraphs'] != null) {
|
||||
paragraphs = <ModuleParagraph>[];
|
||||
json['paragraphs'].forEach((v) {
|
||||
paragraphs!.add(ModuleParagraph.fromJson(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -154,12 +154,10 @@ class DynamicsController extends GetxController {
|
||||
Iterable<Match> 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: {
|
||||
|
@ -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: '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
89
lib/pages/read/controller.dart
Normal file
89
lib/pages/read/controller.dart
Normal file
@ -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<ReadDataModel> cvData = ReadDataModel().obs;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
final StreamController<bool> appbarStream = StreamController<bool>();
|
||||
|
||||
@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<void>(
|
||||
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();
|
||||
}
|
||||
}
|
4
lib/pages/read/index.dart
Normal file
4
lib/pages/read/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library read;
|
||||
|
||||
export 'controller.dart';
|
||||
export 'view.dart';
|
342
lib/pages/read/view.dart
Normal file
342
lib/pages/read/view.dart
Normal file
@ -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<ReadPage> createState() => _ReadPageState();
|
||||
}
|
||||
|
||||
class _ReadPageState extends State<ReadPage> {
|
||||
final ReadPageController controller = Get.put(ReadPageController());
|
||||
late Future _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futureBuilderFuture = controller.fetchCvData();
|
||||
}
|
||||
|
||||
List<String> 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<String> picList = _extractPicList(cvData);
|
||||
final List<String> 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<String> _extractPicList(ReadDataModel cvData) {
|
||||
final List<String> picList = [];
|
||||
if (cvData.readInfo!.opus != null) {
|
||||
final List<ModuleParagraph> 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<String> 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<String> 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<Widget> _buildPics(ModuleParagraph paragraph, List<String> 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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()),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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':
|
||||
|
Reference in New Issue
Block a user