opt member page

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-08-13 10:29:26 +08:00
parent aea1992f5d
commit 086c93d24f
6 changed files with 299 additions and 512 deletions

View File

@@ -1,180 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// https://github.com/flutter/flutter/issues/18345#issuecomment-1627644396
class DynamicSliverAppBar extends StatefulWidget {
const DynamicSliverAppBar({
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.callback,
});
final ValueChanged<double>? callback;
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<DynamicSliverAppBar> createState() => _DynamicSliverAppBarState();
}
class _DynamicSliverAppBarState extends State<DynamicSliverAppBar> {
final GlobalKey _childKey = GlobalKey();
// As long as the height is 0 instead of the sliver app bar a sliver to box adapter will be used
// to calculate dynamically the size for the sliver app bar
double _height = 0;
@override
void initState() {
super.initState();
_updateHeight();
}
void _updateHeight() {
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
// otherwise this will throw an error
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (_childKey.currentContext == null) return;
setState(() {
_height = (_childKey.currentContext!.findRenderObject()! as RenderBox)
.size
.height;
widget.callback?.call(_height);
});
});
}
@override
void didChangeDependencies() {
_height = 0;
_updateHeight();
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
if (_height == 0) {
return SliverToBoxAdapter(
child: SizedBox(
key: _childKey,
child: widget.flexibleSpace ?? const SizedBox(height: kToolbarHeight),
),
);
}
MediaQuery.orientationOf(context);
return SliverAppBar(
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,
collapsedHeight: widget.collapsedHeight,
floating: widget.floating,
pinned: widget.pinned,
snap: widget.snap,
stretch: widget.stretch,
stretchTriggerOffset: widget.stretchTriggerOffset,
onStretchTrigger: widget.onStretchTrigger,
shape: widget.shape,
toolbarHeight: widget.toolbarHeight,
expandedHeight: _height,
leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,
systemOverlayStyle: widget.systemOverlayStyle,
forceMaterialTransparency: widget.forceMaterialTransparency,
clipBehavior: widget.clipBehavior,
flexibleSpace: FlexibleSpaceBar(background: widget.flexibleSpace),
);
}
}

View File

