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
|
## feat
|
||||||
|
|
||||||
|
- [x] 显示ops专栏
|
||||||
- [x] 私信发图
|
- [x] 私信发图
|
||||||
- [x] 投币动画
|
- [x] 投币动画
|
||||||
- [x] 取消/追番,更新追番状态
|
- [x] 取消/追番,更新追番状态
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
|
|
||||||
## opt
|
## opt
|
||||||
|
|
||||||
|
- [x] 专栏界面
|
||||||
- [x] 私信界面
|
- [x] 私信界面
|
||||||
- [x] 收藏面板
|
- [x] 收藏面板
|
||||||
- [x] PIP
|
- [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/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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 '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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user