mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-01 00:28:18 +08:00
show member collection top
Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
@@ -1,15 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart' show RenderProxyBox;
|
import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult;
|
||||||
|
|
||||||
class CustomHeightWidget extends SingleChildRenderObjectWidget {
|
class CustomHeightWidget extends SingleChildRenderObjectWidget {
|
||||||
const CustomHeightWidget({
|
const CustomHeightWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.height,
|
this.height,
|
||||||
this.offset = .zero,
|
this.offset = .zero,
|
||||||
required super.child,
|
required Widget super.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
final double height;
|
final double? height;
|
||||||
|
|
||||||
final Offset offset;
|
final Offset offset;
|
||||||
|
|
||||||
@@ -34,14 +34,14 @@ class CustomHeightWidget extends SingleChildRenderObjectWidget {
|
|||||||
|
|
||||||
class RenderCustomHeightWidget extends RenderProxyBox {
|
class RenderCustomHeightWidget extends RenderProxyBox {
|
||||||
RenderCustomHeightWidget({
|
RenderCustomHeightWidget({
|
||||||
required double height,
|
double? height,
|
||||||
required Offset offset,
|
required Offset offset,
|
||||||
}) : _height = height,
|
}) : _height = height,
|
||||||
_offset = offset;
|
_offset = offset;
|
||||||
|
|
||||||
double _height;
|
double? _height;
|
||||||
double get height => _height;
|
double? get height => _height;
|
||||||
set height(double value) {
|
set height(double? value) {
|
||||||
if (_height == value) return;
|
if (_height == value) return;
|
||||||
_height = value;
|
_height = value;
|
||||||
markNeedsLayout();
|
markNeedsLayout();
|
||||||
@@ -57,12 +57,40 @@ class RenderCustomHeightWidget extends RenderProxyBox {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
child!.layout(constraints);
|
if (height != null) {
|
||||||
size = constraints.constrainDimensions(constraints.maxWidth, height);
|
child!.layout(constraints.copyWith(maxHeight: .infinity));
|
||||||
|
size = constraints.constrainDimensions(constraints.maxWidth, height!);
|
||||||
|
} else {
|
||||||
|
child!.layout(
|
||||||
|
constraints.copyWith(maxHeight: .infinity),
|
||||||
|
parentUsesSize: true,
|
||||||
|
);
|
||||||
|
size = constraints.constrainDimensions(
|
||||||
|
constraints.maxWidth,
|
||||||
|
child!.size.height,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void paint(PaintingContext context, Offset offset) {
|
void paint(PaintingContext context, Offset offset) {
|
||||||
context.paintChild(child!, offset + _offset);
|
context.paintChild(child!, offset + _offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||||
|
return result.addWithPaintOffset(
|
||||||
|
offset: _offset,
|
||||||
|
position: position,
|
||||||
|
hitTest: (BoxHitTestResult result, Offset transformed) {
|
||||||
|
assert(transformed == position - _offset);
|
||||||
|
return child!.hitTest(result, position: transformed);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
|
||||||
|
transform.translateByDouble(_offset.dx, _offset.dy, 0.0, 1.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:PiliPlus/common/widgets/custom_height_widget.dart';
|
||||||
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/rendering/sliver_persistent_header.dart';
|
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/rendering/sliver_persistent_header.dart';
|
||||||
import 'package:PiliPlus/common/widgets/dynamic_sliver_app_bar/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'
|
import 'package:PiliPlus/common/widgets/only_layout_widget.dart'
|
||||||
@@ -24,6 +25,7 @@ import 'package:PiliPlus/common/widgets/only_layout_widget.dart'
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart'
|
import 'package:flutter/material.dart'
|
||||||
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
|
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
|
||||||
|
import 'package:flutter/rendering.dart' show RenderOpacity, OpacityLayer;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
/// ref [SliverAppBar]
|
/// ref [SliverAppBar]
|
||||||
@@ -133,11 +135,9 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
title: effectiveTitle,
|
title: effectiveTitle,
|
||||||
actions: actions,
|
actions: actions,
|
||||||
automaticallyImplyActions: automaticallyImplyActions,
|
automaticallyImplyActions: automaticallyImplyActions,
|
||||||
flexibleSpace: maxExtent == .infinity
|
flexibleSpace: IgnorePointer(
|
||||||
? flexibleSpace
|
|
||||||
: IgnorePointer(
|
|
||||||
ignoring: isScrolledUnder,
|
ignoring: isScrolledUnder,
|
||||||
child: FlexibleSpaceBar(background: flexibleSpace),
|
child: DynamicFlexibleSpaceBar(background: flexibleSpace),
|
||||||
),
|
),
|
||||||
bottom: bottom,
|
bottom: bottom,
|
||||||
elevation: isScrolledUnder ? elevation : 0.0,
|
elevation: isScrolledUnder ? elevation : 0.0,
|
||||||
@@ -350,3 +350,148 @@ class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ref [FlexibleSpaceBar]
|
||||||
|
class DynamicFlexibleSpaceBar extends StatefulWidget {
|
||||||
|
const DynamicFlexibleSpaceBar({
|
||||||
|
super.key,
|
||||||
|
required this.background,
|
||||||
|
this.collapseMode = CollapseMode.parallax,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget background;
|
||||||
|
|
||||||
|
final CollapseMode collapseMode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DynamicFlexibleSpaceBar> createState() =>
|
||||||
|
_DynamicFlexibleSpaceBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DynamicFlexibleSpaceBarState extends State<DynamicFlexibleSpaceBar> {
|
||||||
|
double _getCollapsePadding(double t, FlexibleSpaceBarSettings settings) {
|
||||||
|
switch (widget.collapseMode) {
|
||||||
|
case CollapseMode.pin:
|
||||||
|
return -(settings.maxExtent - settings.currentExtent);
|
||||||
|
case CollapseMode.none:
|
||||||
|
return 0.0;
|
||||||
|
case CollapseMode.parallax:
|
||||||
|
final double deltaExtent = settings.maxExtent - settings.minExtent;
|
||||||
|
return -Tween<double>(begin: 0.0, end: deltaExtent / 4.0).transform(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final FlexibleSpaceBarSettings settings = context
|
||||||
|
.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
|
||||||
|
|
||||||
|
double? height;
|
||||||
|
final double opacity;
|
||||||
|
final double topPadding;
|
||||||
|
if (settings.maxExtent == .infinity) {
|
||||||
|
opacity = 1.0;
|
||||||
|
topPadding = 0.0;
|
||||||
|
} else {
|
||||||
|
final double deltaExtent = settings.maxExtent - settings.minExtent;
|
||||||
|
|
||||||
|
// 0.0 -> Expanded
|
||||||
|
// 1.0 -> Collapsed to toolbar
|
||||||
|
final double t = clampDouble(
|
||||||
|
1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent,
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
final double fadeStart = math.max(
|
||||||
|
0.0,
|
||||||
|
1.0 - kToolbarHeight / deltaExtent,
|
||||||
|
);
|
||||||
|
const fadeEnd = 1.0;
|
||||||
|
assert(fadeStart <= fadeEnd);
|
||||||
|
// If the min and max extent are the same, the app bar cannot collapse
|
||||||
|
// and the content should be visible, so opacity = 1.
|
||||||
|
opacity = settings.maxExtent == settings.minExtent
|
||||||
|
? 1.0
|
||||||
|
: 1.0 - Interval(fadeStart, fadeEnd).transform(t);
|
||||||
|
|
||||||
|
topPadding = _getCollapsePadding(t, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClipRect(
|
||||||
|
child: CustomHeightWidget(
|
||||||
|
height: height,
|
||||||
|
offset: Offset(0.0, topPadding),
|
||||||
|
child: _FlexibleSpaceHeaderOpacity(
|
||||||
|
// IOS is relying on this semantics node to correctly traverse
|
||||||
|
// through the app bar when it is collapsed.
|
||||||
|
alwaysIncludeSemantics: true,
|
||||||
|
opacity: opacity,
|
||||||
|
child: widget.background,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [_FlexibleSpaceHeaderOpacity]
|
||||||
|
class _FlexibleSpaceHeaderOpacity extends SingleChildRenderObjectWidget {
|
||||||
|
const _FlexibleSpaceHeaderOpacity({
|
||||||
|
required this.opacity,
|
||||||
|
required super.child,
|
||||||
|
required this.alwaysIncludeSemantics,
|
||||||
|
});
|
||||||
|
|
||||||
|
final double opacity;
|
||||||
|
final bool alwaysIncludeSemantics;
|
||||||
|
|
||||||
|
@override
|
||||||
|
RenderObject createRenderObject(BuildContext context) {
|
||||||
|
return _RenderFlexibleSpaceHeaderOpacity(
|
||||||
|
opacity: opacity,
|
||||||
|
alwaysIncludeSemantics: alwaysIncludeSemantics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(
|
||||||
|
BuildContext context,
|
||||||
|
covariant _RenderFlexibleSpaceHeaderOpacity renderObject,
|
||||||
|
) {
|
||||||
|
renderObject
|
||||||
|
..alwaysIncludeSemantics = alwaysIncludeSemantics
|
||||||
|
..opacity = opacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RenderFlexibleSpaceHeaderOpacity extends RenderOpacity {
|
||||||
|
_RenderFlexibleSpaceHeaderOpacity({
|
||||||
|
super.opacity,
|
||||||
|
super.alwaysIncludeSemantics,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isRepaintBoundary => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(PaintingContext context, Offset offset) {
|
||||||
|
if (child == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((opacity * 255).roundToDouble() <= 0) {
|
||||||
|
layer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert(needsCompositing);
|
||||||
|
layer = context.pushOpacity(
|
||||||
|
offset,
|
||||||
|
(opacity * 255).round(),
|
||||||
|
super.paint,
|
||||||
|
oldLayer: layer as OpacityLayer?,
|
||||||
|
);
|
||||||
|
assert(() {
|
||||||
|
layer!.debugCreator = debugCreator;
|
||||||
|
return true;
|
||||||
|
}());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class GalleryViewer extends StatefulWidget {
|
|||||||
required this.quality,
|
required this.quality,
|
||||||
required this.sources,
|
required this.sources,
|
||||||
this.initIndex = 0,
|
this.initIndex = 0,
|
||||||
|
this.onPageChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
final double minScale;
|
final double minScale;
|
||||||
@@ -61,6 +62,7 @@ class GalleryViewer extends StatefulWidget {
|
|||||||
final int quality;
|
final int quality;
|
||||||
final List<SourceModel> sources;
|
final List<SourceModel> sources;
|
||||||
final int initIndex;
|
final int initIndex;
|
||||||
|
final ValueChanged<int>? onPageChanged;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GalleryViewer> createState() => _GalleryViewerState();
|
State<GalleryViewer> createState() => _GalleryViewerState();
|
||||||
@@ -346,6 +348,7 @@ class _GalleryViewerState extends State<GalleryViewer>
|
|||||||
_player?.pause();
|
_player?.pause();
|
||||||
_playIfNeeded(widget.sources[index]);
|
_playIfNeeded(widget.sources[index]);
|
||||||
_currIndex.value = index;
|
_currIndex.value = index;
|
||||||
|
widget.onPageChanged?.call(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
late final ValueChanged<int>? _onChangePage = widget.sources.length == 1
|
late final ValueChanged<int>? _onChangePage = widget.sources.length == 1
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ class PendantAvatar extends StatelessWidget {
|
|||||||
final String? garbPendantImage;
|
final String? garbPendantImage;
|
||||||
final int? roomId;
|
final int? roomId;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
|
final bool isMemberAvatar;
|
||||||
|
|
||||||
const PendantAvatar({
|
const PendantAvatar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.avatar,
|
required this.avatar,
|
||||||
this.size = 80,
|
required this.size,
|
||||||
|
this.isMemberAvatar = false,
|
||||||
double? badgeSize,
|
double? badgeSize,
|
||||||
bool isVip = false,
|
bool isVip = false,
|
||||||
int? officialType,
|
int? officialType,
|
||||||
@@ -42,13 +44,12 @@ class PendantAvatar extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final isMemberAvatar = size == 80;
|
|
||||||
Widget? pendant;
|
Widget? pendant;
|
||||||
if (showDynDecorate && !garbPendantImage.isNullOrEmpty) {
|
if (showDynDecorate && !garbPendantImage.isNullOrEmpty) {
|
||||||
final pendantSize = size * 1.75;
|
final pendantSize = size * 1.75;
|
||||||
pendant = Positioned(
|
pendant = Positioned(
|
||||||
// -(size * 1.75 - size) / 2
|
// -(size * 1.75 - size) / 2
|
||||||
top: -0.375 * size + (size == 80 ? 2 : 0),
|
top: -0.375 * size + (isMemberAvatar ? 2 : 0),
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: NetworkImgLayer(
|
child: NetworkImgLayer(
|
||||||
type: .emote,
|
type: .emote,
|
||||||
|
|||||||
@@ -1,9 +1,40 @@
|
|||||||
|
import 'package:PiliPlus/utils/parse_string.dart';
|
||||||
|
|
||||||
class Top {
|
class Top {
|
||||||
dynamic result;
|
List<TopImage>? imgUrls;
|
||||||
|
|
||||||
Top({this.result});
|
Top({this.imgUrls});
|
||||||
|
|
||||||
factory Top.fromJson(Map<String, dynamic> json) => Top(
|
Top.fromJson(Map<String, dynamic> json) {
|
||||||
result: json['result'] as dynamic,
|
try {
|
||||||
);
|
final list = json['result'] as List<dynamic>?;
|
||||||
|
if (list != null && list.isNotEmpty) {
|
||||||
|
imgUrls = list.map((e) => TopImage.fromJson(e)).toList();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TopImage {
|
||||||
|
late final String cover;
|
||||||
|
late final double dy;
|
||||||
|
|
||||||
|
TopImage.fromJson(Map<String, dynamic> json) {
|
||||||
|
cover =
|
||||||
|
noneNullOrEmptyString(json['item']?['image']?['default_image']) ??
|
||||||
|
json['cover'];
|
||||||
|
try {
|
||||||
|
final Map image = json['item']['image'] ?? json['item']['animation'];
|
||||||
|
final num halfHeight = (image['height'] as num) / 2;
|
||||||
|
final List<num> location = (image['location'] as String)
|
||||||
|
.split('-')
|
||||||
|
.map(num.parse)
|
||||||
|
.toList();
|
||||||
|
final start = location[1];
|
||||||
|
final end = location[2];
|
||||||
|
dy = (start + (end - start) / 2 - halfHeight) / halfHeight;
|
||||||
|
} catch (_) {
|
||||||
|
dy = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class _MemberPageState extends State<MemberPage> {
|
|||||||
late final int _mid;
|
late final int _mid;
|
||||||
late final String _heroTag;
|
late final String _heroTag;
|
||||||
late final MemberController _userController;
|
late final MemberController _userController;
|
||||||
|
PageController? _headerController;
|
||||||
|
PageController get headerController => _headerController ??= PageController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -51,6 +53,13 @@ class _MemberPageState extends State<MemberPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_headerController?.dispose();
|
||||||
|
_headerController = null;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context).colorScheme;
|
final theme = Theme.of(context).colorScheme;
|
||||||
@@ -358,6 +367,7 @@ class _MemberPageState extends State<MemberPage> {
|
|||||||
onFollow: () => _userController.onFollow(context),
|
onFollow: () => _userController.onFollow(context),
|
||||||
live: _userController.live,
|
live: _userController.live,
|
||||||
silence: _userController.silence,
|
silence: _userController.silence,
|
||||||
|
headerControllerBuilder: () => headerController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ import 'package:flutter/rendering.dart' show BoxHitTestResult, BoxParentData;
|
|||||||
const double kHeaderHeight = 135.0;
|
const double kHeaderHeight = 135.0;
|
||||||
|
|
||||||
const double kAvatarSize = 80.0;
|
const double kAvatarSize = 80.0;
|
||||||
|
const double kPendantAvatarSize = 70.0;
|
||||||
const double _kAvatarLeftPadding = 20.0;
|
const double _kAvatarLeftPadding = 20.0;
|
||||||
const double _kAvatarTopPadding = 110.0;
|
const double _kAvatarTopPadding = 115.0;
|
||||||
const double _kAvatarEffectiveHeight =
|
const double _kAvatarEffectiveHeight =
|
||||||
kAvatarSize - (kHeaderHeight - _kAvatarTopPadding);
|
kAvatarSize - (kHeaderHeight - _kAvatarTopPadding);
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:PiliPlus/common/constants.dart';
|
|||||||
import 'package:PiliPlus/common/widgets/avatars.dart';
|
import 'package:PiliPlus/common/widgets/avatars.dart';
|
||||||
import 'package:PiliPlus/common/widgets/image_viewer/hero.dart';
|
import 'package:PiliPlus/common/widgets/image_viewer/hero.dart';
|
||||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||||
|
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||||
import 'package:PiliPlus/common/widgets/view_safe_area.dart';
|
import 'package:PiliPlus/common/widgets/view_safe_area.dart';
|
||||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||||
import 'package:PiliPlus/models/common/member/user_info_type.dart';
|
import 'package:PiliPlus/models/common/member/user_info_type.dart';
|
||||||
@@ -10,6 +11,7 @@ import 'package:PiliPlus/models_new/space/space/followings_followed_upper.dart';
|
|||||||
import 'package:PiliPlus/models_new/space/space/images.dart';
|
import 'package:PiliPlus/models_new/space/space/images.dart';
|
||||||
import 'package:PiliPlus/models_new/space/space/live.dart';
|
import 'package:PiliPlus/models_new/space/space/live.dart';
|
||||||
import 'package:PiliPlus/models_new/space/space/pr_info.dart';
|
import 'package:PiliPlus/models_new/space/space/pr_info.dart';
|
||||||
|
import 'package:PiliPlus/models_new/space/space/top.dart';
|
||||||
import 'package:PiliPlus/pages/fan/view.dart';
|
import 'package:PiliPlus/pages/fan/view.dart';
|
||||||
import 'package:PiliPlus/pages/follow/view.dart';
|
import 'package:PiliPlus/pages/follow/view.dart';
|
||||||
import 'package:PiliPlus/pages/follow_type/followed/view.dart';
|
import 'package:PiliPlus/pages/follow_type/followed/view.dart';
|
||||||
@@ -38,6 +40,7 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
required this.onFollow,
|
required this.onFollow,
|
||||||
this.live,
|
this.live,
|
||||||
this.silence,
|
this.silence,
|
||||||
|
required this.headerControllerBuilder,
|
||||||
});
|
});
|
||||||
|
|
||||||
final bool isOwner;
|
final bool isOwner;
|
||||||
@@ -47,6 +50,7 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
final VoidCallback onFollow;
|
final VoidCallback onFollow;
|
||||||
final Live? live;
|
final Live? live;
|
||||||
final int? silence;
|
final int? silence;
|
||||||
|
final ValueGetter<PageController> headerControllerBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -111,37 +115,6 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(
|
|
||||||
BuildContext context,
|
|
||||||
ColorScheme colorScheme,
|
|
||||||
bool isLight,
|
|
||||||
double width,
|
|
||||||
) {
|
|
||||||
String imgUrl =
|
|
||||||
(isLight
|
|
||||||
? images.imgUrl
|
|
||||||
: images.nightImgurl.isNullOrEmpty
|
|
||||||
? images.imgUrl
|
|
||||||
: images.nightImgurl)
|
|
||||||
.http2https;
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => PageUtils.imageView(imgList: [SourceModel(url: imgUrl)]),
|
|
||||||
child: fromHero(
|
|
||||||
tag: imgUrl,
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
fit: .cover,
|
|
||||||
height: kHeaderHeight,
|
|
||||||
width: width,
|
|
||||||
memCacheWidth: width.cacheSize(context),
|
|
||||||
imageUrl: ImageUtils.thumbnailUrl(imgUrl),
|
|
||||||
placeholder: (_, _) => const SizedBox.shrink(),
|
|
||||||
color: isLight ? const Color(0x5DFFFFFF) : const Color(0x8D000000),
|
|
||||||
colorBlendMode: isLight ? BlendMode.lighten : BlendMode.darken,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Widget> _buildLeft(
|
List<Widget> _buildLeft(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ColorScheme colorScheme,
|
ColorScheme colorScheme,
|
||||||
@@ -455,15 +428,16 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget get _buildAvatar => fromHero(
|
Widget _buildAvatar(bool hasPendant) => fromHero(
|
||||||
tag: card.face ?? '',
|
tag: card.face ?? '',
|
||||||
child: PendantAvatar(
|
child: PendantAvatar(
|
||||||
avatar: card.face,
|
avatar: card.face,
|
||||||
size: kAvatarSize,
|
size: hasPendant ? kPendantAvatarSize : kAvatarSize,
|
||||||
|
isMemberAvatar: true,
|
||||||
badgeSize: 20,
|
badgeSize: 20,
|
||||||
officialType: card.officialVerify?.type,
|
officialType: card.officialVerify?.type,
|
||||||
isVip: (card.vip?.status ?? -1) > 0,
|
isVip: (card.vip?.status ?? -1) > 0,
|
||||||
garbPendantImage: card.pendant!.image!,
|
garbPendantImage: card.pendant?.image,
|
||||||
roomId: live?.liveStatus == 1 ? live!.roomid : null,
|
roomId: live?.liveStatus == 1 ? live!.roomid : null,
|
||||||
onTap: () => PageUtils.imageView(
|
onTap: () => PageUtils.imageView(
|
||||||
imgList: [SourceModel(url: card.face.http2https)],
|
imgList: [SourceModel(url: card.face.http2https)],
|
||||||
@@ -473,25 +447,146 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
|
|
||||||
Column _buildV(
|
Column _buildV(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ColorScheme colorScheme,
|
ColorScheme scheme,
|
||||||
bool isLight,
|
bool isLight,
|
||||||
double width,
|
double width,
|
||||||
) => Column(
|
) {
|
||||||
|
final hasPendant = card.pendant?.image?.isNotEmpty ?? false;
|
||||||
|
final imgUrls = images.collectionTopSimple?.top?.imgUrls;
|
||||||
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
HeaderLayoutWidget(
|
HeaderLayoutWidget(
|
||||||
header: _buildHeader(context, colorScheme, isLight, width),
|
header: imgUrls != null
|
||||||
avatar: _buildAvatar,
|
? _buildCollectionHeader(context, scheme, isLight, imgUrls, width)
|
||||||
actions: _buildRight(colorScheme),
|
: _buildHeader(
|
||||||
|
context,
|
||||||
|
isLight,
|
||||||
|
width,
|
||||||
|
(isLight
|
||||||
|
? images.imgUrl
|
||||||
|
: images.nightImgurl.isNullOrEmpty
|
||||||
|
? images.imgUrl
|
||||||
|
: images.nightImgurl)
|
||||||
|
.http2https,
|
||||||
|
),
|
||||||
|
avatar: _buildAvatar(hasPendant),
|
||||||
|
actions: _buildRight(scheme),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
..._buildLeft(context, colorScheme, isLight),
|
..._buildLeft(context, scheme, isLight),
|
||||||
if (card.prInfo?.content?.isNotEmpty == true)
|
if (card.prInfo?.content?.isNotEmpty == true)
|
||||||
buildPrInfo(context, colorScheme, isLight, card.prInfo!),
|
buildPrInfo(context, scheme, isLight, card.prInfo!),
|
||||||
const SizedBox(height: 5),
|
const SizedBox(height: 5),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildCollectionHeader(
|
||||||
|
BuildContext context,
|
||||||
|
ColorScheme scheme,
|
||||||
|
bool isLight,
|
||||||
|
List<TopImage> imgUrls,
|
||||||
|
double width,
|
||||||
|
) {
|
||||||
|
if (imgUrls.length == 1) {
|
||||||
|
return _buildHeader(
|
||||||
|
context,
|
||||||
|
isLight,
|
||||||
|
width,
|
||||||
|
imgUrls.single.cover,
|
||||||
|
filter: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final controller = headerControllerBuilder();
|
||||||
|
final memCacheWidth = width.cacheSize(context);
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: .opaque,
|
||||||
|
onTap: () => PageUtils.imageView(
|
||||||
|
initialPage: controller.page?.round() ?? 0,
|
||||||
|
imgList: imgUrls.map((e) => SourceModel(url: e.cover)).toList(),
|
||||||
|
onPageChanged: controller.jumpToPage,
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: .infinity,
|
||||||
|
height: kHeaderHeight,
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: controller,
|
||||||
|
itemCount: imgUrls.length,
|
||||||
|
physics: clampingScrollPhysics,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final img = imgUrls[index];
|
||||||
|
return fromHero(
|
||||||
|
tag: img.cover,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fit: .cover,
|
||||||
|
alignment: Alignment(0.0, img.dy),
|
||||||
|
height: kHeaderHeight,
|
||||||
|
width: width,
|
||||||
|
memCacheWidth: memCacheWidth,
|
||||||
|
imageUrl: ImageUtils.thumbnailUrl(img.cover),
|
||||||
|
fadeInDuration: const Duration(milliseconds: 120),
|
||||||
|
fadeOutDuration: const Duration(milliseconds: 120),
|
||||||
|
placeholder: (_, _) =>
|
||||||
|
const SizedBox(width: .infinity, height: kHeaderHeight),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: HeaderIndicator(
|
||||||
|
length: imgUrls.length,
|
||||||
|
pageController: controller,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(
|
||||||
|
BuildContext context,
|
||||||
|
bool isLight,
|
||||||
|
double width,
|
||||||
|
String imgUrl, {
|
||||||
|
bool filter = true,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: .opaque,
|
||||||
|
onTap: () => PageUtils.imageView(imgList: [SourceModel(url: imgUrl)]),
|
||||||
|
child: fromHero(
|
||||||
|
tag: imgUrl,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
fit: .cover,
|
||||||
|
height: kHeaderHeight,
|
||||||
|
width: width,
|
||||||
|
memCacheWidth: width.cacheSize(context),
|
||||||
|
imageUrl: ImageUtils.thumbnailUrl(imgUrl),
|
||||||
|
placeholder: (_, _) =>
|
||||||
|
const SizedBox(width: .infinity, height: kHeaderHeight),
|
||||||
|
color: filter
|
||||||
|
? isLight
|
||||||
|
? const Color(0x5DFFFFFF)
|
||||||
|
: const Color(0x8D000000)
|
||||||
|
: null,
|
||||||
|
colorBlendMode: filter
|
||||||
|
? isLight
|
||||||
|
? BlendMode.lighten
|
||||||
|
: BlendMode.darken
|
||||||
|
: null,
|
||||||
|
fadeInDuration: const Duration(milliseconds: 120),
|
||||||
|
fadeOutDuration: const Duration(milliseconds: 120),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildPrInfo(
|
Widget buildPrInfo(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -520,6 +615,8 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
memCacheHeight: 20.cacheSize(context),
|
memCacheHeight: 20.cacheSize(context),
|
||||||
imageUrl: ImageUtils.thumbnailUrl(icon),
|
imageUrl: ImageUtils.thumbnailUrl(icon),
|
||||||
placeholder: (_, _) => const SizedBox.shrink(),
|
placeholder: (_, _) => const SizedBox.shrink(),
|
||||||
|
fadeInDuration: .zero,
|
||||||
|
fadeOutDuration: .zero,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
],
|
],
|
||||||
@@ -554,7 +651,7 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// _buildHeader(context),
|
// _buildHeader(context),
|
||||||
const SizedBox(height: 56),
|
const SizedBox(height: kToolbarHeight),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
@@ -563,7 +660,7 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
top: 10,
|
top: 10,
|
||||||
bottom: card.prInfo?.content?.isNotEmpty == true ? 0 : 10,
|
bottom: card.prInfo?.content?.isNotEmpty == true ? 0 : 10,
|
||||||
),
|
),
|
||||||
child: _buildAvatar,
|
child: _buildAvatar(card.pendant?.image?.isNotEmpty ?? false),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -632,3 +729,54 @@ class UserInfoCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HeaderIndicator extends StatefulWidget {
|
||||||
|
const HeaderIndicator({
|
||||||
|
super.key,
|
||||||
|
required this.length,
|
||||||
|
required this.pageController,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int length;
|
||||||
|
final PageController pageController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HeaderIndicator> createState() => _HeaderIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HeaderIndicatorState extends State<HeaderIndicator> {
|
||||||
|
late double _progress;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateProgress();
|
||||||
|
widget.pageController.addListener(_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listener() {
|
||||||
|
_updateProgress();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateProgress() {
|
||||||
|
_progress = ((widget.pageController.page ?? 0) + 1) / widget.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
widget.pageController.removeListener(_listener);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
// ignore: deprecated_member_use
|
||||||
|
year2023: true,
|
||||||
|
minHeight: 3.5,
|
||||||
|
backgroundColor: const Color(0xA09E9E9E),
|
||||||
|
value: _progress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ abstract final class PageUtils {
|
|||||||
int initialPage = 0,
|
int initialPage = 0,
|
||||||
required List<SourceModel> imgList,
|
required List<SourceModel> imgList,
|
||||||
int? quality,
|
int? quality,
|
||||||
|
ValueChanged<int>? onPageChanged,
|
||||||
}) {
|
}) {
|
||||||
return Get.key.currentState!.push<void>(
|
return Get.key.currentState!.push<void>(
|
||||||
HeroDialogRoute(
|
HeroDialogRoute(
|
||||||
@@ -55,6 +56,7 @@ abstract final class PageUtils {
|
|||||||
sources: imgList,
|
sources: imgList,
|
||||||
initIndex: initialPage,
|
initIndex: initialPage,
|
||||||
quality: quality ?? GlobalData().imgQuality,
|
quality: quality ?? GlobalData().imgQuality,
|
||||||
|
onPageChanged: onPageChanged,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user