diff --git a/lib/common/constants.dart b/lib/common/constants.dart
index a72f65cbf..1ef09d1f2 100644
--- a/lib/common/constants.dart
+++ b/lib/common/constants.dart
@@ -16,6 +16,13 @@ abstract final class StyleString {
maxWidth: 420,
);
static const topBarHeight = 52.0;
+ static const buttonStyle = ButtonStyle(
+ visualDensity: VisualDensity(
+ horizontal: -2,
+ vertical: -1.25,
+ ),
+ tapTargetSize: .shrinkWrap,
+ );
}
abstract final class Constants {
diff --git a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart b/lib/common/widgets/custom_sliver_persistent_header_delegate.dart
deleted file mode 100644
index bfc354861..000000000
--- a/lib/common/widgets/custom_sliver_persistent_header_delegate.dart
+++ /dev/null
@@ -1,98 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart' show RenderProxyBox;
-
-class CustomSliverPersistentHeaderDelegate
- extends SliverPersistentHeaderDelegate {
- const CustomSliverPersistentHeaderDelegate({
- required this.child,
- this.bgColor,
- this.extent = 45,
- this.needRebuild = false,
- });
- final double extent;
- final Widget child;
- final Color? bgColor;
- final bool needRebuild;
-
- @override
- Widget build(
- BuildContext context,
- double shrinkOffset,
- bool overlapsContent,
- ) {
- //创建child子组件
- //shrinkOffset:child偏移值minExtent~maxExtent
- //overlapsContent:SliverPersistentHeader覆盖其他子组件返回true,否则返回false
- return _DecoratedBox(color: bgColor, child: child);
- }
-
- //SliverPersistentHeader最大高度
- @override
- double get maxExtent => extent;
-
- //SliverPersistentHeader最小高度
- @override
- double get minExtent => extent;
-
- @override
- bool shouldRebuild(CustomSliverPersistentHeaderDelegate oldDelegate) {
- return oldDelegate.bgColor != bgColor ||
- (needRebuild && oldDelegate.child != child);
- }
-}
-
-class _DecoratedBox extends SingleChildRenderObjectWidget {
- const _DecoratedBox({
- this.color,
- super.child,
- });
-
- final Color? color;
-
- @override
- RenderObject createRenderObject(BuildContext context) {
- return _RenderDecoratedBox(color: color);
- }
-
- @override
- void updateRenderObject(
- BuildContext context,
- _RenderDecoratedBox renderObject,
- ) {
- renderObject.color = color;
- }
-}
-
-class _RenderDecoratedBox extends RenderProxyBox {
- _RenderDecoratedBox({
- Color? color,
- }) : _color = color;
-
- Color? _color;
- Color? get color => _color;
- set color(Color? value) {
- if (_color == value) return;
- _color = value;
- markNeedsPaint();
- }
-
- @override
- void paint(PaintingContext context, Offset offset) {
- if (_color case final color?) {
- final size = this.size;
- context.canvas.drawRect(
- Rect.fromLTWH(
- offset.dx,
- offset.dy - 2,
- size.width,
- size.height + 2,
- ),
- Paint()..color = color,
- );
- }
- super.paint(context, offset);
- }
-
- @override
- bool hitTestSelf(Offset position) => true;
-}
diff --git a/lib/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart b/lib/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart
new file mode 100644
index 000000000..efa57d2b6
--- /dev/null
+++ b/lib/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart
@@ -0,0 +1,357 @@
+/*
+ * This file is part of PiliPlus
+ *
+ * PiliPlus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PiliPlus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PiliPlus. If not, see .
+ */
+
+import 'dart:math' as math;
+
+import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart';
+import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart';
+import 'package:PiliPlus/common/widgets/only_layout_widget.dart'
+ show LayoutCallback;
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart'
+ hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
+import 'package:flutter/services.dart';
+
+/// ref [SliverAppBar]
+class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
+ _SliverAppBarDelegate({
+ required this.leading,
+ required this.automaticallyImplyLeading,
+ required this.title,
+ required this.actions,
+ required this.automaticallyImplyActions,
+ required this.flexibleSpace,
+ required this.bottom,
+ required this.elevation,
+ required this.scrolledUnderElevation,
+ required this.shadowColor,
+ required this.surfaceTintColor,
+ required this.forceElevated,
+ required this.backgroundColor,
+ required this.foregroundColor,
+ required this.iconTheme,
+ required this.actionsIconTheme,
+ required this.primary,
+ required this.centerTitle,
+ required this.excludeHeaderSemantics,
+ required this.titleSpacing,
+ required this.collapsedHeight,
+ required this.topPadding,
+ required this.shape,
+ required this.toolbarHeight,
+ required this.leadingWidth,
+ required this.toolbarTextStyle,
+ required this.titleTextStyle,
+ required this.systemOverlayStyle,
+ required this.forceMaterialTransparency,
+ required this.useDefaultSemanticsOrder,
+ required this.clipBehavior,
+ required this.accessibleNavigation,
+ required this.actionsPadding,
+ }) : assert(primary || topPadding == 0.0),
+ _bottomHeight = bottom?.preferredSize.height ?? 0.0;
+
+ final Widget? leading;
+ final bool automaticallyImplyLeading;
+ final Widget title;
+ final List? actions;
+ final bool automaticallyImplyActions;
+ final Widget flexibleSpace;
+ final PreferredSizeWidget? bottom;
+ final double? elevation;
+ final double? scrolledUnderElevation;
+ final Color? shadowColor;
+ final Color? surfaceTintColor;
+ final bool forceElevated;
+ final Color? backgroundColor;
+ final Color? foregroundColor;
+ final IconThemeData? iconTheme;
+ final IconThemeData? actionsIconTheme;
+ final bool primary;
+ final bool? centerTitle;
+ final bool excludeHeaderSemantics;
+ final double? titleSpacing;
+ final double collapsedHeight;
+ final double topPadding;
+ final ShapeBorder? shape;
+ final double? toolbarHeight;
+ final double? leadingWidth;
+ final TextStyle? toolbarTextStyle;
+ final TextStyle? titleTextStyle;
+ final SystemUiOverlayStyle? systemOverlayStyle;
+ final double _bottomHeight;
+ final bool forceMaterialTransparency;
+ final bool useDefaultSemanticsOrder;
+ final Clip? clipBehavior;
+ final bool accessibleNavigation;
+ final EdgeInsetsGeometry? actionsPadding;
+
+ @override
+ double get minExtent => collapsedHeight;
+
+ @override
+ Widget build(
+ BuildContext context,
+ double shrinkOffset,
+ bool overlapsContent,
+ double? maxExtent,
+ ) {
+ maxExtent ??= double.infinity;
+ final bool isScrolledUnder =
+ overlapsContent ||
+ forceElevated ||
+ (shrinkOffset > maxExtent - minExtent);
+ final effectiveTitle = AnimatedOpacity(
+ opacity: isScrolledUnder ? 1 : 0,
+ duration: const Duration(milliseconds: 500),
+ curve: const Cubic(0.2, 0.0, 0.0, 1.0),
+ child: title,
+ );
+
+ return FlexibleSpaceBar.createSettings(
+ minExtent: minExtent,
+ maxExtent: maxExtent,
+ currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
+ isScrolledUnder: isScrolledUnder,
+ hasLeading: leading != null || automaticallyImplyLeading,
+ child: AppBar(
+ clipBehavior: clipBehavior,
+ leading: leading,
+ automaticallyImplyLeading: automaticallyImplyLeading,
+ title: effectiveTitle,
+ actions: actions,
+ automaticallyImplyActions: automaticallyImplyActions,
+ flexibleSpace: maxExtent == .infinity
+ ? flexibleSpace
+ : FlexibleSpaceBar(background: flexibleSpace),
+ bottom: bottom,
+ elevation: isScrolledUnder ? elevation : 0.0,
+ scrolledUnderElevation: scrolledUnderElevation,
+ shadowColor: shadowColor,
+ surfaceTintColor: surfaceTintColor,
+ backgroundColor: backgroundColor,
+ foregroundColor: foregroundColor,
+ iconTheme: iconTheme,
+ actionsIconTheme: actionsIconTheme,
+ primary: primary,
+ centerTitle: centerTitle,
+ excludeHeaderSemantics: excludeHeaderSemantics,
+ titleSpacing: titleSpacing,
+ shape: shape,
+ toolbarHeight: toolbarHeight,
+ leadingWidth: leadingWidth,
+ toolbarTextStyle: toolbarTextStyle,
+ titleTextStyle: titleTextStyle,
+ systemOverlayStyle: systemOverlayStyle,
+ forceMaterialTransparency: forceMaterialTransparency,
+ useDefaultSemanticsOrder: useDefaultSemanticsOrder,
+ actionsPadding: actionsPadding,
+ ),
+ );
+ }
+
+ @override
+ bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) {
+ return leading != oldDelegate.leading ||
+ automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading ||
+ title != oldDelegate.title ||
+ actions != oldDelegate.actions ||
+ automaticallyImplyActions != oldDelegate.automaticallyImplyActions ||
+ flexibleSpace != oldDelegate.flexibleSpace ||
+ bottom != oldDelegate.bottom ||
+ _bottomHeight != oldDelegate._bottomHeight ||
+ elevation != oldDelegate.elevation ||
+ shadowColor != oldDelegate.shadowColor ||
+ backgroundColor != oldDelegate.backgroundColor ||
+ foregroundColor != oldDelegate.foregroundColor ||
+ iconTheme != oldDelegate.iconTheme ||
+ actionsIconTheme != oldDelegate.actionsIconTheme ||
+ primary != oldDelegate.primary ||
+ centerTitle != oldDelegate.centerTitle ||
+ titleSpacing != oldDelegate.titleSpacing ||
+ topPadding != oldDelegate.topPadding ||
+ forceElevated != oldDelegate.forceElevated ||
+ toolbarHeight != oldDelegate.toolbarHeight ||
+ leadingWidth != oldDelegate.leadingWidth ||
+ toolbarTextStyle != oldDelegate.toolbarTextStyle ||
+ titleTextStyle != oldDelegate.titleTextStyle ||
+ systemOverlayStyle != oldDelegate.systemOverlayStyle ||
+ forceMaterialTransparency != oldDelegate.forceMaterialTransparency ||
+ useDefaultSemanticsOrder != oldDelegate.useDefaultSemanticsOrder ||
+ accessibleNavigation != oldDelegate.accessibleNavigation ||
+ actionsPadding != oldDelegate.actionsPadding;
+ }
+
+ @override
+ String toString() {
+ return '${describeIdentity(this)}(topPadding: ${topPadding.toStringAsFixed(1)}, bottomHeight: ${_bottomHeight.toStringAsFixed(1)}, ...)';
+ }
+}
+
+class DynamicSliverAppBar extends StatefulWidget {
+ const DynamicSliverAppBar.medium({
+ super.key,
+ this.leading,
+ this.automaticallyImplyLeading = true,
+ required this.title,
+ this.actions,
+ this.automaticallyImplyActions = true,
+ required this.flexibleSpace,
+ this.bottom,
+ this.elevation,
+ this.scrolledUnderElevation,
+ this.shadowColor,
+ this.surfaceTintColor,
+ this.forceElevated = false,
+ this.backgroundColor,
+ this.foregroundColor,
+ this.iconTheme,
+ this.actionsIconTheme,
+ this.primary = true,
+ this.centerTitle,
+ this.excludeHeaderSemantics = false,
+ this.titleSpacing,
+ this.shape,
+ this.leadingWidth,
+ this.toolbarTextStyle,
+ this.titleTextStyle,
+ this.systemOverlayStyle,
+ this.forceMaterialTransparency = false,
+ this.useDefaultSemanticsOrder = true,
+ this.clipBehavior,
+ this.actionsPadding,
+ this.onPerformLayout,
+ });
+
+ final LayoutCallback? onPerformLayout;
+
+ final Widget? leading;
+
+ final bool automaticallyImplyLeading;
+
+ final Widget title;
+
+ final List? actions;
+
+ final bool automaticallyImplyActions;
+
+ final Widget flexibleSpace;
+
+ final PreferredSizeWidget? bottom;
+
+ final double? elevation;
+
+ final double? scrolledUnderElevation;
+
+ final Color? shadowColor;
+
+ final Color? surfaceTintColor;
+
+ final bool forceElevated;
+
+ final Color? backgroundColor;
+
+ final Color? foregroundColor;
+
+ final IconThemeData? iconTheme;
+
+ final IconThemeData? actionsIconTheme;
+
+ final bool primary;
+
+ final bool? centerTitle;
+
+ final bool excludeHeaderSemantics;
+
+ final double? titleSpacing;
+
+ final ShapeBorder? shape;
+
+ final double? leadingWidth;
+
+ final TextStyle? toolbarTextStyle;
+
+ final TextStyle? titleTextStyle;
+
+ final SystemUiOverlayStyle? systemOverlayStyle;
+
+ final bool forceMaterialTransparency;
+
+ final bool useDefaultSemanticsOrder;
+
+ final Clip? clipBehavior;
+
+ final EdgeInsetsGeometry? actionsPadding;
+
+ @override
+ State createState() => _DynamicSliverAppBarState();
+}
+
+class _DynamicSliverAppBarState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final double bottomHeight = widget.bottom?.preferredSize.height ?? 0.0;
+ final double topPadding = widget.primary
+ ? MediaQuery.viewPaddingOf(context).top
+ : 0.0;
+ final double effectiveCollapsedHeight =
+ topPadding + kToolbarHeight + bottomHeight + 1;
+
+ return MediaQuery.removePadding(
+ context: context,
+ removeBottom: true,
+ child: SliverPinnedHeader(
+ onPerformLayout: widget.onPerformLayout,
+ delegate: _SliverAppBarDelegate(
+ leading: widget.leading,
+ automaticallyImplyLeading: widget.automaticallyImplyLeading,
+ title: widget.title,
+ actions: widget.actions,
+ automaticallyImplyActions: widget.automaticallyImplyActions,
+ flexibleSpace: widget.flexibleSpace,
+ 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: effectiveCollapsedHeight,
+ topPadding: topPadding,
+ shape: widget.shape,
+ toolbarHeight: kToolbarHeight,
+ leadingWidth: widget.leadingWidth,
+ toolbarTextStyle: widget.toolbarTextStyle,
+ titleTextStyle: widget.titleTextStyle,
+ systemOverlayStyle: widget.systemOverlayStyle,
+ forceMaterialTransparency: widget.forceMaterialTransparency,
+ useDefaultSemanticsOrder: widget.useDefaultSemanticsOrder,
+ clipBehavior: widget.clipBehavior,
+ accessibleNavigation: MediaQuery.of(context).accessibleNavigation,
+ actionsPadding: widget.actionsPadding,
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart b/lib/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart
new file mode 100644
index 000000000..d4f31aea6
--- /dev/null
+++ b/lib/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart
@@ -0,0 +1,285 @@
+/*
+ * This file is part of PiliPlus
+ *
+ * PiliPlus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PiliPlus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PiliPlus. If not, see .
+ */
+
+import 'dart:math' as math;
+
+import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart';
+import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/rendering.dart' hide LayoutCallback;
+import 'package:flutter/widgets.dart'
+ hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
+
+/// ref [SliverPersistentHeader]
+
+Rect? _trim(
+ Rect? original, {
+ double top = -double.infinity,
+ double right = double.infinity,
+ double bottom = double.infinity,
+ double left = -double.infinity,
+}) => original?.intersect(Rect.fromLTRB(left, top, right, bottom));
+
+abstract class RenderSliverPersistentHeader extends RenderSliver
+ with RenderObjectWithChildMixin, RenderSliverHelpers {
+ RenderSliverPersistentHeader({RenderBox? child}) {
+ this.child = child;
+ }
+
+ SliverPersistentHeaderElement? element;
+
+ double get minExtent =>
+ (element!.widget as SliverPinnedHeader).delegate.minExtent;
+
+ bool _needsUpdateChild = true;
+
+ double get lastShrinkOffset => _lastShrinkOffset;
+ double _lastShrinkOffset = 0.0;
+
+ bool get lastOverlapsContent => _lastOverlapsContent;
+ bool _lastOverlapsContent = false;
+
+ @protected
+ void updateChild(
+ double shrinkOffset,
+ bool overlapsContent,
+ double? maxExtent,
+ ) {
+ assert(element != null);
+ element!.build(shrinkOffset, overlapsContent, maxExtent);
+ }
+
+ @override
+ void markNeedsLayout() {
+ _needsUpdateChild = true;
+ super.markNeedsLayout();
+ }
+
+ @protected
+ void updateChildIfNeeded(
+ double scrollOffset,
+ double? maxExtent, {
+ bool overlapsContent = false,
+ }) {
+ final double shrinkOffset = maxExtent == null
+ ? scrollOffset
+ : math.min(scrollOffset, maxExtent);
+ if (_needsUpdateChild ||
+ _lastShrinkOffset != shrinkOffset ||
+ _lastOverlapsContent != overlapsContent) {
+ invokeLayoutCallback((SliverConstraints constraints) {
+ assert(constraints == this.constraints);
+ updateChild(shrinkOffset, overlapsContent, maxExtent);
+ });
+ _lastShrinkOffset = shrinkOffset;
+ _lastOverlapsContent = overlapsContent;
+ _needsUpdateChild = false;
+ }
+ }
+
+ @override
+ double childMainAxisPosition(covariant RenderObject child) =>
+ super.childMainAxisPosition(child);
+
+ @override
+ bool hitTestChildren(
+ SliverHitTestResult result, {
+ required double mainAxisPosition,
+ required double crossAxisPosition,
+ }) {
+ assert(geometry!.hitTestExtent > 0.0);
+ if (child != null) {
+ return hitTestBoxChild(
+ BoxHitTestResult.wrap(result),
+ child!,
+ mainAxisPosition: mainAxisPosition,
+ crossAxisPosition: crossAxisPosition,
+ );
+ }
+ return false;
+ }
+
+ @override
+ void applyPaintTransform(RenderObject child, Matrix4 transform) {
+ assert(child == this.child);
+ applyPaintTransformForBoxChild(child as RenderBox, transform);
+ }
+
+ void triggerRebuild() {
+ markNeedsLayout();
+ }
+}
+
+class SliverPinnedHeader extends RenderObjectWidget {
+ const SliverPinnedHeader({
+ super.key,
+ required this.delegate,
+ this.onPerformLayout,
+ });
+
+ final SliverPersistentHeaderDelegate delegate;
+ final LayoutCallback? onPerformLayout;
+
+ @override
+ SliverPersistentHeaderElement createElement() =>
+ SliverPersistentHeaderElement(this);
+
+ @override
+ RenderSliverPinnedHeader createRenderObject(BuildContext context) {
+ return RenderSliverPinnedHeader(onPerformLayout: onPerformLayout);
+ }
+
+ @override
+ void updateRenderObject(
+ BuildContext context,
+ RenderSliverPinnedHeader renderObject,
+ ) {
+ renderObject.onPerformLayout = onPerformLayout;
+ }
+}
+
+class RenderSliverPinnedHeader extends RenderSliverPersistentHeader {
+ RenderSliverPinnedHeader({
+ super.child,
+ this.onPerformLayout,
+ });
+
+ LayoutCallback? onPerformLayout;
+
+ ({double crossAxisExtent, double maxExtent})? _maxExtent;
+ double? get maxExtent => _maxExtent?.maxExtent;
+
+ void _rawLayout() {
+ child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
+ _maxExtent = (
+ crossAxisExtent: constraints.crossAxisExtent,
+ maxExtent: child!.size.height,
+ );
+ onPerformLayout?.call(child!.size);
+ }
+
+ void _layout() {
+ final double shrinkOffset = math.min(
+ constraints.scrollOffset,
+ _maxExtent!.maxExtent,
+ );
+ child!.layout(
+ constraints.asBoxConstraints(
+ maxExtent: math.max(minExtent, _maxExtent!.maxExtent - shrinkOffset),
+ ),
+ parentUsesSize: true,
+ );
+ }
+
+ @override
+ void performLayout() {
+ final constraints = this.constraints;
+ final bool overlapsContent = constraints.overlap > 0.0;
+
+ if (_maxExtent == null) {
+ updateChildIfNeeded(
+ constraints.scrollOffset,
+ _maxExtent?.maxExtent,
+ overlapsContent: overlapsContent,
+ );
+ _rawLayout();
+ } else {
+ if (_maxExtent!.crossAxisExtent == constraints.crossAxisExtent) {
+ updateChildIfNeeded(
+ constraints.scrollOffset,
+ _maxExtent?.maxExtent,
+ overlapsContent: overlapsContent,
+ );
+ _layout();
+ } else {
+ _needsUpdateChild = true;
+ updateChildIfNeeded(
+ constraints.scrollOffset,
+ null,
+ overlapsContent: overlapsContent,
+ );
+ _rawLayout();
+ if (constraints.scrollOffset > 0.0) {
+ _needsUpdateChild = true;
+ updateChildIfNeeded(
+ constraints.scrollOffset,
+ _maxExtent?.maxExtent,
+ overlapsContent: overlapsContent,
+ );
+ _layout();
+ }
+ }
+ }
+ final childExtent = child!.size.height;
+ final maxExtent = _maxExtent!.maxExtent;
+ final double effectiveRemainingPaintExtent = math.max(
+ 0,
+ constraints.remainingPaintExtent - constraints.overlap,
+ );
+ final double layoutExtent = clampDouble(
+ maxExtent - constraints.scrollOffset,
+ 0.0,
+ effectiveRemainingPaintExtent,
+ );
+ geometry = SliverGeometry(
+ scrollExtent: maxExtent,
+ paintOrigin: constraints.overlap,
+ paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
+ layoutExtent: layoutExtent,
+ maxPaintExtent: maxExtent,
+ maxScrollObstructionExtent: minExtent,
+ cacheExtent: layoutExtent > 0.0
+ ? -constraints.cacheOrigin + layoutExtent
+ : layoutExtent,
+ hasVisualOverflow: false,
+ );
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ if (child != null && geometry!.visible) {
+ context.paintChild(child!, offset);
+ }
+ }
+
+ @override
+ double childMainAxisPosition(RenderBox child) => 0.0;
+
+ @override
+ void showOnScreen({
+ RenderObject? descendant,
+ Rect? rect,
+ Duration duration = Duration.zero,
+ Curve curve = Curves.ease,
+ }) {
+ final Rect? localBounds = descendant != null
+ ? MatrixUtils.transformRect(
+ descendant.getTransformTo(this),
+ rect ?? descendant.paintBounds,
+ )
+ : rect;
+
+ final Rect? newRect = _trim(localBounds, top: 0);
+
+ super.showOnScreen(
+ descendant: this,
+ rect: newRect,
+ duration: duration,
+ curve: curve,
+ );
+ }
+}
diff --git a/lib/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart b/lib/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart
new file mode 100644
index 000000000..c9fb7753f
--- /dev/null
+++ b/lib/common/widgets/dynamic_sliver_app_bar/sliver_persistent_header.dart
@@ -0,0 +1,148 @@
+/*
+ * This file is part of PiliPlus
+ *
+ * PiliPlus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PiliPlus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PiliPlus. If not, see .
+ */
+
+import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/redering/sliver_persistent_header.dart';
+import 'package:flutter/widgets.dart';
+
+/// ref [SliverPersistentHeader]
+
+abstract class SliverPersistentHeaderDelegate {
+ const SliverPersistentHeaderDelegate();
+
+ Widget build(
+ BuildContext context,
+ double shrinkOffset,
+ bool overlapsContent,
+ double? maxExtent,
+ );
+
+ double get minExtent;
+
+ bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate);
+}
+
+class SliverPersistentHeaderElement extends RenderObjectElement {
+ SliverPersistentHeaderElement(
+ SliverPinnedHeader super.widget,
+ );
+
+ @override
+ RenderSliverPinnedHeader get renderObject =>
+ super.renderObject as RenderSliverPinnedHeader;
+
+ @override
+ void mount(Element? parent, Object? newSlot) {
+ super.mount(parent, newSlot);
+ renderObject.element = this;
+ }
+
+ @override
+ void unmount() {
+ renderObject.element = null;
+ super.unmount();
+ }
+
+ @override
+ void update(SliverPinnedHeader newWidget) {
+ final oldWidget = widget as SliverPinnedHeader;
+ super.update(newWidget);
+ final SliverPersistentHeaderDelegate newDelegate = newWidget.delegate;
+ final SliverPersistentHeaderDelegate oldDelegate = oldWidget.delegate;
+ if (newDelegate != oldDelegate &&
+ (newDelegate.runtimeType != oldDelegate.runtimeType ||
+ newDelegate.shouldRebuild(oldDelegate))) {
+ final RenderSliverPinnedHeader renderObject = this.renderObject;
+ _updateChild(
+ newDelegate,
+ renderObject.lastShrinkOffset,
+ renderObject.lastOverlapsContent,
+ renderObject.maxExtent,
+ );
+ renderObject.triggerRebuild();
+ }
+ }
+
+ @override
+ void performRebuild() {
+ super.performRebuild();
+ renderObject.triggerRebuild();
+ }
+
+ Element? child;
+
+ void _updateChild(
+ SliverPersistentHeaderDelegate delegate,
+ double shrinkOffset,
+ bool overlapsContent,
+ double? maxExtent,
+ ) {
+ final Widget newWidget = delegate.build(
+ this,
+ shrinkOffset,
+ overlapsContent,
+ maxExtent,
+ );
+ child = updateChild(child, newWidget, null);
+ }
+
+ void build(double shrinkOffset, bool overlapsContent, double? maxExtent) {
+ owner!.buildScope(this, () {
+ final sliverPersistentHeaderRenderObjectWidget =
+ widget as SliverPinnedHeader;
+ _updateChild(
+ sliverPersistentHeaderRenderObjectWidget.delegate,
+ shrinkOffset,
+ overlapsContent,
+ maxExtent,
+ );
+ });
+ }
+
+ @override
+ void forgetChild(Element child) {
+ assert(child == this.child);
+ this.child = null;
+ super.forgetChild(child);
+ }
+
+ @override
+ void insertRenderObjectChild(covariant RenderBox child, Object? slot) {
+ assert(renderObject.debugValidateChild(child));
+ renderObject.child = child;
+ }
+
+ @override
+ void moveRenderObjectChild(
+ covariant RenderObject child,
+ Object? oldSlot,
+ Object? newSlot,
+ ) {
+ assert(false);
+ }
+
+ @override
+ void removeRenderObjectChild(covariant RenderObject child, Object? slot) {
+ renderObject.child = null;
+ }
+
+ @override
+ void visitChildren(ElementVisitor visitor) {
+ if (child != null) {
+ visitor(child!);
+ }
+ }
+}
diff --git a/lib/common/widgets/dynamic_sliver_appbar_medium.dart b/lib/common/widgets/dynamic_sliver_appbar_medium.dart
deleted file mode 100644
index c29859ff6..000000000
--- a/lib/common/widgets/dynamic_sliver_appbar_medium.dart
+++ /dev/null
@@ -1,171 +0,0 @@
-import 'package:PiliPlus/common/widgets/only_layout_widget.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-
-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,
- this.onPerformLayout,
- });
-
- final ValueChanged? onPerformLayout;
- 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 {
- double? _height;
- double? _width;
- late double _topPadding;
-
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- _topPadding = MediaQuery.viewPaddingOf(context).top;
- final width = MediaQuery.widthOf(context);
- if (_width != width) {
- _width = width;
- _height = null;
- }
- }
-
- @override
- Widget build(BuildContext context) {
- if (_height == null) {
- return SliverToBoxAdapter(
- child: OnlyLayoutWidget(
- onPerformLayout: (Size size) {
- if (!mounted) return;
- _height = size.height;
- widget.onPerformLayout?.call(_height!);
- setState(() {});
- },
- child: UnconstrainedBox(
- alignment: Alignment.topLeft,
- child: SizedBox(
- width: _width,
- child: widget.flexibleSpace,
- ),
- ),
- ),
- );
- }
-
- 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,
- floating: widget.floating,
- pinned: widget.pinned,
- snap: widget.snap,
- stretch: widget.stretch,
- stretchTriggerOffset: widget.stretchTriggerOffset,
- onStretchTrigger: widget.onStretchTrigger,
- shape: widget.shape,
- toolbarHeight: kToolbarHeight,
- collapsedHeight: kToolbarHeight + _topPadding + 1,
- expandedHeight: _height! - _topPadding,
- 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/common/widgets/chat_list_view.dart b/lib/common/widgets/flutter/chat_list_view.dart
similarity index 100%
rename from lib/common/widgets/chat_list_view.dart
rename to lib/common/widgets/flutter/chat_list_view.dart
diff --git a/lib/common/widgets/sliver/sliver_floating_header.dart b/lib/common/widgets/sliver/sliver_floating_header.dart
new file mode 100644
index 000000000..30b7e9370
--- /dev/null
+++ b/lib/common/widgets/sliver/sliver_floating_header.dart
@@ -0,0 +1,153 @@
+/*
+ * This file is part of PiliPlus
+ *
+ * PiliPlus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PiliPlus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PiliPlus. If not, see .
+ */
+
+import 'dart:math' as math;
+
+import 'package:flutter/foundation.dart' show clampDouble;
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart'
+ show RenderSliverSingleBoxAdapter, SliverGeometry;
+
+/// ref [SliverFloatingHeader]
+
+class SliverFloatingHeaderWidget extends SingleChildRenderObjectWidget {
+ const SliverFloatingHeaderWidget({
+ super.key,
+ required Widget super.child,
+ this.backgroundColor,
+ });
+
+ final Color? backgroundColor;
+
+ @override
+ RenderObject createRenderObject(BuildContext context) =>
+ RenderSliverFloatingHeader(backgroundColor: backgroundColor);
+
+ @override
+ void updateRenderObject(
+ BuildContext context,
+ RenderSliverFloatingHeader renderObject,
+ ) {
+ renderObject.backgroundColor = backgroundColor;
+ }
+}
+
+class RenderSliverFloatingHeader extends RenderSliverSingleBoxAdapter {
+ RenderSliverFloatingHeader({
+ required Color? backgroundColor,
+ }) : _backgroundColor = backgroundColor;
+
+ Color? _backgroundColor;
+ set backgroundColor(Color? value) {
+ if (_backgroundColor == value) return;
+ _backgroundColor = value;
+ markNeedsPaint();
+ }
+
+ double? _childPosition;
+
+ double? lastScrollOffset;
+
+ late double effectiveScrollOffset;
+
+ bool get floatingHeaderNeedsToBeUpdated {
+ return lastScrollOffset != null &&
+ (constraints.scrollOffset < lastScrollOffset! ||
+ effectiveScrollOffset < child!.size.height);
+ }
+
+ @override
+ void performLayout() {
+ if (!floatingHeaderNeedsToBeUpdated) {
+ effectiveScrollOffset = constraints.scrollOffset;
+ } else {
+ double delta =
+ lastScrollOffset! -
+ constraints.scrollOffset; // > 0 when the header is growing
+ if (constraints.userScrollDirection == .forward) {
+ final childExtent = child!.size.height;
+ if (effectiveScrollOffset > childExtent) {
+ effectiveScrollOffset =
+ childExtent; // The header is now just above the start edge of viewport.
+ }
+ } else {
+ // delta > 0 and scrolling forward is a contradiction. Assume that it's noise (set delta to 0).
+ delta = clampDouble(delta, -double.infinity, 0);
+ }
+ effectiveScrollOffset = clampDouble(
+ effectiveScrollOffset - delta,
+ 0.0,
+ constraints.scrollOffset,
+ );
+ }
+
+ child?.layout(constraints.asBoxConstraints(), parentUsesSize: true);
+ final childExtent = child!.size.height;
+ final double paintExtent = childExtent - effectiveScrollOffset;
+ final double layoutExtent = childExtent - constraints.scrollOffset;
+ geometry = SliverGeometry(
+ paintOrigin: math.min(constraints.overlap, 0.0),
+ scrollExtent: childExtent,
+ paintExtent: clampDouble(
+ paintExtent,
+ 0.0,
+ constraints.remainingPaintExtent,
+ ),
+ layoutExtent: clampDouble(
+ layoutExtent,
+ 0.0,
+ constraints.remainingPaintExtent,
+ ),
+ maxPaintExtent: childExtent,
+ hasVisualOverflow: false,
+ );
+
+ _childPosition = math.min(0.0, paintExtent - childExtent);
+ lastScrollOffset = constraints.scrollOffset;
+ }
+
+ @override
+ double childMainAxisPosition(covariant RenderObject child) {
+ return _childPosition ?? 0;
+ }
+
+ @override
+ void applyPaintTransform(RenderObject child, Matrix4 transform) {
+ assert(child == this.child);
+ applyPaintTransformForBoxChild(child as RenderBox, transform);
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ if (child != null && geometry!.visible) {
+ offset += Offset(0.0, childMainAxisPosition(child!));
+ if (_backgroundColor != null) {
+ final size = child!.size;
+ context.canvas.drawRect(
+ Rect.fromLTWH(
+ offset.dx,
+ offset.dy - 2,
+ size.width,
+ size.height + 2,
+ ),
+ Paint()..color = _backgroundColor!,
+ );
+ }
+ context.paintChild(child!, offset);
+ }
+ }
+}
diff --git a/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart b/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart
new file mode 100644
index 000000000..725e32d81
--- /dev/null
+++ b/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart
@@ -0,0 +1,123 @@
+/*
+ * This file is part of PiliPlus
+ *
+ * PiliPlus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PiliPlus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PiliPlus. If not, see .
+ */
+
+import 'dart:math' as math;
+
+import 'package:flutter/foundation.dart' show clampDouble;
+import 'package:flutter/rendering.dart'
+ show RenderSliverSingleBoxAdapter, SliverConstraints, SliverGeometry;
+import 'package:flutter/widgets.dart';
+
+/// ref [SliverPersistentHeader]
+class SliverPinnedDynamicHeader extends SingleChildRenderObjectWidget {
+ const SliverPinnedDynamicHeader({
+ super.key,
+ required Widget super.child,
+ required this.minExtent,
+ required this.maxExtent,
+ });
+
+ final double minExtent;
+ final double maxExtent;
+
+ @override
+ RenderObject createRenderObject(BuildContext context) {
+ return RenderSliverPinnedDynamicHeader(
+ minExtent: minExtent,
+ maxExtent: maxExtent,
+ );
+ }
+
+ @override
+ void updateRenderObject(
+ BuildContext context,
+ RenderSliverPinnedDynamicHeader renderObject,
+ ) {
+ renderObject
+ ..minExtent = minExtent
+ ..maxExtent = maxExtent;
+ }
+}
+
+class RenderSliverPinnedDynamicHeader extends RenderSliverSingleBoxAdapter {
+ RenderSliverPinnedDynamicHeader({
+ required double minExtent,
+ required double maxExtent,
+ }) : _minExtent = minExtent,
+ _maxExtent = maxExtent;
+
+ double _minExtent;
+ double get minExtent => _minExtent;
+ set minExtent(double value) {
+ if (_minExtent == value) return;
+ _minExtent = value;
+ markNeedsLayout();
+ }
+
+ double _maxExtent;
+ double get maxExtent => _maxExtent;
+ set maxExtent(double value) {
+ // removed
+ // if (_maxExtent == value) return;
+ _maxExtent = value;
+ markNeedsLayout();
+ }
+
+ @override
+ void performLayout() {
+ final SliverConstraints constraints = this.constraints;
+ final double shrinkOffset = math.min(constraints.scrollOffset, maxExtent);
+ child!.layout(
+ constraints.asBoxConstraints(
+ maxExtent: math.max(minExtent, maxExtent - shrinkOffset),
+ ),
+ parentUsesSize: true,
+ );
+ final double childExtent = child!.size.height;
+ final double effectiveRemainingPaintExtent = math.max(
+ 0,
+ constraints.remainingPaintExtent - constraints.overlap,
+ );
+ final double layoutExtent = clampDouble(
+ maxExtent - constraints.scrollOffset,
+ 0.0,
+ effectiveRemainingPaintExtent,
+ );
+ geometry = SliverGeometry(
+ scrollExtent: maxExtent,
+ paintOrigin: constraints.overlap,
+ paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
+ layoutExtent: layoutExtent,
+ maxPaintExtent: maxExtent,
+ maxScrollObstructionExtent: minExtent,
+ cacheExtent: layoutExtent > 0.0
+ ? -constraints.cacheOrigin + layoutExtent
+ : layoutExtent,
+ hasVisualOverflow: false,
+ );
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ if (child != null && geometry!.visible) {
+ context.paintChild(child!, offset);
+ }
+ }
+
+ @override
+ double childMainAxisPosition(RenderBox child) => 0.0;
+}
diff --git a/lib/common/widgets/sliver/sliver_pinned_header.dart b/lib/common/widgets/sliver/sliver_pinned_header.dart
new file mode 100644
index 000000000..2a1df95a8
--- /dev/null
+++ b/lib/common/widgets/sliver/sliver_pinned_header.dart
@@ -0,0 +1,118 @@
+/*
+ * This file is part of PiliPlus
+ *
+ * PiliPlus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PiliPlus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PiliPlus. If not, see .
+ */
+
+import 'dart:math' as math;
+
+import 'package:flutter/foundation.dart' show clampDouble;
+import 'package:flutter/rendering.dart'
+ show RenderSliverSingleBoxAdapter, SliverGeometry;
+import 'package:flutter/widgets.dart';
+
+/// ref [SliverPersistentHeader]
+class SliverPinnedHeader extends SingleChildRenderObjectWidget {
+ const SliverPinnedHeader({
+ super.key,
+ required Widget super.child,
+ this.backgroundColor,
+ });
+
+ final Color? backgroundColor;
+
+ @override
+ RenderObject createRenderObject(BuildContext context) =>
+ RenderSliverPinnedHeader(backgroundColor: backgroundColor);
+
+ @override
+ void updateRenderObject(
+ BuildContext context,
+ RenderSliverPinnedHeader renderObject,
+ ) {
+ renderObject.backgroundColor = backgroundColor;
+ }
+}
+
+class RenderSliverPinnedHeader extends RenderSliverSingleBoxAdapter {
+ RenderSliverPinnedHeader({
+ required Color? backgroundColor,
+ }) : _backgroundColor = backgroundColor;
+
+ Color? _backgroundColor;
+ set backgroundColor(Color? value) {
+ if (_backgroundColor == value) return;
+ _backgroundColor = value;
+ if (_isPinned) markNeedsPaint();
+ }
+
+ bool _isPinned = false;
+
+ @override
+ void performLayout() {
+ final constraints = this.constraints;
+ child!.layout(constraints.asBoxConstraints(), parentUsesSize: true);
+ final double childExtent = child!.size.height;
+ final double effectiveRemainingPaintExtent = math.max(
+ 0,
+ constraints.remainingPaintExtent - constraints.overlap,
+ );
+ final double layoutExtent = clampDouble(
+ childExtent - constraints.scrollOffset,
+ 0.0,
+ effectiveRemainingPaintExtent,
+ );
+ _isPinned = constraints.overlap > 0.0 || constraints.scrollOffset > 0.0;
+ geometry = SliverGeometry(
+ scrollExtent: childExtent,
+ paintOrigin: constraints.overlap,
+ paintExtent: math.min(childExtent, effectiveRemainingPaintExtent),
+ layoutExtent: layoutExtent,
+ maxPaintExtent: childExtent,
+ maxScrollObstructionExtent: childExtent,
+ cacheExtent: layoutExtent > 0.0
+ ? -constraints.cacheOrigin + layoutExtent
+ : layoutExtent,
+ hasVisualOverflow: false,
+ );
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ if (child != null && geometry!.visible) {
+ if (_isPinned && _backgroundColor != null) {
+ final size = child!.size;
+ context.canvas.drawRect(
+ Rect.fromLTWH(
+ offset.dx,
+ offset.dy - 2,
+ size.width,
+ size.height + 2,
+ ),
+ Paint()..color = _backgroundColor!,
+ );
+ }
+ context.paintChild(child!, offset);
+ }
+ }
+
+ @override
+ double childMainAxisPosition(RenderBox child) => 0.0;
+
+ @override
+ bool hitTestSelf({
+ required double mainAxisPosition,
+ required double crossAxisPosition,
+ }) => true;
+}
diff --git a/lib/pages/common/dyn/common_dyn_page.dart b/lib/pages/common/dyn/common_dyn_page.dart
index e96df0ed2..e8ad0b64b 100644
--- a/lib/pages/common/dyn/common_dyn_page.dart
+++ b/lib/pages/common/dyn/common_dyn_page.dart
@@ -1,8 +1,9 @@
import 'dart:math' show pi;
+import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart';
import 'package:PiliPlus/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo;
@@ -103,44 +104,33 @@ abstract class CommonDynPageState extends State
Widget buildReplyHeader(ThemeData theme) {
final secondary = theme.colorScheme.secondary;
- return SliverPersistentHeader(
- pinned: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 45,
- bgColor: theme.colorScheme.surface,
- child: Container(
- height: 45,
- padding: const EdgeInsets.only(left: 12, right: 6),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Obx(
- () {
- final count = controller.count.value;
- return Text(
- '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复',
- );
- },
- ),
- SizedBox(
- height: 35,
- child: TextButton.icon(
- onPressed: controller.queryBySort,
- icon: Icon(
- Icons.sort,
- size: 16,
- color: secondary,
- ),
- label: Obx(
- () => Text(
- controller.sortType.value.label,
- style: TextStyle(fontSize: 13, color: secondary),
- ),
- ),
+ return SliverPinnedHeader(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: const .fromLTRB(12, 2.5, 6, 2.5),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Obx(
+ () {
+ final count = controller.count.value;
+ return Text(
+ '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复',
+ );
+ },
+ ),
+ TextButton.icon(
+ style: StyleString.buttonStyle,
+ onPressed: controller.queryBySort,
+ icon: Icon(Icons.sort, size: 16, color: secondary),
+ label: Obx(
+ () => Text(
+ controller.sortType.value.label,
+ style: TextStyle(fontSize: 13, color: secondary),
),
),
- ],
- ),
+ ),
+ ],
),
),
);
diff --git a/lib/pages/dynamics_mention/view.dart b/lib/pages/dynamics_mention/view.dart
index 6b14f96fb..219659688 100644
--- a/lib/pages/dynamics_mention/view.dart
+++ b/lib/pages/dynamics_mention/view.dart
@@ -1,11 +1,11 @@
import 'dart:async';
import 'dart:math';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart'
as topic_sheet;
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart';
import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
@@ -247,18 +247,14 @@ class _DynMentionPanelState
}
return SliverMainAxisGroup(
slivers: [
- SliverPersistentHeader(
- pinned: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 40,
- needRebuild: true,
- bgColor: theme.colorScheme.surface,
- child: Container(
- height: 40,
- alignment: Alignment.centerLeft,
- padding: const EdgeInsets.symmetric(horizontal: 16),
- child: Text(group.groupName!),
+ SliverPinnedHeader(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: const .symmetric(
+ horizontal: 16,
+ vertical: 10,
),
+ child: Text(group.groupName!),
),
),
SliverList.builder(
diff --git a/lib/pages/dynamics_topic/view.dart b/lib/pages/dynamics_topic/view.dart
index edf9f5eee..03451d572 100644
--- a/lib/pages/dynamics_topic/view.dart
+++ b/lib/pages/dynamics_topic/view.dart
@@ -1,10 +1,10 @@
import 'package:PiliPlus/common/widgets/custom_icon.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/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.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/pair.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/item.dart';
@@ -78,56 +78,49 @@ class _DynTopicPageState extends State with DynMixin {
Obx(() {
final allSortBy = _controller.topicSortByConf.value?.allSortBy;
if (allSortBy != null && allSortBy.isNotEmpty) {
- return SliverPersistentHeader(
- pinned: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 36,
- needRebuild: true,
- bgColor: theme.colorScheme.surface,
- child: Container(
- height: 36,
- padding: EdgeInsets.only(
- left: 12 + padding.left,
- top: 6,
- bottom: 6,
- ),
- child: Builder(
- builder: (context) {
- return ToggleButtons(
- fillColor: theme.colorScheme.secondaryContainer,
- selectedColor:
- theme.colorScheme.onSecondaryContainer,
- constraints: const BoxConstraints(
- minWidth: 54,
- minHeight: 24,
- ),
- tapTargetSize: MaterialTapTargetSize.shrinkWrap,
- borderRadius: const .all(.circular(25)),
- onPressed: (index) {
- _controller.onSort(allSortBy[index].sortBy!);
- (context as Element).markNeedsBuild();
- },
- isSelected: allSortBy
- .map((e) => e.sortBy == _controller.sortBy)
- .toList(),
- children: 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 SliverPinnedHeader(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: EdgeInsets.only(
+ left: 12 + padding.left,
+ top: 6,
+ bottom: 6,
+ ),
+ child: Builder(
+ builder: (context) {
+ return ToggleButtons(
+ fillColor: theme.colorScheme.secondaryContainer,
+ selectedColor: theme.colorScheme.onSecondaryContainer,
+ constraints: const BoxConstraints(
+ minWidth: 54,
+ minHeight: 24,
+ ),
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ borderRadius: const .all(.circular(25)),
+ onPressed: (index) {
+ _controller.onSort(allSortBy[index].sortBy!);
+ (context as Element).markNeedsBuild();
+ },
+ isSelected: allSortBy
+ .map((e) => e.sortBy == _controller.sortBy)
+ .toList(),
+ children: 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(),
+ );
+ },
),
),
);
@@ -157,10 +150,9 @@ class _DynTopicPageState extends State with DynMixin {
) {
return switch (topState) {
Loading() => const SliverAppBar(),
- Success(:final response) when response != null => DynamicSliverAppBarMedium(
- pinned: true,
- onPerformLayout: (value) =>
- _controller.appbarOffset = value - kToolbarHeight - padding.top,
+ Success(:final response) when response != null => DynamicSliverAppBar.medium(
+ onPerformLayout: (value) => _controller.appbarOffset =
+ value.height - kToolbarHeight - padding.top,
title: IgnorePointer(child: Text(response.topicItem!.name)),
flexibleSpace: Container(
decoration: BoxDecoration(
diff --git a/lib/pages/live_dm_block/view.dart b/lib/pages/live_dm_block/view.dart
index f7f097d89..f181a1f35 100644
--- a/lib/pages/live_dm_block/view.dart
+++ b/lib/pages/live_dm_block/view.dart
@@ -1,8 +1,8 @@
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart';
import 'package:PiliPlus/models/common/live/live_dm_silent_type.dart';
import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart';
import 'package:PiliPlus/pages/live_dm_block/controller.dart';
@@ -102,13 +102,7 @@ class _LiveDmBlockPageState extends State {
ExtendedNestedScrollView.sliverOverlapAbsorberHandleFor(
context,
),
- sliver: SliverPersistentHeader(
- pinned: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 48,
- child: tabBar,
- ),
- ),
+ sliver: SliverPinnedHeader(child: tabBar),
),
];
},
diff --git a/lib/pages/main_reply/view.dart b/lib/pages/main_reply/view.dart
index e52aff774..097d91a88 100644
--- a/lib/pages/main_reply/view.dart
+++ b/lib/pages/main_reply/view.dart
@@ -1,7 +1,8 @@
+import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
import 'package:PiliPlus/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo;
@@ -15,7 +16,6 @@ import 'package:PiliPlus/utils/num_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart' show ScrollDirection;
import 'package:get/get.dart';
class MainReplyPage extends StatefulWidget {
@@ -61,9 +61,9 @@ class _MainReplyPageState extends State {
body: NotificationListener(
onNotification: (notification) {
final direction = notification.direction;
- if (direction == ScrollDirection.forward) {
+ if (direction == .forward) {
_controller.showFab();
- } else if (direction == ScrollDirection.reverse) {
+ } else if (direction == .reverse) {
_controller.hideFab();
}
return false;
@@ -172,44 +172,33 @@ class _MainReplyPageState extends State {
Widget buildReplyHeader(ColorScheme colorScheme) {
final secondary = colorScheme.secondary;
- return SliverPersistentHeader(
- floating: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 45,
- bgColor: colorScheme.surface,
- child: Container(
- height: 45,
- padding: const EdgeInsets.only(left: 12, right: 6),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Obx(
- () {
- final count = _controller.count.value;
- return Text(
- '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复',
- );
- },
- ),
- SizedBox(
- height: 35,
- child: TextButton.icon(
- onPressed: _controller.queryBySort,
- icon: Icon(
- Icons.sort,
- size: 16,
- color: secondary,
- ),
- label: Obx(
- () => Text(
- _controller.sortType.value.label,
- style: TextStyle(fontSize: 13, color: secondary),
- ),
- ),
+ return SliverFloatingHeaderWidget(
+ backgroundColor: colorScheme.surface,
+ child: Padding(
+ padding: const .fromLTRB(12, 2.5, 6, 2.5),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Obx(
+ () {
+ final count = _controller.count.value;
+ return Text(
+ '${count == -1 ? 0 : NumUtils.numFormat(count)}条回复',
+ );
+ },
+ ),
+ TextButton.icon(
+ style: StyleString.buttonStyle,
+ onPressed: _controller.queryBySort,
+ icon: Icon(Icons.sort, size: 16, color: secondary),
+ label: Obx(
+ () => Text(
+ _controller.sortType.value.label,
+ style: TextStyle(fontSize: 13, color: secondary),
),
),
- ],
- ),
+ ),
+ ],
),
),
);
diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart
index 6f6140a0c..c7df772c7 100644
--- a/lib/pages/member/view.dart
+++ b/lib/pages/member/view.dart
@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/widgets/dialog/report_member.dart';
-import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar_medium.dart';
+import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/http/loading_state.dart';
@@ -328,8 +328,7 @@ class _MemberPageState extends State {
return const CircularProgressIndicator();
case Success(:final response):
if (response != null) {
- return DynamicSliverAppBarMedium(
- pinned: true,
+ return DynamicSliverAppBar.medium(
actions: _actions(theme),
title: Text(_userController.username ?? ''),
flexibleSpace: Obx(
diff --git a/lib/pages/member_audio/view.dart b/lib/pages/member_audio/view.dart
index 3c53df03c..1e0f978f4 100644
--- a/lib/pages/member_audio/view.dart
+++ b/lib/pages/member_audio/view.dart
@@ -1,8 +1,8 @@
import 'package:PiliPlus/common/constants.dart';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/space/space_audio/item.dart';
import 'package:PiliPlus/pages/member_audio/controller.dart';
@@ -80,44 +80,36 @@ class _MemberAudioState extends State
response != null && response.isNotEmpty
? SliverMainAxisGroup(
slivers: [
- SliverPersistentHeader(
- floating: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 40,
- bgColor: colorScheme.surface,
- child: SizedBox(
- height: 40,
- child: Row(
- children: [
- const SizedBox(width: 8),
- Padding(
- padding: const EdgeInsets.only(left: 6),
- child: Text(
- '共${_controller.totalSize ?? 0}首',
- style: const TextStyle(fontSize: 13),
+ SliverFloatingHeaderWidget(
+ backgroundColor: colorScheme.surface,
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5),
+ child: Row(
+ children: [
+ Text(
+ '共${_controller.totalSize ?? 0}首',
+ style: const TextStyle(fontSize: 13),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(left: 6),
+ child: TextButton.icon(
+ style: StyleString.buttonStyle,
+ onPressed: _controller.toViewPlayAll,
+ icon: Icon(
+ Icons.play_circle_outline_rounded,
+ size: 16,
+ color: colorScheme.secondary,
),
- ),
- Container(
- height: 35,
- padding: const EdgeInsets.only(left: 6),
- child: TextButton.icon(
- onPressed: _controller.toViewPlayAll,
- icon: Icon(
- Icons.play_circle_outline_rounded,
- size: 16,
+ label: Text(
+ '播放全部',
+ style: TextStyle(
+ fontSize: 13,
color: colorScheme.secondary,
),
- label: Text(
- '播放全部',
- style: TextStyle(
- fontSize: 13,
- color: colorScheme.secondary,
- ),
- ),
),
),
- ],
- ),
+ ),
+ ],
),
),
),
diff --git a/lib/pages/member_contribute/view.dart b/lib/pages/member_contribute/view.dart
index 6cf17a3e1..06a6443a6 100644
--- a/lib/pages/member_contribute/view.dart
+++ b/lib/pages/member_contribute/view.dart
@@ -97,13 +97,14 @@ class _MemberContributeState extends State
}
Widget _getPageFromType(SpaceTab2Item item) {
+ final isSingle = _controller.tabs == null;
return switch (item.param) {
'video' => MemberVideo(
type: ContributeType.video,
heroTag: widget.heroTag,
mid: widget.mid,
title: item.title,
- isSingle: _controller.tabs == null,
+ isSingle: isSingle,
),
'charging_video' => MemberVideo(
type: ContributeType.charging,
@@ -116,9 +117,9 @@ class _MemberContributeState extends State
mid: widget.mid,
),
'opus' => MemberOpus(
- isSingle: _controller.tabs == null,
heroTag: widget.heroTag,
mid: widget.mid,
+ isSingle: isSingle,
),
'audio' => MemberAudio(
heroTag: widget.heroTag,
diff --git a/lib/pages/member_favorite/view.dart b/lib/pages/member_favorite/view.dart
index 8962a092e..599f33eae 100644
--- a/lib/pages/member_favorite/view.dart
+++ b/lib/pages/member_favorite/view.dart
@@ -1,7 +1,7 @@
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/space/space_fav/data.dart';
import 'package:PiliPlus/pages/member_favorite/controller.dart';
@@ -109,56 +109,51 @@ class _MemberFavoriteState extends State
}) {
return SliverMainAxisGroup(
slivers: [
- SliverPersistentHeader(
- pinned: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- child: Material(
- color: theme.colorScheme.surface,
- child: Builder(
- builder: (context) {
- return InkWell(
- onTap: () {
- _controller.setExpand(isFav);
- (context as Element).markNeedsBuild();
- data.refresh();
- if (!isEnd.value) {
- isEnd.refresh();
- }
- },
- child: Container(
- height: 45,
- alignment: .centerLeft,
- padding: const .only(left: 12),
- child: Text.rich(
- TextSpan(
- children: [
- WidgetSpan(
- alignment: .middle,
- child: Icon(
- _controller.isExpand(isFav)
- ? Icons.expand_less
- : Icons.expand_more,
- color: theme.colorScheme.outline,
- ),
+ SliverPinnedHeader(
+ child: Material(
+ color: theme.colorScheme.surface,
+ child: Builder(
+ builder: (context) {
+ return InkWell(
+ onTap: () {
+ _controller.setExpand(isFav);
+ (context as Element).markNeedsBuild();
+ data.refresh();
+ if (!isEnd.value) {
+ isEnd.refresh();
+ }
+ },
+ child: Padding(
+ padding: const .symmetric(horizontal: 12, vertical: 10),
+ child: Text.rich(
+ TextSpan(
+ children: [
+ WidgetSpan(
+ alignment: .middle,
+ child: Icon(
+ _controller.isExpand(isFav)
+ ? Icons.expand_less
+ : Icons.expand_more,
+ color: theme.colorScheme.outline,
),
- TextSpan(
- text: ' ${data.value.name}',
- style: const TextStyle(fontSize: 14),
+ ),
+ TextSpan(
+ text: ' ${data.value.name}',
+ style: const TextStyle(fontSize: 14),
+ ),
+ TextSpan(
+ text: ' ${data.value.mediaListResponse?.count}',
+ style: TextStyle(
+ fontSize: 13,
+ color: theme.colorScheme.outline,
),
- TextSpan(
- text: ' ${data.value.mediaListResponse?.count}',
- style: TextStyle(
- fontSize: 13,
- color: theme.colorScheme.outline,
- ),
- ),
- ],
- ),
+ ),
+ ],
),
),
- );
- },
- ),
+ ),
+ );
+ },
),
),
),
diff --git a/lib/pages/member_video/view.dart b/lib/pages/member_video/view.dart
index 5d2e27fd6..2a9096755 100644
--- a/lib/pages/member_video/view.dart
+++ b/lib/pages/member_video/view.dart
@@ -1,7 +1,8 @@
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
+import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/member/contribute_type.dart';
import 'package:PiliPlus/models_new/space/space_archive/item.dart';
@@ -173,91 +174,78 @@ class _MemberVideoState extends State
response != null && response.isNotEmpty
? SliverMainAxisGroup(
slivers: [
- SliverPersistentHeader(
- pinned: false,
- floating: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 40,
- bgColor: theme.colorScheme.surface,
- child: SizedBox(
- height: 40,
- child: Row(
- children: [
- const SizedBox(width: 8),
- Padding(
- padding: const EdgeInsets.only(left: 6),
- child: Obx(
- () {
- final count = _controller.count.value;
- return Text(
- count != -1 ? '共$count视频' : '',
- style: const TextStyle(fontSize: 13),
- );
- },
- ),
- ),
- Obx(
- () {
- final episodicButton =
- _controller.episodicButton.value;
- return episodicButton.uri?.isNotEmpty == true
- ? Container(
- height: 35,
- padding: EdgeInsets.only(
- left: _controller.count.value != -1
- ? 6
- : 0,
+ SliverFloatingHeaderWidget(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5),
+ child: Row(
+ children: [
+ Obx(
+ () {
+ final count = _controller.count.value;
+ return Text(
+ count != -1 ? '共$count视频' : '',
+ style: const TextStyle(fontSize: 13),
+ );
+ },
+ ),
+ Obx(
+ () {
+ final episodicButton =
+ _controller.episodicButton.value;
+ return episodicButton.uri?.isNotEmpty == true
+ ? Padding(
+ padding: EdgeInsets.only(
+ left: _controller.count.value != -1
+ ? 6
+ : 0,
+ ),
+ child: TextButton.icon(
+ style: StyleString.buttonStyle,
+ onPressed: _controller.toViewPlayAll,
+ icon: Icon(
+ Icons.play_circle_outline_rounded,
+ size: 16,
+ color: theme.colorScheme.secondary,
),
- child: TextButton.icon(
- onPressed: _controller.toViewPlayAll,
- icon: Icon(
- Icons.play_circle_outline_rounded,
- size: 16,
+ label: Text(
+ episodicButton.text ?? '播放全部',
+ style: TextStyle(
+ fontSize: 13,
color: theme.colorScheme.secondary,
),
- label: Text(
- episodicButton.text ?? '播放全部',
- style: TextStyle(
- fontSize: 13,
- color:
- theme.colorScheme.secondary,
- ),
- ),
),
- )
- : const SizedBox.shrink();
- },
+ ),
+ )
+ : const SizedBox.shrink();
+ },
+ ),
+ const Spacer(),
+ TextButton.icon(
+ style: StyleString.buttonStyle,
+ onPressed: _controller.queryBySort,
+ icon: Icon(
+ Icons.sort,
+ size: 16,
+ color: theme.colorScheme.secondary,
),
- const Spacer(),
- SizedBox(
- height: 35,
- child: TextButton.icon(
- onPressed: _controller.queryBySort,
- icon: Icon(
- Icons.sort,
- size: 16,
+ label: Obx(
+ () => Text(
+ widget.type == ContributeType.video
+ ? _controller.order.value == 'pubdate'
+ ? '最新发布'
+ : '最多播放'
+ : _controller.sort.value == 'desc'
+ ? '默认'
+ : '倒序',
+ style: TextStyle(
+ fontSize: 13,
color: theme.colorScheme.secondary,
),
- label: Obx(
- () => Text(
- widget.type == ContributeType.video
- ? _controller.order.value == 'pubdate'
- ? '最新发布'
- : '最多播放'
- : _controller.sort.value == 'desc'
- ? '默认'
- : '倒序',
- style: TextStyle(
- fontSize: 13,
- color: theme.colorScheme.secondary,
- ),
- ),
- ),
),
),
- const SizedBox(width: 8),
- ],
- ),
+ ),
+ ],
),
),
),
diff --git a/lib/pages/pgc_review/child/view.dart b/lib/pages/pgc_review/child/view.dart
index 84eea7084..c292b5d6d 100644
--- a/lib/pages/pgc_review/child/view.dart
+++ b/lib/pages/pgc_review/child/view.dart
@@ -1,11 +1,12 @@
+import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/custom_icon.dart';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/flutter/selectable_text/selectable_text.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/sliver/sliver_floating_header.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/image_type.dart';
import 'package:PiliPlus/models/common/pgc_review_type.dart';
@@ -383,51 +384,43 @@ class _PgcReviewChildPageState extends State
);
}
- Widget _buildHeader(ThemeData theme) => SliverPersistentHeader(
- pinned: false,
- floating: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 40,
- bgColor: theme.colorScheme.surface,
- child: Container(
- height: 40,
- padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Obx(
- () {
- final count = _controller.count.value;
- return count == null
- ? const SizedBox.shrink()
- : Text(
- '${NumUtils.numFormat(count)}条点评',
- style: const TextStyle(fontSize: 13),
- );
- },
+ Widget _buildHeader(ThemeData theme) => SliverFloatingHeaderWidget(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(12, 2.5, 6, 2.5),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Obx(
+ () {
+ final count = _controller.count.value;
+ return count == null
+ ? const SizedBox.shrink()
+ : Text(
+ '${NumUtils.numFormat(count)}条点评',
+ style: const TextStyle(fontSize: 13),
+ );
+ },
+ ),
+ TextButton.icon(
+ style: StyleString.buttonStyle,
+ onPressed: _controller.queryBySort,
+ icon: Icon(
+ Icons.sort,
+ size: 16,
+ color: theme.colorScheme.secondary,
),
- SizedBox(
- height: 35,
- child: TextButton.icon(
- onPressed: _controller.queryBySort,
- icon: Icon(
- Icons.sort,
- size: 16,
+ label: Obx(
+ () => Text(
+ _controller.sortType.value.label,
+ style: TextStyle(
+ fontSize: 13,
color: theme.colorScheme.secondary,
),
- label: Obx(
- () => Text(
- _controller.sortType.value.label,
- style: TextStyle(
- fontSize: 13,
- color: theme.colorScheme.secondary,
- ),
- ),
- ),
),
),
- ],
- ),
+ ),
+ ],
),
),
);
diff --git a/lib/pages/popular_series/view.dart b/lib/pages/popular_series/view.dart
index 3b402773d..cb9b061ad 100644
--- a/lib/pages/popular_series/view.dart
+++ b/lib/pages/popular_series/view.dart
@@ -1,9 +1,9 @@
import 'dart:math';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart';
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
import 'package:PiliPlus/http/loading_state.dart';
@@ -201,17 +201,11 @@ class _PopularSeriesPageState extends State with GridMixin {
],
);
}
- final height = MediaQuery.textScalerOf(context).scale(27);
- return SliverPersistentHeader(
- floating: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: height,
- child: Container(
- height: height,
- padding: const EdgeInsets.only(left: 14, bottom: 7),
- child: child,
- ),
- bgColor: colorScheme.surface,
+ return SliverFloatingHeaderWidget(
+ backgroundColor: colorScheme.surface,
+ child: Padding(
+ padding: const .only(left: 14, bottom: 7),
+ child: child,
),
);
}
diff --git a/lib/pages/search_panel/article/view.dart b/lib/pages/search_panel/article/view.dart
index c65086a93..69892c830 100644
--- a/lib/pages/search_panel/article/view.dart
+++ b/lib/pages/search_panel/article/view.dart
@@ -1,4 +1,4 @@
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
import 'package:PiliPlus/models/search/result.dart';
import 'package:PiliPlus/pages/search_panel/article/controller.dart';
import 'package:PiliPlus/pages/search_panel/article/widgets/item.dart';
@@ -45,51 +45,45 @@ class _SearchArticlePanelState
@override
Widget buildHeader(ThemeData theme) {
- return SliverPersistentHeader(
- pinned: false,
- floating: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 40,
- bgColor: theme.colorScheme.surface,
- child: Container(
- height: 40,
- padding: const EdgeInsets.only(left: 25, right: 12),
- child: Row(
- children: [
- Obx(
- () => Text(
- '排序: ${controller.articleOrderType.value.label}',
- maxLines: 1,
- style: TextStyle(color: theme.colorScheme.outline),
+ return SliverFloatingHeaderWidget(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: const .fromLTRB(25, 0, 12, 4),
+ child: Row(
+ children: [
+ Obx(
+ () => Text(
+ '排序: ${controller.articleOrderType.value.label}',
+ maxLines: 1,
+ style: TextStyle(color: theme.colorScheme.outline),
+ ),
+ ),
+ const Spacer(),
+ Obx(
+ () => Text(
+ '分区: ${controller.articleZoneType!.value.label}',
+ maxLines: 1,
+ style: TextStyle(color: theme.colorScheme.outline),
+ ),
+ ),
+ const Spacer(),
+ SizedBox(
+ width: 32,
+ height: 32,
+ child: IconButton(
+ tooltip: '筛选',
+ style: const ButtonStyle(
+ padding: WidgetStatePropertyAll(EdgeInsets.zero),
+ ),
+ onPressed: () => controller.onShowFilterDialog(context),
+ icon: Icon(
+ Icons.filter_list_outlined,
+ size: 18,
+ color: theme.colorScheme.primary,
),
),
- const Spacer(),
- Obx(
- () => Text(
- '分区: ${controller.articleZoneType!.value.label}',
- maxLines: 1,
- style: TextStyle(color: theme.colorScheme.outline),
- ),
- ),
- const Spacer(),
- SizedBox(
- width: 32,
- height: 32,
- child: IconButton(
- tooltip: '筛选',
- style: const ButtonStyle(
- padding: WidgetStatePropertyAll(EdgeInsets.zero),
- ),
- onPressed: () => controller.onShowFilterDialog(context),
- icon: Icon(
- Icons.filter_list_outlined,
- size: 18,
- color: theme.colorScheme.primary,
- ),
- ),
- ),
- ],
- ),
+ ),
+ ],
),
),
);
diff --git a/lib/pages/search_panel/user/view.dart b/lib/pages/search_panel/user/view.dart
index 65c1aa8d4..dc1008bec 100644
--- a/lib/pages/search_panel/user/view.dart
+++ b/lib/pages/search_panel/user/view.dart
@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/skeleton/msg_feed_top.dart';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
import 'package:PiliPlus/models/search/result.dart';
import 'package:PiliPlus/pages/search_panel/user/controller.dart';
import 'package:PiliPlus/pages/search_panel/user/widgets/item.dart';
@@ -46,51 +46,45 @@ class _SearchUserPanelState
@override
Widget buildHeader(ThemeData theme) {
- return SliverPersistentHeader(
- pinned: false,
- floating: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 40,
- bgColor: theme.colorScheme.surface,
- child: Container(
- height: 40,
- padding: const EdgeInsets.only(left: 25, right: 12),
- child: Row(
- children: [
- Obx(
- () => Text(
- '排序: ${controller.userOrderType!.value.label}',
- maxLines: 1,
- style: TextStyle(color: theme.colorScheme.outline),
+ return SliverFloatingHeaderWidget(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: const .fromLTRB(25, 0, 12, 4),
+ child: Row(
+ children: [
+ Obx(
+ () => Text(
+ '排序: ${controller.userOrderType!.value.label}',
+ maxLines: 1,
+ style: TextStyle(color: theme.colorScheme.outline),
+ ),
+ ),
+ const Spacer(),
+ Obx(
+ () => Text(
+ '用户类型: ${controller.userType!.value.label}',
+ maxLines: 1,
+ style: TextStyle(color: theme.colorScheme.outline),
+ ),
+ ),
+ const Spacer(),
+ SizedBox(
+ width: 32,
+ height: 32,
+ child: IconButton(
+ tooltip: '筛选',
+ style: const ButtonStyle(
+ padding: WidgetStatePropertyAll(EdgeInsets.zero),
+ ),
+ onPressed: () => controller.onShowFilterDialog(context),
+ icon: Icon(
+ Icons.filter_list_outlined,
+ size: 18,
+ color: theme.colorScheme.primary,
),
),
- const Spacer(),
- Obx(
- () => Text(
- '用户类型: ${controller.userType!.value.label}',
- maxLines: 1,
- style: TextStyle(color: theme.colorScheme.outline),
- ),
- ),
- const Spacer(),
- SizedBox(
- width: 32,
- height: 32,
- child: IconButton(
- tooltip: '筛选',
- style: const ButtonStyle(
- padding: WidgetStatePropertyAll(EdgeInsets.zero),
- ),
- onPressed: () => controller.onShowFilterDialog(context),
- icon: Icon(
- Icons.filter_list_outlined,
- size: 18,
- color: theme.colorScheme.primary,
- ),
- ),
- ),
- ],
- ),
+ ),
+ ],
),
),
);
diff --git a/lib/pages/search_panel/video/view.dart b/lib/pages/search_panel/video/view.dart
index 14265c5f8..d7b28050b 100644
--- a/lib/pages/search_panel/video/view.dart
+++ b/lib/pages/search_panel/video/view.dart
@@ -1,4 +1,4 @@
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart';
import 'package:PiliPlus/models/common/search/video_search_type.dart';
import 'package:PiliPlus/models/search/result.dart';
@@ -47,61 +47,55 @@ class _SearchVideoPanelState
@override
Widget buildHeader(ThemeData theme) {
- return SliverPersistentHeader(
- pinned: false,
- floating: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 34,
- bgColor: theme.colorScheme.surface,
- child: Container(
- height: 34,
- padding: const EdgeInsets.symmetric(horizontal: 12),
- child: Row(
- children: [
- Expanded(
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Wrap(
- children: [
- for (final e in ArchiveFilterType.values)
- Obx(
- () => SearchText(
- fontSize: 13,
- text: e.desc,
- bgColor: Colors.transparent,
- textColor: controller.selectedType.value == e
- ? theme.colorScheme.primary
- : theme.colorScheme.outline,
- onTap: (_) => controller
- ..order = e.name
- ..selectedType.value = e
- ..onSortSearch(getBack: false),
- ),
+ return SliverFloatingHeaderWidget(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: const .fromLTRB(12, 0, 12, 4),
+ child: Row(
+ children: [
+ Expanded(
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Wrap(
+ children: [
+ for (final e in ArchiveFilterType.values)
+ Obx(
+ () => SearchText(
+ fontSize: 13,
+ text: e.desc,
+ bgColor: Colors.transparent,
+ textColor: controller.selectedType.value == e
+ ? theme.colorScheme.primary
+ : theme.colorScheme.outline,
+ onTap: (_) => controller
+ ..order = e.name
+ ..selectedType.value = e
+ ..onSortSearch(getBack: false),
),
- ],
- ),
+ ),
+ ],
),
),
- const VerticalDivider(indent: 7, endIndent: 8),
- const SizedBox(width: 3),
- SizedBox(
- width: 32,
- height: 32,
- child: IconButton(
- tooltip: '筛选',
- style: const ButtonStyle(
- padding: WidgetStatePropertyAll(EdgeInsets.zero),
- ),
- onPressed: () => controller.onShowFilterDialog(context),
- icon: Icon(
- Icons.filter_list_outlined,
- size: 18,
- color: theme.colorScheme.primary,
- ),
+ ),
+ const VerticalDivider(indent: 7, endIndent: 8),
+ const SizedBox(width: 3),
+ SizedBox(
+ width: 32,
+ height: 32,
+ child: IconButton(
+ tooltip: '筛选',
+ style: const ButtonStyle(
+ padding: WidgetStatePropertyAll(EdgeInsets.zero),
+ ),
+ onPressed: () => controller.onShowFilterDialog(context),
+ icon: Icon(
+ Icons.filter_list_outlined,
+ size: 18,
+ color: theme.colorScheme.primary,
),
),
- ],
- ),
+ ),
+ ],
),
),
);
diff --git a/lib/pages/video/member/view.dart b/lib/pages/video/member/view.dart
index 50366a670..ab8276cef 100644
--- a/lib/pages/video/member/view.dart
+++ b/lib/pages/video/member/view.dart
@@ -1,3 +1,4 @@
+import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
@@ -136,24 +137,22 @@ class _HorizontalMemberPageState extends State {
);
},
),
- SizedBox(
- height: 35,
- child: TextButton.icon(
- onPressed: () => _controller
- ..lastAid = widget.videoDetailController.aid.toString()
- ..queryBySort(),
- icon: Icon(
- Icons.sort,
- size: 16,
- color: theme.colorScheme.secondary,
- ),
- label: Obx(
- () => Text(
- _controller.order.value == 'pubdate' ? '最新发布' : '最多播放',
- style: TextStyle(
- fontSize: 13,
- color: theme.colorScheme.secondary,
- ),
+ TextButton.icon(
+ style: StyleString.buttonStyle,
+ onPressed: () => _controller
+ ..lastAid = widget.videoDetailController.aid.toString()
+ ..queryBySort(),
+ icon: Icon(
+ Icons.sort,
+ size: 16,
+ color: theme.colorScheme.secondary,
+ ),
+ label: Obx(
+ () => Text(
+ _controller.order.value == 'pubdate' ? '最新发布' : '最多播放',
+ style: TextStyle(
+ fontSize: 13,
+ color: theme.colorScheme.secondary,
),
),
),
diff --git a/lib/pages/video/reply/view.dart b/lib/pages/video/reply/view.dart
index 38841e6ee..e3be23813 100644
--- a/lib/pages/video/reply/view.dart
+++ b/lib/pages/video/reply/view.dart
@@ -1,7 +1,8 @@
+import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo;
import 'package:PiliPlus/http/loading_state.dart';
@@ -83,47 +84,39 @@ class _VideoReplyPanelState extends State
: _videoReplyController.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
key: const PageStorageKey(_VideoReplyPanelState),
- slivers: [
- SliverPersistentHeader(
- pinned: false,
- floating: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 40,
- bgColor: theme.colorScheme.surface,
- child: Container(
- height: 40,
- padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Obx(
- () => Text(
- _videoReplyController.sortType.value.title,
- style: const TextStyle(fontSize: 13),
- ),
+ slivers: [
+ SliverFloatingHeaderWidget(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: const .fromLTRB(12, 2.5, 6, 2.5),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Obx(
+ () => Text(
+ _videoReplyController.sortType.value.title,
+ style: const TextStyle(fontSize: 13),
),
- SizedBox(
- height: 35,
- child: TextButton.icon(
- onPressed: _videoReplyController.queryBySort,
- icon: Icon(
- Icons.sort,
- size: 16,
+ ),
+ TextButton.icon(
+ style: StyleString.buttonStyle,
+ onPressed: _videoReplyController.queryBySort,
+ icon: Icon(
+ Icons.sort,
+ size: 16,
+ color: theme.colorScheme.secondary,
+ ),
+ label: Obx(
+ () => Text(
+ _videoReplyController.sortType.value.label,
+ style: TextStyle(
+ fontSize: 13,
color: theme.colorScheme.secondary,
),
- label: Obx(
- () => Text(
- _videoReplyController.sortType.value.label,
- style: TextStyle(
- fontSize: 13,
- color: theme.colorScheme.secondary,
- ),
- ),
- ),
),
),
- ],
- ),
+ ),
+ ],
),
),
),
diff --git a/lib/pages/video/reply_reply/view.dart b/lib/pages/video/reply_reply/view.dart
index 09ebf3c47..e73dc14dd 100644
--- a/lib/pages/video/reply_reply/view.dart
+++ b/lib/pages/video/reply_reply/view.dart
@@ -1,8 +1,9 @@
+import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_reply.dart';
import 'package:PiliPlus/common/widgets/colored_box_transition.dart';
-import 'package:PiliPlus/common/widgets/custom_sliver_persistent_header_delegate.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart';
import 'package:PiliPlus/common/widgets/view_safe_area.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo, Mode;
@@ -240,52 +241,43 @@ class _VideoReplyReplyPanelState extends State
}
Widget _sortWidget(ThemeData theme) {
- return SliverPersistentHeader(
- pinned: true,
- delegate: CustomSliverPersistentHeaderDelegate(
- extent: 40,
- bgColor: theme.colorScheme.surface,
- child: Container(
- height: 40,
- padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Obx(
- () {
- final count = _controller.count.value;
- return count != -1
- ? Text(
- '相关回复共${NumUtils.numFormat(count)}条',
- style: const TextStyle(fontSize: 13),
- )
- : const SizedBox.shrink();
- },
+ return SliverPinnedHeader(
+ backgroundColor: theme.colorScheme.surface,
+ child: Padding(
+ padding: const EdgeInsets.fromLTRB(12, 2.5, 6, 2.5),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Obx(
+ () {
+ final count = _controller.count.value;
+ return count != -1
+ ? Text(
+ '相关回复共${NumUtils.numFormat(count)}条',
+ style: const TextStyle(fontSize: 13),
+ )
+ : const SizedBox.shrink();
+ },
+ ),
+ TextButton.icon(
+ style: StyleString.buttonStyle,
+ onPressed: _controller.queryBySort,
+ icon: Icon(
+ Icons.sort,
+ size: 16,
+ color: theme.colorScheme.secondary,
),
- SizedBox(
- height: 35,
- child: TextButton.icon(
- onPressed: _controller.queryBySort,
- icon: Icon(
- Icons.sort,
- size: 16,
+ label: Obx(
+ () => Text(
+ _controller.mode.value == Mode.MAIN_LIST_HOT ? '按热度' : '按时间',
+ style: TextStyle(
+ fontSize: 13,
color: theme.colorScheme.secondary,
),
- label: Obx(
- () => Text(
- _controller.mode.value == Mode.MAIN_LIST_HOT
- ? '按热度'
- : '按时间',
- style: TextStyle(
- fontSize: 13,
- color: theme.colorScheme.secondary,
- ),
- ),
- ),
),
),
- ],
- ),
+ ),
+ ],
),
),
);
diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart
index c3b8dd517..df3db77b3 100644
--- a/lib/pages/video/view.dart
+++ b/lib/pages/video/view.dart
@@ -9,6 +9,7 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/image_viewer/hero_dialog_route.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
+import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_dynamic_header.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/main.dart';
import 'package:PiliPlus/models/common/episode_panel_type.dart';
@@ -648,14 +649,10 @@ class _VideoDetailPageVState extends State
? animHeight
: videoDetailController.videoHeight;
return [
- SliverAppBar(
- elevation: 0,
- scrolledUnderElevation: 0,
- primary: false,
- automaticallyImplyLeading: false,
- pinned: true,
- expandedHeight: height,
- flexibleSpace: Stack(
+ SliverPinnedDynamicHeader(
+ minExtent: kToolbarHeight,
+ maxExtent: height,
+ child: Stack(
clipBehavior: Clip.none,
children: [
SizedBox(
diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart
index 7cd2dbd52..ff57111ae 100644
--- a/lib/pages/whisper_detail/view.dart
+++ b/lib/pages/whisper_detail/view.dart
@@ -1,8 +1,8 @@
import 'dart:async';
import 'dart:io' show File;
-import 'package:PiliPlus/common/widgets/chat_list_view.dart';
import 'package:PiliPlus/common/widgets/dialog/report.dart';
+import 'package:PiliPlus/common/widgets/flutter/chat_list_view.dart';
import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';