import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/html_render.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/read/opus.dart'; import 'package:pilipala/models/read/read.dart'; import 'package:pilipala/pages/opus/text_helper.dart'; import 'package:pilipala/utils/utils.dart'; import 'controller.dart'; class ReadPage extends StatefulWidget { const ReadPage({super.key}); @override State createState() => _ReadPageState(); } class _ReadPageState extends State { final ReadPageController controller = Get.put(ReadPageController()); late Future _futureBuilderFuture; @override void initState() { super.initState(); _futureBuilderFuture = controller.fetchCvData(); } List extractDataSrc(String input) { final regex = RegExp(r'data-src="([^"]*)"'); final matches = regex.allMatches(input); return matches.map((match) { final dataSrc = match.group(1)!; return dataSrc.startsWith('//') ? 'https:$dataSrc' : dataSrc; }).toList(); } @override Widget build(BuildContext context) { return Scaffold( appBar: _buildAppBar(), body: SingleChildScrollView( controller: controller.scrollController, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTitle(), _buildFutureContent(), ], ), ), ); } AppBar _buildAppBar() { return AppBar( title: StreamBuilder( stream: controller.appbarStream.stream.distinct(), initialData: false, builder: (BuildContext context, AsyncSnapshot snapshot) { return AnimatedOpacity( opacity: snapshot.data ? 1 : 0, curve: Curves.easeOut, duration: const Duration(milliseconds: 500), child: Obx( () => Text( controller.title.value, style: const TextStyle(fontSize: 16), ), ), ); }, ), actions: [ PopupMenuButton( icon: const Icon(Icons.more_vert_outlined), itemBuilder: (BuildContext context) => [ PopupMenuItem( onTap: controller.onJumpWebview, child: const Text('查看原网页'), ) ], ), const SizedBox(width: 16), ], ); } Widget _buildTitle() { return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), child: Obx( () => Text( controller.title.value, style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, letterSpacing: 1, height: 1.5, ), ), ), ); } Widget _buildFutureContent() { return FutureBuilder( future: _futureBuilderFuture, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.data == null) { return const SizedBox(); } if (snapshot.data['status']) { return _buildContent(snapshot.data['data']); } else { return _buildError(snapshot.data['message']); } } else { return _buildLoading(); } }, ); } Widget _buildContent(ReadDataModel cvData) { final List picList = _extractPicList(cvData); final List imgList = extractDataSrc(cvData.readInfo!.content!); return Padding( padding: EdgeInsets.fromLTRB( 16, 0, 16, MediaQuery.of(context).padding.bottom + 40), child: cvData.readInfo!.opus == null ? _buildNonOpusContent(cvData, imgList) : _buildOpusContent(cvData, picList), ); } List _extractPicList(ReadDataModel cvData) { final List picList = []; if (cvData.readInfo!.opus != null) { final List paragraphs = cvData.readInfo!.opus!.content!.paragraphs!; for (var paragraph in paragraphs) { if (paragraph.paraType == 2) { for (var pic in paragraph.pic!.pics!) { picList.add(pic.url!); } } } } return picList; } Widget _buildNonOpusContent(ReadDataModel cvData, List imgList) { return Column( children: [ Padding( padding: const EdgeInsets.only(bottom: 30), child: _buildStatsWidget(cvData), ), Padding( padding: const EdgeInsets.only(bottom: 20), child: _buildAuthorWidget(cvData), ), HtmlRender( htmlContent: cvData.readInfo!.content!, imgList: imgList, ), ], ); } Widget _buildOpusContent(ReadDataModel cvData, List picList) { return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.only(bottom: 30), child: _buildStatsWidget(cvData), ), Padding( padding: const EdgeInsets.only(bottom: 20), child: _buildAuthorWidget(cvData), ), ...cvData.readInfo!.opus!.content!.paragraphs!.map( (ModuleParagraph paragraph) { return Column( children: [ if (paragraph.paraType == 1) _buildTextParagraph(paragraph) else if (paragraph.paraType == 2) ..._buildPics(paragraph, picList) else const SizedBox(), ], ); }, ), ], ); } Widget _buildTextParagraph(ModuleParagraph paragraph) { return Container( alignment: TextHelper.getAlignment(paragraph.align), margin: const EdgeInsets.only(bottom: 10), child: Text.rich( TextSpan( children: paragraph.text?.nodes?.map((node) { return TextHelper.buildTextSpan(node, paragraph.align, context); }).toList() ?? [], ), ), ); } Widget _buildError(String message) { return SizedBox( height: 100, child: Center( child: Text(message), ), ); } Widget _buildLoading() { return const SizedBox( height: 100, child: Center( child: CircularProgressIndicator(), ), ); } Widget _buildStatsWidget(ReadDataModel cvData) { return Row( children: [ StyledText(Utils.CustomStamp_str( timestamp: cvData.readInfo!.publishTime!, date: 'YY-MM-DD hh:mm', toInt: false, )), const SizedBox(width: 10), StyledText('${Utils.numFormat(cvData.readInfo!.stats!['view'])}浏览'), const StyledText(' · '), StyledText('${cvData.readInfo!.stats!['like']}点赞'), // const StyledText(' · '), // StyledText('${cvData.readInfo!.stats!['reply']}评论'), ], ); } Widget _buildAuthorWidget(ReadDataModel cvData) { final Author author = cvData.readInfo!.author!; return Row( children: [ NetworkImgLayer( width: 48, height: 48, type: 'avatar', src: author.face, ), const SizedBox(width: 10), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( author.name!, style: TextStyle( color: author.vip!.nicknameColor != null ? Color(author.vip!.nicknameColor!) : null, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 6), Image.asset( 'assets/images/lv/lv${author.level}.png', height: 11, ), ], ), Row( children: [ StyledText('粉丝: ${Utils.numFormat(author.fans)}'), const SizedBox(width: 10), StyledText( '文章: ${Utils.numFormat(cvData.readInfo!.totalArtNum)}'), ], ), ], ), ], ); } List _buildPics(ModuleParagraph paragraph, List picList) { return paragraph.pic?.pics ?.map( (Pic pic) => Center( child: Padding( padding: const EdgeInsets.only(top: 10, bottom: 10), child: InkWell( onTap: () { controller.onPreviewImg( picList, picList.indexOf(pic.url!), context, ); }, child: NetworkImgLayer( src: pic.url, width: (Get.size.width - 32) * pic.scale!, height: (Get.size.width - 32) * pic.scale! / pic.aspectRatio!, type: 'emote', ), ), ), ), ) .toList() ?? []; } } class StyledText extends StatelessWidget { final String text; const StyledText(this.text, {Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Text( text, style: TextStyle( fontSize: 13, color: Theme.of(context).colorScheme.outline, ), ); } }