mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-08-02 16:50:13 +08:00
refa persistent header & dynamic sliver appbar
Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
@@ -16,6 +16,13 @@ abstract final class StyleString {
|
|||||||
maxWidth: 420,
|
maxWidth: 420,
|
||||||
);
|
);
|
||||||
static const topBarHeight = 52.0;
|
static const topBarHeight = 52.0;
|
||||||
|
static const buttonStyle = ButtonStyle(
|
||||||
|
visualDensity: VisualDensity(
|
||||||
|
horizontal: -2,
|
||||||
|
vertical: -1.25,
|
||||||
|
),
|
||||||
|
tapTargetSize: .shrinkWrap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract final class Constants {
|
abstract final class Constants {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Widget>? 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<Widget>? 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<DynamicSliverAppBar> createState() => _DynamicSliverAppBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
||||||
|
@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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<RenderBox>, 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>((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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<double>? onPerformLayout;
|
|
||||||
final Widget? flexibleSpace;
|
|
||||||
final Widget? leading;
|
|
||||||
final bool automaticallyImplyLeading;
|
|
||||||
final Widget? title;
|
|
||||||
final List<Widget>? 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<Path>? appBarClipper;
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DynamicSliverAppBarMedium> createState() =>
|
|
||||||
_DynamicSliverAppBarMediumState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
153
lib/common/widgets/sliver/sliver_floating_header.dart
Normal file
153
lib/common/widgets/sliver/sliver_floating_header.dart
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart
Normal file
123
lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
118
lib/common/widgets/sliver/sliver_pinned_header.dart
Normal file
118
lib/common/widgets/sliver/sliver_pinned_header.dart
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'dart:math' show pi;
|
import 'dart:math' show pi;
|
||||||
|
|
||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/skeleton/video_reply.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/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/common/widgets/view_safe_area.dart';
|
||||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||||
show ReplyInfo;
|
show ReplyInfo;
|
||||||
@@ -103,44 +104,33 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
|||||||
|
|
||||||
Widget buildReplyHeader(ThemeData theme) {
|
Widget buildReplyHeader(ThemeData theme) {
|
||||||
final secondary = theme.colorScheme.secondary;
|
final secondary = theme.colorScheme.secondary;
|
||||||
return SliverPersistentHeader(
|
return SliverPinnedHeader(
|
||||||
pinned: true,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
child: Padding(
|
||||||
extent: 45,
|
padding: const .fromLTRB(12, 2.5, 6, 2.5),
|
||||||
bgColor: theme.colorScheme.surface,
|
child: Row(
|
||||||
child: Container(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
height: 45,
|
children: [
|
||||||
padding: const EdgeInsets.only(left: 12, right: 6),
|
Obx(
|
||||||
child: Row(
|
() {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
final count = controller.count.value;
|
||||||
children: [
|
return Text(
|
||||||
Obx(
|
'${count == -1 ? 0 : NumUtils.numFormat(count)}条回复',
|
||||||
() {
|
);
|
||||||
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),
|
||||||
SizedBox(
|
label: Obx(
|
||||||
height: 35,
|
() => Text(
|
||||||
child: TextButton.icon(
|
controller.sortType.value.label,
|
||||||
onPressed: controller.queryBySort,
|
style: TextStyle(fontSize: 13, color: secondary),
|
||||||
icon: Icon(
|
|
||||||
Icons.sort,
|
|
||||||
size: 16,
|
|
||||||
color: secondary,
|
|
||||||
),
|
|
||||||
label: Obx(
|
|
||||||
() => Text(
|
|
||||||
controller.sortType.value.label,
|
|
||||||
style: TextStyle(fontSize: 13, color: secondary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:math';
|
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'
|
import 'package:PiliPlus/common/widgets/flutter/draggable_sheet/draggable_scrollable_sheet_topic.dart'
|
||||||
as topic_sheet;
|
as topic_sheet;
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/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/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart';
|
import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart';
|
||||||
import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
|
import 'package:PiliPlus/pages/dynamics_mention/controller.dart';
|
||||||
@@ -247,18 +247,14 @@ class _DynMentionPanelState
|
|||||||
}
|
}
|
||||||
return SliverMainAxisGroup(
|
return SliverMainAxisGroup(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverPersistentHeader(
|
SliverPinnedHeader(
|
||||||
pinned: true,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
child: Padding(
|
||||||
extent: 40,
|
padding: const .symmetric(
|
||||||
needRebuild: true,
|
horizontal: 16,
|
||||||
bgColor: theme.colorScheme.surface,
|
vertical: 10,
|
||||||
child: Container(
|
|
||||||
height: 40,
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Text(group.groupName!),
|
|
||||||
),
|
),
|
||||||
|
child: Text(group.groupName!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverList.builder(
|
SliverList.builder(
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import 'package:PiliPlus/common/widgets/custom_icon.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/dynamic_sliver_app_bar/dynamic_sliver_app_bar.dart';
|
||||||
import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar_medium.dart';
|
|
||||||
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.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/image/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||||
import 'package:PiliPlus/common/widgets/pair.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/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models/common/image_type.dart';
|
import 'package:PiliPlus/models/common/image_type.dart';
|
||||||
import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/item.dart';
|
import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/item.dart';
|
||||||
@@ -78,56 +78,49 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
|
|||||||
Obx(() {
|
Obx(() {
|
||||||
final allSortBy = _controller.topicSortByConf.value?.allSortBy;
|
final allSortBy = _controller.topicSortByConf.value?.allSortBy;
|
||||||
if (allSortBy != null && allSortBy.isNotEmpty) {
|
if (allSortBy != null && allSortBy.isNotEmpty) {
|
||||||
return SliverPersistentHeader(
|
return SliverPinnedHeader(
|
||||||
pinned: true,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
child: Padding(
|
||||||
extent: 36,
|
padding: EdgeInsets.only(
|
||||||
needRebuild: true,
|
left: 12 + padding.left,
|
||||||
bgColor: theme.colorScheme.surface,
|
top: 6,
|
||||||
child: Container(
|
bottom: 6,
|
||||||
height: 36,
|
),
|
||||||
padding: EdgeInsets.only(
|
child: Builder(
|
||||||
left: 12 + padding.left,
|
builder: (context) {
|
||||||
top: 6,
|
return ToggleButtons(
|
||||||
bottom: 6,
|
fillColor: theme.colorScheme.secondaryContainer,
|
||||||
),
|
selectedColor: theme.colorScheme.onSecondaryContainer,
|
||||||
child: Builder(
|
constraints: const BoxConstraints(
|
||||||
builder: (context) {
|
minWidth: 54,
|
||||||
return ToggleButtons(
|
minHeight: 24,
|
||||||
fillColor: theme.colorScheme.secondaryContainer,
|
),
|
||||||
selectedColor:
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
theme.colorScheme.onSecondaryContainer,
|
borderRadius: const .all(.circular(25)),
|
||||||
constraints: const BoxConstraints(
|
onPressed: (index) {
|
||||||
minWidth: 54,
|
_controller.onSort(allSortBy[index].sortBy!);
|
||||||
minHeight: 24,
|
(context as Element).markNeedsBuild();
|
||||||
),
|
},
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
isSelected: allSortBy
|
||||||
borderRadius: const .all(.circular(25)),
|
.map((e) => e.sortBy == _controller.sortBy)
|
||||||
onPressed: (index) {
|
.toList(),
|
||||||
_controller.onSort(allSortBy[index].sortBy!);
|
children: allSortBy.map((e) {
|
||||||
(context as Element).markNeedsBuild();
|
return Text(
|
||||||
},
|
e.sortName!,
|
||||||
isSelected: allSortBy
|
style: const TextStyle(
|
||||||
.map((e) => e.sortBy == _controller.sortBy)
|
fontSize: 13,
|
||||||
.toList(),
|
height: 1,
|
||||||
children: allSortBy.map((e) {
|
),
|
||||||
return Text(
|
strutStyle: const StrutStyle(
|
||||||
e.sortName!,
|
height: 1,
|
||||||
style: const TextStyle(
|
leading: 0,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
height: 1,
|
),
|
||||||
),
|
textScaler: TextScaler.noScaling,
|
||||||
strutStyle: const StrutStyle(
|
);
|
||||||
height: 1,
|
}).toList(),
|
||||||
leading: 0,
|
);
|
||||||
fontSize: 13,
|
},
|
||||||
),
|
|
||||||
textScaler: TextScaler.noScaling,
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -157,10 +150,9 @@ class _DynTopicPageState extends State<DynTopicPage> with DynMixin {
|
|||||||
) {
|
) {
|
||||||
return switch (topState) {
|
return switch (topState) {
|
||||||
Loading() => const SliverAppBar(),
|
Loading() => const SliverAppBar(),
|
||||||
Success(:final response) when response != null => DynamicSliverAppBarMedium(
|
Success(:final response) when response != null => DynamicSliverAppBar.medium(
|
||||||
pinned: true,
|
onPerformLayout: (value) => _controller.appbarOffset =
|
||||||
onPerformLayout: (value) =>
|
value.height - kToolbarHeight - padding.top,
|
||||||
_controller.appbarOffset = value - kToolbarHeight - padding.top,
|
|
||||||
title: IgnorePointer(child: Text(response.topicItem!.name)),
|
title: IgnorePointer(child: Text(response.topicItem!.name)),
|
||||||
flexibleSpace: Container(
|
flexibleSpace: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -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/dialog/dialog.dart';
|
||||||
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.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/loading_widget/loading_widget.dart';
|
||||||
import 'package:PiliPlus/common/widgets/scroll_physics.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/common/live/live_dm_silent_type.dart';
|
||||||
import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart';
|
import 'package:PiliPlus/models_new/live/live_dm_block/shield_user_list.dart';
|
||||||
import 'package:PiliPlus/pages/live_dm_block/controller.dart';
|
import 'package:PiliPlus/pages/live_dm_block/controller.dart';
|
||||||
@@ -102,13 +102,7 @@ class _LiveDmBlockPageState extends State<LiveDmBlockPage> {
|
|||||||
ExtendedNestedScrollView.sliverOverlapAbsorberHandleFor(
|
ExtendedNestedScrollView.sliverOverlapAbsorberHandleFor(
|
||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
sliver: SliverPersistentHeader(
|
sliver: SliverPinnedHeader(child: tabBar),
|
||||||
pinned: true,
|
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
|
||||||
extent: 48,
|
|
||||||
child: tabBar,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/skeleton/video_reply.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/flutter/refresh_indicator.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/common/widgets/view_safe_area.dart';
|
||||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||||
show ReplyInfo;
|
show ReplyInfo;
|
||||||
@@ -15,7 +16,6 @@ import 'package:PiliPlus/utils/num_utils.dart';
|
|||||||
import 'package:PiliPlus/utils/utils.dart';
|
import 'package:PiliPlus/utils/utils.dart';
|
||||||
import 'package:easy_debounce/easy_throttle.dart';
|
import 'package:easy_debounce/easy_throttle.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart' show ScrollDirection;
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
class MainReplyPage extends StatefulWidget {
|
class MainReplyPage extends StatefulWidget {
|
||||||
@@ -61,9 +61,9 @@ class _MainReplyPageState extends State<MainReplyPage> {
|
|||||||
body: NotificationListener<UserScrollNotification>(
|
body: NotificationListener<UserScrollNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
final direction = notification.direction;
|
final direction = notification.direction;
|
||||||
if (direction == ScrollDirection.forward) {
|
if (direction == .forward) {
|
||||||
_controller.showFab();
|
_controller.showFab();
|
||||||
} else if (direction == ScrollDirection.reverse) {
|
} else if (direction == .reverse) {
|
||||||
_controller.hideFab();
|
_controller.hideFab();
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -172,44 +172,33 @@ class _MainReplyPageState extends State<MainReplyPage> {
|
|||||||
|
|
||||||
Widget buildReplyHeader(ColorScheme colorScheme) {
|
Widget buildReplyHeader(ColorScheme colorScheme) {
|
||||||
final secondary = colorScheme.secondary;
|
final secondary = colorScheme.secondary;
|
||||||
return SliverPersistentHeader(
|
return SliverFloatingHeaderWidget(
|
||||||
floating: true,
|
backgroundColor: colorScheme.surface,
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
child: Padding(
|
||||||
extent: 45,
|
padding: const .fromLTRB(12, 2.5, 6, 2.5),
|
||||||
bgColor: colorScheme.surface,
|
child: Row(
|
||||||
child: Container(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
height: 45,
|
children: [
|
||||||
padding: const EdgeInsets.only(left: 12, right: 6),
|
Obx(
|
||||||
child: Row(
|
() {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
final count = _controller.count.value;
|
||||||
children: [
|
return Text(
|
||||||
Obx(
|
'${count == -1 ? 0 : NumUtils.numFormat(count)}条回复',
|
||||||
() {
|
);
|
||||||
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),
|
||||||
SizedBox(
|
label: Obx(
|
||||||
height: 35,
|
() => Text(
|
||||||
child: TextButton.icon(
|
_controller.sortType.value.label,
|
||||||
onPressed: _controller.queryBySort,
|
style: TextStyle(fontSize: 13, color: secondary),
|
||||||
icon: Icon(
|
|
||||||
Icons.sort,
|
|
||||||
size: 16,
|
|
||||||
color: secondary,
|
|
||||||
),
|
|
||||||
label: Obx(
|
|
||||||
() => Text(
|
|
||||||
_controller.sortType.value.label,
|
|
||||||
style: TextStyle(fontSize: 13, color: secondary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:PiliPlus/common/widgets/dialog/report_member.dart';
|
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/loading_widget/loading_widget.dart';
|
||||||
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
@@ -328,8 +328,7 @@ class _MemberPageState extends State<MemberPage> {
|
|||||||
return const CircularProgressIndicator();
|
return const CircularProgressIndicator();
|
||||||
case Success<SpaceData?>(:final response):
|
case Success<SpaceData?>(:final response):
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
return DynamicSliverAppBarMedium(
|
return DynamicSliverAppBar.medium(
|
||||||
pinned: true,
|
|
||||||
actions: _actions(theme),
|
actions: _actions(theme),
|
||||||
title: Text(_userController.username ?? ''),
|
title: Text(_userController.username ?? ''),
|
||||||
flexibleSpace: Obx(
|
flexibleSpace: Obx(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:PiliPlus/common/constants.dart';
|
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/flutter/refresh_indicator.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/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/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models_new/space/space_audio/item.dart';
|
import 'package:PiliPlus/models_new/space/space_audio/item.dart';
|
||||||
import 'package:PiliPlus/pages/member_audio/controller.dart';
|
import 'package:PiliPlus/pages/member_audio/controller.dart';
|
||||||
@@ -80,44 +80,36 @@ class _MemberAudioState extends State<MemberAudio>
|
|||||||
response != null && response.isNotEmpty
|
response != null && response.isNotEmpty
|
||||||
? SliverMainAxisGroup(
|
? SliverMainAxisGroup(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverPersistentHeader(
|
SliverFloatingHeaderWidget(
|
||||||
floating: true,
|
backgroundColor: colorScheme.surface,
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
child: Padding(
|
||||||
extent: 40,
|
padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5),
|
||||||
bgColor: colorScheme.surface,
|
child: Row(
|
||||||
child: SizedBox(
|
children: [
|
||||||
height: 40,
|
Text(
|
||||||
child: Row(
|
'共${_controller.totalSize ?? 0}首',
|
||||||
children: [
|
style: const TextStyle(fontSize: 13),
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 6),
|
padding: const EdgeInsets.only(left: 6),
|
||||||
child: Text(
|
child: TextButton.icon(
|
||||||
'共${_controller.totalSize ?? 0}首',
|
style: StyleString.buttonStyle,
|
||||||
style: const TextStyle(fontSize: 13),
|
onPressed: _controller.toViewPlayAll,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.play_circle_outline_rounded,
|
||||||
|
size: 16,
|
||||||
|
color: colorScheme.secondary,
|
||||||
),
|
),
|
||||||
),
|
label: Text(
|
||||||
Container(
|
'播放全部',
|
||||||
height: 35,
|
style: TextStyle(
|
||||||
padding: const EdgeInsets.only(left: 6),
|
fontSize: 13,
|
||||||
child: TextButton.icon(
|
|
||||||
onPressed: _controller.toViewPlayAll,
|
|
||||||
icon: Icon(
|
|
||||||
Icons.play_circle_outline_rounded,
|
|
||||||
size: 16,
|
|
||||||
color: colorScheme.secondary,
|
color: colorScheme.secondary,
|
||||||
),
|
),
|
||||||
label: Text(
|
|
||||||
'播放全部',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: colorScheme.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -97,13 +97,14 @@ class _MemberContributeState extends State<MemberContribute>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _getPageFromType(SpaceTab2Item item) {
|
Widget _getPageFromType(SpaceTab2Item item) {
|
||||||
|
final isSingle = _controller.tabs == null;
|
||||||
return switch (item.param) {
|
return switch (item.param) {
|
||||||
'video' => MemberVideo(
|
'video' => MemberVideo(
|
||||||
type: ContributeType.video,
|
type: ContributeType.video,
|
||||||
heroTag: widget.heroTag,
|
heroTag: widget.heroTag,
|
||||||
mid: widget.mid,
|
mid: widget.mid,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
isSingle: _controller.tabs == null,
|
isSingle: isSingle,
|
||||||
),
|
),
|
||||||
'charging_video' => MemberVideo(
|
'charging_video' => MemberVideo(
|
||||||
type: ContributeType.charging,
|
type: ContributeType.charging,
|
||||||
@@ -116,9 +117,9 @@ class _MemberContributeState extends State<MemberContribute>
|
|||||||
mid: widget.mid,
|
mid: widget.mid,
|
||||||
),
|
),
|
||||||
'opus' => MemberOpus(
|
'opus' => MemberOpus(
|
||||||
isSingle: _controller.tabs == null,
|
|
||||||
heroTag: widget.heroTag,
|
heroTag: widget.heroTag,
|
||||||
mid: widget.mid,
|
mid: widget.mid,
|
||||||
|
isSingle: isSingle,
|
||||||
),
|
),
|
||||||
'audio' => MemberAudio(
|
'audio' => MemberAudio(
|
||||||
heroTag: widget.heroTag,
|
heroTag: widget.heroTag,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
|
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/flutter/refresh_indicator.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models_new/space/space_fav/data.dart';
|
import 'package:PiliPlus/models_new/space/space_fav/data.dart';
|
||||||
import 'package:PiliPlus/pages/member_favorite/controller.dart';
|
import 'package:PiliPlus/pages/member_favorite/controller.dart';
|
||||||
@@ -109,56 +109,51 @@ class _MemberFavoriteState extends State<MemberFavorite>
|
|||||||
}) {
|
}) {
|
||||||
return SliverMainAxisGroup(
|
return SliverMainAxisGroup(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverPersistentHeader(
|
SliverPinnedHeader(
|
||||||
pinned: true,
|
child: Material(
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
color: theme.colorScheme.surface,
|
||||||
child: Material(
|
child: Builder(
|
||||||
color: theme.colorScheme.surface,
|
builder: (context) {
|
||||||
child: Builder(
|
return InkWell(
|
||||||
builder: (context) {
|
onTap: () {
|
||||||
return InkWell(
|
_controller.setExpand(isFav);
|
||||||
onTap: () {
|
(context as Element).markNeedsBuild();
|
||||||
_controller.setExpand(isFav);
|
data.refresh();
|
||||||
(context as Element).markNeedsBuild();
|
if (!isEnd.value) {
|
||||||
data.refresh();
|
isEnd.refresh();
|
||||||
if (!isEnd.value) {
|
}
|
||||||
isEnd.refresh();
|
},
|
||||||
}
|
child: Padding(
|
||||||
},
|
padding: const .symmetric(horizontal: 12, vertical: 10),
|
||||||
child: Container(
|
child: Text.rich(
|
||||||
height: 45,
|
TextSpan(
|
||||||
alignment: .centerLeft,
|
children: [
|
||||||
padding: const .only(left: 12),
|
WidgetSpan(
|
||||||
child: Text.rich(
|
alignment: .middle,
|
||||||
TextSpan(
|
child: Icon(
|
||||||
children: [
|
_controller.isExpand(isFav)
|
||||||
WidgetSpan(
|
? Icons.expand_less
|
||||||
alignment: .middle,
|
: Icons.expand_more,
|
||||||
child: Icon(
|
color: theme.colorScheme.outline,
|
||||||
_controller.isExpand(isFav)
|
|
||||||
? Icons.expand_less
|
|
||||||
: Icons.expand_more,
|
|
||||||
color: theme.colorScheme.outline,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
TextSpan(
|
),
|
||||||
text: ' ${data.value.name}',
|
TextSpan(
|
||||||
style: const TextStyle(fontSize: 14),
|
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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/flutter/refresh_indicator.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||||
import 'package:PiliPlus/common/widgets/scroll_physics.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/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models/common/member/contribute_type.dart';
|
import 'package:PiliPlus/models/common/member/contribute_type.dart';
|
||||||
import 'package:PiliPlus/models_new/space/space_archive/item.dart';
|
import 'package:PiliPlus/models_new/space/space_archive/item.dart';
|
||||||
@@ -173,91 +174,78 @@ class _MemberVideoState extends State<MemberVideo>
|
|||||||
response != null && response.isNotEmpty
|
response != null && response.isNotEmpty
|
||||||
? SliverMainAxisGroup(
|
? SliverMainAxisGroup(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverPersistentHeader(
|
SliverFloatingHeaderWidget(
|
||||||
pinned: false,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
floating: true,
|
child: Padding(
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
padding: const EdgeInsets.fromLTRB(14, 2.5, 8, 2.5),
|
||||||
extent: 40,
|
child: Row(
|
||||||
bgColor: theme.colorScheme.surface,
|
children: [
|
||||||
child: SizedBox(
|
Obx(
|
||||||
height: 40,
|
() {
|
||||||
child: Row(
|
final count = _controller.count.value;
|
||||||
children: [
|
return Text(
|
||||||
const SizedBox(width: 8),
|
count != -1 ? '共$count视频' : '',
|
||||||
Padding(
|
style: const TextStyle(fontSize: 13),
|
||||||
padding: const EdgeInsets.only(left: 6),
|
);
|
||||||
child: Obx(
|
},
|
||||||
() {
|
),
|
||||||
final count = _controller.count.value;
|
Obx(
|
||||||
return Text(
|
() {
|
||||||
count != -1 ? '共$count视频' : '',
|
final episodicButton =
|
||||||
style: const TextStyle(fontSize: 13),
|
_controller.episodicButton.value;
|
||||||
);
|
return episodicButton.uri?.isNotEmpty == true
|
||||||
},
|
? Padding(
|
||||||
),
|
padding: EdgeInsets.only(
|
||||||
),
|
left: _controller.count.value != -1
|
||||||
Obx(
|
? 6
|
||||||
() {
|
: 0,
|
||||||
final episodicButton =
|
),
|
||||||
_controller.episodicButton.value;
|
child: TextButton.icon(
|
||||||
return episodicButton.uri?.isNotEmpty == true
|
style: StyleString.buttonStyle,
|
||||||
? Container(
|
onPressed: _controller.toViewPlayAll,
|
||||||
height: 35,
|
icon: Icon(
|
||||||
padding: EdgeInsets.only(
|
Icons.play_circle_outline_rounded,
|
||||||
left: _controller.count.value != -1
|
size: 16,
|
||||||
? 6
|
color: theme.colorScheme.secondary,
|
||||||
: 0,
|
|
||||||
),
|
),
|
||||||
child: TextButton.icon(
|
label: Text(
|
||||||
onPressed: _controller.toViewPlayAll,
|
episodicButton.text ?? '播放全部',
|
||||||
icon: Icon(
|
style: TextStyle(
|
||||||
Icons.play_circle_outline_rounded,
|
fontSize: 13,
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.secondary,
|
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(),
|
label: Obx(
|
||||||
SizedBox(
|
() => Text(
|
||||||
height: 35,
|
widget.type == ContributeType.video
|
||||||
child: TextButton.icon(
|
? _controller.order.value == 'pubdate'
|
||||||
onPressed: _controller.queryBySort,
|
? '最新发布'
|
||||||
icon: Icon(
|
: '最多播放'
|
||||||
Icons.sort,
|
: _controller.sort.value == 'desc'
|
||||||
size: 16,
|
? '默认'
|
||||||
|
: '倒序',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
color: theme.colorScheme.secondary,
|
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),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/skeleton/video_reply.dart';
|
import 'package:PiliPlus/common/skeleton/video_reply.dart';
|
||||||
import 'package:PiliPlus/common/widgets/custom_icon.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/dialog/dialog.dart';
|
||||||
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.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/flutter/selectable_text/selectable_text.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.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/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/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/models/common/image_type.dart';
|
import 'package:PiliPlus/models/common/image_type.dart';
|
||||||
import 'package:PiliPlus/models/common/pgc_review_type.dart';
|
import 'package:PiliPlus/models/common/pgc_review_type.dart';
|
||||||
@@ -383,51 +384,43 @@ class _PgcReviewChildPageState extends State<PgcReviewChildPage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(ThemeData theme) => SliverPersistentHeader(
|
Widget _buildHeader(ThemeData theme) => SliverFloatingHeaderWidget(
|
||||||
pinned: false,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
floating: true,
|
child: Padding(
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
padding: const EdgeInsets.fromLTRB(12, 2.5, 6, 2.5),
|
||||||
extent: 40,
|
child: Row(
|
||||||
bgColor: theme.colorScheme.surface,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
child: Container(
|
children: [
|
||||||
height: 40,
|
Obx(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
|
() {
|
||||||
child: Row(
|
final count = _controller.count.value;
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
return count == null
|
||||||
children: [
|
? const SizedBox.shrink()
|
||||||
Obx(
|
: Text(
|
||||||
() {
|
'${NumUtils.numFormat(count)}条点评',
|
||||||
final count = _controller.count.value;
|
style: const TextStyle(fontSize: 13),
|
||||||
return count == null
|
);
|
||||||
? const SizedBox.shrink()
|
},
|
||||||
: Text(
|
),
|
||||||
'${NumUtils.numFormat(count)}条点评',
|
TextButton.icon(
|
||||||
style: const TextStyle(fontSize: 13),
|
style: StyleString.buttonStyle,
|
||||||
);
|
onPressed: _controller.queryBySort,
|
||||||
},
|
icon: Icon(
|
||||||
|
Icons.sort,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.secondary,
|
||||||
),
|
),
|
||||||
SizedBox(
|
label: Obx(
|
||||||
height: 35,
|
() => Text(
|
||||||
child: TextButton.icon(
|
_controller.sortType.value.label,
|
||||||
onPressed: _controller.queryBySort,
|
style: TextStyle(
|
||||||
icon: Icon(
|
fontSize: 13,
|
||||||
Icons.sort,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.secondary,
|
color: theme.colorScheme.secondary,
|
||||||
),
|
),
|
||||||
label: Obx(
|
|
||||||
() => Text(
|
|
||||||
_controller.sortType.value.label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: theme.colorScheme.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'dart:math';
|
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/flutter/refresh_indicator.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
|
||||||
import 'package:PiliPlus/common/widgets/scroll_physics.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/video_card/video_card_h.dart';
|
||||||
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
|
import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart';
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
@@ -201,17 +201,11 @@ class _PopularSeriesPageState extends State<PopularSeriesPage> with GridMixin {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final height = MediaQuery.textScalerOf(context).scale(27);
|
return SliverFloatingHeaderWidget(
|
||||||
return SliverPersistentHeader(
|
backgroundColor: colorScheme.surface,
|
||||||
floating: true,
|
child: Padding(
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
padding: const .only(left: 14, bottom: 7),
|
||||||
extent: height,
|
child: child,
|
||||||
child: Container(
|
|
||||||
height: height,
|
|
||||||
padding: const EdgeInsets.only(left: 14, bottom: 7),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
bgColor: colorScheme.surface,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/models/search/result.dart';
|
||||||
import 'package:PiliPlus/pages/search_panel/article/controller.dart';
|
import 'package:PiliPlus/pages/search_panel/article/controller.dart';
|
||||||
import 'package:PiliPlus/pages/search_panel/article/widgets/item.dart';
|
import 'package:PiliPlus/pages/search_panel/article/widgets/item.dart';
|
||||||
@@ -45,51 +45,45 @@ class _SearchArticlePanelState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildHeader(ThemeData theme) {
|
Widget buildHeader(ThemeData theme) {
|
||||||
return SliverPersistentHeader(
|
return SliverFloatingHeaderWidget(
|
||||||
pinned: false,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
floating: true,
|
child: Padding(
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
padding: const .fromLTRB(25, 0, 12, 4),
|
||||||
extent: 40,
|
child: Row(
|
||||||
bgColor: theme.colorScheme.surface,
|
children: [
|
||||||
child: Container(
|
Obx(
|
||||||
height: 40,
|
() => Text(
|
||||||
padding: const EdgeInsets.only(left: 25, right: 12),
|
'排序: ${controller.articleOrderType.value.label}',
|
||||||
child: Row(
|
maxLines: 1,
|
||||||
children: [
|
style: TextStyle(color: theme.colorScheme.outline),
|
||||||
Obx(
|
),
|
||||||
() => Text(
|
),
|
||||||
'排序: ${controller.articleOrderType.value.label}',
|
const Spacer(),
|
||||||
maxLines: 1,
|
Obx(
|
||||||
style: TextStyle(color: theme.colorScheme.outline),
|
() => 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:PiliPlus/common/skeleton/msg_feed_top.dart';
|
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/models/search/result.dart';
|
||||||
import 'package:PiliPlus/pages/search_panel/user/controller.dart';
|
import 'package:PiliPlus/pages/search_panel/user/controller.dart';
|
||||||
import 'package:PiliPlus/pages/search_panel/user/widgets/item.dart';
|
import 'package:PiliPlus/pages/search_panel/user/widgets/item.dart';
|
||||||
@@ -46,51 +46,45 @@ class _SearchUserPanelState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildHeader(ThemeData theme) {
|
Widget buildHeader(ThemeData theme) {
|
||||||
return SliverPersistentHeader(
|
return SliverFloatingHeaderWidget(
|
||||||
pinned: false,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
floating: true,
|
child: Padding(
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
padding: const .fromLTRB(25, 0, 12, 4),
|
||||||
extent: 40,
|
child: Row(
|
||||||
bgColor: theme.colorScheme.surface,
|
children: [
|
||||||
child: Container(
|
Obx(
|
||||||
height: 40,
|
() => Text(
|
||||||
padding: const EdgeInsets.only(left: 25, right: 12),
|
'排序: ${controller.userOrderType!.value.label}',
|
||||||
child: Row(
|
maxLines: 1,
|
||||||
children: [
|
style: TextStyle(color: theme.colorScheme.outline),
|
||||||
Obx(
|
),
|
||||||
() => Text(
|
),
|
||||||
'排序: ${controller.userOrderType!.value.label}',
|
const Spacer(),
|
||||||
maxLines: 1,
|
Obx(
|
||||||
style: TextStyle(color: theme.colorScheme.outline),
|
() => 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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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/common/widgets/video_card/video_card_h.dart';
|
||||||
import 'package:PiliPlus/models/common/search/video_search_type.dart';
|
import 'package:PiliPlus/models/common/search/video_search_type.dart';
|
||||||
import 'package:PiliPlus/models/search/result.dart';
|
import 'package:PiliPlus/models/search/result.dart';
|
||||||
@@ -47,61 +47,55 @@ class _SearchVideoPanelState
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildHeader(ThemeData theme) {
|
Widget buildHeader(ThemeData theme) {
|
||||||
return SliverPersistentHeader(
|
return SliverFloatingHeaderWidget(
|
||||||
pinned: false,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
floating: true,
|
child: Padding(
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
padding: const .fromLTRB(12, 0, 12, 4),
|
||||||
extent: 34,
|
child: Row(
|
||||||
bgColor: theme.colorScheme.surface,
|
children: [
|
||||||
child: Container(
|
Expanded(
|
||||||
height: 34,
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(
|
child: Wrap(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
for (final e in ArchiveFilterType.values)
|
||||||
child: SingleChildScrollView(
|
Obx(
|
||||||
scrollDirection: Axis.horizontal,
|
() => SearchText(
|
||||||
child: Wrap(
|
fontSize: 13,
|
||||||
children: [
|
text: e.desc,
|
||||||
for (final e in ArchiveFilterType.values)
|
bgColor: Colors.transparent,
|
||||||
Obx(
|
textColor: controller.selectedType.value == e
|
||||||
() => SearchText(
|
? theme.colorScheme.primary
|
||||||
fontSize: 13,
|
: theme.colorScheme.outline,
|
||||||
text: e.desc,
|
onTap: (_) => controller
|
||||||
bgColor: Colors.transparent,
|
..order = e.name
|
||||||
textColor: controller.selectedType.value == e
|
..selectedType.value = e
|
||||||
? theme.colorScheme.primary
|
..onSortSearch(getBack: false),
|
||||||
: theme.colorScheme.outline,
|
|
||||||
onTap: (_) => controller
|
|
||||||
..order = e.name
|
|
||||||
..selectedType.value = e
|
|
||||||
..onSortSearch(getBack: false),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const VerticalDivider(indent: 7, endIndent: 8),
|
),
|
||||||
const SizedBox(width: 3),
|
const VerticalDivider(indent: 7, endIndent: 8),
|
||||||
SizedBox(
|
const SizedBox(width: 3),
|
||||||
width: 32,
|
SizedBox(
|
||||||
height: 32,
|
width: 32,
|
||||||
child: IconButton(
|
height: 32,
|
||||||
tooltip: '筛选',
|
child: IconButton(
|
||||||
style: const ButtonStyle(
|
tooltip: '筛选',
|
||||||
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
style: const ButtonStyle(
|
||||||
),
|
padding: WidgetStatePropertyAll(EdgeInsets.zero),
|
||||||
onPressed: () => controller.onShowFilterDialog(context),
|
),
|
||||||
icon: Icon(
|
onPressed: () => controller.onShowFilterDialog(context),
|
||||||
Icons.filter_list_outlined,
|
icon: Icon(
|
||||||
size: 18,
|
Icons.filter_list_outlined,
|
||||||
color: theme.colorScheme.primary,
|
size: 18,
|
||||||
),
|
color: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
|
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
|
||||||
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.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/image/network_img_layer.dart';
|
||||||
@@ -136,24 +137,22 @@ class _HorizontalMemberPageState extends State<HorizontalMemberPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SizedBox(
|
TextButton.icon(
|
||||||
height: 35,
|
style: StyleString.buttonStyle,
|
||||||
child: TextButton.icon(
|
onPressed: () => _controller
|
||||||
onPressed: () => _controller
|
..lastAid = widget.videoDetailController.aid.toString()
|
||||||
..lastAid = widget.videoDetailController.aid.toString()
|
..queryBySort(),
|
||||||
..queryBySort(),
|
icon: Icon(
|
||||||
icon: Icon(
|
Icons.sort,
|
||||||
Icons.sort,
|
size: 16,
|
||||||
size: 16,
|
color: theme.colorScheme.secondary,
|
||||||
color: theme.colorScheme.secondary,
|
),
|
||||||
),
|
label: Obx(
|
||||||
label: Obx(
|
() => Text(
|
||||||
() => Text(
|
_controller.order.value == 'pubdate' ? '最新发布' : '最多播放',
|
||||||
_controller.order.value == 'pubdate' ? '最新发布' : '最多播放',
|
style: TextStyle(
|
||||||
style: TextStyle(
|
fontSize: 13,
|
||||||
fontSize: 13,
|
color: theme.colorScheme.secondary,
|
||||||
color: theme.colorScheme.secondary,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/skeleton/video_reply.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/flutter/refresh_indicator.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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'
|
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||||
show ReplyInfo;
|
show ReplyInfo;
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
@@ -83,47 +84,39 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
|||||||
: _videoReplyController.scrollController,
|
: _videoReplyController.scrollController,
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
key: const PageStorageKey(_VideoReplyPanelState),
|
key: const PageStorageKey(_VideoReplyPanelState),
|
||||||
slivers: <Widget>[
|
slivers: [
|
||||||
SliverPersistentHeader(
|
SliverFloatingHeaderWidget(
|
||||||
pinned: false,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
floating: true,
|
child: Padding(
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
padding: const .fromLTRB(12, 2.5, 6, 2.5),
|
||||||
extent: 40,
|
child: Row(
|
||||||
bgColor: theme.colorScheme.surface,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
child: Container(
|
children: [
|
||||||
height: 40,
|
Obx(
|
||||||
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
|
() => Text(
|
||||||
child: Row(
|
_videoReplyController.sortType.value.title,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
style: const TextStyle(fontSize: 13),
|
||||||
children: [
|
|
||||||
Obx(
|
|
||||||
() => Text(
|
|
||||||
_videoReplyController.sortType.value.title,
|
|
||||||
style: const TextStyle(fontSize: 13),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
SizedBox(
|
),
|
||||||
height: 35,
|
TextButton.icon(
|
||||||
child: TextButton.icon(
|
style: StyleString.buttonStyle,
|
||||||
onPressed: _videoReplyController.queryBySort,
|
onPressed: _videoReplyController.queryBySort,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.sort,
|
Icons.sort,
|
||||||
size: 16,
|
size: 16,
|
||||||
|
color: theme.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
label: Obx(
|
||||||
|
() => Text(
|
||||||
|
_videoReplyController.sortType.value.label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
color: theme.colorScheme.secondary,
|
color: theme.colorScheme.secondary,
|
||||||
),
|
),
|
||||||
label: Obx(
|
|
||||||
() => Text(
|
|
||||||
_videoReplyController.sortType.value.label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: theme.colorScheme.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import 'package:PiliPlus/common/constants.dart';
|
||||||
import 'package:PiliPlus/common/skeleton/video_reply.dart';
|
import 'package:PiliPlus/common/skeleton/video_reply.dart';
|
||||||
import 'package:PiliPlus/common/widgets/colored_box_transition.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/flutter/refresh_indicator.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/http_error.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/common/widgets/view_safe_area.dart';
|
||||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||||
show ReplyInfo, Mode;
|
show ReplyInfo, Mode;
|
||||||
@@ -240,52 +241,43 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _sortWidget(ThemeData theme) {
|
Widget _sortWidget(ThemeData theme) {
|
||||||
return SliverPersistentHeader(
|
return SliverPinnedHeader(
|
||||||
pinned: true,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
delegate: CustomSliverPersistentHeaderDelegate(
|
child: Padding(
|
||||||
extent: 40,
|
padding: const EdgeInsets.fromLTRB(12, 2.5, 6, 2.5),
|
||||||
bgColor: theme.colorScheme.surface,
|
child: Row(
|
||||||
child: Container(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
height: 40,
|
children: [
|
||||||
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
|
Obx(
|
||||||
child: Row(
|
() {
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
final count = _controller.count.value;
|
||||||
children: [
|
return count != -1
|
||||||
Obx(
|
? Text(
|
||||||
() {
|
'相关回复共${NumUtils.numFormat(count)}条',
|
||||||
final count = _controller.count.value;
|
style: const TextStyle(fontSize: 13),
|
||||||
return count != -1
|
)
|
||||||
? Text(
|
: const SizedBox.shrink();
|
||||||
'相关回复共${NumUtils.numFormat(count)}条',
|
},
|
||||||
style: const TextStyle(fontSize: 13),
|
),
|
||||||
)
|
TextButton.icon(
|
||||||
: const SizedBox.shrink();
|
style: StyleString.buttonStyle,
|
||||||
},
|
onPressed: _controller.queryBySort,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.sort,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.secondary,
|
||||||
),
|
),
|
||||||
SizedBox(
|
label: Obx(
|
||||||
height: 35,
|
() => Text(
|
||||||
child: TextButton.icon(
|
_controller.mode.value == Mode.MAIN_LIST_HOT ? '按热度' : '按时间',
|
||||||
onPressed: _controller.queryBySort,
|
style: TextStyle(
|
||||||
icon: Icon(
|
fontSize: 13,
|
||||||
Icons.sort,
|
|
||||||
size: 16,
|
|
||||||
color: theme.colorScheme.secondary,
|
color: theme.colorScheme.secondary,
|
||||||
),
|
),
|
||||||
label: Obx(
|
|
||||||
() => Text(
|
|
||||||
_controller.mode.value == Mode.MAIN_LIST_HOT
|
|
||||||
? '按热度'
|
|
||||||
: '按时间',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: theme.colorScheme.secondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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/image_viewer/hero_dialog_route.dart';
|
||||||
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
|
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
|
||||||
import 'package:PiliPlus/common/widgets/scroll_physics.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/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/main.dart';
|
import 'package:PiliPlus/main.dart';
|
||||||
import 'package:PiliPlus/models/common/episode_panel_type.dart';
|
import 'package:PiliPlus/models/common/episode_panel_type.dart';
|
||||||
@@ -648,14 +649,10 @@ class _VideoDetailPageVState extends State<VideoDetailPageV>
|
|||||||
? animHeight
|
? animHeight
|
||||||
: videoDetailController.videoHeight;
|
: videoDetailController.videoHeight;
|
||||||
return [
|
return [
|
||||||
SliverAppBar(
|
SliverPinnedDynamicHeader(
|
||||||
elevation: 0,
|
minExtent: kToolbarHeight,
|
||||||
scrolledUnderElevation: 0,
|
maxExtent: height,
|
||||||
primary: false,
|
child: Stack(
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
pinned: true,
|
|
||||||
expandedHeight: height,
|
|
||||||
flexibleSpace: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io' show File;
|
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/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/flutter/text_field/text_field.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
|
||||||
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user