mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
feat: show ops article
refactor: HtmlRenderPage
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
|
||||
## feat
|
||||
|
||||
- [x] 显示ops专栏
|
||||
- [x] 私信发图
|
||||
- [x] 投币动画
|
||||
- [x] 取消/追番,更新追番状态
|
||||
@@ -61,6 +62,7 @@
|
||||
|
||||
## opt
|
||||
|
||||
- [x] 专栏界面
|
||||
- [x] 私信界面
|
||||
- [x] 收藏面板
|
||||
- [x] PIP
|
||||
|
||||
57
lib/common/widgets/article_content.dart
Normal file
57
lib/common/widgets/article_content.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
72
lib/models/dynamics/article_content_model.dart
Normal file
72
lib/models/dynamics/article_content_model.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user