@@ -99,12 +99,6 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
// to calculate dynamically the size for the sliver app bar
double _height = 0;
@override
void initState() {
super.initState();
_updateHeight();
}
void _updateHeight() {
// Gets the new height and updates the sliver app bar. Needs to be called after the last frame has been rebuild
// otherwise this will throw an error
@@ -120,26 +114,39 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
}
Orientation? _orientation;
late Size size;
@override
Widget build(BuildContext context) {
final orientation = MediaQuery.orientationOf(context);
void didChangeDependencies() {
super.didChangeDependencies();
size = MediaQuery.sizeOf(context);
final orientation = size.width > size.height
? Orientation.landscape
: Orientation.portrait;
if (orientation != _orientation) {
_orientation = orientation;
_height = 0;
_updateHeight();
}
}
@override
Widget build(BuildContext context) {
//Needed to lay out the flexibleSpace the first time, so we can calculate its intrinsic height
if (_height == 0) {
return SliverToBoxAdapter(
child: UnconstrainedBox(
alignment: Alignment.topLeft,
child: SizedBox(
key: _childKey,
child: widget.flexibleSpace ?? const SizedBox(height: kToolbarHeight),
width: size.width,
child: widget.flexibleSpace,
),
),
);
}
final padding = MediaQuery.paddingOf(context).top;
return SliverAppBar.medium(
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
@@ -159,7 +166,6 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
centerTitle: widget.centerTitle,
excludeHeaderSemantics: widget.excludeHeaderSemantics,
titleSpacing: widget.titleSpacing,
collapsedHeight: widget.collapsedHeight,
floating: widget.floating,
pinned: widget.pinned,
snap: widget.snap,
@@ -167,8 +173,9 @@ class _DynamicSliverAppBarMediumState extends State<DynamicSliverAppBarMedium> {
stretchTriggerOffset: widget.stretchTriggerOffset,
onStretchTrigger: widget.onStretchTrigger,
shape: widget.shape,
toolbarHeight: widget.toolbarHeight,
expandedHeight: _height - MediaQuery.paddingOf(context).top,
toolbarHeight: kToolbarHeight,
collapsedHeight: kToolbarHeight + padding + 1,
expandedHeight: _height - padding,
leadingWidth: widget.leadingWidth,
toolbarTextStyle: widget.toolbarTextStyle,
titleTextStyle: widget.titleTextStyle,

View File

@@ -42,6 +42,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final padding = MediaQuery.paddingOf(context);
return Scaffold(
resizeToAvoidBottomInset: false,
floatingActionButton: FloatingActionButton.extended(
@@ -70,7 +71,13 @@ class _DynTopicPageState extends State<DynTopicPage> {
controller: _controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
Obx(() => _buildAppBar(theme, _controller.topState.value)),
Obx(
() => _buildAppBar(
theme,
padding.top,
_controller.topState.value,
),
),
Obx(() {
final allSortBy = _controller.topicSortByConf.value?.allSortBy;
if (allSortBy != null && allSortBy.isNotEmpty) {
@@ -133,9 +140,7 @@ class _DynTopicPageState extends State<DynTopicPage> {
return const SliverToBoxAdapter();
}),
SliverPadding(
padding: EdgeInsets.only(
bottom: MediaQuery.paddingOf(context).bottom + 80,
),
padding: EdgeInsets.only(bottom: padding.bottom + 80),
sliver: Obx(() => _buildBody(_controller.loadingState.value)),
),
],
@@ -145,15 +150,18 @@ class _DynTopicPageState extends State<DynTopicPage> {
);
}
Widget _buildAppBar(ThemeData theme, LoadingState<TopDetails?> topState) {
late final paddingTop = MediaQuery.paddingOf(context).top;
Widget _buildAppBar(
ThemeData theme,
double paddingTop,
LoadingState<TopDetails?> topState,
) {
return switch (topState) {
Loading() => const SliverAppBar(),
Success(:var response) when (topState.dataOrNull != null) =>
DynamicSliverAppBarMedium(
pinned: true,
callback: (value) => _controller.appbarOffset =
value - kToolbarHeight - paddingTop - 7,
callback: (value) =>
_controller.appbarOffset = value - kToolbarHeight - paddingTop,
title: IgnorePointer(child: Text(response!.topicItem!.name)),
flexibleSpace: Container(
decoration: BoxDecoration(

View File

@@ -47,7 +47,6 @@ class MemberController extends CommonDataController<SpaceData, SpaceData?>
final fromViewAid = Get.parameters['from_view_aid'];
final key = GlobalKey<ExtendedNestedScrollViewState>();
int offset = 120;
@override
void onInit() {

View File

@@ -1,5 +1,5 @@
import 'package:PiliPlus/common/widgets/dialog/report_member.dart';
import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar.dart';
import 'package:PiliPlus/common/widgets/dynamic_sliver_appbar_medium.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/http/loading_state.dart';
@@ -43,41 +43,56 @@ class _MemberPageState extends State<MemberPage> {
MemberController(mid: _mid),
tag: _heroTag,
);
_userController.scrollController.addListener(listener);
}
void listener() {
if (_userController.scrollController.hasClients) {
_userController.showUname.value =
_userController.scrollController.offset >= _userController.offset;
}
}
@override
void dispose() {
_userController.scrollController.removeListener(listener);
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
extendBody: true,
extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: false,
appBar: AppBar(
forceMaterialTransparency: true,
title: IgnorePointer(
child: Obx(
() =>
_userController.showUname.value &&
_userController.username != null
? Text(_userController.username!)
: const SizedBox.shrink(),
return Material(
color: theme.colorScheme.surface,
child: Obx(() {
if (_userController.loadingState.value.isSuccess) {
return ExtendedNestedScrollView(
key: _userController.key,
controller: _userController.scrollController,
onlyOneScrollInBody: true,
pinnedHeaderSliverHeightBuilder: () =>
kToolbarHeight + MediaQuery.paddingOf(context).top,
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
_buildUserInfo(
theme,
_userController.loadingState.value,
),
];
},
body: _userController.tab2?.isNotEmpty == true
? SafeArea(
top: false,
bottom: false,
child: Column(
children: [
if ((_userController.tab2?.length ?? 0) > 1)
TabBar(
controller: _userController.tabController,
tabs: _userController.tabs,
onTap: _userController.onTapTab,
),
actions: [
Expanded(child: _buildBody),
],
),
)
: const Center(child: Text('EMPTY')),
);
}
return Center(
child: _buildUserInfo(theme, _userController.loadingState.value),
);
}),
);
}
List<Widget> _actions(ThemeData theme) => [
IconButton(
tooltip: '搜索',
onPressed: () => Get.toNamed(
@@ -98,9 +113,7 @@ class _MemberPageState extends State<MemberPage> {
const Icon(Icons.block, size: 19),
const SizedBox(width: 10),
Text(
_userController.relation.value != 128
? '加入黑名单'
: '移除黑名单',
_userController.relation.value != 128 ? '加入黑名单' : '移除黑名单',
),
],
),
@@ -126,9 +139,7 @@ class _MemberPageState extends State<MemberPage> {
const Icon(Icons.share_outlined, size: 19),
const SizedBox(width: 10),
Text(
_userController.accountService.mid != _mid
? '分享UP主'
: '分享我的主页',
_userController.accountService.mid != _mid ? '分享UP主' : '分享我的主页',
),
],
),
@@ -150,8 +161,7 @@ class _MemberPageState extends State<MemberPage> {
),
),
if (_userController.accountService.isLogin.value)
if (_userController.mid ==
_userController.accountService.mid) ...[
if (_userController.mid == _userController.accountService.mid) ...[
if ((_userController
.loadingState
.value
@@ -262,58 +272,7 @@ class _MemberPageState extends State<MemberPage> {
],
),
const SizedBox(width: 4),
],
),
body: Obx(
() => _userController.loadingState.value.isSuccess
? LayoutBuilder(
builder: (context, constraints) {
return ExtendedNestedScrollView(
key: _userController.key,
controller: _userController.scrollController,
onlyOneScrollInBody: true,
pinnedHeaderSliverHeightBuilder: () {
return kToolbarHeight +
MediaQuery.paddingOf(this.context).top.toInt();
},
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
_buildAppBar(
isV: constraints.maxHeight > constraints.maxWidth,
),
];
},
body: _userController.tab2?.isNotEmpty == true
? SafeArea(
top: false,
bottom: false,
child: Column(
children: [
if ((_userController.tab2?.length ?? 0) > 1)
_buildTab(theme),
Expanded(child: _buildBody),
],
),
)
: const Center(child: Text('EMPTY')),
);
},
)
: Center(
child: _buildUserInfo(_userController.loadingState.value),
),
),
);
}
Widget _buildTab(ThemeData theme) => Material(
color: theme.colorScheme.surface,
child: TabBar(
controller: _userController.tabController,
tabs: _userController.tabs,
onTap: _userController.onTapTab,
),
);
Widget get _buildBody => tabBarView(
controller: _userController.tabController,
@@ -345,29 +304,18 @@ class _MemberPageState extends State<MemberPage> {
}).toList(),
);
Widget _buildAppBar({bool isV = true}) {
final top = MediaQuery.paddingOf(context).top;
return DynamicSliverAppBar(
Widget _buildUserInfo(ThemeData theme, LoadingState<SpaceData?> userState) {
switch (userState) {
case Loading():
return const CircularProgressIndicator();
case Success<SpaceData?>(:var response):
if (response != null) {
return DynamicSliverAppBarMedium(
pinned: true,
primary: false,
automaticallyImplyLeading: false,
toolbarHeight: kToolbarHeight + top,
flexibleSpace: _buildUserInfo(_userController.loadingState.value, isV),
callback: (value) {
_userController.offset = (value - 56 - top).toInt();
listener();
},
);
}
Widget _buildUserInfo(LoadingState<SpaceData?> userState, [bool isV = true]) {
return switch (userState) {
Loading() => const CircularProgressIndicator(),
Success(:var response) =>
response != null
? Obx(
actions: _actions(theme),
title: Text(_userController.username ?? ''),
flexibleSpace: Obx(
() => UserInfoCard(
isV: isV,
isOwner:
_userController.mid == _userController.accountService.mid,
relation: _userController.relation.value,
@@ -377,16 +325,23 @@ class _MemberPageState extends State<MemberPage> {
live: _userController.live,
silence: _userController.silence,
),
)
: GestureDetector(
),
);
}
return SliverAppBar(
pinned: true,
actions: _actions(theme),
title: GestureDetector(
onTap: _userController.onReload,
behavior: HitTestBehavior.opaque,
child: const SizedBox(height: 56, width: double.infinity),
child: Text(_userController.username ?? ''),
),
Error(:var errMsg) => scrollErrorWidget(
);
case Error(:var errMsg):
return scrollErrorWidget(
errMsg: errMsg,
onReload: _userController.onReload,
),
};
);
}
}
}

View File

@@ -5,6 +5,7 @@ import 'package:PiliPlus/models_new/space/space/card.dart';
import 'package:PiliPlus/models_new/space/space/images.dart';
import 'package:PiliPlus/models_new/space/space/live.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/context_ext.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/image_util.dart';
import 'package:PiliPlus/utils/num_util.dart';
@@ -12,12 +13,11 @@ import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get.dart' hide ContextExtensionss;
class UserInfoCard extends StatelessWidget {
const UserInfoCard({
super.key,
required this.isV,
required this.isOwner,
required this.card,
required this.images,
@@ -27,7 +27,6 @@ class UserInfoCard extends StatelessWidget {
this.silence,
});
final bool isV;
final bool isOwner;
final int relation;
final SpaceCard card;
@@ -39,7 +38,9 @@ class UserInfoCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return isV ? _buildV(context, theme) : _buildH(context, theme);
return context.isPortrait
? _buildV(context, theme)
: _buildH(context, theme);
}
Widget _countWidget({
@@ -541,11 +542,8 @@ class UserInfoCard extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// _buildHeader(context),
SizedBox(
height: Get.mediaQuery.padding.top + 56,
),
const SizedBox(height: 56),
SafeArea(
top: false,
bottom: false,
child: Row(
children: [