diff --git a/README.md b/README.md index e1b2a57d..e70fca1d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ ## feat +- [x] 显示ops专栏 - [x] 私信发图 - [x] 投币动画 - [x] 取消/追番,更新追番状态 @@ -61,6 +62,7 @@ ## opt +- [x] 专栏界面 - [x] 私信界面 - [x] 收藏面板 - [x] PIP diff --git a/lib/common/widgets/article_content.dart b/lib/common/widgets/article_content.dart new file mode 100644 index 00000000..facb7ce9 --- /dev/null +++ b/lib/common/widgets/article_content.dart @@ -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 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), + ); + } +} diff --git a/lib/http/html.dart b/lib/http/html.dart index 07db4868..b7819e2c 100644 --- a/lib/http/html.dart +++ b/lib/http/html.dart @@ -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 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), }; } } diff --git a/lib/models/dynamics/article_content_model.dart b/lib/models/dynamics/article_content_model.dart new file mode 100644 index 00000000..231ad452 --- /dev/null +++ b/lib/models/dynamics/article_content_model.dart @@ -0,0 +1,72 @@ +class ArticleContentModel { + ArticleContentModel({ + this.attributes, + this.insert, + }); + Attributes? attributes; + dynamic insert; + + ArticleContentModel.fromJson(Map 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 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 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 json) { + clazz = json['class']; + bold = json['bold']; + } +} diff --git a/lib/pages/html/view.dart b/lib/pages/html/view.dart index 3abb3b38..af634fe3 100644 --- a/lib/pages/html/view.dart +++ b/lib/pages/html/view.dart @@ -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 ), body: Stack( children: [ - OrientationBuilder(builder: (context, orientation) { - double padding = max(context.width / 2 - Grid.maxRowWidth, 0); - return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: SingleChildScrollView( - controller: orientation == Orientation.portrait - ? _htmlRenderCtr.scrollController - : ScrollController(), - child: Padding( - padding: orientation == Orientation.portrait - ? EdgeInsets.symmetric(horizontal: padding) - : EdgeInsets.only(left: padding / 2), - 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), - ), - ] - ], - ) - : 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(), + OrientationBuilder( + builder: (context, orientation) { + double padding = max(context.width / 2 - Grid.maxRowWidth, 0); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: CustomScrollView( + controller: orientation == Orientation.portrait + ? _htmlRenderCtr.scrollController + : ScrollController(), + slivers: [ + SliverPadding( + padding: orientation == Orientation.portrait + ? EdgeInsets.symmetric(horizontal: padding) + : EdgeInsets.only(left: padding / 2), + sliver: SliverToBoxAdapter( + child: Obx( + () => _htmlRenderCtr.loaded.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)), + 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( bottom: MediaQuery.of(context).padding.bottom + 14, right: 14, @@ -365,9 +329,7 @@ class _HtmlRenderPageState extends State 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 }, ) : loadingState is Error - ? CustomScrollView( - shrinkWrap: true, - slivers: [ - HttpError( - errMsg: loadingState.errMsg, - fn: _htmlRenderCtr.onReload, - ), - ], + ? 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 ), ); } + + 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(), + ), + ), + ); }