diff --git a/assets/images/topic-header-bg.png b/assets/images/topic-header-bg.png new file mode 100644 index 000000000..6070c4ab7 Binary files /dev/null and b/assets/images/topic-header-bg.png differ diff --git a/assets/images/trending_banner.png b/assets/images/trending_banner.png new file mode 100644 index 000000000..4353ec8d8 Binary files /dev/null and b/assets/images/trending_banner.png differ diff --git a/lib/common/widgets/dynamic_sliver_appbar_medium.dart b/lib/common/widgets/dynamic_sliver_appbar_medium.dart new file mode 100644 index 000000000..2e4b03b8e --- /dev/null +++ b/lib/common/widgets/dynamic_sliver_appbar_medium.dart @@ -0,0 +1,176 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// https://github.com/flutter/flutter/issues/18345#issuecomment-1627644396 +class DynamicSliverAppBarMedium extends StatefulWidget { + const DynamicSliverAppBarMedium({ + this.flexibleSpace, + super.key, + this.leading, + this.automaticallyImplyLeading = true, + this.title, + this.actions, + this.bottom, + this.elevation, + this.scrolledUnderElevation, + this.shadowColor, + this.surfaceTintColor, + this.forceElevated = false, + this.backgroundColor, + this.backgroundGradient, + this.foregroundColor, + this.iconTheme, + this.actionsIconTheme, + this.primary = true, + this.centerTitle, + this.excludeHeaderSemantics = false, + this.titleSpacing, + this.collapsedHeight, + this.expandedHeight, + this.floating = false, + this.pinned = false, + this.snap = false, + this.stretch = false, + this.stretchTriggerOffset = 100.0, + this.onStretchTrigger, + this.shape, + this.toolbarHeight = kToolbarHeight, + this.leadingWidth, + this.toolbarTextStyle, + this.titleTextStyle, + this.systemOverlayStyle, + this.forceMaterialTransparency = false, + this.clipBehavior, + this.appBarClipper, + }); + + final Widget? flexibleSpace; + final Widget? leading; + final bool automaticallyImplyLeading; + final Widget? title; + final List? actions; + final PreferredSizeWidget? bottom; + final double? elevation; + final double? scrolledUnderElevation; + final Color? shadowColor; + final Color? surfaceTintColor; + final bool forceElevated; + final Color? backgroundColor; + + /// If backgroundGradient is non null, backgroundColor will be ignored + final LinearGradient? backgroundGradient; + final Color? foregroundColor; + final IconThemeData? iconTheme; + final IconThemeData? actionsIconTheme; + final bool primary; + final bool? centerTitle; + final bool excludeHeaderSemantics; + final double? titleSpacing; + final double? expandedHeight; + final double? collapsedHeight; + final bool floating; + final bool pinned; + final ShapeBorder? shape; + final double toolbarHeight; + final double? leadingWidth; + final TextStyle? toolbarTextStyle; + final TextStyle? titleTextStyle; + final SystemUiOverlayStyle? systemOverlayStyle; + final bool forceMaterialTransparency; + final Clip? clipBehavior; + final bool snap; + final bool stretch; + final double stretchTriggerOffset; + final AsyncCallback? onStretchTrigger; + final CustomClipper? appBarClipper; + + @override + State createState() => + _DynamicSliverAppBarMediumState(); +} + +class _DynamicSliverAppBarMediumState extends State { + final GlobalKey _childKey = GlobalKey(); + + // As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used + // to calculate dynamically the size for the sliver app bar + double _height = 0; + + @override + void initState() { + super.initState(); + _updateHeight(); + } + + void _updateHeight() { + // Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild + // otherwise this will throw an error + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (_childKey.currentContext == null) return; + setState(() { + _height = (_childKey.currentContext!.findRenderObject()! as RenderBox) + .size + .height; + }); + }); + } + + @override + void didChangeDependencies() { + _height = 0; + _updateHeight(); + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + //Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height + if (_height == 0) { + return SliverToBoxAdapter( + child: SizedBox( + key: _childKey, + child: widget.flexibleSpace ?? const SizedBox(height: kToolbarHeight), + ), + ); + } + + return SliverAppBar.medium( + leading: widget.leading, + automaticallyImplyLeading: widget.automaticallyImplyLeading, + title: widget.title, + actions: widget.actions, + bottom: widget.bottom, + elevation: widget.elevation, + scrolledUnderElevation: widget.scrolledUnderElevation, + shadowColor: widget.shadowColor, + surfaceTintColor: widget.surfaceTintColor, + forceElevated: widget.forceElevated, + backgroundColor: widget.backgroundColor, + foregroundColor: widget.foregroundColor, + iconTheme: widget.iconTheme, + actionsIconTheme: widget.actionsIconTheme, + primary: widget.primary, + centerTitle: widget.centerTitle, + excludeHeaderSemantics: widget.excludeHeaderSemantics, + titleSpacing: widget.titleSpacing, + collapsedHeight: widget.collapsedHeight, + floating: widget.floating, + pinned: widget.pinned, + snap: widget.snap, + stretch: widget.stretch, + stretchTriggerOffset: widget.stretchTriggerOffset, + onStretchTrigger: widget.onStretchTrigger, + shape: widget.shape, + toolbarHeight: widget.toolbarHeight, + expandedHeight: _height - MediaQuery.paddingOf(context).top, + leadingWidth: widget.leadingWidth, + toolbarTextStyle: widget.toolbarTextStyle, + titleTextStyle: widget.titleTextStyle, + systemOverlayStyle: widget.systemOverlayStyle, + forceMaterialTransparency: widget.forceMaterialTransparency, + clipBehavior: widget.clipBehavior, + flexibleSpace: FlexibleSpaceBar(background: widget.flexibleSpace), + ); + } +} diff --git a/lib/http/api.dart b/lib/http/api.dart index d86ea690d..1460ae5c9 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -847,4 +847,12 @@ class Api { '${HttpString.tUrl}/link_setting/v1/link_setting/set_push_ss'; static const String dynReserve = '/x/dynamic/feed/reserve/click'; + + static const String favTopicList = '/x/topic/web/fav/list'; + + static const String addFavTopic = '/x/topic/fav/sub/add'; + + static const String delFavTopic = '/x/topic/fav/sub/cancel'; + + static const String likeTopic = '/x/topic/like'; } diff --git a/lib/http/user.dart b/lib/http/user.dart index f57d6ff9c..79c3e25c7 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/model_hot_video_item.dart'; import 'package:PiliPlus/models/user/fav_detail.dart'; import 'package:PiliPlus/models/user/fav_folder.dart'; +import 'package:PiliPlus/models/user/fav_topic/data.dart'; import 'package:PiliPlus/models/user/history.dart'; import 'package:PiliPlus/models/user/info.dart'; import 'package:PiliPlus/models/user/stat.dart'; @@ -479,13 +480,85 @@ class UserHttp { } } + static Future> favTopic({ + required int page, + }) async { + var res = await Request().get( + Api.favTopicList, + queryParameters: { + 'page_size': 24, + 'page_num': page, + 'web_location': 333.1387, + }, + ); + if (res.data['code'] == 0) { + return Success(FavTopicData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } + + static Future addFavTopic(topicId) async { + var res = await Request().post( + Api.addFavTopic, + data: { + 'topic_id': topicId, + 'csrf': Accounts.main.csrf, + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future delFavTopic(topicId) async { + var res = await Request().post( + Api.delFavTopic, + data: { + 'topic_id': topicId, + 'csrf': Accounts.main.csrf, + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future likeTopic(topicId, bool isLike) async { + var res = await Request().post( + Api.likeTopic, + data: { + 'action': isLike ? 'cancel_like' : 'like', + 'up_mid': Accounts.main.mid, + 'topic_id': topicId, + 'csrf': Accounts.main.csrf, + 'business': 'topic', + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + static Future favArticle({ required int page, }) async { - var res = await Request().get(Api.favArticle, queryParameters: { - 'page_size': 20, - 'page': page, - }); + var res = await Request().get( + Api.favArticle, + queryParameters: { + 'page_size': 20, + 'page': page, + }, + ); if (res.data['code'] == 0) { return Success(res.data['data']); } else { diff --git a/lib/main.dart b/lib/main.dart index e581245c4..93bbdebe6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -42,8 +42,7 @@ void main() async { } } } - if (GStorage.setting - .get(SettingBoxKey.horizontalScreen, defaultValue: false)) { + if (GStorage.horizontalScreen) { await SystemChrome.setPreferredOrientations( //支持竖屏与横屏 [ diff --git a/lib/models/common/fav_type.dart b/lib/models/common/fav_type.dart index 6c0936833..01ed454cc 100644 --- a/lib/models/common/fav_type.dart +++ b/lib/models/common/fav_type.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/pages/fav/article/view.dart'; import 'package:PiliPlus/pages/fav/note/view.dart'; import 'package:PiliPlus/pages/fav/pgc/view.dart'; +import 'package:PiliPlus/pages/fav/topic/view.dart'; import 'package:PiliPlus/pages/fav/video/view.dart'; import 'package:flutter/material.dart'; @@ -9,7 +10,8 @@ enum FavTabType { bangumi('追番', FavPgcPage(type: 1)), cinema('追剧', FavPgcPage(type: 2)), article('专栏', FavArticlePage()), - note('笔记', FavNotePage()); + note('笔记', FavNotePage()), + topic('话题', FavTopicPage()); final String title; final Widget page; diff --git a/lib/models/dynamics/dyn_topic_top/topic_item.dart b/lib/models/dynamics/dyn_topic_top/topic_item.dart index ea04fea31..822f887f8 100644 --- a/lib/models/dynamics/dyn_topic_top/topic_item.dart +++ b/lib/models/dynamics/dyn_topic_top/topic_item.dart @@ -3,7 +3,8 @@ class TopicItem { String? name; int? view; int? discuss; - int? fav; + late int fav; + late int like; int? dynamics; String? jumpUrl; String? backColor; @@ -12,13 +13,16 @@ class TopicItem { String? shareUrl; int? ctime; bool? showInteractData; + bool? isFav; + bool? isLike; TopicItem({ this.id, this.name, this.view, this.discuss, - this.fav, + required this.fav, + required this.like, this.dynamics, this.jumpUrl, this.backColor, @@ -27,14 +31,17 @@ class TopicItem { this.shareUrl, this.ctime, this.showInteractData, + this.isFav, + this.isLike, }); factory TopicItem.fromJson(Map json) => TopicItem( id: json['id'] as int?, name: json['name'] as String?, - view: json['view'] as int?, - discuss: json['discuss'] as int?, - fav: json['fav'] as int?, + view: json['view'] as int? ?? 0, + discuss: json['discuss'] as int? ?? 0, + fav: json['fav'] as int? ?? 0, + like: json['like'] as int? ?? 0, dynamics: json['dynamics'] as int?, jumpUrl: json['jump_url'] as String?, backColor: json['back_color'] as String?, @@ -43,6 +50,8 @@ class TopicItem { shareUrl: json['share_url'] as String?, ctime: json['ctime'] as int?, showInteractData: json['show_interact_data'] as bool?, + isFav: json['is_fav'] as bool?, + isLike: json['is_like'] as bool?, ); Map toJson() => { diff --git a/lib/models/fav_topic/data.dart b/lib/models/fav_topic/data.dart new file mode 100644 index 000000000..5034de2b8 --- /dev/null +++ b/lib/models/fav_topic/data.dart @@ -0,0 +1,17 @@ +import 'package:PiliPlus/models/user/fav_topic/topic_list.dart'; + +class FavTopicData { + TopicList? topicList; + + FavTopicData({this.topicList}); + + factory FavTopicData.fromJson(Map json) => FavTopicData( + topicList: json['topic_list'] == null + ? null + : TopicList.fromJson(json['topic_list'] as Map), + ); + + Map toJson() => { + 'topic_list': topicList?.toJson(), + }; +} diff --git a/lib/models/fav_topic/fav_topic.dart b/lib/models/fav_topic/fav_topic.dart new file mode 100644 index 000000000..46d85b899 --- /dev/null +++ b/lib/models/fav_topic/fav_topic.dart @@ -0,0 +1,26 @@ +import 'package:PiliPlus/models/fav_topic/data.dart'; + +class FavTopic { + int? code; + String? message; + int? ttl; + FavTopicData? data; + + FavTopic({this.code, this.message, this.ttl, this.data}); + + factory FavTopic.fromJson(Map json) => FavTopic( + code: json['code'] as int?, + message: json['message'] as String?, + ttl: json['ttl'] as int?, + data: json['data'] == null + ? null + : FavTopicData.fromJson(json['data'] as Map), + ); + + Map toJson() => { + 'code': code, + 'message': message, + 'ttl': ttl, + 'data': data?.toJson(), + }; +} diff --git a/lib/models/fav_topic/page_info.dart b/lib/models/fav_topic/page_info.dart new file mode 100644 index 000000000..537724e6d --- /dev/null +++ b/lib/models/fav_topic/page_info.dart @@ -0,0 +1,16 @@ +class PageInfo { + int? curPageNum; + int? total; + + PageInfo({this.curPageNum, this.total}); + + factory PageInfo.fromJson(Map json) => PageInfo( + curPageNum: json['cur_page_num'] as int?, + total: json['total'] as int?, + ); + + Map toJson() => { + 'cur_page_num': curPageNum, + 'total': total, + }; +} diff --git a/lib/models/fav_topic/topic_item.dart b/lib/models/fav_topic/topic_item.dart new file mode 100644 index 000000000..85be4d5d0 --- /dev/null +++ b/lib/models/fav_topic/topic_item.dart @@ -0,0 +1,39 @@ +class FavTopicModel { + int? id; + String? name; + int? view; + int? discuss; + String? jumpUrl; + String? statDesc; + bool? showInteractData; + + FavTopicModel({ + this.id, + this.name, + this.view, + this.discuss, + this.jumpUrl, + this.statDesc, + this.showInteractData, + }); + + factory FavTopicModel.fromJson(Map json) => FavTopicModel( + id: json['id'] as int?, + name: json['name'] as String?, + view: json['view'] as int?, + discuss: json['discuss'] as int?, + jumpUrl: json['jump_url'] as String?, + statDesc: json['stat_desc'] as String?, + showInteractData: json['show_interact_data'] as bool?, + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'view': view, + 'discuss': discuss, + 'jump_url': jumpUrl, + 'stat_desc': statDesc, + 'show_interact_data': showInteractData, + }; +} diff --git a/lib/models/fav_topic/topic_list.dart b/lib/models/fav_topic/topic_list.dart new file mode 100644 index 000000000..bb354ab4f --- /dev/null +++ b/lib/models/fav_topic/topic_list.dart @@ -0,0 +1,23 @@ +import 'package:PiliPlus/models/user/fav_topic/page_info.dart'; +import 'package:PiliPlus/models/user/fav_topic/topic_item.dart'; + +class TopicList { + List? topicItems; + PageInfo? pageInfo; + + TopicList({this.topicItems, this.pageInfo}); + + factory TopicList.fromJson(Map json) => TopicList( + topicItems: (json['topic_items'] as List?) + ?.map((e) => FavTopicModel.fromJson(e as Map)) + .toList(), + pageInfo: json['page_info'] == null + ? null + : PageInfo.fromJson(json['page_info'] as Map), + ); + + Map toJson() => { + 'topic_items': topicItems?.map((e) => e.toJson()).toList(), + 'page_info': pageInfo?.toJson(), + }; +} diff --git a/lib/models/user/fav_topic/data.dart b/lib/models/user/fav_topic/data.dart new file mode 100644 index 000000000..5034de2b8 --- /dev/null +++ b/lib/models/user/fav_topic/data.dart @@ -0,0 +1,17 @@ +import 'package:PiliPlus/models/user/fav_topic/topic_list.dart'; + +class FavTopicData { + TopicList? topicList; + + FavTopicData({this.topicList}); + + factory FavTopicData.fromJson(Map json) => FavTopicData( + topicList: json['topic_list'] == null + ? null + : TopicList.fromJson(json['topic_list'] as Map), + ); + + Map toJson() => { + 'topic_list': topicList?.toJson(), + }; +} diff --git a/lib/models/user/fav_topic/page_info.dart b/lib/models/user/fav_topic/page_info.dart new file mode 100644 index 000000000..537724e6d --- /dev/null +++ b/lib/models/user/fav_topic/page_info.dart @@ -0,0 +1,16 @@ +class PageInfo { + int? curPageNum; + int? total; + + PageInfo({this.curPageNum, this.total}); + + factory PageInfo.fromJson(Map json) => PageInfo( + curPageNum: json['cur_page_num'] as int?, + total: json['total'] as int?, + ); + + Map toJson() => { + 'cur_page_num': curPageNum, + 'total': total, + }; +} diff --git a/lib/models/user/fav_topic/topic_item.dart b/lib/models/user/fav_topic/topic_item.dart new file mode 100644 index 000000000..85be4d5d0 --- /dev/null +++ b/lib/models/user/fav_topic/topic_item.dart @@ -0,0 +1,39 @@ +class FavTopicModel { + int? id; + String? name; + int? view; + int? discuss; + String? jumpUrl; + String? statDesc; + bool? showInteractData; + + FavTopicModel({ + this.id, + this.name, + this.view, + this.discuss, + this.jumpUrl, + this.statDesc, + this.showInteractData, + }); + + factory FavTopicModel.fromJson(Map json) => FavTopicModel( + id: json['id'] as int?, + name: json['name'] as String?, + view: json['view'] as int?, + discuss: json['discuss'] as int?, + jumpUrl: json['jump_url'] as String?, + statDesc: json['stat_desc'] as String?, + showInteractData: json['show_interact_data'] as bool?, + ); + + Map toJson() => { + 'id': id, + 'name': name, + 'view': view, + 'discuss': discuss, + 'jump_url': jumpUrl, + 'stat_desc': statDesc, + 'show_interact_data': showInteractData, + }; +} diff --git a/lib/models/user/fav_topic/topic_list.dart b/lib/models/user/fav_topic/topic_list.dart new file mode 100644 index 000000000..bb354ab4f --- /dev/null +++ b/lib/models/user/fav_topic/topic_list.dart @@ -0,0 +1,23 @@ +import 'package:PiliPlus/models/user/fav_topic/page_info.dart'; +import 'package:PiliPlus/models/user/fav_topic/topic_item.dart'; + +class TopicList { + List? topicItems; + PageInfo? pageInfo; + + TopicList({this.topicItems, this.pageInfo}); + + factory TopicList.fromJson(Map json) => TopicList( + topicItems: (json['topic_items'] as List?) + ?.map((e) => FavTopicModel.fromJson(e as Map)) + .toList(), + pageInfo: json['page_info'] == null + ? null + : PageInfo.fromJson(json['page_info'] as Map), + ); + + Map toJson() => { + 'topic_items': topicItems?.map((e) => e.toJson()).toList(), + 'page_info': pageInfo?.toJson(), + }; +} diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index 68ad91474..0d5a9cc4c 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; @@ -51,7 +52,8 @@ class _ArticlePageState extends State ); bool _isFabVisible = true; bool? _imageStatus; - late AnimationController fabAnimationCtr; + late final AnimationController fabAnimationCtr; + late final Animation _anim; late final List _ratio = GStorage.dynamicDetailRatio; @@ -109,6 +111,13 @@ class _ArticlePageState extends State vsync: this, duration: const Duration(milliseconds: 300), ); + _anim = Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: fabAnimationCtr, + curve: Curves.easeInOut, + )); fabAnimationCtr.forward(); _articleCtr.scrollController.addListener(listener); } @@ -267,7 +276,7 @@ class _ArticlePageState extends State color: theme.dividerColor.withValues(alpha: 0.05), ), ), - _buildReplyHeader, + _buildReplyHeader(theme), Obx(() => _buildReplyList( theme, _articleCtr.loadingState.value)), ], @@ -319,7 +328,7 @@ class _ArticlePageState extends State controller: _articleCtr.scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ - _buildReplyHeader, + _buildReplyHeader(theme), Obx(() => _buildReplyList( theme, _articleCtr.loadingState.value)), ], @@ -606,29 +615,35 @@ class _ArticlePageState extends State }; } - Widget get _buildReplyHeader { - return SliverToBoxAdapter( - child: Container( - height: 45, - padding: const EdgeInsets.only(left: 12, right: 6), - child: Row( - children: [ - const Text('回复'), - const Spacer(), - SizedBox( - height: 35, - child: TextButton.icon( - onPressed: () => _articleCtr.queryBySort(), - icon: const Icon(Icons.sort, size: 16), - label: Obx( - () => Text( - _articleCtr.sortType.value.label, - style: const TextStyle(fontSize: 13), + Widget _buildReplyHeader(ThemeData theme) { + return SliverPersistentHeader( + pinned: true, + delegate: CustomSliverPersistentHeaderDelegate( + extent: 40, + bgColor: theme.colorScheme.surface, + child: Container( + height: 45, + padding: const EdgeInsets.only(left: 12, right: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx(() => Text( + '${_articleCtr.count.value == -1 ? 0 : Utils.numFormat(_articleCtr.count.value)}条回复')), + SizedBox( + height: 35, + child: TextButton.icon( + onPressed: () => _articleCtr.queryBySort(), + icon: const Icon(Icons.sort, size: 16), + label: Obx( + () => Text( + _articleCtr.sortType.value.label, + style: const TextStyle(fontSize: 13), + ), ), ), - ), - ) - ], + ) + ], + ), ), ), ); @@ -768,13 +783,7 @@ class _ArticlePageState extends State bottom: 0, right: 0, child: SlideTransition( - position: Tween( - begin: const Offset(0, 1), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: fabAnimationCtr, - curve: Curves.easeInOut, - )), + position: _anim, child: Builder( builder: (context) { Widget button() => FloatingActionButton( diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index 36c3611b3..e83527638 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -8,6 +8,7 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/bangumi/list.dart'; import 'package:PiliPlus/models/bangumi/pgc_timeline/result.dart'; +import 'package:PiliPlus/models/common/fav_type.dart'; import 'package:PiliPlus/models/common/home_tab_type.dart'; import 'package:PiliPlus/pages/bangumi/controller.dart'; import 'package:PiliPlus/pages/bangumi/widgets/bangumi_card_v.dart'; @@ -375,8 +376,9 @@ class _BangumiPageState extends CommonPageState onTap: () { Get.toNamed( '/fav', - arguments: - widget.tabType == HomeTabType.bangumi ? 1 : 2, + arguments: widget.tabType == HomeTabType.bangumi + ? FavTabType.bangumi.index + : FavTabType.cinema.index, ); }, child: Padding( diff --git a/lib/pages/blacklist/controller.dart b/lib/pages/blacklist/controller.dart index 13fd6d657..44279f4bf 100644 --- a/lib/pages/blacklist/controller.dart +++ b/lib/pages/blacklist/controller.dart @@ -45,9 +45,10 @@ class BlackListController onConfirm: () async { var result = await VideoHttp.relationMod(mid: mid, act: 6, reSrc: 11); if (result['status']) { - loadingState.value.data!.removeAt(index); + loadingState + ..value.data!.removeAt(index) + ..refresh(); total.value -= 1; - loadingState.refresh(); SmartDialog.showToast('操作成功'); } }, diff --git a/lib/pages/common/common_slide_page.dart b/lib/pages/common/common_slide_page.dart index 9b94b8f57..0c5cbb9b3 100644 --- a/lib/pages/common/common_slide_page.dart +++ b/lib/pages/common/common_slide_page.dart @@ -8,21 +8,41 @@ abstract class CommonSlidePage extends StatefulWidget { final bool? enableSlide; } -abstract class CommonSlidePageState - extends State { +abstract class CommonSlidePageState extends State + with TickerProviderStateMixin { Offset? downPos; bool? isSliding; - late double padding = 0.0; - late final enableSlide = - widget.enableSlide != false && GStorage.slideDismissReplyPage; + late final bool enableSlide; + AnimationController? _animController; + Animation? _anim; + + @override + void initState() { + super.initState(); + enableSlide = widget.enableSlide != false && GStorage.slideDismissReplyPage; + if (enableSlide) { + _animController = AnimationController( + vsync: this, + reverseDuration: const Duration(milliseconds: 500), + ); + _anim = Tween(begin: Offset.zero, end: const Offset(0, 1)) + .animate(_animController!); + } + } + + @override + void dispose() { + _animController?.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final theme = Theme.of(context); return enableSlide - ? Padding( - padding: EdgeInsets.only(top: padding), + ? SlideTransition( + position: _anim!, child: buildPage(theme), ) : buildPage(theme); @@ -32,61 +52,60 @@ abstract class CommonSlidePageState Widget buildList(ThemeData theme) => throw UnimplementedError(); - Widget slideList(ThemeData theme, [Widget? buildList]) => GestureDetector( - onPanDown: (event) { - if (event.localPosition.dx > 30) { - isSliding = false; - } else { - downPos = event.localPosition; - } - }, - onPanUpdate: (event) { - if (isSliding == false) { - return; - } else if (isSliding == null) { - if (downPos != null) { - Offset cumulativeDelta = event.localPosition - downPos!; - if (cumulativeDelta.dx.abs() >= cumulativeDelta.dy.abs()) { - isSliding = true; - setState(() { - padding = event.localPosition.dx.abs(); - }); + Widget slideList(ThemeData theme, [Widget? buildList]) => LayoutBuilder( + builder: (_, constrains) { + final maxWidth = constrains.maxWidth; + + void onDismiss() { + if (isSliding == true) { + if (_animController!.value * maxWidth >= 100) { + Get.back(); } else { - isSliding = false; + _animController!.reverse(); } } - } else if (isSliding == true) { - setState(() { - padding = event.localPosition.dx.abs(); - }); + downPos = null; + isSliding = null; } - }, - onPanCancel: () { - if (isSliding == true) { - if (padding >= 100) { - Get.back(); - } else { - setState(() { - padding = 0; - }); + + void onPan(Offset localPosition) { + if (isSliding == false) { + return; + } else if (isSliding == null) { + if (downPos != null) { + Offset cumulativeDelta = localPosition - downPos!; + if (cumulativeDelta.dx.abs() >= cumulativeDelta.dy.abs()) { + isSliding = true; + _animController!.value = localPosition.dx.abs() / maxWidth; + } else { + isSliding = false; + } + } + } else if (isSliding == true) { + _animController!.value = localPosition.dx.abs() / maxWidth; } } - downPos = null; - isSliding = null; + + return GestureDetector( + onPanDown: (details) { + if (details.localPosition.dx > 30) { + isSliding = false; + } else { + downPos = details.localPosition; + } + }, + onPanStart: (details) { + onPan(details.localPosition); + }, + onPanUpdate: (details) { + onPan(details.localPosition); + }, + onPanCancel: onDismiss, + onPanEnd: (_) { + onDismiss(); + }, + child: buildList ?? this.buildList(theme), + ); }, - onPanEnd: (event) { - if (isSliding == true) { - if (padding >= 100) { - Get.back(); - } else { - setState(() { - padding = 0; - }); - } - } - downPos = null; - isSliding = null; - }, - child: buildList ?? this.buildList(theme), ); } diff --git a/lib/pages/dynamics_detail/view.dart b/lib/pages/dynamics_detail/view.dart index 4bdd653b1..a645230ce 100644 --- a/lib/pages/dynamics_detail/view.dart +++ b/lib/pages/dynamics_detail/view.dart @@ -40,7 +40,8 @@ class DynamicDetailPage extends StatefulWidget { class _DynamicDetailPageState extends State with TickerProviderStateMixin { late DynamicDetailController _dynamicDetailController; - AnimationController? _fabAnimationCtr; + late final AnimationController _fabAnimationCtr; + late final Animation _anim; final RxBool _visibleTitle = false.obs; // 回复类型 late int replyType; @@ -109,7 +110,14 @@ class _DynamicDetailPageState extends State vsync: this, duration: const Duration(milliseconds: 300), ); - _fabAnimationCtr?.forward(); + _anim = Tween( + begin: const Offset(0, 1), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _fabAnimationCtr, + curve: Curves.easeInOut, + )); + _fabAnimationCtr.forward(); _dynamicDetailController.scrollController.addListener(listener); } @@ -264,21 +272,20 @@ class _DynamicDetailPageState extends State void _showFab() { if (!_isFabVisible) { _isFabVisible = true; - _fabAnimationCtr?.forward(); + _fabAnimationCtr.forward(); } } void _hideFab() { if (_isFabVisible) { _isFabVisible = false; - _fabAnimationCtr?.reverse(); + _fabAnimationCtr.reverse(); } } @override void dispose() { - _fabAnimationCtr?.dispose(); - _fabAnimationCtr = null; + _fabAnimationCtr.dispose(); _dynamicDetailController.scrollController.removeListener(listener); super.dispose(); } @@ -454,161 +461,110 @@ class _DynamicDetailPageState extends State } }, ), - if (_fabAnimationCtr != null) - Positioned( - left: 0, - right: 0, - bottom: 0, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 1), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _fabAnimationCtr!, - curve: Curves.easeInOut, - )), - child: Builder( - builder: (context) { - Widget button() => FloatingActionButton( - heroTag: null, - onPressed: () { - feedBack(); - _dynamicDetailController.onReply( - context, - oid: _dynamicDetailController.oid, - replyType: ReplyType.values[replyType], - ); - }, - tooltip: '评论动态', - child: const Icon(Icons.reply), - ); - return _dynamicDetailController.showDynActionBar.not - ? Align( - alignment: Alignment.bottomRight, - child: Padding( - padding: EdgeInsets.only( - right: 14, - bottom: - MediaQuery.paddingOf(context).bottom + 14, - ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: SlideTransition( + position: _anim, + child: Builder( + builder: (context) { + Widget button() => FloatingActionButton( + heroTag: null, + onPressed: () { + feedBack(); + _dynamicDetailController.onReply( + context, + oid: _dynamicDetailController.oid, + replyType: ReplyType.values[replyType], + ); + }, + tooltip: '评论动态', + child: const Icon(Icons.reply), + ); + return _dynamicDetailController.showDynActionBar.not + ? Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: EdgeInsets.only( + right: 14, + bottom: MediaQuery.paddingOf(context).bottom + 14, + ), + child: button(), + ), + ) + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Padding( + padding: + const EdgeInsets.only(right: 14, bottom: 14), child: button(), ), - ) - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only( - right: 14, bottom: 14), - child: button(), - ), - Container( - decoration: BoxDecoration( - color: theme.colorScheme.surface, - border: Border( - top: BorderSide( - color: theme.colorScheme.outline - .withValues(alpha: 0.08), - ), + Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + border: Border( + top: BorderSide( + color: theme.colorScheme.outline + .withValues(alpha: 0.08), ), ), - padding: EdgeInsets.only( - bottom: - MediaQuery.paddingOf(context).bottom), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceAround, - children: [ - Expanded( - child: Builder( - builder: (btnContext) => - TextButton.icon( - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - useSafeArea: true, - builder: (context) => RepostPanel( - item: _dynamicDetailController - .item, - callback: () { - int count = - _dynamicDetailController - .item - .modules - .moduleStat - ?.forward - ?.count ?? - 0; - _dynamicDetailController - .item - .modules - .moduleStat ??= - ModuleStatModel(); - _dynamicDetailController - .item - .modules - .moduleStat - ?.forward ??= - DynamicStat(); - _dynamicDetailController - .item - .modules - .moduleStat! - .forward! - .count = count + 1; - if (btnContext.mounted) { - (btnContext as Element?) - ?.markNeedsBuild(); - } - }, - ), - ); - }, - icon: Icon( - FontAwesomeIcons.shareFromSquare, - size: 16, - color: theme.colorScheme.outline, - semanticLabel: "转发", - ), - style: TextButton.styleFrom( - padding: const EdgeInsets.fromLTRB( - 15, 0, 15, 0), - foregroundColor: - theme.colorScheme.outline, - ), - label: Text( - _dynamicDetailController - .item - .modules - .moduleStat - ?.forward - ?.count != - null - ? Utils.numFormat( - _dynamicDetailController - .item - .modules - .moduleStat! - .forward! - .count) - : '转发', - ), - ), - ), - ), - Expanded( - child: TextButton.icon( + ), + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + Expanded( + child: Builder( + builder: (btnContext) => TextButton.icon( onPressed: () { - Utils.shareText( - '${HttpString.dynamicShareBaseUrl}/${_dynamicDetailController.item.idStr}'); + showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + builder: (context) => RepostPanel( + item: + _dynamicDetailController.item, + callback: () { + int count = + _dynamicDetailController + .item + .modules + .moduleStat + ?.forward + ?.count ?? + 0; + _dynamicDetailController.item + .modules.moduleStat ??= + ModuleStatModel(); + _dynamicDetailController + .item + .modules + .moduleStat + ?.forward ??= DynamicStat(); + _dynamicDetailController + .item + .modules + .moduleStat! + .forward! + .count = count + 1; + if (btnContext.mounted) { + (btnContext as Element?) + ?.markNeedsBuild(); + } + }, + ), + ); }, icon: Icon( - FontAwesomeIcons.shareNodes, + FontAwesomeIcons.shareFromSquare, size: 16, color: theme.colorScheme.outline, - semanticLabel: "分享", + semanticLabel: "转发", ), style: TextButton.styleFrom( padding: const EdgeInsets.fromLTRB( @@ -616,109 +572,146 @@ class _DynamicDetailPageState extends State foregroundColor: theme.colorScheme.outline, ), - label: const Text('分享'), + label: Text( + _dynamicDetailController + .item + .modules + .moduleStat + ?.forward + ?.count != + null + ? Utils.numFormat( + _dynamicDetailController + .item + .modules + .moduleStat! + .forward! + .count) + : '转发', + ), ), ), - Expanded( - child: Builder( - builder: (context) => TextButton.icon( - onPressed: () => - RequestUtils.onLikeDynamic( - _dynamicDetailController.item, - () { - if (context.mounted) { - (context as Element?) - ?.markNeedsBuild(); - } - }, - ), - icon: Icon( - _dynamicDetailController - .item - .modules - .moduleStat - ?.like - ?.status == - true - ? FontAwesomeIcons.solidThumbsUp - : FontAwesomeIcons.thumbsUp, - size: 16, - color: _dynamicDetailController - .item - .modules - .moduleStat - ?.like - ?.status == - true - ? theme.colorScheme.primary - : theme.colorScheme.outline, - semanticLabel: - _dynamicDetailController - .item - .modules - .moduleStat - ?.like - ?.status == - true - ? "已赞" - : "点赞", - ), - style: TextButton.styleFrom( - padding: const EdgeInsets.fromLTRB( - 15, 0, 15, 0), - foregroundColor: - theme.colorScheme.outline, - ), - label: AnimatedSwitcher( - duration: const Duration( - milliseconds: 400), - transitionBuilder: (Widget child, - Animation animation) { - return ScaleTransition( - scale: animation, - child: child); - }, - child: Text( + ), + Expanded( + child: TextButton.icon( + onPressed: () { + Utils.shareText( + '${HttpString.dynamicShareBaseUrl}/${_dynamicDetailController.item.idStr}'); + }, + icon: Icon( + FontAwesomeIcons.shareNodes, + size: 16, + color: theme.colorScheme.outline, + semanticLabel: "分享", + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.fromLTRB( + 15, 0, 15, 0), + foregroundColor: + theme.colorScheme.outline, + ), + label: const Text('分享'), + ), + ), + Expanded( + child: Builder( + builder: (context) => TextButton.icon( + onPressed: () => + RequestUtils.onLikeDynamic( + _dynamicDetailController.item, + () { + if (context.mounted) { + (context as Element?) + ?.markNeedsBuild(); + } + }, + ), + icon: Icon( + _dynamicDetailController + .item + .modules + .moduleStat + ?.like + ?.status == + true + ? FontAwesomeIcons.solidThumbsUp + : FontAwesomeIcons.thumbsUp, + size: 16, + color: _dynamicDetailController + .item + .modules + .moduleStat + ?.like + ?.status == + true + ? theme.colorScheme.primary + : theme.colorScheme.outline, + semanticLabel: _dynamicDetailController .item .modules .moduleStat ?.like - ?.count != - null - ? Utils.numFormat( - _dynamicDetailController + ?.status == + true + ? "已赞" + : "点赞", + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.fromLTRB( + 15, 0, 15, 0), + foregroundColor: + theme.colorScheme.outline, + ), + label: AnimatedSwitcher( + duration: + const Duration(milliseconds: 400), + transitionBuilder: (Widget child, + Animation animation) { + return ScaleTransition( + scale: animation, child: child); + }, + child: Text( + _dynamicDetailController + .item + .modules + .moduleStat + ?.like + ?.count != + null + ? Utils.numFormat( + _dynamicDetailController + .item + .modules + .moduleStat! + .like! + .count) + : '点赞', + style: TextStyle( + color: _dynamicDetailController .item .modules - .moduleStat! - .like! - .count) - : '点赞', - style: TextStyle( - color: _dynamicDetailController - .item - .modules - .moduleStat - ?.like - ?.status == - true - ? theme.colorScheme.primary - : theme.colorScheme.outline, - ), + .moduleStat + ?.like + ?.status == + true + ? theme.colorScheme.primary + : theme.colorScheme.outline, ), ), ), ), ), - ], - ), + ), + ], ), - ], - ); - }, - ), + ), + ], + ); + }, ), ), + ), ], ); @@ -739,7 +732,7 @@ class _DynamicDetailPageState extends State return ScaleTransition(scale: animation, child: child); }, child: Text( - '${_dynamicDetailController.count.value != -1 ? _dynamicDetailController.count.value : 0}条回复', + '${_dynamicDetailController.count.value == -1 ? 0 : Utils.numFormat(_dynamicDetailController.count.value)}条回复', key: ValueKey(_dynamicDetailController.count.value), ), ), diff --git a/lib/pages/dynamics_topic/controller.dart b/lib/pages/dynamics_topic/controller.dart index 7933d7bcf..d5ffe2d6d 100644 --- a/lib/pages/dynamics_topic/controller.dart +++ b/lib/pages/dynamics_topic/controller.dart @@ -1,35 +1,47 @@ import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/models/dynamics/dyn_topic_feed/item.dart'; import 'package:PiliPlus/models/dynamics/dyn_topic_feed/topic_card_list.dart'; import 'package:PiliPlus/models/dynamics/dyn_topic_feed/topic_sort_by_conf.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_top/top_details.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/utils/extension.dart'; +import 'package:PiliPlus/utils/storage.dart' show Accounts; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class DynTopicController extends CommonListController { final topicId = Get.parameters['id']!; - final topicName = Get.parameters['name']!; + String topicName = Get.parameters['name'] ?? ''; int sortBy = 0; String offset = ''; Rx topicSortByConf = Rx(null); // top - // Rx> topState = - // LoadingState.loading().obs; + final isLogin = Accounts.main.isLogin; + Rx isFav = Rx(null); + Rx isLike = Rx(null); + Rx> topState = + LoadingState.loading().obs; @override void onInit() { super.onInit(); - // queryTop(); + queryTop(); queryData(); } - // Future queryTop() async { - // topState.value = await DynamicsHttp.topicTop(topicId: topicId); - // } + Future queryTop() async { + topState.value = await DynamicsHttp.topicTop(topicId: topicId); + if (topState.value.isSuccess) { + topicName = topState.value.data!.topicItem!.name!; + isFav.value = topState.value.data!.topicItem!.isFav; + isLike.value = topState.value.data!.topicItem!.isLike; + } + } @override List? getDataList(TopicCardList? response) { @@ -45,14 +57,12 @@ class DynTopicController @override Future onRefresh() { offset = ''; + queryTop(); return super.onRefresh(); } @override Future onReload() { - // if (topState.value is! Success) { - // queryTop(); - // } scrollController.jumpToTop(); return super.onReload(); } @@ -69,4 +79,44 @@ class DynTopicController this.sortBy = sortBy; onReload(); } + + Future onFav() async { + if (!isLogin) { + SmartDialog.showToast('账号未登录'); + return; + } + bool isFav = this.isFav.value ?? false; + var res = isFav + ? await UserHttp.delFavTopic(topicId) + : await UserHttp.addFavTopic(topicId); + if (res['status']) { + if (isFav) { + topState.value.data!.topicItem!.fav -= 1; + } else { + topState.value.data!.topicItem!.fav += 1; + } + this.isFav.value = !isFav; + } else { + SmartDialog.showToast(res['msg']); + } + } + + Future onLike() async { + if (!isLogin) { + SmartDialog.showToast('账号未登录'); + return; + } + bool isLike = this.isLike.value ?? false; + var res = await UserHttp.likeTopic(topicId, isLike); + if (res['status']) { + if (isLike) { + topState.value.data!.topicItem!.like -= 1; + } else { + topState.value.data!.topicItem!.like += 1; + } + this.isLike.value = !isLike; + } else { + SmartDialog.showToast(res['msg']); + } + } } diff --git a/lib/pages/dynamics_topic/view.dart b/lib/pages/dynamics_topic/view.dart index 0ad689200..dd3a59471 100644 --- a/lib/pages/dynamics_topic/view.dart +++ b/lib/pages/dynamics_topic/view.dart @@ -1,16 +1,25 @@ import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart'; +import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar_medium.dart'; +import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models/dynamics/dyn_topic_feed/item.dart'; +import 'package:PiliPlus/models/dynamics/dyn_topic_top/top_details.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; import 'package:PiliPlus/pages/dynamics_tab/view.dart'; import 'package:PiliPlus/pages/dynamics_topic/controller.dart'; import 'package:PiliPlus/utils/grid.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:waterfall_flow/waterfall_flow.dart'; class DynTopicPage extends StatefulWidget { @@ -28,33 +37,8 @@ class _DynTopicPageState extends State { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); return Scaffold( - appBar: AppBar( - title: Text(_controller.topicName), - actions: [ - Obx(() { - if (_controller.topicSortByConf.value?.allSortBy?.isNotEmpty == - true) { - return PopupMenuButton( - initialValue: _controller.sortBy, - itemBuilder: (context) { - return _controller.topicSortByConf.value!.allSortBy! - .map((e) { - return PopupMenuItem( - value: e.sortBy, - child: Text(e.sortName!), - onTap: () { - _controller.onSort(e.sortBy!); - }, - ); - }).toList(); - }, - ); - } - return const SizedBox.shrink(); - }) - ], - ), body: SafeArea( top: false, bottom: false, @@ -63,6 +47,68 @@ class _DynTopicPageState extends State { child: CustomScrollView( controller: _controller.scrollController, slivers: [ + Obx(() => _buildAppBar(theme, _controller.topState.value)), + Obx(() { + if (_controller.topicSortByConf.value?.allSortBy?.isNotEmpty == + true) { + return SliverPersistentHeader( + pinned: true, + delegate: CustomSliverPersistentHeaderDelegate( + extent: 30, + bgColor: theme.colorScheme.surface, + child: SizedBox( + height: 30, + child: Builder( + builder: (context) { + return Padding( + padding: + const EdgeInsets.only(left: 12, bottom: 6), + child: ToggleButtons( + fillColor: theme.colorScheme.secondaryContainer, + selectedColor: + theme.colorScheme.onSecondaryContainer, + constraints: const BoxConstraints( + minWidth: 54, minHeight: 24), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + borderRadius: + const BorderRadius.all(Radius.circular(25)), + onPressed: (index) { + _controller.onSort(_controller.topicSortByConf + .value!.allSortBy![index].sortBy!); + (context as Element).markNeedsBuild(); + }, + isSelected: _controller + .topicSortByConf.value!.allSortBy! + .map((e) { + return e.sortBy == _controller.sortBy; + }).toList(), + children: _controller + .topicSortByConf.value!.allSortBy! + .map((e) { + return Text( + e.sortName!, + style: const TextStyle( + fontSize: 13, + height: 1, + ), + strutStyle: const StrutStyle( + height: 1, + leading: 0, + fontSize: 13, + ), + textScaler: TextScaler.noScaling, + ); + }).toList(), + ), + ); + }, + ), + ), + ), + ); + } + return const SliverToBoxAdapter(); + }), Obx(() => _buildBody(_controller.loadingState.value)), ], ), @@ -71,6 +117,187 @@ class _DynTopicPageState extends State { ); } + Widget _buildAppBar(ThemeData theme, LoadingState topState) { + return switch (topState) { + Loading() => const SliverAppBar(), + Success(:var response) when (topState.dataOrNull != null) => + DynamicSliverAppBarMedium( + pinned: true, + title: IgnorePointer(child: Text(response!.topicItem!.name!)), + flexibleSpace: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: Image.asset( + 'assets/images/topic-header-bg.png', + ).image, + filterQuality: FilterQuality.low, + fit: BoxFit.cover, + ), + ), + padding: EdgeInsets.only( + top: MediaQuery.paddingOf(context).top, + left: 12, + right: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: kToolbarHeight, + alignment: Alignment.centerLeft, + margin: const EdgeInsets.only(left: 45, right: 78), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Get.toNamed('/member?mid=${response.topicCreator!.uid}'); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + NetworkImgLayer( + width: 28, + height: 28, + src: response.topicCreator!.face!, + type: ImageType.avatar, + ), + const SizedBox(width: 10), + Flexible( + child: Text( + response.topicCreator!.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + ' 发起', + style: TextStyle(color: theme.colorScheme.outline), + ), + ], + ), + ), + ), + Text( + response.topicItem!.name!, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 6), + SelectableText( + response.topicItem!.description!, + style: TextStyle(color: theme.colorScheme.onSurfaceVariant), + ), + const SizedBox(height: 10), + Row( + children: [ + Text( + '${Utils.numFormat(response.topicItem!.view)}浏览 · ${Utils.numFormat(response.topicItem!.discuss)}讨论', + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.outline, + ), + ), + const Spacer(), + OutlinedButton.icon( + style: OutlinedButton.styleFrom( + side: BorderSide( + width: 1, + color: + theme.colorScheme.outline.withValues(alpha: 0.2), + ), + foregroundColor: _controller.isLike.value == true + ? null + : theme.colorScheme.onSurfaceVariant, + padding: const EdgeInsets.symmetric(horizontal: 10), + visualDensity: + const VisualDensity(horizontal: -4, vertical: -4), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: _controller.onLike, + icon: _controller.isLike.value == true + ? const Icon(FontAwesomeIcons.solidThumbsUp, size: 13) + : const Icon(FontAwesomeIcons.thumbsUp, size: 13), + label: Text( + Utils.numFormat(response.topicItem!.like), + style: const TextStyle(fontSize: 13), + textScaler: TextScaler.noScaling, + ), + ), + const SizedBox(width: 10), + OutlinedButton.icon( + style: OutlinedButton.styleFrom( + side: BorderSide( + width: 1, + color: + theme.colorScheme.outline.withValues(alpha: 0.2), + ), + foregroundColor: _controller.isFav.value == true + ? null + : theme.colorScheme.onSurfaceVariant, + padding: const EdgeInsets.symmetric(horizontal: 10), + visualDensity: + const VisualDensity(horizontal: -4, vertical: -4), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: _controller.onFav, + icon: _controller.isFav.value == true + ? const Icon(FontAwesomeIcons.solidStar, size: 13) + : const Icon(FontAwesomeIcons.star, size: 13), + label: Text( + Utils.numFormat(response.topicItem!.fav), + style: const TextStyle(fontSize: 13), + textScaler: TextScaler.noScaling, + ), + ), + ], + ), + const SizedBox(height: 6), + ], + ), + ), + actions: [ + IconButton( + onPressed: () { + // https://www.bilibili.com/v/topic/detail?topic_id=${_controller.topicId} + Utils.shareText( + '${_controller.topicName} https://m.bilibili.com/topic-detail?topic_id=${_controller.topicId}'); + }, + icon: Icon(MdiIcons.share), + ), + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + onTap: _controller.onFav, + child: Text( + '${_controller.isFav.value == true ? '取消' : ''}收藏'), + ), + PopupMenuItem( + child: const Text('举报'), + onTap: () { + if (!_controller.isLogin) { + SmartDialog.showToast('账号未登录'); + return; + } + final isDark = Get.isDarkMode; + PageUtils.inAppWebview( + 'https://www.bilibili.com/h5/topic-active/topic-report?topic_id=${_controller.topicId}&topic_name=${_controller.topicName}&native.theme=${isDark ? 2 : 1}&night=${isDark ? 1 : 0}'); + }, + ), + ]; + }, + ), + const SizedBox(width: 4), + ], + ), + _ => SliverAppBar( + pinned: true, + title: Text(_controller.topicName), + ), + }; + } + Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { Loading() => DynamicsTabPage.dynSkeleton(dynamicsWaterfallFlow), diff --git a/lib/pages/episode_panel/view.dart b/lib/pages/episode_panel/view.dart index 96af6da1a..d4bb73726 100644 --- a/lib/pages/episode_panel/view.dart +++ b/lib/pages/episode_panel/view.dart @@ -74,8 +74,7 @@ class EpisodePanel extends CommonSlidePage { State createState() => _EpisodePanelState(); } -class _EpisodePanelState extends CommonSlidePageState - with SingleTickerProviderStateMixin { +class _EpisodePanelState extends CommonSlidePageState { // tab late final TabController _tabController = TabController( initialIndex: widget.initialTabIndex, diff --git a/lib/pages/fan/controller.dart b/lib/pages/fan/controller.dart index af6d6dbd2..55a46c521 100644 --- a/lib/pages/fan/controller.dart +++ b/lib/pages/fan/controller.dart @@ -38,8 +38,9 @@ class FansController reSrc: 11, ); if (res['status']) { - loadingState.value.data!.removeAt(index); - loadingState.refresh(); + loadingState + ..value.data!.removeAt(index) + ..refresh(); SmartDialog.showToast('移除成功'); } else { SmartDialog.showToast(res['msg']); diff --git a/lib/pages/fav/article/controller.dart b/lib/pages/fav/article/controller.dart index bf6eb7aad..4898da821 100644 --- a/lib/pages/fav/article/controller.dart +++ b/lib/pages/fav/article/controller.dart @@ -29,8 +29,9 @@ class FavArticleController extends CommonListController { Future onRemove(index, id) async { final res = await UserHttp.communityAction(opusId: id, action: 4); if (res['status']) { - loadingState.value.data!.removeAt(index); - loadingState.refresh(); + loadingState + ..value.data!.removeAt(index) + ..refresh(); SmartDialog.showToast('已取消收藏'); } else { SmartDialog.showToast(res['msg']); diff --git a/lib/pages/fav/article/view.dart b/lib/pages/fav/article/view.dart index b4e66d7a8..784c4b044 100644 --- a/lib/pages/fav/article/view.dart +++ b/lib/pages/fav/article/view.dart @@ -68,14 +68,15 @@ class _FavArticlePageState extends State item: response[index], onDelete: () { showConfirmDialog( - context: context, - title: '确定取消收藏?', - onConfirm: () { - _favArticleController.onRemove( - index, - response[index]['opus_id'], - ); - }); + context: context, + title: '确定取消收藏?', + onConfirm: () { + _favArticleController.onRemove( + index, + response[index]['opus_id'], + ); + }, + ); }, ); }, diff --git a/lib/pages/fav/pgc/controller.dart b/lib/pages/fav/pgc/controller.dart index c730cbb6e..c2c706952 100644 --- a/lib/pages/fav/pgc/controller.dart +++ b/lib/pages/fav/pgc/controller.dart @@ -57,8 +57,9 @@ class FavPgcController Future bangumiDel(index, seasonId) async { var result = await VideoHttp.bangumiDel(seasonId: seasonId); if (result['status']) { - loadingState.value.data!.removeAt(index); - loadingState.refresh(); + loadingState + ..value.data!.removeAt(index) + ..refresh(); } SmartDialog.showToast(result['msg']); } diff --git a/lib/pages/fav/topic/controller.dart b/lib/pages/fav/topic/controller.dart new file mode 100644 index 000000000..47ed4dcdc --- /dev/null +++ b/lib/pages/fav/topic/controller.dart @@ -0,0 +1,52 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/user.dart'; +import 'package:PiliPlus/models/user/fav_topic/data.dart'; +import 'package:PiliPlus/models/user/fav_topic/topic_item.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + +class FavTopicController + extends CommonListController { + int? total; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + void checkIsEnd(int length) { + if (total != null && length >= total!) { + isEnd = true; + } + } + + @override + List? getDataList(FavTopicData response) { + total = response.topicList?.pageInfo?.total; + return response.topicList?.topicItems; + } + + @override + Future onRefresh() { + total = null; + return super.onRefresh(); + } + + @override + Future> customGetData() => + UserHttp.favTopic(page: page); + + Future onRemove(index, id) async { + var res = await UserHttp.delFavTopic(id); + if (res['status']) { + loadingState + ..value.data!.removeAt(index) + ..refresh(); + SmartDialog.showToast('已取消收藏'); + } else { + SmartDialog.showToast(res['msg']); + } + } +} diff --git a/lib/pages/fav/topic/view.dart b/lib/pages/fav/topic/view.dart new file mode 100644 index 000000000..1e9d10347 --- /dev/null +++ b/lib/pages/fav/topic/view.dart @@ -0,0 +1,117 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/user/fav_topic/topic_item.dart'; +import 'package:PiliPlus/pages/fav/topic/controller.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class FavTopicPage extends StatefulWidget { + const FavTopicPage({super.key}); + + @override + State createState() => _FavTopicPageState(); +} + +class _FavTopicPageState extends State + with AutomaticKeepAliveClientMixin { + final FavTopicController _controller = Get.put(FavTopicController()); + + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + final ThemeData theme = Theme.of(context); + return refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + top: StyleString.safeSpace, + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: + Obx(() => _buildBody(theme, _controller.loadingState.value)), + ), + ], + ), + ); + } + + Widget _buildBody( + ThemeData theme, LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => const SliverToBoxAdapter(child: LinearProgressIndicator()), + Success(:var response) => response?.isNotEmpty == true + ? SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + mainAxisSpacing: 12, + crossAxisSpacing: 12, + maxCrossAxisExtent: Grid.smallCardWidth, + mainAxisExtent: MediaQuery.textScalerOf(context).scale(30), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == response.length - 1) { + _controller.onLoadMore(); + } + final item = response[index]; + return Material( + color: theme.colorScheme.onInverseSurface, + borderRadius: const BorderRadius.all(Radius.circular(6)), + child: InkWell( + onTap: () { + Get.toNamed( + '/dynTopic', + parameters: { + 'id': item.id!.toString(), + 'name': item.name!, + }, + ); + }, + onLongPress: () { + showConfirmDialog( + context: context, + title: '确定取消收藏?', + onConfirm: () { + _controller.onRemove(index, item.id); + }, + ); + }, + borderRadius: const BorderRadius.all(Radius.circular(6)), + child: Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: 11, vertical: 5), + child: Text( + '# ${item.name}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 14, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ); + }, + childCount: response!.length, + ), + ) + : HttpError(onReload: _controller.onReload), + Error(:var errMsg) => HttpError( + errMsg: errMsg, + onReload: _controller.onReload, + ), + }; + } +} diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart index 130aedbee..548664a13 100644 --- a/lib/pages/fav/view.dart +++ b/lib/pages/fav/view.dart @@ -116,6 +116,8 @@ class _FavPageState extends State with SingleTickerProviderStateMixin { ], bottom: TabBar( controller: _tabController, + isScrollable: true, + tabAlignment: TabAlignment.start, tabs: FavTabType.values.map((item) => Tab(text: item.title)).toList(), ), ), diff --git a/lib/pages/fav_search/controller.dart b/lib/pages/fav_search/controller.dart index aef3ff847..8f0185d5d 100644 --- a/lib/pages/fav_search/controller.dart +++ b/lib/pages/fav_search/controller.dart @@ -45,8 +45,9 @@ class FavSearchController type: type, ); if (result['status']) { - loadingState.value.data!.removeAt(index); - loadingState.refresh(); + loadingState + ..value.data!.removeAt(index) + ..refresh(); SmartDialog.showToast('取消收藏'); } } diff --git a/lib/pages/history_search/controller.dart b/lib/pages/history_search/controller.dart index a02695c6f..de56c168b 100644 --- a/lib/pages/history_search/controller.dart +++ b/lib/pages/history_search/controller.dart @@ -27,8 +27,9 @@ class HistorySearchController var res = await UserHttp.delHistory([resKid]); if (res['status']) { - loadingState.value.data!.removeAt(index); - loadingState.refresh(); + loadingState + ..value.data!.removeAt(index) + ..refresh(); SmartDialog.showToast(res['msg']); } } diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index 085b35a8b..3c8831b55 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -533,7 +533,7 @@ class UserInfoCard extends StatelessWidget { children: [ // _buildHeader(context), SizedBox( - height: Get.mediaQuery.padding.bottom + 56, + height: Get.mediaQuery.padding.top + 56, ), SafeArea( top: false, @@ -548,6 +548,7 @@ class UserInfoCard extends StatelessWidget { ), child: _buildAvatar(context), ), + const SizedBox(width: 10), Expanded( child: Column( mainAxisSize: MainAxisSize.min, diff --git a/lib/pages/member_contribute/view.dart b/lib/pages/member_contribute/view.dart index 008e859f5..a7042f2b1 100644 --- a/lib/pages/member_contribute/view.dart +++ b/lib/pages/member_contribute/view.dart @@ -95,6 +95,7 @@ class _MemberContributeState extends State heroTag: widget.heroTag, mid: widget.mid, title: item.title, + isSingle: _controller.tabs == null, ), 'charging_video' => MemberVideo( type: ContributeType.charging, diff --git a/lib/pages/member_dynamics/controller.dart b/lib/pages/member_dynamics/controller.dart index 995f359a3..928bf8aef 100644 --- a/lib/pages/member_dynamics/controller.dart +++ b/lib/pages/member_dynamics/controller.dart @@ -59,8 +59,9 @@ class MemberDynamicsController Future onRemove(dynamicId) async { var res = await MsgHttp.removeDynamic(dynIdStr: dynamicId); if (res['status']) { - loadingState.value.data!.removeWhere((item) => item.idStr == dynamicId); - loadingState.refresh(); + loadingState + ..value.data!.removeWhere((item) => item.idStr == dynamicId) + ..refresh(); SmartDialog.showToast('删除成功'); } else { SmartDialog.showToast(res['msg']); diff --git a/lib/pages/member_home/view.dart b/lib/pages/member_home/view.dart index e41128d5b..64a5dbbb5 100644 --- a/lib/pages/member_home/view.dart +++ b/lib/pages/member_home/view.dart @@ -262,17 +262,19 @@ class _MemberHomeState extends State .items!; int index1 = items.indexWhere((item) => item.param == param1); - try { - final contributeCtr = - Get.find(tag: widget.heroTag); - // contributeCtr.tabController?.animateTo(index1); - if (contributeCtr.tabController?.index != index1) { - contributeCtr.tabController?.index = index1; + if (index1 != -1) { + try { + final contributeCtr = + Get.find(tag: widget.heroTag); + // contributeCtr.tabController?.animateTo(index1); + if (contributeCtr.tabController?.index != index1) { + contributeCtr.tabController?.index = index1; + } + debugPrint('initialized'); + } catch (e) { + _ctr.contributeInitialIndex.value = index1; + debugPrint('not initialized'); } - debugPrint('initialized'); - } catch (e) { - _ctr.contributeInitialIndex.value = index1; - debugPrint('not initialized'); } } _ctr.tabController?.animateTo(index); diff --git a/lib/pages/member_video/view.dart b/lib/pages/member_video/view.dart index 265e8aa58..c945d83f6 100644 --- a/lib/pages/member_video/view.dart +++ b/lib/pages/member_video/view.dart @@ -21,6 +21,7 @@ class MemberVideo extends StatefulWidget { this.seasonId, this.seriesId, this.title, + this.isSingle = false, }); final ContributeType type; @@ -29,6 +30,7 @@ class MemberVideo extends StatefulWidget { final int? seasonId; final int? seriesId; final String? title; + final bool isSingle; @override State createState() => _MemberVideoState(); @@ -109,13 +111,17 @@ class _MemberVideoState extends State Widget _buildBody( ThemeData theme, LoadingState?> loadingState) { return switch (loadingState) { - Loading() => SliverGrid( - gridDelegate: Grid.videoCardHDelegate(context), - delegate: SliverChildBuilderDelegate( - (context, index) { - return const VideoCardHSkeleton(); - }, - childCount: 10, + Loading() => SliverPadding( + padding: + widget.isSingle ? const EdgeInsets.only(top: 7) : EdgeInsets.zero, + sliver: SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardHSkeleton(); + }, + childCount: 10, + ), ), ), Success(:var response) => response?.isNotEmpty == true diff --git a/lib/pages/msg_feed_top/at_me/controller.dart b/lib/pages/msg_feed_top/at_me/controller.dart index 9c605ad06..2dcef05e6 100644 --- a/lib/pages/msg_feed_top/at_me/controller.dart +++ b/lib/pages/msg_feed_top/at_me/controller.dart @@ -45,8 +45,9 @@ class AtMeController extends CommonListController { try { var res = await MsgHttp.delMsgfeed(2, id); if (res['status']) { - loadingState.value.data!.removeAt(index); - loadingState.refresh(); + loadingState + ..value.data!.removeAt(index) + ..refresh(); SmartDialog.showToast('删除成功'); } else { SmartDialog.showToast(res['msg']); diff --git a/lib/pages/msg_feed_top/reply_me/controller.dart b/lib/pages/msg_feed_top/reply_me/controller.dart index 9f6226116..1bd7dd545 100644 --- a/lib/pages/msg_feed_top/reply_me/controller.dart +++ b/lib/pages/msg_feed_top/reply_me/controller.dart @@ -46,8 +46,9 @@ class ReplyMeController try { var res = await MsgHttp.delMsgfeed(1, id); if (res['status']) { - loadingState.value.data!.removeAt(index); - loadingState.refresh(); + loadingState + ..value.data!.removeAt(index) + ..refresh(); SmartDialog.showToast('删除成功'); } else { SmartDialog.showToast(res['msg']); diff --git a/lib/pages/msg_feed_top/sys_msg/controller.dart b/lib/pages/msg_feed_top/sys_msg/controller.dart index 553724cce..224f1dcc7 100644 --- a/lib/pages/msg_feed_top/sys_msg/controller.dart +++ b/lib/pages/msg_feed_top/sys_msg/controller.dart @@ -43,8 +43,9 @@ class SysMsgController try { var res = await MsgHttp.delSysMsg(id); if (res['status']) { - loadingState.value.data!.removeAt(index); - loadingState.refresh(); + loadingState + ..value.data!.removeAt(index) + ..refresh(); SmartDialog.showToast('删除成功'); } else { SmartDialog.showToast(res['msg']); diff --git a/lib/pages/pgc_index/view.dart b/lib/pages/pgc_index/view.dart index 9e207db67..d384e93fb 100644 --- a/lib/pages/pgc_index/view.dart +++ b/lib/pages/pgc_index/view.dart @@ -205,7 +205,7 @@ class _PgcIndexPageState extends State Widget _buildList(LoadingState?> loadingState) { return switch (loadingState) { - Loading() => const HttpError(errMsg: '加载中'), + Loading() => const SliverToBoxAdapter(child: LinearProgressIndicator()), Success(:var response) => response?.isNotEmpty == true ? SliverGrid( gridDelegate: SliverGridDelegateWithExtentAndRatio( diff --git a/lib/pages/search_trending/view.dart b/lib/pages/search_trending/view.dart index b0175a67c..bbe05986c 100644 --- a/lib/pages/search_trending/view.dart +++ b/lib/pages/search_trending/view.dart @@ -115,18 +115,10 @@ class _SearchTrendingPageState extends State { controller: _controller.scrollController, slivers: [ SliverToBoxAdapter( - child: CachedNetworkImage( + child: Image.asset( + 'assets/images/trending_banner.png', fit: BoxFit.fitWidth, - fadeInDuration: const Duration(milliseconds: 120), - fadeOutDuration: const Duration(milliseconds: 120), - imageUrl: - 'https://activity.hdslb.com/blackboard/activity59158/img/hot_banner.fbb081df.png', - placeholder: (context, url) { - return AspectRatio( - aspectRatio: 1125 / 528, - child: Image.asset('assets/images/loading.png'), - ); - }, + filterQuality: FilterQuality.low, ), ), Obx(() => diff --git a/lib/pages/setting/widgets/model.dart b/lib/pages/setting/widgets/model.dart index ca3ec0149..ea646c1ca 100644 --- a/lib/pages/setting/widgets/model.dart +++ b/lib/pages/setting/widgets/model.dart @@ -132,7 +132,7 @@ List get styleSettings => [ subtitle: '启用横屏布局与逻辑,平板、折叠屏等可开启;建议全屏方向设为【不改变当前方向】', leading: const Icon(Icons.phonelink_outlined), setKey: SettingBoxKey.horizontalScreen, - defaultVal: false, + defaultVal: GStorage.horizontalScreen, onChanged: (value) { if (value) { autoScreen(); diff --git a/lib/pages/subscription/controller.dart b/lib/pages/subscription/controller.dart index d3b8bff5a..bfa12327b 100644 --- a/lib/pages/subscription/controller.dart +++ b/lib/pages/subscription/controller.dart @@ -49,8 +49,9 @@ class SubController var res = await UserHttp.cancelSub( id: subFolderItem.id!, type: subFolderItem.type!); if (res['status']) { - loadingState.value.data!.remove(subFolderItem); - loadingState.refresh(); + loadingState + ..value.data!.remove(subFolderItem) + ..refresh(); SmartDialog.showToast('取消订阅成功'); } else { SmartDialog.showToast(res['msg']); diff --git a/lib/pages/video/pay_coins/view.dart b/lib/pages/video/pay_coins/view.dart index 6ec82c5e5..dcd584cf1 100644 --- a/lib/pages/video/pay_coins/view.dart +++ b/lib/pages/video/pay_coins/view.dart @@ -107,11 +107,16 @@ class _PayCoinsPageState extends State : 'assets/images/paycoins/ic_22_gun_sister.png'; } - late AnimationController _slide22Controller; - late AnimationController _scale22Controller; - late AnimationController _coinSlideController; - late AnimationController _coinFadeController; - late AnimationController _boxAnimController; + late final AnimationController _slide22Controller; + late final Animation _slide22Anim; + late final AnimationController _scale22Controller; + late final Animation _scale22Anim; + late final AnimationController _coinSlideController; + late final Animation _coinSlideAnim; + late final AnimationController _coinFadeController; + late final Animation _coinFadeAnim; + late final AnimationController _boxAnimController; + late final Animation _boxAnim; final List _images = const [ 'assets/images/paycoins/ic_thunder_1.png', @@ -129,22 +134,45 @@ class _PayCoinsPageState extends State vsync: this, duration: const Duration(milliseconds: 50), ); + _slide22Anim = _slide22Controller.drive( + Tween( + begin: Offset.zero, + end: const Offset(0.0, -0.2), + ), + ); _scale22Controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 50), ); + _scale22Anim = _scale22Controller.drive( + Tween(begin: 1, end: 1.1), + ); _coinSlideController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), ); + _coinSlideAnim = _coinSlideController.drive( + Tween( + begin: Offset.zero, + end: const Offset(0.0, -2), + ), + ); _coinFadeController = AnimationController( vsync: this, duration: const Duration(milliseconds: 100), ); + _coinFadeAnim = + Tween(begin: 1, end: 0).animate(_coinFadeController); _boxAnimController = AnimationController( vsync: this, duration: const Duration(milliseconds: 50), ); + _boxAnim = _boxAnimController.drive( + Tween( + begin: Offset.zero, + end: const Offset(0.0, -0.2), + ), + ); _scale(); } @@ -273,31 +301,15 @@ class _PayCoinsPageState extends State alignment: Alignment.center, children: [ SlideTransition( - position: - _boxAnimController.drive( - Tween( - begin: Offset.zero, - end: - const Offset(0.0, -0.2), - ), - ), + position: _boxAnim, child: Image.asset( 'assets/images/paycoins/ic_pay_coins_box.png', ), ), SlideTransition( - position: - _coinSlideController.drive( - Tween( - begin: Offset.zero, - end: const Offset(0.0, -2), - ), - ), + position: _coinSlideAnim, child: FadeTransition( - opacity: Tween( - begin: 1, end: 0) - .animate( - _coinFadeController), + opacity: _coinFadeAnim, child: Image.asset( height: 35 + (factor * 15), width: 35 + (factor * 15), @@ -357,16 +369,9 @@ class _PayCoinsPageState extends State onPanUpdate: _canPay ? (e) => _handlePanUpdate(e, true) : null, child: ScaleTransition( - scale: _scale22Controller.drive( - Tween(begin: 1, end: 1.1), - ), + scale: _scale22Anim, child: SlideTransition( - position: _slide22Controller.drive( - Tween( - begin: Offset.zero, - end: const Offset(0.0, -0.2), - ), - ), + position: _slide22Anim, child: SizedBox( width: 110, height: 155, diff --git a/lib/pages/video/reply/controller.dart b/lib/pages/video/reply/controller.dart index 63952e523..29d0181f0 100644 --- a/lib/pages/video/reply/controller.dart +++ b/lib/pages/video/reply/controller.dart @@ -21,6 +21,14 @@ class VideoReplyController extends ReplyController vsync: this, duration: const Duration(milliseconds: 100)) ..forward(); + late final anim = Tween( + begin: const Offset(0, 2), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: fabAnimationCtr, + curve: Curves.easeInOut, + )); + void showFab() { if (!_isFabVisible) { _isFabVisible = true; diff --git a/lib/pages/video/reply/view.dart b/lib/pages/video/reply/view.dart index ef53464df..7f1773e7a 100644 --- a/lib/pages/video/reply/view.dart +++ b/lib/pages/video/reply/view.dart @@ -167,13 +167,7 @@ class _VideoReplyPanelState extends State bottom: MediaQuery.of(context).padding.bottom + 14, right: 14, child: SlideTransition( - position: Tween( - begin: const Offset(0, 2), - end: Offset.zero, - ).animate(CurvedAnimation( - parent: _videoReplyController.fabAnimationCtr, - curve: Curves.easeInOut, - )), + position: _videoReplyController.anim, child: FloatingActionButton( heroTag: null, onPressed: () { diff --git a/lib/pages/video/reply_reply/view.dart b/lib/pages/video/reply_reply/view.dart index 376d20e4f..989f4e213 100644 --- a/lib/pages/video/reply_reply/view.dart +++ b/lib/pages/video/reply_reply/view.dart @@ -50,8 +50,7 @@ class VideoReplyReplyPanel extends CommonSlidePage { } class _VideoReplyReplyPanelState - extends CommonSlidePageState - with TickerProviderStateMixin { + extends CommonSlidePageState { late VideoReplyReplyController _videoReplyReplyController; late final _savedReplies = {}; late final itemPositionsListener = ItemPositionsListener.create(); @@ -256,7 +255,7 @@ class _VideoReplyReplyPanelState Obx( () => _videoReplyReplyController.count.value != -1 ? Text( - '相关回复共${_videoReplyReplyController.count.value}条', + '相关回复共${Utils.numFormat(_videoReplyReplyController.count.value)}条', style: const TextStyle(fontSize: 13), ) : const SizedBox.shrink(), diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index 84f929ae4..023d9ecc5 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -90,8 +90,7 @@ class HeaderControlState extends State { if (videoDetailCtr.videoType != SearchType.video) { bangumiIntroController = Get.find(tag: heroTag); } - horizontalScreen = - setting.get(SettingBoxKey.horizontalScreen, defaultValue: false); + horizontalScreen = GStorage.horizontalScreen; defaultCDNService = setting.get(SettingBoxKey.CDNService, defaultValue: CDNService.backupUrl.code); } diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 3b965f797..e749607f5 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -1352,8 +1352,7 @@ class PlPlayerController { late final FullScreenMode mode = FullScreenModeCode.fromCode( setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0)); - late final horizontalScreen = - setting.get(SettingBoxKey.horizontalScreen, defaultValue: false); + late final horizontalScreen = GStorage.horizontalScreen; // 全屏 void triggerFullScreen({bool status = true, int duration = 500}) { diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 9f17da570..4a9543f6f 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -450,6 +450,19 @@ class PiliScheme { return true; } return false; + case 'm.bilibili.com': + // bilibili://m.bilibili.com/topic-detail?topic_id=1028161&frommodule=H5&h5awaken=xxx + final id = + RegExp(r'topic_id=(\d+)').firstMatch(uri.query)?.group(1); + if (id != null) { + PageUtils.toDupNamed( + '/dynTopic', + parameters: {'id': id}, + off: off, + ); + return true; + } + return false; default: if (selfHandle.not) { debugPrint('$uri'); @@ -629,9 +642,10 @@ class PiliScheme { bool isSeason = id.startsWith('ss'); id = id.substring(2); PageUtils.viewBangumi( - seasonId: isSeason ? id : null, - epId: isSeason ? null : id, - progress: uri.queryParameters['start_progress']); + seasonId: isSeason ? id : null, + epId: isSeason ? null : id, + progress: uri.queryParameters['start_progress'], + ); return true; } launchURL(); @@ -658,7 +672,11 @@ class PiliScheme { .firstMatch(path) ?.group(1); if (id != null) { - Get.toNamed('/articleList', parameters: {'id': id}); + PageUtils.toDupNamed( + '/articleList', + parameters: {'id': id}, + off: off, + ); return true; } launchURL(); @@ -707,6 +725,18 @@ class PiliScheme { } launchURL(); return false; + case 'topic-detail': + String? id = RegExp(r'topic_id=(\d+)').firstMatch(uri.query)?.group(1); + if (id != null) { + PageUtils.toDupNamed( + '/dynTopic', + parameters: {'id': id}, + off: off, + ); + return true; + } + launchURL(); + return false; default: Map map = IdUtils.matchAvorBv(input: area?.split('?').first); if (map.isNotEmpty) { diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 46c4fadaf..ebeb85b97 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -37,7 +37,7 @@ import 'package:PiliPlus/utils/login_utils.dart'; import 'package:PiliPlus/utils/set_int_adapter.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:flutter/material.dart'; -import 'package:get/get_navigation/src/routes/transitions_type.dart'; +import 'package:get/get.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:uuid/uuid.dart'; @@ -479,6 +479,20 @@ class GStorage { static bool get optTabletNav => GStorage.setting.get(SettingBoxKey.optTabletNav, defaultValue: true); + static bool get horizontalScreen { + bool isTablet; + if (Get.context != null) { + isTablet = Get.context!.isTablet; + } else { + final view = WidgetsBinding.instance.platformDispatcher.views.first; + final screenSize = view.physicalSize / view.devicePixelRatio; + final shortestSide = min(screenSize.width.abs(), screenSize.height.abs()); + isTablet = shortestSide >= 600; + } + return GStorage.setting + .get(SettingBoxKey.horizontalScreen, defaultValue: isTablet); + } + static List get dynamicDetailRatio => List.from(setting .get(SettingBoxKey.dynamicDetailRatio, defaultValue: const [60.0, 40.0])); diff --git a/pubspec.yaml b/pubspec.yaml index b62b92f8f..a3d711bf6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -101,14 +101,14 @@ dependencies: encrypt: ^5.0.3 # 视频播放器 - media_kit: ^1.1.11 # Primary package. + media_kit: 1.1.11 # Primary package. # media_kit_video: ^1.2.5 # For video rendering. media_kit_video: git: url: https://github.com/bggRGjQaUbCoE/media-kit.git path: media_kit_video ref: version_1.2.5 - media_kit_libs_video: ^1.0.5 + media_kit_libs_video: 1.0.5 # 媒体通知 audio_service: ^0.18.15 @@ -208,9 +208,7 @@ dependencies: dependency_overrides: screen_brightness: ^2.0.1 - path: 1.9.1 - # mime: - # git: https://github.com/orz12/mime.git + path: ^1.9.1 mime: ^2.0.0 fading_edge_scrollview: ^4.1.1 rxdart: ^0.28.0 @@ -226,10 +224,6 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^5.0.0 flutter_launcher_icons: ^0.14.1 - # flutter_launcher_icons: - # git: - # url: https://github.com/nvi9/flutter_launcher_icons.git - # ref: e045d40 hive_generator: ^2.0.1 build_runner: ^2.4.13 flutter_native_splash: ^2.4.3