diff --git a/lib/common/widgets/expandable.dart b/lib/common/widgets/expandable.dart new file mode 100644 index 000000000..3d34f779a --- /dev/null +++ b/lib/common/widgets/expandable.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; + +class ExpandablePanel extends StatelessWidget { + final bool expand; + + final Widget collapsed; + + final Widget expanded; + + const ExpandablePanel({ + super.key, + required this.expand, + required this.collapsed, + required this.expanded, + }); + + @override + Widget build(BuildContext context) { + return _AnimatedCross( + alignment: .topLeft, + firstChild: collapsed, + secondChild: expanded, + sizeCurve: Curves.linear, + crossFadeState: expand ? .showSecond : .showFirst, + duration: const Duration(milliseconds: 300), + ); + } +} + +/// ref [AnimatedCrossFade] +class _AnimatedCross extends StatefulWidget { + const _AnimatedCross({ + required this.firstChild, + required this.secondChild, + this.sizeCurve = Curves.linear, + this.alignment = Alignment.topCenter, + required this.crossFadeState, + required this.duration, + }); + + final Widget firstChild; + + final Widget secondChild; + + final CrossFadeState crossFadeState; + + final Duration duration; + + final Curve sizeCurve; + + final AlignmentGeometry alignment; + + @override + State<_AnimatedCross> createState() => _AnimatedCrossState(); +} + +class _AnimatedCrossState extends State<_AnimatedCross> { + late bool _showFirst; + AnimationStatus? _status; + + @override + void initState() { + super.initState(); + switch (widget.crossFadeState) { + case .showFirst: + _showFirst = true; + case .showSecond: + _showFirst = false; + } + } + + @override + void didUpdateWidget(_AnimatedCross oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.crossFadeState != oldWidget.crossFadeState) { + switch (widget.crossFadeState) { + case .showFirst: + _status = .reverse; + case .showSecond: + _status = .forward; + } + _showFirst = false; + } + } + + void _onEnd() { + if (_status == .reverse) { + _showFirst = true; + } + setState(() {}); + } + + Widget get firstChild => _widgetBuilder(_showFirst, widget.firstChild); + + Widget get secondChild => _widgetBuilder(!_showFirst, widget.secondChild); + + static Widget _widgetBuilder(bool visible, Widget child) => + Opacity(opacity: visible ? 1.0 : 0.0, child: child); + + @override + Widget build(BuildContext context) { + const Key kFirstChildKey = ValueKey(.showFirst); + const Key kSecondChildKey = ValueKey(.showSecond); + + final Key topKey; + Widget topChild; + final Key bottomKey; + Widget bottomChild; + + switch (widget.crossFadeState) { + case .showFirst: + topKey = kFirstChildKey; + topChild = firstChild; + bottomKey = kSecondChildKey; + bottomChild = secondChild; + case .showSecond: + topKey = kSecondChildKey; + topChild = secondChild; + bottomKey = kFirstChildKey; + bottomChild = firstChild; + } + + return ClipRect( + child: AnimatedSize( + clipBehavior: .none, + alignment: widget.alignment, + duration: widget.duration, + curve: widget.sizeCurve, + onEnd: _onEnd, + child: AnimatedCrossFade.defaultLayoutBuilder( + topChild, + topKey, + bottomChild, + bottomKey, + ), + ), + ); + } +} diff --git a/lib/common/widgets/flutter/page/page_view.dart b/lib/common/widgets/flutter/page/page_view.dart index ae23e49fb..18f15dbcb 100644 --- a/lib/common/widgets/flutter/page/page_view.dart +++ b/lib/common/widgets/flutter/page/page_view.dart @@ -430,49 +430,51 @@ class _PageViewState widget.scrollBehavior?.getScrollPhysics(context), ); - return NotificationListener( - onNotification: (ScrollNotification notification) { - if (notification.depth == 0 && - widget.onPageChanged != null && - notification is ScrollUpdateNotification) { - final metrics = notification.metrics as PageMetrics; - final int currentPage = metrics.page!.round(); - if (currentPage != _lastReportedPage) { - _lastReportedPage = currentPage; - widget.onPageChanged!(currentPage); - } - } - return false; + final child = Scrollable( + dragStartBehavior: widget.dragStartBehavior, + axisDirection: axisDirection, + controller: _controller, + physics: physics, + restorationId: widget.restorationId, + hitTestBehavior: widget.hitTestBehavior, + scrollBehavior: + widget.scrollBehavior ?? + ScrollConfiguration.of(context).copyWith(scrollbars: false), + viewportBuilder: (BuildContext context, ViewportOffset position) { + return Viewport( + scrollCacheExtent: widget.scrollCacheExtent, + axisDirection: axisDirection, + offset: position, + clipBehavior: widget.clipBehavior, + slivers: [ + SliverFillViewport( + viewportFraction: _controller.viewportFraction, + delegate: widget.childrenDelegate, + padEnds: widget.padEnds, + allowImplicitScrolling: widget.allowImplicitScrolling, + ), + ], + ); }, - child: Scrollable( - dragStartBehavior: widget.dragStartBehavior, - axisDirection: axisDirection, - controller: _controller, - physics: physics, - restorationId: widget.restorationId, - hitTestBehavior: widget.hitTestBehavior, - scrollBehavior: - widget.scrollBehavior ?? - ScrollConfiguration.of(context).copyWith(scrollbars: false), - viewportBuilder: (BuildContext context, ViewportOffset position) { - return Viewport( - scrollCacheExtent: widget.scrollCacheExtent, - axisDirection: axisDirection, - offset: position, - clipBehavior: widget.clipBehavior, - slivers: [ - SliverFillViewport( - viewportFraction: _controller.viewportFraction, - delegate: widget.childrenDelegate, - padEnds: widget.padEnds, - allowImplicitScrolling: widget.allowImplicitScrolling, - ), - ], - ); - }, - horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, - ), + horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, ); + if (widget.onPageChanged != null) { + return NotificationListener( + onNotification: (notification) { + if (notification.depth == 0) { + final metrics = notification.metrics as PageMetrics; + final int currentPage = metrics.page!.round(); + if (currentPage != _lastReportedPage) { + _lastReportedPage = currentPage; + widget.onPageChanged!(currentPage); + } + } + return false; + }, + child: child, + ); + } + return child; } @override diff --git a/lib/common/widgets/sliver/sliver_to_box_adapter.dart b/lib/common/widgets/sliver/sliver_to_box_adapter.dart index ee5a0a32b..14b5b5b16 100644 --- a/lib/common/widgets/sliver/sliver_to_box_adapter.dart +++ b/lib/common/widgets/sliver/sliver_to_box_adapter.dart @@ -78,7 +78,7 @@ class RenderSliverToBoxWithVisibilityAdapter extends RenderSliverToBoxAdapter { if (_visible != visible) { _visible = visible; WidgetsBinding.instance.addPostFrameCallback( - (_) => onVisibilityChanged(visible), + (_) => onVisibilityChanged(!visible), ); } } diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index c3be4b5f6..bd19b516a 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -456,7 +456,7 @@ abstract final class DynamicsHttp { static Future> topicFeed({ required Object topicId, - required String offset, + String? offset, required int sortBy, }) async { final res = await Request().get( @@ -464,7 +464,7 @@ abstract final class DynamicsHttp { queryParameters: { 'topic_id': topicId, 'sort_by': sortBy, - 'offset': offset, + 'offset': ?offset, 'page_size': 20, 'source': 'Web', 'features': Constants.dynFeatures, diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index ac4dcc6b2..75b827ceb 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -2,13 +2,14 @@ import 'dart:math'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; +import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart'; -import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; -import 'package:PiliPlus/models/common/image_type.dart'; +import 'package:PiliPlus/models/dynamics/article_content_model.dart' show Pic; import 'package:PiliPlus/models/dynamics/result.dart' show DynamicStat; import 'package:PiliPlus/pages/article/controller.dart'; import 'package:PiliPlus/pages/article/widgets/article_ops.dart'; @@ -26,7 +27,7 @@ import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:cached_network_image_ce/cached_network_image.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide PageView; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -73,7 +74,7 @@ class _ArticlePageState extends CommonDynPageState { double padding = max(maxWidth / 2 - Grid.smallCardWidth, 0); if (isPortrait) { return Padding( - padding: EdgeInsets.symmetric(horizontal: padding), + padding: .symmetric(horizontal: padding), child: SelectionArea( child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), @@ -103,7 +104,7 @@ class _ArticlePageState extends CommonDynPageState { final flex = controller.ratio[0].toInt(); final flex1 = controller.ratio[1].toInt(); return Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: .start, children: [ Expanded( flex: flex, @@ -112,7 +113,7 @@ class _ArticlePageState extends CommonDynPageState { physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( - padding: EdgeInsets.only( + padding: .only( left: padding, bottom: this.padding.bottom + 100, ), @@ -135,7 +136,7 @@ class _ArticlePageState extends CommonDynPageState { Expanded( flex: flex1, child: Padding( - padding: EdgeInsets.only(right: padding), + padding: .only(right: padding), child: Scaffold( backgroundColor: Colors.transparent, resizeToAvoidBottomInset: false, @@ -157,7 +158,7 @@ class _ArticlePageState extends CommonDynPageState { } Widget _buildContent(double maxWidth) => SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const .symmetric(horizontal: 12, vertical: 8), sliver: Obx( () { if (controller.isLoaded.value) { @@ -203,185 +204,28 @@ class _ArticlePageState extends CommonDynPageState { maxWidth: maxWidth, ); }, - separatorBuilder: (context, index) => - const SizedBox(height: 10), + separatorBuilder: (_, _) => const SizedBox(height: 10), ); } } else { content = const SliverToBoxAdapter(child: Text('NULL')); } - final pubTime = - controller.opusData?.modules.moduleAuthor?.pubTs ?? - controller.articleData?.publishTime; return SliverMainAxisGroup( slivers: [ if (controller.type != 'read') if (controller.opusData?.modules.moduleTop?.display?.album?.pics case final pics? when pics.isNotEmpty) - SliverToBoxAdapter( - child: Builder( - builder: (context) { - final length = pics.length; - final first = pics.first; - double height; - if (first.height != null && first.width != null) { - final ratio = first.height! / first.width!; - height = min(maxWidth * ratio, maxHeight * 0.55); - } else { - height = maxHeight * 0.55; - } - return Stack( - clipBehavior: Clip.none, - children: [ - Container( - height: height, - width: maxWidth, - margin: const EdgeInsets.only(bottom: 10), - child: PageView.builder( - physics: clampingScrollPhysics, - onPageChanged: (value) => - controller.topIndex.value = value, - itemCount: length, - itemBuilder: (context, index) { - final pic = pics[index]; - int? memCacheWidth, memCacheHeight; - if (pic.isLongPic ?? false) { - memCacheWidth = maxWidth.cacheSize(context); - } else if (pic.width != null && - pic.height != null) { - if (pic.width! > pic.height!) { - memCacheWidth = maxWidth.cacheSize( - context, - ); - } else { - memCacheHeight = height.cacheSize( - context, - ); - } - } - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => PageUtils.imageView( - quality: 60, - imgList: pics - .map((e) => SourceModel(url: e.url!)) - .toList(), - initialPage: index, - ), - child: Hero( - tag: pic.url!, - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - CachedNetworkImage( - height: height, - width: maxWidth, - memCacheWidth: memCacheWidth, - memCacheHeight: memCacheHeight, - fit: pic.isLongPic == true - ? BoxFit.cover - : null, - imageUrl: ImageUtils.thumbnailUrl( - pic.url, - 60, - ), - fadeInDuration: const Duration( - milliseconds: 120, - ), - fadeOutDuration: const Duration( - milliseconds: 120, - ), - placeholder: (_, _) => - const SizedBox.shrink(), - ), - if (pic.isLongPic == true) - const PBadge( - right: 12, - bottom: 12, - text: '长图', - type: .primary, - ), - ], - ), - ), - ); - }, - ), - ), - Obx( - () => PBadge( - top: 12, - right: 12, - type: PBadgeType.gray, - text: - '${controller.topIndex.value + 1}/$length', - ), - ), - ], - ); - }, - ), - ), + SliverToBoxAdapter(child: _buildImageGallery(pics)), if (controller.summary.title != null) SliverToBoxWithVisibilityAdapter( - onVisibilityChanged: (bool visible) => - controller.showTitle.value = !visible, + onVisibilityChanged: controller.showTitle.call, child: Text( controller.summary.title!, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.bold, - ), + style: const TextStyle(fontSize: 17, fontWeight: .bold), ), ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: GestureDetector( - onTap: () => Get.toNamed( - '/member?mid=${controller.summary.author?.mid}', - ), - child: SelectionContainer.disabled( - child: Row( - children: [ - NetworkImgLayer( - width: 40, - height: 40, - type: ImageType.avatar, - src: controller.summary.author?.face, - ), - const SizedBox(width: 10), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - controller.summary.author?.name ?? '', - style: TextStyle( - fontSize: - theme.textTheme.titleSmall!.fontSize, - ), - ), - if (pubTime != null) - Text( - DateFormatUtils.format(pubTime), - style: TextStyle( - color: theme.colorScheme.outline, - fontSize: - theme.textTheme.labelSmall!.fontSize, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), + SliverToBoxAdapter(child: _buildAuthor()), if (controller.type != 'read' && controller.opusData?.modules.moduleCollection != null) SliverToBoxAdapter( @@ -405,7 +249,7 @@ class _ArticlePageState extends CommonDynPageState { PreferredSizeWidget _buildAppBar() => AppBar( title: Obx(() { if (controller.isLoaded.value && controller.showTitle.value) { - return Text(controller.summary.title ?? ''); + return Text(controller.summary.title!); } return const SizedBox.shrink(); }), @@ -423,10 +267,10 @@ class _ArticlePageState extends CommonDynPageState { PopupMenuItem( onTap: () => ShareUtils.shareText(controller.url), child: const Row( - mainAxisSize: MainAxisSize.min, + spacing: 10, + mainAxisSize: .min, children: [ Icon(Icons.share_outlined, size: 19), - SizedBox(width: 10), Text('分享'), ], ), @@ -434,10 +278,10 @@ class _ArticlePageState extends CommonDynPageState { PopupMenuItem( onTap: () => Utils.copyText(controller.url), child: const Row( - mainAxisSize: MainAxisSize.min, + spacing: 10, + mainAxisSize: .min, children: [ Icon(Icons.copy_rounded, size: 19), - SizedBox(width: 10), Text('复制链接'), ], ), @@ -473,10 +317,10 @@ class _ArticlePageState extends CommonDynPageState { } }, child: const Row( - mainAxisSize: MainAxisSize.min, + spacing: 10, + mainAxisSize: .min, children: [ Icon(Icons.forward_to_inbox, size: 19), - SizedBox(width: 10), Text('分享至消息'), ], ), @@ -496,7 +340,7 @@ class _ArticlePageState extends CommonDynPageState { late final outline = theme.colorScheme.outline; late final btnStyle = TextButton.styleFrom( tapTargetSize: .padded, - padding: const EdgeInsets.symmetric(horizontal: 15), + padding: const .symmetric(horizontal: 15), foregroundColor: outline, ); @@ -530,7 +374,7 @@ class _ArticlePageState extends CommonDynPageState { final stats = controller.stats.value; Widget btn = Padding( - padding: EdgeInsets.only( + padding: .only( right: kFloatingActionButtonMargin, bottom: kFloatingActionButtonMargin + @@ -540,15 +384,12 @@ class _ArticlePageState extends CommonDynPageState { ); if (stats == null) { - return Align( - alignment: Alignment.bottomRight, - child: btn, - ); + return Align(alignment: .bottomRight, child: btn); } return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: .min, + crossAxisAlignment: .end, children: [ btn, Container( @@ -556,13 +397,11 @@ class _ArticlePageState extends CommonDynPageState { color: theme.colorScheme.surface, border: Border( top: BorderSide( - color: theme.colorScheme.outline.withValues( - alpha: 0.08, - ), + color: theme.colorScheme.outline.withValues(alpha: 0.08), ), ), ), - padding: EdgeInsets.only(bottom: padding.bottom), + padding: .only(bottom: padding.bottom), child: Row( children: [ Expanded( @@ -643,4 +482,141 @@ class _ArticlePageState extends CommonDynPageState { }), ); } + + Widget? _buildImageGallery(List pics) { + final length = pics.length; + final first = pics.first; + double height; + if (first.height != null && first.width != null) { + final ratio = first.height! / first.width!; + height = min(maxWidth * ratio, maxHeight * 0.55); + } else { + height = maxHeight * 0.55; + } + return Stack( + clipBehavior: .none, + children: [ + Container( + height: height, + width: maxWidth, + margin: const .only(bottom: 10), + child: PageView.builder( + physics: clampingScrollPhysics, + horizontalDragGestureRecognizer: + CustomHorizontalDragGestureRecognizer.new, + onPageChanged: controller.topIndex.call, + itemCount: length, + itemBuilder: (context, index) { + final pic = pics[index]; + int? memCacheWidth, memCacheHeight; + if (pic.isLongPic ?? false) { + memCacheWidth = maxWidth.cacheSize(context); + } else if (pic.width != null && pic.height != null) { + if (pic.width! > pic.height!) { + memCacheWidth = maxWidth.cacheSize( + context, + ); + } else { + memCacheHeight = height.cacheSize( + context, + ); + } + } + return GestureDetector( + behavior: .opaque, + onTap: () => PageUtils.imageView( + quality: 60, + imgList: pics.map((e) => SourceModel(url: e.url!)).toList(), + initialPage: index, + ), + child: Hero( + tag: pic.url!, + child: Stack( + clipBehavior: .none, + alignment: Alignment.center, + children: [ + CachedNetworkImage( + height: height, + width: maxWidth, + memCacheWidth: memCacheWidth, + memCacheHeight: memCacheHeight, + fit: pic.isLongPic == true ? BoxFit.cover : null, + imageUrl: ImageUtils.thumbnailUrl(pic.url, 60), + fadeInDuration: const Duration(milliseconds: 120), + fadeOutDuration: const Duration(milliseconds: 120), + placeholder: (_, _) => const SizedBox.shrink(), + ), + if (pic.isLongPic == true) + const PBadge( + right: 12, + bottom: 12, + text: '长图', + type: .primary, + ), + ], + ), + ), + ); + }, + ), + ), + Obx( + () => PBadge( + top: 12, + right: 12, + type: .gray, + text: '${controller.topIndex.value + 1}/$length', + ), + ), + ], + ); + } + + Widget? _buildAuthor() { + final pubTime = + controller.opusData?.modules.moduleAuthor?.pubTs ?? + controller.articleData?.publishTime; + return Padding( + padding: const .symmetric(vertical: 10), + child: GestureDetector( + onTap: () => Get.toNamed( + '/member?mid=${controller.summary.author?.mid}', + ), + child: SelectionContainer.disabled( + child: Row( + spacing: 10, + children: [ + NetworkImgLayer( + width: 40, + height: 40, + type: .avatar, + src: controller.summary.author?.face, + ), + Flexible( + child: Column( + crossAxisAlignment: .start, + children: [ + Text( + controller.summary.author?.name ?? '', + style: TextStyle( + fontSize: theme.textTheme.titleSmall!.fontSize, + ), + ), + if (pubTime != null) + Text( + DateFormatUtils.format(pubTime), + style: TextStyle( + color: theme.colorScheme.outline, + fontSize: theme.textTheme.labelSmall!.fontSize, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } } diff --git a/lib/pages/dynamics_create/view.dart b/lib/pages/dynamics_create/view.dart index 9eeb8614f..4f1fcac79 100644 --- a/lib/pages/dynamics_create/view.dart +++ b/lib/pages/dynamics_create/view.dart @@ -385,7 +385,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { return PopupMenuButton( requestFocus: false, initialValue: _isPrivate.value, - onSelected: (value) => _isPrivate.value = value, + onSelected: _isPrivate.call, itemBuilder: (context) => List.generate( 2, (index) => PopupMenuItem( diff --git a/lib/pages/dynamics_create_reserve/view.dart b/lib/pages/dynamics_create_reserve/view.dart index 28f443245..408f42ad6 100644 --- a/lib/pages/dynamics_create_reserve/view.dart +++ b/lib/pages/dynamics_create_reserve/view.dart @@ -67,7 +67,7 @@ class _CreateReservePageState extends State { () => PopupMenuButton( requestFocus: false, initialValue: _controller.subType.value, - onSelected: (value) => _controller.subType.value = value, + onSelected: _controller.subType.call, itemBuilder: (context) { return const [ PopupMenuItem( diff --git a/lib/pages/dynamics_create_vote/view.dart b/lib/pages/dynamics_create_vote/view.dart index f23524260..1e1198017 100644 --- a/lib/pages/dynamics_create_vote/view.dart +++ b/lib/pages/dynamics_create_vote/view.dart @@ -90,7 +90,7 @@ class _CreateVotePageState extends State { theme, key: ValueKey('${_controller.key}desc'), initialValue: _controller.desc.value, - onChanged: (value) => _controller.desc.value = value, + onChanged: _controller.desc.call, desc: '投票说明', inputFormatters: [LengthLimitingTextInputFormatter(100)], ), @@ -190,10 +190,7 @@ class _CreateVotePageState extends State { child: PopupMenuButton( initialValue: choiceCnt, requestFocus: false, - child: Text( - choiceCnt == 1 ? '单选 ' : '最多选$choiceCnt项', - ), - onSelected: (value) => _controller.choiceCnt.value = value, + onSelected: _controller.choiceCnt.call, itemBuilder: (context) { return choices .map( @@ -204,6 +201,9 @@ class _CreateVotePageState extends State { ) .toList(); }, + child: Text( + choiceCnt == 1 ? '单选 ' : '最多选$choiceCnt项', + ), ), ); }), diff --git a/lib/pages/dynamics_topic/controller.dart b/lib/pages/dynamics_topic/controller.dart index 3150ccdf6..5c8b572c5 100644 --- a/lib/pages/dynamics_topic/controller.dart +++ b/lib/pages/dynamics_topic/controller.dart @@ -17,7 +17,7 @@ class DynTopicController String topicName = Get.parameters['name'] ?? ''; int sortBy = 0; - String offset = ''; + String? offset; final topicSortByConf = Rxn(); double? appbarOffset; @@ -49,13 +49,16 @@ class DynTopicController @override List? getDataList(TopicCardList? response) { - offset = response?.offset ?? ''; - topicSortByConf.value = response?.topicSortByConf; - sortBy = response?.topicSortByConf?.showSortBy ?? 0; - if (response?.hasMore == false) { - isEnd = true; + if (response != null) { + offset = response.offset; + topicSortByConf.value = response.topicSortByConf; + sortBy = response.topicSortByConf?.showSortBy ?? 0; + if (response.hasMore == false) { + isEnd = true; + } + return response.items; } - return response?.items; + return null; } @override diff --git a/lib/pages/live_area/view.dart b/lib/pages/live_area/view.dart index 7ea4458a4..2bec49271 100644 --- a/lib/pages/live_area/view.dart +++ b/lib/pages/live_area/view.dart @@ -322,10 +322,7 @@ class _LiveAreaPageState extends State { text: item.name!, fontSize: 14, bgColor: Colors.transparent, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), + padding: const .symmetric(horizontal: 12, vertical: 4), onTap: (value) { if (_controller.isEditing.value) { onPressed(); diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index a76479688..61a8f4cae 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -1,5 +1,5 @@ -import 'dart:async'; -import 'dart:convert'; +import 'dart:async' show Timer, StreamSubscription; +import 'dart:convert' show jsonDecode; import 'dart:math' as math; import 'package:PiliPlus/common/widgets/dialog/report.dart'; @@ -149,6 +149,27 @@ class LiveRoomController extends GetxController { return const SizedBox.shrink(); }); + StreamSubscription? _sizeSub; + + void _onSizeChanged((int, int) value) { + final isVertical = value.$2 > value.$1; + isPortrait.value = isVertical; + plPlayerController.isVertical = isVertical; + } + + void _startSizeSub() { + if (isPortrait.value) return; + _stopSizeSub(); + _sizeSub = plPlayerController.videoPlayerController?.stream.size.listen( + _onSizeChanged, + ); + } + + void _stopSizeSub() { + _sizeSub?.cancel(); + _sizeSub = null; + } + @override void onInit() { super.onInit(); @@ -283,7 +304,7 @@ class LiveRoomController extends GetxController { currentQnDesc.value = LiveQuality.fromCode(currentQn)?.desc ?? currentQn.toString(); videoUrl = VideoUtils.getLiveCdnUrl(item, index: liveUrlIndex); - return playerInit(); + return playerInit()?.whenComplete(_startSizeSub); } Future queryLiveInfoH5() async { @@ -438,6 +459,7 @@ class LiveRoomController extends GetxController { @override void onClose() { + _stopSizeSub(); closeLiveMsg(); cancelLikeTimer(); cancelLiveTimer(); diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 8ed9a008a..a44d9cd73 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -764,14 +764,13 @@ class _LiveRoomPageState extends State ..onSendDanmaku(), ); return Padding( - padding: EdgeInsets.only(bottom: 12, top: isPortrait ? 12 : 0), + padding: .only(bottom: 12, top: isPortrait ? 12 : 0), child: _liveRoomController.showSuperChat ? PageView( key: pageKey, controller: _liveRoomController.pageController, physics: clampingScrollPhysics, - onPageChanged: (value) => - _liveRoomController.pageIndex.value = value, + onPageChanged: _liveRoomController.pageIndex.call, horizontalDragGestureRecognizer: CustomHorizontalDragGestureRecognizer.new, children: [ diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index 6afb0bfa4..61ef7532d 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -153,7 +153,7 @@ class _SearchPageState extends State { text: e.text, style: e.isEm ? TextStyle( - fontWeight: FontWeight.bold, + fontWeight: .bold, color: Theme.of( context, ).colorScheme.primary, @@ -181,7 +181,7 @@ class _SearchPageState extends State { strutStyle: const StrutStyle(leading: 0, height: 1), style: theme.textTheme.titleMedium!.copyWith( height: 1, - fontWeight: FontWeight.bold, + fontWeight: .bold, ), ); final outline = theme.colorScheme.outline; @@ -192,7 +192,7 @@ class _SearchPageState extends State { color: outline, ); return SliverPadding( - padding: EdgeInsets.fromLTRB( + padding: .fromLTRB( 10, !isTrending && (isPortrait || _searchController.enableTrending) ? 4 @@ -203,10 +203,10 @@ class _SearchPageState extends State { sliver: SliverMainAxisGroup( slivers: [ SliverPadding( - padding: const EdgeInsets.fromLTRB(6, 0, 6, 6), + padding: const .fromLTRB(6, 0, 6, 6), sliver: SliverToBoxAdapter( child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: .spaceBetween, children: [ isTrending ? Row( @@ -263,10 +263,7 @@ class _SearchPageState extends State { label: Text( '刷新', strutStyle: const StrutStyle(leading: 0, height: 1), - style: TextStyle( - height: 1, - color: secondary, - ), + style: TextStyle(height: 1, color: secondary), ), ), ], @@ -296,7 +293,7 @@ class _SearchPageState extends State { } final secondary = theme.colorScheme.secondary; return SliverPadding( - padding: EdgeInsets.fromLTRB( + padding: .fromLTRB( 10, !isPortrait ? 25 @@ -309,7 +306,7 @@ class _SearchPageState extends State { sliver: SliverMainAxisGroup( slivers: [ SliverPadding( - padding: const EdgeInsets.fromLTRB(6, 0, 6, 6), + padding: const .fromLTRB(6, 0, 6, 6), sliver: SliverToBoxAdapter( child: Row( children: [ @@ -318,7 +315,7 @@ class _SearchPageState extends State { strutStyle: const StrutStyle(leading: 0, height: 1), style: theme.textTheme.titleMedium!.copyWith( height: 1, - fontWeight: FontWeight.bold, + fontWeight: .bold, ), ), const SizedBox(width: 12), @@ -341,10 +338,7 @@ class _SearchPageState extends State { ), label: Text( '清空', - style: TextStyle( - height: 1, - color: secondary, - ), + style: TextStyle(height: 1, color: secondary), ), ), ], @@ -365,10 +359,7 @@ class _SearchPageState extends State { onLongPress: _searchController.onLongSelect, fontSize: 14, height: 1, - padding: const EdgeInsets.symmetric( - horizontal: 11, - vertical: 8, - ), + padding: const .fromLTRB(11, 8, 11, 0), ), ), ), diff --git a/lib/pages/search/widgets/hot_keyword.dart b/lib/pages/search/widgets/hot_keyword.dart index d0e53adeb..5f4f263de 100644 --- a/lib/pages/search/widgets/hot_keyword.dart +++ b/lib/pages/search/widgets/hot_keyword.dart @@ -38,20 +38,20 @@ class SliverHotKeyword extends StatelessWidget { children: hotSearchList .map( (i) => Material( - type: MaterialType.transparency, - borderRadius: const BorderRadius.all(Radius.circular(3)), + type: .transparency, + borderRadius: const .all(.circular(3)), child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(3)), + borderRadius: const .all(.circular(3)), onTap: () => onClick?.call(i.keyword), child: Padding( - padding: const EdgeInsets.only(left: 2, right: 10), + padding: const .only(left: 2, right: 10), child: Tooltip( message: i.keyword, child: Row( children: [ Flexible( child: Padding( - padding: const EdgeInsets.fromLTRB(6, 5, 0, 5), + padding: const .fromLTRB(6, 5, 0, 5), child: Text( i.keyword!, overflow: TextOverflow.ellipsis, @@ -62,7 +62,7 @@ class SliverHotKeyword extends StatelessWidget { ), if (!i.icon.isNullOrEmpty) Padding( - padding: const EdgeInsets.only(left: 4), + padding: const .only(left: 4), child: CachedNetworkImage( height: 15, memCacheHeight: cacheHeight, @@ -72,7 +72,7 @@ class SliverHotKeyword extends StatelessWidget { ) else if (i.showLiveIcon == true) Padding( - padding: const EdgeInsets.only(left: 4), + padding: const .only(left: 4), child: Image.asset( Assets.livingRect, width: 48, diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 8a99d1686..422d35b3e 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -567,7 +567,7 @@ class VideoDetailController extends GetxController alpha: 0.8, ), textColor: theme.colorScheme.onSecondaryContainer, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const .symmetric(horizontal: 8, vertical: 4), fontSize: 14, text: item is SegmentModel ? '跳过: ${item.segmentType.shortTitle}' diff --git a/lib/pages/video/introduction/ugc/controller.dart b/lib/pages/video/introduction/ugc/controller.dart index cbfcc2eda..ada200d55 100644 --- a/lib/pages/video/introduction/ugc/controller.dart +++ b/lib/pages/video/introduction/ugc/controller.dart @@ -41,15 +41,13 @@ import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:expandable/expandable.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class UgcIntroController extends CommonIntroController with ReloadMixin { - late ExpandableController expandableCtr; - + late final RxBool expand; final RxBool status = true.obs; // up主粉丝数 @@ -72,18 +70,15 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { @override void onInit() { super.onInit(); - bool alwaysExpandIntroPanel = Pref.alwaysExpandIntroPanel; - expandableCtr = ExpandableController( - initialExpanded: alwaysExpandIntroPanel, - ); + final alwaysExpandIntroPanel = Pref.alwaysExpandIntroPanel; + expand = RxBool(alwaysExpandIntroPanel); if (!alwaysExpandIntroPanel && Pref.expandIntroPanelH) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (!expandableCtr.expanded && !DeviceUtils.size.isPortrait) { - expandableCtr.toggle(); + if (!expand.value && !DeviceUtils.size.isPortrait) { + expand.toggle(); } }); } - videoDetail.value.title = Get.arguments['title'] ?? ''; } @@ -558,12 +553,6 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { } } - @override - void onClose() { - expandableCtr.dispose(); - super.onClose(); - } - /// 播放上一个 @override bool prevPlay([bool skipPart = false]) { diff --git a/lib/pages/video/introduction/ugc/view.dart b/lib/pages/video/introduction/ugc/view.dart index 8e4dd7f3e..3d32ca8ae 100644 --- a/lib/pages/video/introduction/ugc/view.dart +++ b/lib/pages/video/introduction/ugc/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/expandable.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; @@ -37,7 +38,6 @@ import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -82,13 +82,6 @@ class _UgcIntroPanelState extends State { @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); - const expandTheme = ExpandableThemeData( - animationDuration: Duration(milliseconds: 300), - scrollAnimationDuration: Duration(milliseconds: 300), - crossFadePoint: 0, - fadeCurve: Curves.ease, - sizeCurve: Curves.linear, - ); final isPortrait = widget.isPortrait; final isHorizontal = !isPortrait && widget.isHorizontal; return SliverPadding( @@ -110,7 +103,7 @@ class _UgcIntroPanelState extends State { return; } feedBack(); - introController.expandableCtr.toggle(); + introController.expand.toggle(); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -191,11 +184,16 @@ class _UgcIntroPanelState extends State { ), ) else - ExpandablePanel( - controller: introController.expandableCtr, - collapsed: _buildTitle(theme, videoDetail), - expanded: _buildTitle(theme, videoDetail, isExpand: true), - theme: expandTheme, + Obx( + () => ExpandablePanel( + collapsed: _buildTitle(theme, videoDetail), + expanded: _buildTitle( + theme, + videoDetail, + isExpand: true, + ), + expand: introController.expand.value, + ), ), const SizedBox(height: 8), Stack( @@ -236,15 +234,16 @@ class _UgcIntroPanelState extends State { if (isHorizontal && PlatformUtils.isDesktop) ..._infos(theme, videoDetail) else - ExpandablePanel( - controller: introController.expandableCtr, - collapsed: const SizedBox.shrink(), - expanded: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: _infos(theme, videoDetail), + Obx( + () => ExpandablePanel( + collapsed: const SizedBox(width: .infinity, height: 0), + expanded: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _infos(theme, videoDetail), + ), + expand: introController.expand.value, ), - theme: expandTheme, ), Obx( () => introController.status.value diff --git a/lib/pages/video/view_point/view.dart b/lib/pages/video/view_point/view.dart index 9acc3f532..3b3e3e500 100644 --- a/lib/pages/video/view_point/view.dart +++ b/lib/pages/video/view_point/view.dart @@ -54,8 +54,7 @@ class _ViewPointsPageState extends State scale: 0.8, child: Switch( value: videoDetailController.showVP.value, - onChanged: (value) => - videoDetailController.showVP.value = value, + onChanged: videoDetailController.showVP.call, ), ), ), diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 26e199f32..1a68edaeb 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -181,8 +181,13 @@ class PlPlayerController with BlockConfigMixin { final RxBool isBuffering = true.obs; /// 全屏方向 + // ignore: unnecessary_getters_setters bool get isVertical => _isVertical; + set isVertical(bool value) { + _isVertical = value; + } + /// 弹幕开关 late final RxBool enableShowDanmaku = Pref.enableShowDanmaku.obs; late final RxBool enableShowLiveDanmaku = Pref.enableShowLiveDanmaku.obs; diff --git a/pubspec.lock b/pubspec.lock index 7eb046004..93783c853 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -464,14 +464,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - expandable: - dependency: "direct main" - description: - name: expandable - sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" - url: "https://pub.dev" - source: hosted - version: "5.0.1" extended_list_library: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a7387fe24..69aef51cb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,7 +79,6 @@ dependencies: dynamic_color: ^1.8.1 easy_debounce: ^2.0.3 encrypt: ^5.0.3 - expandable: ^5.0.1 extended_nested_scroll_view: git: url: https://github.com/bggRGjQaUbCoE/extended_nested_scroll_view.git