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/rendering.dart' show RenderProxyBox;
|
||||
import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult;
|
||||
|
||||
class CustomHeightWidget extends SingleChildRenderObjectWidget {
|
||||
const CustomHeightWidget({
|
||||
super.key,
|
||||
required this.height,
|
||||
this.height,
|
||||
this.offset = .zero,
|
||||
required super.child,
|
||||
required Widget super.child,
|
||||
});
|
||||
|
||||
final double height;
|
||||
final double? height;
|
||||
|
||||
final Offset offset;
|
||||
|
||||
@@ -34,14 +34,14 @@ class CustomHeightWidget extends SingleChildRenderObjectWidget {
|
||||
|
||||
class RenderCustomHeightWidget extends RenderProxyBox {
|
||||
RenderCustomHeightWidget({
|
||||
required double height,
|
||||
double? height,
|
||||
required Offset offset,
|
||||
}) : _height = height,
|
||||
_offset = offset;
|
||||
|
||||
double _height;
|
||||
double get height => _height;
|
||||
set height(double value) {
|
||||
double? _height;
|
||||
double? get height => _height;
|
||||
set height(double? value) {
|
||||
if (_height == value) return;
|
||||
_height = value;
|
||||
markNeedsLayout();
|
||||
@@ -57,12 +57,40 @@ class RenderCustomHeightWidget extends RenderProxyBox {
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
child!.layout(constraints);
|
||||
size = constraints.constrainDimensions(constraints.maxWidth, height);
|
||||
if (height != null) {
|
||||
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
|
||||
void paint(PaintingContext context, 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 '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/sliver_persistent_header.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/material.dart'
|
||||
hide SliverPersistentHeader, SliverPersistentHeaderDelegate;
|
||||
import 'package:flutter/rendering.dart' show RenderOpacity, OpacityLayer;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// ref [SliverAppBar]
|
||||
@@ -133,11 +135,9 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
title: effectiveTitle,
|
||||
actions: actions,
|
||||
automaticallyImplyActions: automaticallyImplyActions,
|
||||
flexibleSpace: maxExtent == .infinity
|
||||
? flexibleSpace
|
||||
: IgnorePointer(
|
||||
flexibleSpace: IgnorePointer(
|
||||
ignoring: isScrolledUnder,
|
||||
child: FlexibleSpaceBar(background: flexibleSpace),
|
||||
child: DynamicFlexibleSpaceBar(background: flexibleSpace),
|
||||
),
|
||||
bottom: bottom,
|
||||
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.sources,
|
||||
this.initIndex = 0,
|
||||
this.onPageChanged,
|
||||
});
|
||||
|
||||
final double minScale;
|
||||
@@ -61,6 +62,7 @@ class GalleryViewer extends StatefulWidget {
|
||||
final int quality;
|
||||
final List<SourceModel> sources;
|
||||
final int initIndex;
|
||||
final ValueChanged<int>? onPageChanged;
|
||||
|
||||
@override
|
||||
State<GalleryViewer> createState() => _GalleryViewerState();
|
||||
@@ -346,6 +348,7 @@ class _GalleryViewerState extends State<GalleryViewer>
|
||||
_player?.pause();
|
||||
_playIfNeeded(widget.sources[index]);
|
||||
_currIndex.value = index;
|
||||
widget.onPageChanged?.call(index);
|
||||
}
|
||||
|
||||
late final ValueChanged<int>? _onChangePage = widget.sources.length == 1
|
||||
|
||||
@@ -15,11 +15,13 @@ class PendantAvatar extends StatelessWidget {
|
||||
final String? garbPendantImage;
|
||||
final int? roomId;
|
||||
final VoidCallback? onTap;
|
||||
final bool isMemberAvatar;
|
||||
|
||||
const PendantAvatar({
|
||||
super.key,
|
||||
required this.avatar,
|
||||
this.size = 80,
|
||||
required this.size,
|
||||
this.isMemberAvatar = false,
|
||||
double? badgeSize,
|
||||
bool isVip = false,
|
||||
int? officialType,
|
||||
@@ -42,13 +44,12 @@ class PendantAvatar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final isMemberAvatar = size == 80;
|
||||
Widget? pendant;
|
||||
if (showDynDecorate && !garbPendantImage.isNullOrEmpty) {
|
||||
final pendantSize = size * 1.75;
|
||||
pendant = Positioned(
|
||||
// -(size * 1.75 - size) / 2
|
||||
top: -0.375 * size + (size == 80 ? 2 : 0),
|
||||
top: -0.375 * size + (isMemberAvatar ? 2 : 0),
|
||||
child: IgnorePointer(
|
||||
child: NetworkImgLayer(
|
||||
type: .emote,
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
import 'package:PiliPlus/utils/parse_string.dart';
|
||||
|
||||
class Top {
|
||||
dynamic result;
|
||||
List<TopImage>? imgUrls;
|
||||
|
||||
Top({this.result});
|
||||
Top({this.imgUrls});
|
||||
|
||||
factory Top.fromJson(Map<String, dynamic> json) => Top(
|
||||
result: json['result'] as dynamic,
|
||||
);
|
||||
Top.fromJson(Map<String, dynamic> json) {
|
||||
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 String _heroTag;
|
||||
late final MemberController _userController;
|
||||
PageController? _headerController;
|
||||
PageController get headerController => _headerController ??= PageController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -51,6 +53,13 @@ class _MemberPageState extends State<MemberPage> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_headerController?.dispose();
|
||||
_headerController = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context).colorScheme;
|
||||
@@ -358,6 +367,7 @@ class _MemberPageState extends State<MemberPage> {
|
||||
onFollow: () => _userController.onFollow(context),
|
||||
live: _userController.live,
|
||||
silence: _userController.silence,
|
||||
headerControllerBuilder: () => headerController,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -23,8 +23,9 @@ import 'package:flutter/rendering.dart' show BoxHitTestResult, BoxParentData;
|
||||
const double kHeaderHeight = 135.0;
|
||||
|
||||
const double kAvatarSize = 80.0;
|
||||
const double kPendantAvatarSize = 70.0;
|
||||
const double _kAvatarLeftPadding = 20.0;
|
||||
const double _kAvatarTopPadding = 110.0;
|
||||
const double _kAvatarTopPadding = 115.0;
|
||||
const double _kAvatarEffectiveHeight =
|
||||
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/image_viewer/hero.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/models/common/image_preview_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/live.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/follow/view.dart';
|
||||
import 'package:PiliPlus/pages/follow_type/followed/view.dart';
|
||||
@@ -38,6 +40,7 @@ class UserInfoCard extends StatelessWidget {
|
||||
required this.onFollow,
|
||||
this.live,
|
||||
this.silence,
|
||||
required this.headerControllerBuilder,
|
||||
});
|
||||
|
||||
final bool isOwner;
|
||||
@@ -47,6 +50,7 @@ class UserInfoCard extends StatelessWidget {
|
||||
final VoidCallback onFollow;
|
||||
final Live? live;
|
||||
final int? silence;
|
||||
final ValueGetter<PageController> headerControllerBuilder;
|
||||
|
||||
@override
|
||||
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(
|
||||
BuildContext context,
|
||||
ColorScheme colorScheme,
|
||||
@@ -455,15 +428,16 @@ class UserInfoCard extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
|
||||
Widget get _buildAvatar => fromHero(
|
||||
Widget _buildAvatar(bool hasPendant) => fromHero(
|
||||
tag: card.face ?? '',
|
||||
child: PendantAvatar(
|
||||
avatar: card.face,
|
||||
size: kAvatarSize,
|
||||
size: hasPendant ? kPendantAvatarSize : kAvatarSize,
|
||||
isMemberAvatar: true,
|
||||
badgeSize: 20,
|
||||
officialType: card.officialVerify?.type,
|
||||
isVip: (card.vip?.status ?? -1) > 0,
|
||||
garbPendantImage: card.pendant!.image!,
|
||||
garbPendantImage: card.pendant?.image,
|
||||
roomId: live?.liveStatus == 1 ? live!.roomid : null,
|
||||
onTap: () => PageUtils.imageView(
|
||||
imgList: [SourceModel(url: card.face.http2https)],
|
||||
@@ -473,25 +447,146 @@ class UserInfoCard extends StatelessWidget {
|
||||
|
||||
Column _buildV(
|
||||
BuildContext context,
|
||||
ColorScheme colorScheme,
|
||||
ColorScheme scheme,
|
||||
bool isLight,
|
||||
double width,
|
||||
) => Column(
|
||||
) {
|
||||
final hasPendant = card.pendant?.image?.isNotEmpty ?? false;
|
||||
final imgUrls = images.collectionTopSimple?.top?.imgUrls;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
HeaderLayoutWidget(
|
||||
header: _buildHeader(context, colorScheme, isLight, width),
|
||||
avatar: _buildAvatar,
|
||||
actions: _buildRight(colorScheme),
|
||||
header: imgUrls != null
|
||||
? _buildCollectionHeader(context, scheme, isLight, imgUrls, width)
|
||||
: _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),
|
||||
..._buildLeft(context, colorScheme, isLight),
|
||||
..._buildLeft(context, scheme, isLight),
|
||||
if (card.prInfo?.content?.isNotEmpty == true)
|
||||
buildPrInfo(context, colorScheme, isLight, card.prInfo!),
|
||||
buildPrInfo(context, scheme, isLight, card.prInfo!),
|
||||
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(
|
||||
BuildContext context,
|
||||
@@ -520,6 +615,8 @@ class UserInfoCard extends StatelessWidget {
|
||||
memCacheHeight: 20.cacheSize(context),
|
||||
imageUrl: ImageUtils.thumbnailUrl(icon),
|
||||
placeholder: (_, _) => const SizedBox.shrink(),
|
||||
fadeInDuration: .zero,
|
||||
fadeOutDuration: .zero,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
],
|
||||
@@ -554,7 +651,7 @@ class UserInfoCard extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// _buildHeader(context),
|
||||
const SizedBox(height: 56),
|
||||
const SizedBox(height: kToolbarHeight),
|
||||
Row(
|
||||
children: [
|
||||
const SizedBox(width: 20),
|
||||
@@ -563,7 +660,7 @@ class UserInfoCard extends StatelessWidget {
|
||||
top: 10,
|
||||
bottom: card.prInfo?.content?.isNotEmpty == true ? 0 : 10,
|
||||
),
|
||||
child: _buildAvatar,
|
||||
child: _buildAvatar(card.pendant?.image?.isNotEmpty ?? false),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
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,
|
||||
required List<SourceModel> imgList,
|
||||
int? quality,
|
||||
ValueChanged<int>? onPageChanged,
|
||||
}) {
|
||||
return Get.key.currentState!.push<void>(
|
||||
HeroDialogRoute(
|
||||
@@ -55,6 +56,7 @@ abstract final class PageUtils {
|
||||
sources: imgList,
|
||||
initIndex: initialPage,
|
||||
quality: quality ?? GlobalData().imgQuality,
|
||||
onPageChanged: onPageChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user