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 ## feat
- [x] 显示ops专栏
- [x] 私信发图 - [x] 私信发图
- [x] 投币动画 - [x] 投币动画
- [x] 取消/追番,更新追番状态 - [x] 取消/追番,更新追番状态
@@ -61,6 +62,7 @@
## opt ## opt
- [x] 专栏界面
- [x] 私信界面 - [x] 私信界面
- [x] 收藏面板 - [x] 收藏面板
- [x] PIP - [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/dom.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'index.dart'; import 'index.dart';
@@ -94,8 +96,31 @@ class HtmlHttp {
// print(updateTime); // print(updateTime);
// //
String opusContent = dynamic opusContent =
opusDetail.querySelector('#read-article-holder')?.innerHtml ?? ''; 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+'); RegExp digitRegExp = RegExp(r'\d+');
Iterable<Match> matches = digitRegExp.allMatches(id); Iterable<Match> matches = digitRegExp.allMatches(id);
String number = matches.first.group(0)!; String number = matches.first.group(0)!;
@@ -105,7 +130,8 @@ class HtmlHttp {
'uname': uname, 'uname': uname,
'updateTime': '', 'updateTime': '',
'content': opusContent, '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 'dart:math';
import 'package:PiliPalaX/common/widgets/article_content.dart';
import 'package:PiliPalaX/common/widgets/http_error.dart'; import 'package:PiliPalaX/common/widgets/http_error.dart';
import 'package:PiliPalaX/http/loading_state.dart'; import 'package:PiliPalaX/http/loading_state.dart';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
@@ -219,120 +220,83 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
), ),
body: Stack( body: Stack(
children: [ children: [
OrientationBuilder(builder: (context, orientation) { OrientationBuilder(
double padding = max(context.width / 2 - Grid.maxRowWidth, 0); builder: (context, orientation) {
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ double padding = max(context.width / 2 - Grid.maxRowWidth, 0);
Expanded( return Row(
child: SingleChildScrollView( crossAxisAlignment: CrossAxisAlignment.start,
controller: orientation == Orientation.portrait children: [
? _htmlRenderCtr.scrollController Expanded(
: ScrollController(), child: CustomScrollView(
child: Padding( controller: orientation == Orientation.portrait
padding: orientation == Orientation.portrait ? _htmlRenderCtr.scrollController
? EdgeInsets.symmetric(horizontal: padding) : ScrollController(),
: EdgeInsets.only(left: padding / 2), slivers: [
child: Obx( SliverPadding(
() => _htmlRenderCtr.loaded.value padding: orientation == Orientation.portrait
? Column( ? EdgeInsets.symmetric(horizontal: padding)
children: [ : EdgeInsets.only(left: padding / 2),
Padding( sliver: SliverToBoxAdapter(
padding: child: Obx(
const EdgeInsets.fromLTRB(12, 12, 12, 8), () => _htmlRenderCtr.loaded.value
child: Row( ? _buildHeader
children: [ : const SizedBox(),
NetworkImgLayer( ),
width: 40, ),
height: 40, ),
type: 'avatar', SliverPadding(
src: _htmlRenderCtr.response['avatar']!, padding: orientation == Orientation.portrait
), ? EdgeInsets.symmetric(horizontal: padding)
const SizedBox(width: 10), : EdgeInsets.only(left: padding / 2),
Column( sliver: _buildContent,
crossAxisAlignment: ),
CrossAxisAlignment.start, if (orientation == Orientation.portrait) ...[
children: [ SliverToBoxAdapter(
Text(_htmlRenderCtr.response['uname'], child: Divider(
style: TextStyle( thickness: 8,
fontSize: Theme.of(context) color: Theme.of(context)
.textTheme .dividerColor
.titleSmall! .withOpacity(0.05),
.fontSize, ),
)), ),
Text( SliverToBoxAdapter(child: replyHeader()),
_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),
),
]
],
)
: const SizedBox(),
),
),
),
),
if (orientation == Orientation.landscape) ...[
VerticalDivider(
thickness: 8,
color: Theme.of(context).dividerColor.withOpacity(0.05)),
Expanded(
child: SingleChildScrollView(
controller: _htmlRenderCtr.scrollController,
child: Padding(
padding: EdgeInsets.only(right: padding / 2),
child: Column(
children: [
replyHeader(),
Obx( Obx(
() => replyList(_htmlRenderCtr.loadingState.value), () => replyList(_htmlRenderCtr.loadingState.value),
), ),
], ],
), ],
), ),
), ),
), if (orientation == Orientation.landscape) ...[
] VerticalDivider(
]); thickness: 8,
}), color:
Theme.of(context).dividerColor.withOpacity(0.05)),
Expanded(
child: CustomScrollView(
controller: _htmlRenderCtr.scrollController,
slivers: [
SliverPadding(
padding: EdgeInsets.only(right: padding / 2),
sliver: SliverToBoxAdapter(
child: replyHeader(),
),
),
SliverPadding(
padding: EdgeInsets.only(right: padding / 2),
sliver: Obx(
() =>
replyList(_htmlRenderCtr.loadingState.value),
),
),
],
),
),
],
],
);
},
),
Positioned( Positioned(
bottom: MediaQuery.of(context).padding.bottom + 14, bottom: MediaQuery.of(context).padding.bottom + 14,
right: 14, right: 14,
@@ -365,9 +329,7 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
Widget replyList(LoadingState loadingState) { Widget replyList(LoadingState loadingState) {
return loadingState is Success return loadingState is Success
? ListView.builder( ? SliverList.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: loadingState.response.length + 1, itemCount: loadingState.response.length + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == loadingState.response.length) { if (index == loadingState.response.length) {
@@ -407,19 +369,12 @@ class _HtmlRenderPageState extends State<HtmlRenderPage>
}, },
) )
: loadingState is Error : loadingState is Error
? CustomScrollView( ? HttpError(
shrinkWrap: true, errMsg: loadingState.errMsg,
slivers: [ fn: _htmlRenderCtr.onReload,
HttpError(
errMsg: loadingState.errMsg,
fn: _htmlRenderCtr.onReload,
),
],
) )
: ListView.builder( : SliverList.builder(
itemCount: 5, itemCount: 5,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return const VideoReplySkeleton(); 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(),
),
),
);
} }