diff --git a/lib/models/read/opus.dart b/lib/models/read/opus.dart index f2cf3d9c..e7b9aca1 100644 --- a/lib/models/read/opus.dart +++ b/lib/models/read/opus.dart @@ -149,16 +149,19 @@ class ModuleAuthor { this.face, this.mid, this.name, + this.pubTime, }); String? face; int? mid; String? name; + String? pubTime; ModuleAuthor.fromJson(Map json) { face = json['face']; mid = json['mid']; name = json['name']; + pubTime = json['pub_time']; } } diff --git a/lib/pages/opus/controller.dart b/lib/pages/opus/controller.dart index b950a948..f5c35770 100644 --- a/lib/pages/opus/controller.dart +++ b/lib/pages/opus/controller.dart @@ -1,23 +1,31 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/http/read.dart'; import 'package:pilipala/models/read/opus.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart'; +import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart'; class OpusController extends GetxController { late String url; - late String title; + RxString title = ''.obs; late String id; late String articleType; Rx opusData = OpusDataModel().obs; + final ScrollController scrollController = ScrollController(); + late StreamController appbarStream = StreamController.broadcast(); @override void onInit() { super.onInit(); - title = Get.parameters['title']!; + title.value = Get.parameters['title'] ?? ''; id = Get.parameters['id']!; articleType = Get.parameters['articleType']!; if (articleType == 'opus') { url = 'https://www.bilibili.com/opus/$id'; } + scrollController.addListener(_scrollListener); } Future fetchOpusData() async { @@ -27,4 +35,57 @@ class OpusController extends GetxController { } return res; } + + void _scrollListener() { + final double offset = scrollController.position.pixels; + if (offset > 100) { + appbarStream.add(true); + } else { + appbarStream.add(false); + } + } + + void onPreviewImg(picList, initIndex, context) { + Navigator.of(context).push( + HeroDialogRoute( + builder: (BuildContext context) => InteractiveviewerGallery( + sources: picList, + initIndex: initIndex, + itemBuilder: ( + BuildContext context, + int index, + bool isFocus, + bool enablePageView, + ) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (enablePageView) { + Navigator.of(context).pop(); + } + }, + child: Center( + child: Hero( + tag: picList[index], + child: CachedNetworkImage( + fadeInDuration: const Duration(milliseconds: 0), + imageUrl: picList[index], + fit: BoxFit.contain, + ), + ), + ), + ); + }, + onPageChanged: (int pageIndex) {}, + ), + ), + ); + } + + @override + void onClose() { + scrollController.removeListener(_scrollListener); + appbarStream.close(); + super.onClose(); + } } diff --git a/lib/pages/opus/view.dart b/lib/pages/opus/view.dart index 08f1cf33..a36b4813 100644 --- a/lib/pages/opus/view.dart +++ b/lib/pages/opus/view.dart @@ -1,10 +1,7 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/read/opus.dart'; -import 'package:pilipala/plugin/pl_gallery/hero_dialog_route.dart'; -import 'package:pilipala/plugin/pl_gallery/interactiveviewer_gallery.dart'; import 'controller.dart'; import 'text_helper.dart'; @@ -25,211 +22,262 @@ class _OpusPageState extends State { _futureBuilderFuture = controller.fetchOpusData(); } - void onPreviewImg(picList, initIndex, context) { - Navigator.of(context).push( - HeroDialogRoute( - builder: (BuildContext context) => InteractiveviewerGallery( - sources: picList, - initIndex: initIndex, - itemBuilder: ( - BuildContext context, - int index, - bool isFocus, - bool enablePageView, - ) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (enablePageView) { - Navigator.of(context).pop(); - } - }, - child: Center( - child: Hero( - tag: picList[index], - child: CachedNetworkImage( - fadeInDuration: const Duration(milliseconds: 0), - imageUrl: picList[index], - fit: BoxFit.contain, - ), - ), - ), - ); - }, - onPageChanged: (int pageIndex) {}, - ), - ), - ); - } - @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - actions: [ - IconButton( - icon: const Icon(Icons.more_vert_rounded), - onPressed: () {}, - ), - const SizedBox(width: 16), - ], - ), + appBar: _buildAppBar(), body: SingleChildScrollView( + controller: controller.scrollController, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), - child: Text( - controller.title, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - letterSpacing: 1, - height: 1.5, - ), - ), - ), - FutureBuilder( - future: _futureBuilderFuture, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data == null) { - return const SizedBox(); - } - if (snapshot.data['status']) { - final modules = controller.opusData.value.detail!.modules!; - final ModuleStat moduleStat = modules.last.moduleStat!; - late ModuleContent moduleContent; - final int moduleIndex = modules - .indexWhere((module) => module.moduleContent != null); - if (moduleIndex != -1) { - moduleContent = modules[moduleIndex].moduleContent!; - } else { - print('No moduleContent found'); - } - // 获取所有的图片链接 - final List picList = []; - for (var paragraph in moduleContent.paragraphs!) { - if (paragraph.paraType == 2) { - for (var pic in paragraph.pic!.pics!) { - picList.add(pic.url!); - } - } - } - return Padding( - padding: EdgeInsets.fromLTRB(16, 0, 16, - MediaQuery.of(context).padding.bottom + 40), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 20), - child: SelectableText.rich( - TextSpan( - style: TextStyle( - color: Theme.of(context).colorScheme.outline, - fontSize: 12, - letterSpacing: 1, - ), - children: [ - TextSpan( - text: '${moduleStat.comment!.count}评论'), - const TextSpan(text: ' '), - const TextSpan(text: ' '), - TextSpan(text: '${moduleStat.like!.count}赞'), - const TextSpan(text: ' '), - const TextSpan(text: ' '), - TextSpan( - text: '${moduleStat.favorite!.count}转发'), - ], - ), - ), - ), - ...moduleContent.paragraphs!.map( - (ModuleParagraph paragraph) { - return Column( - children: [ - if (paragraph.paraType == 1) ...[ - 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() ?? - [], - ), - ), - ) - ] else if (paragraph.paraType == 2) ...[ - ...paragraph.pic?.pics?.map( - (Pic pic) => Center( - child: Padding( - padding: const EdgeInsets.only( - top: 10, bottom: 10), - child: InkWell( - onTap: () { - 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', - ), - ), - ), - ), - ) ?? - [], - ] else - const SizedBox(), - ], - ); - }, - ), - ], - ), - ); - } else { - // 请求错误 - return SizedBox( - height: 100, - child: Center( - child: Text(snapshot.data['message']), - ), - ); - } - } else { - return const SizedBox( - height: 100, - child: Center( - child: CircularProgressIndicator(), - ), - ); - } - }, - ) + _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: [ + IconButton( + icon: const Icon(Icons.more_vert_rounded), + onPressed: () {}, + ), + 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(controller.opusData.value); + } else { + return _buildError(snapshot.data['message']); + } + } else { + return _buildLoading(); + } + }, + ); + } + + Widget _buildContent(OpusDataModel opusData) { + final modules = opusData.detail!.modules!; + late ModuleContent moduleContent; + // 获取所有的图片链接 + final List picList = []; + final int moduleIndex = + modules.indexWhere((module) => module.moduleContent != null); + if (moduleIndex != -1) { + moduleContent = modules[moduleIndex].moduleContent!; + for (var paragraph in moduleContent.paragraphs!) { + if (paragraph.paraType == 2) { + for (var pic in paragraph.pic!.pics!) { + picList.add(pic.url!); + } + } + } + } else { + print('No moduleContent found'); + } + + return Padding( + padding: EdgeInsets.fromLTRB( + 16, 0, 16, MediaQuery.of(context).padding.bottom + 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 30), + child: _buildStatsWidget(opusData), + ), + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: _buildAuthorWidget(opusData), + ), + ...moduleContent.paragraphs!.map( + (ModuleParagraph paragraph) { + return Column( + children: [ + if (paragraph.paraType == 1) ...[ + 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() ?? + [], + ), + ), + ) + ] else if (paragraph.paraType == 2) ...[ + ...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', + ), + ), + ), + ), + ) ?? + [], + ] else + const SizedBox(), + ], + ); + }, + ), + ], + ), + ); + } + + Widget _buildAuthorWidget(OpusDataModel opusData) { + final modules = opusData.detail!.modules!; + late ModuleAuthor moduleAuthor; + final int moduleIndex = + modules.indexWhere((module) => module.moduleAuthor != null); + if (moduleIndex != -1) { + moduleAuthor = modules[moduleIndex].moduleAuthor!; + } else { + return const SizedBox(); + } + return Row( + children: [ + NetworkImgLayer( + width: 48, + height: 48, + type: 'avatar', + src: moduleAuthor.face, + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + moduleAuthor.name!, + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + StyledText(moduleAuthor.pubTime!), + ], + ), + ], + ); + } + + Widget _buildStatsWidget(OpusDataModel opusData) { + final modules = opusData.detail!.modules!; + final ModuleStat moduleStat = modules.last.moduleStat!; + return Row( + children: [ + StyledText('${moduleStat.comment!.count}评论'), + const SizedBox(width: 10), + StyledText('${moduleStat.like!.count}赞'), + const SizedBox(width: 10), + StyledText('${moduleStat.favorite!.count}转发'), + ], + ); + } + + Widget _buildError(String message) { + return SizedBox( + height: 100, + child: Center( + child: Text(message), + ), + ); + } + + Widget _buildLoading() { + return const SizedBox( + height: 100, + child: Center( + child: CircularProgressIndicator(), + ), + ); + } +} + +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, + ), + ); + } }