feat: show ops article

refactor: HtmlRenderPage
This commit is contained in:
bggRGjQaUbCoE
2024-10-05 15:43:01 +08:00
parent 3357433f57
commit 9f6c50aaac
5 changed files with 291 additions and 124 deletions

View File

@@ -41,6 +41,7 @@
## feat
- [x] 显示ops专栏
- [x] 私信发图
- [x] 投币动画
- [x] 取消/追番,更新追番状态
@@ -61,6 +62,7 @@
## opt
- [x] 专栏界面
- [x] 私信界面
- [x] 收藏面板
- [x] PIP

View File

@@ -0,0 +1,57 @@
import 'package:PiliPalaX/common/widgets/network_img_layer.dart';
import 'package:PiliPalaX/models/dynamics/article_content_model.dart';
import 'package:flutter/material.dart';
class ArticleContent extends StatelessWidget {
const ArticleContent({
super.key,
required this.htmlContent,
});
final dynamic htmlContent;
@override
Widget build(BuildContext context) {
List<ArticleContentModel> list = (htmlContent['ops'] as List)
.map((item) => ArticleContentModel.fromJson(item))
.toList();
return SliverList.separated(
itemCount: list.length,
itemBuilder: (_, index) {
ArticleContentModel item = list[index];
if (item.insert is String) {
return Text(
item.insert,
style: TextStyle(
fontWeight:
item.attributes?.bold == true ? FontWeight.bold : null,
),
);
} else if (item.attributes?.clazz == 'normal-img') {
return LayoutBuilder(
builder: (_, constraints) => NetworkImgLayer(
width: constraints.maxWidth,
height: constraints.maxWidth *
item.insert.nativeImage?.height /
item.insert.nativeImage?.width,
src: item.insert.nativeImage?.url,
),
);
// return image(
// constrainedWidth,
// [
// ImageModel(
// width: item.insert.nativeImage?.width,
// height: item.insert.nativeImage?.height,
// url: item.insert.nativeImage?.url,
// ),
// ],
// );
} else {
return Text('unsupported content');
}
},
separatorBuilder: (context, index) => const SizedBox(height: 10),
);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:html/dom.dart';
import 'package:html/parser.dart';
import 'index.dart';
@@ -94,8 +96,31 @@ class HtmlHttp {
// print(updateTime);
//
String opusContent =
dynamic opusContent =
opusDetail.querySelector('#read-article-holder')?.innerHtml ?? '';
bool isJsonContent = false;
if (opusContent.isEmpty) {
final regex = RegExp(r'window\.__INITIAL_STATE__\s*=\s*(\{.*?\});');
final match = regex.firstMatch(response.data);
if (match != null) {
final jsonString = match.group(1);
if (jsonString != null) {
try {
opusContent = jsonDecode(jsonString)['readInfo']['content'];
try {
opusContent = jsonDecode(opusContent);
isJsonContent = true;
} catch (e) {
print('second: $e');
}
} catch (e) {
print('first: $e');
}
}
}
}
RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(id);
String number = matches.first.group(0)!;
@@ -105,7 +130,8 @@ class HtmlHttp {
'uname': uname,
'updateTime': '',
'content': opusContent,
'commentId': int.parse(number)
'isJsonContent': isJsonContent,
'commentId': int.parse(number),
};
}
}

View File

@@ -0,0 +1,72 @@
class ArticleContentModel {
ArticleContentModel({
this.attributes,
this.insert,
});
Attributes? attributes;
dynamic insert;
ArticleContentModel.fromJson(Map<String, dynamic> json) {
attributes = json['attributes'] == null
? null
: Attributes.fromJson(json['attributes']);
insert = json['insert'] == null
? null
: json['attributes']?['class'] == 'normal-img'
? Insert.fromJson(json['insert'])
: json['insert'];
}
}
class Insert {
Insert({
this.nativeImage,
});
NativeImage? nativeImage;
Insert.fromJson(Map<String, dynamic> json) {
nativeImage = json['native-image'] == null
? null
: NativeImage.fromJson(json['native-image']);
}
}
class NativeImage {
NativeImage({
this.alt,
this.url,
this.width,
this.height,
this.size,
this.status,
});
dynamic alt;
dynamic url;
dynamic width;
dynamic height;
dynamic size;
dynamic status;
NativeImage.fromJson(Map<String, dynamic> json) {
alt = json['alt'];
url = json['url'];
width = json['width'];
height = json['height'];
size = json['size'];
status = json['status'];
}
}
class Attributes {
Attributes({
this.clazz,
});
String? clazz;
bool? bold;
Attributes.fromJson(Map<String, dynamic> json) {
clazz = json['class'];
bold = json['bold'];
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:math';
import 'package:PiliPalaX/common/widgets/article_content.dart';
import 'package:PiliPalaX/common/widgets/http_error.dart';
import 'package:PiliPalaX/http/loading_state.dart';
import 'package:easy_debounce/easy_throttle.dart';
@@ -219,120 +220,83 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
),
body: Stack(
children: [
OrientationBuilder(builder: (context, orientation) {
OrientationBuilder(
builder: (context, orientation) {
double padding = max(context.width / 2 - Grid.maxRowWidth, 0);
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: SingleChildScrollView(
child: CustomScrollView(
controller: orientation == Orientation.portrait
? _htmlRenderCtr.scrollController
: ScrollController(),
child: Padding(
slivers: [
SliverPadding(
padding: orientation == Orientation.portrait
? EdgeInsets.symmetric(horizontal: padding)
: EdgeInsets.only(left: padding / 2),
sliver: SliverToBoxAdapter(
child: Obx(
() => _htmlRenderCtr.loaded.value
? 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, 8, 12, 8),
child: LayoutBuilder(
builder: (context, boxConstraints) {
return HtmlRender(
htmlContent:
_htmlRenderCtr.response['content'],
constrainedWidth:
boxConstraints.maxWidth,
);
},
),
),
if (orientation == Orientation.portrait) ...[
Divider(
thickness: 8,
color: Theme.of(context)
.dividerColor
.withOpacity(0.05)),
replyHeader(),
Obx(
() => replyList(
_htmlRenderCtr.loadingState.value),
),
]
],
)
? _buildHeader
: const SizedBox(),
),
),
),
SliverPadding(
padding: orientation == Orientation.portrait
? EdgeInsets.symmetric(horizontal: padding)
: EdgeInsets.only(left: padding / 2),
sliver: _buildContent,
),
if (orientation == Orientation.portrait) ...[
SliverToBoxAdapter(
child: Divider(
thickness: 8,
color: Theme.of(context)
.dividerColor
.withOpacity(0.05),
),
),
SliverToBoxAdapter(child: replyHeader()),
Obx(
() => replyList(_htmlRenderCtr.loadingState.value),
),
],
],
),
),
if (orientation == Orientation.landscape) ...[
VerticalDivider(
thickness: 8,
color: Theme.of(context).dividerColor.withOpacity(0.05)),
color:
Theme.of(context).dividerColor.withOpacity(0.05)),
Expanded(
child: SingleChildScrollView(
child: CustomScrollView(
controller: _htmlRenderCtr.scrollController,
child: Padding(
slivers: [
SliverPadding(
padding: EdgeInsets.only(right: padding / 2),
child: Column(
children: [
replyHeader(),
Obx(
() => replyList(_htmlRenderCtr.loadingState.value),
sliver: SliverToBoxAdapter(
child: replyHeader(),
),
),
SliverPadding(
padding: EdgeInsets.only(right: padding / 2),
sliver: Obx(
() =>
replyList(_htmlRenderCtr.loadingState.value),
),
),
],
),
),
],
],
);
},
),
),
]
]);
}),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 14,
right: 14,
@@ -365,9 +329,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
Widget replyList(LoadingState loadingState) {
return loadingState is Success
? ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
? SliverList.builder(
itemCount: loadingState.response.length + 1,
itemBuilder: (context, index) {
if (index == loadingState.response.length) {
@@ -407,19 +369,12 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
},
)
: loadingState is Error
? CustomScrollView(
shrinkWrap: true,
slivers: [
HttpError(
? HttpError(
errMsg: loadingState.errMsg,
fn: _htmlRenderCtr.onReload,
),
],
)
: ListView.builder(
: SliverList.builder(
itemCount: 5,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
return const VideoReplySkeleton();
},
@@ -451,4 +406,59 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
),
);
}
Widget get _buildHeader => 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(),
],
),
);
Widget get _buildContent => SliverPadding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
sliver: Obx(
() => _htmlRenderCtr.loaded.value
? _htmlRenderCtr.response['isJsonContent']
? ArticleContent(
htmlContent: _htmlRenderCtr.response['content'],
)
: SliverToBoxAdapter(
child: LayoutBuilder(
builder: (_, constraints) => HtmlRender(
htmlContent: _htmlRenderCtr.response['content'],
constrainedWidth: constraints.maxWidth,
),
),
)
: SliverToBoxAdapter(
child: const SizedBox(),
),
),
);
}