💄 Optimize profile page

This commit is contained in:
2025-08-01 16:29:23 +08:00
parent 7253e2d3ef
commit 28335dd548
3 changed files with 339 additions and 205 deletions

View File

@@ -338,28 +338,31 @@ class _DiscoveryActivityItem extends StatelessWidget {
).padding(horizontal: 20, top: 8, bottom: 4), ).padding(horizontal: 20, top: 8, bottom: 4),
SizedBox( SizedBox(
height: 180, height: 180,
child: ListView.builder( child: ConstrainedBox(
scrollDirection: Axis.horizontal, constraints: const BoxConstraints(maxHeight: 200),
itemCount: items.length, child: CarouselView.weighted(
padding: const EdgeInsets.symmetric(horizontal: 8), flexWeights:
itemBuilder: (context, index) { isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1],
final item = items[index]; consumeMaxWeight: false,
return switch (type) { children: [
'realm' => RealmCard( for (final item in items)
realm: SnRealm.fromJson(item['data']), switch (type) {
maxWidth: 280, 'realm' => RealmCard(
), realm: SnRealm.fromJson(item['data']),
'publisher' => PublisherCard( maxWidth: 280,
publisher: SnPublisher.fromJson(item['data']), ),
maxWidth: 280, 'publisher' => PublisherCard(
), publisher: SnPublisher.fromJson(item['data']),
'article' => WebArticleCard( maxWidth: 280,
article: SnWebArticle.fromJson(item['data']), ),
maxWidth: 280, 'article' => WebArticleCard(
), article: SnWebArticle.fromJson(item['data']),
_ => Placeholder(), maxWidth: 280,
}; ),
}, _ => Placeholder(),
},
],
),
), ),
).padding(bottom: 8), ).padding(bottom: 8),
], ],

View File

@@ -11,6 +11,7 @@ import 'package:island/models/user.dart';
import 'package:island/pods/config.dart'; import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart'; import 'package:island/pods/network.dart';
import 'package:island/services/color.dart'; import 'package:island/services/color.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/account/account_name.dart'; import 'package:island/widgets/account/account_name.dart';
import 'package:island/widgets/account/badge.dart'; import 'package:island/widgets/account/badge.dart';
import 'package:island/widgets/account/status.dart'; import 'package:island/widgets/account/status.dart';
@@ -121,200 +122,270 @@ class PublisherProfileScreen extends HookConsumerWidget {
offset: Offset(1.0, 1.0), offset: Offset(1.0, 1.0),
); );
Widget publisherBasisWidget(SnPublisher data) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 20,
children: [
GestureDetector(
child: Badge(
isLabelVisible: data.type == 0,
padding: EdgeInsets.all(4),
label: Icon(
Symbols.launch,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
backgroundColor: Theme.of(context).colorScheme.primary,
offset: Offset(0, 48),
child: ProfilePictureWidget(file: data.picture, radius: 32),
),
onTap: () {
Navigator.pop(context, true);
if (data.account?.name != null) {
context.pushNamed(
'accountProfile',
pathParameters: {'name': data.account!.name},
);
}
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 6,
children: [
Text(data.nick).fontSize(20),
if (data.verification != null)
VerificationMark(mark: data.verification!),
Expanded(
child: Text(
'@${data.name}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
).fontSize(14).opacity(0.85),
),
],
),
if (data.type == 0 && data.account != null)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 6,
children: [
Icon(
data.type == 0 ? Symbols.person : Symbols.workspaces,
fill: 1,
size: 17,
),
Text(
'publisherBelongsTo'.tr(args: ['@${data.account!.name}']),
).fontSize(14),
],
).opacity(0.85).padding(bottom: 6),
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: data.account!.name,
padding: EdgeInsets.zero,
),
subStatus
.when(
data:
(status) => FilledButton.icon(
onPressed:
subscribing.value
? null
: (status.isSubscribed
? unsubscribe
: subscribe),
icon: Icon(
status.isSubscribed
? Symbols.remove_circle
: Symbols.add_circle,
),
label:
Text(
status.isSubscribed
? 'unsubscribe'
: 'subscribe',
).tr(),
style: ButtonStyle(
visualDensity: VisualDensity(vertical: -2),
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
height: 36,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
),
)
.padding(top: 8),
],
),
),
],
).padding(horizontal: 24, top: 24);
Widget publisherVerificationWidget(SnPublisher data) => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
children: [
if (badges.value?.isNotEmpty ?? false)
BadgeList(badges: badges.value!).padding(top: 16),
if (data.verification != null)
VerificationStatusCard(mark: data.verification!),
],
),
).padding(top: 16);
Widget publisherDetailWidget(SnPublisher data) => Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('bio').tr().bold().padding(bottom: 2),
Text(data.bio.isEmpty ? 'descriptionNone'.tr() : data.bio),
],
).padding(horizontal: 20, vertical: 16),
);
return publisher.when( return publisher.when(
data: data:
(data) => AppScaffold( (data) => AppScaffold(
noBackground: false, noBackground: false,
body: CustomScrollView( appBar:
slivers: [ isWideScreen(context)
SliverAppBar( ? AppBar(
foregroundColor: appbarColor.value, foregroundColor: appbarColor.value,
expandedHeight: 180, leading: PageBackButton(
pinned: true, color: appbarColor.value,
leading: PageBackButton( shadows: [appbarShadow],
color: appbarColor.value,
shadows: [appbarShadow],
),
flexibleSpace: Stack(
children: [
Positioned.fill(
child:
data.background?.id != null
? CloudImageWidget(file: data.background)
: Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
), ),
FlexibleSpaceBar( flexibleSpace: Stack(
title: Text( children: [
data.nick, Positioned.fill(
style: TextStyle( child:
color: data.background?.id != null
appbarColor.value ?? ? CloudImageWidget(file: data.background)
Theme.of(context).appBarTheme.foregroundColor, : Container(
color:
Theme.of(
context,
).appBarTheme.backgroundColor,
),
),
FlexibleSpaceBar(
title: Text(
data.nick,
style: TextStyle(
color:
appbarColor.value ??
Theme.of(
context,
).appBarTheme.foregroundColor,
shadows: [appbarShadow],
),
),
background:
Container(), // Empty container since background is handled by Stack
),
],
),
)
: null,
body:
isWideScreen(context)
? Row(
children: [
Flexible(
flex: 4,
child: CustomScrollView(
slivers: [
SliverGap(16),
SliverPostList(pubName: name),
SliverGap(
MediaQuery.of(context).padding.bottom + 16,
),
],
).padding(left: 8),
),
Flexible(
flex: 3,
child: Align(
alignment: Alignment.topLeft,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
publisherBasisWidget(data),
publisherVerificationWidget(data),
publisherDetailWidget(data),
],
),
),
),
),
],
)
: CustomScrollView(
slivers: [
SliverAppBar(
foregroundColor: appbarColor.value,
expandedHeight: 180,
pinned: true,
leading: PageBackButton(
color: appbarColor.value,
shadows: [appbarShadow], shadows: [appbarShadow],
), ),
), flexibleSpace: Stack(
background: children: [
Container(), // Empty container since background is handled by Stack Positioned.fill(
), child:
], data.background?.id != null
), ? CloudImageWidget(
), file: data.background,
SliverToBoxAdapter( )
child: Row( : Container(
crossAxisAlignment: CrossAxisAlignment.start, color:
spacing: 20, Theme.of(
children: [ context,
GestureDetector( ).appBarTheme.backgroundColor,
child: Badge( ),
isLabelVisible: data.type == 0,
padding: EdgeInsets.all(4),
label: Icon(
Symbols.launch,
size: 16,
color: Theme.of(context).colorScheme.onPrimary,
),
backgroundColor:
Theme.of(context).colorScheme.primary,
offset: Offset(0, 48),
child: ProfilePictureWidget(
file: data.picture,
radius: 32,
),
),
onTap: () {
Navigator.pop(context, true);
if (data.account?.name != null) {
context.pushNamed(
'accountProfile',
pathParameters: {'name': data.account!.name},
);
}
},
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
spacing: 6,
children: [
Text(data.nick).fontSize(20),
if (data.verification != null)
VerificationMark(mark: data.verification!),
Text(
'@${data.name}',
).fontSize(14).opacity(0.85),
],
),
if (data.type == 0 && data.account != null)
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 6,
children: [
Icon(
data.type == 0
? Symbols.person
: Symbols.workspaces,
fill: 1,
size: 17,
),
Text(
'publisherBelongsTo'.tr(
args: ['@${data.account!.name}'],
),
).fontSize(14),
],
).opacity(0.85).padding(bottom: 6),
if (data.type == 0 && data.account != null)
AccountStatusWidget(
uname: data.account!.name,
padding: EdgeInsets.zero,
), ),
subStatus FlexibleSpaceBar(
.when( title: Text(
data: data.nick,
(status) => FilledButton.icon( style: TextStyle(
onPressed: color:
subscribing.value appbarColor.value ??
? null Theme.of(
: (status.isSubscribed context,
? unsubscribe ).appBarTheme.foregroundColor,
: subscribe), shadows: [appbarShadow],
icon: Icon( ),
status.isSubscribed ),
? Symbols.remove_circle background:
: Symbols.add_circle, Container(), // Empty container since background is handled by Stack
), ),
label: ],
Text( ),
status.isSubscribed
? 'unsubscribe'
: 'subscribe',
).tr(),
style: ButtonStyle(
visualDensity: VisualDensity(
vertical: -2,
),
),
),
error: (_, _) => const SizedBox(),
loading:
() => const SizedBox(
height: 36,
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
),
)
.padding(top: 8),
],
), ),
), SliverToBoxAdapter(child: publisherBasisWidget(data)),
], SliverToBoxAdapter(
).padding(horizontal: 24, top: 24), child: publisherVerificationWidget(data),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(child: publisherDetailWidget(data)),
child: Card( SliverPostList(pubName: name),
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4), SliverGap(MediaQuery.of(context).padding.bottom + 16),
child: Column(
children: [
if (badges.value?.isNotEmpty ?? false)
BadgeList(badges: badges.value!).padding(top: 16),
if (data.verification != null)
VerificationStatusCard(mark: data.verification!),
], ],
), ),
).padding(top: 16),
),
SliverToBoxAdapter(
child: Card(
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('bio').tr().bold().padding(bottom: 2),
Text(
data.bio.isEmpty ? 'descriptionNone'.tr() : data.bio,
),
],
).padding(horizontal: 20, vertical: 16),
),
),
SliverPostList(pubName: name),
SliverGap(MediaQuery.of(context).padding.bottom + 16),
],
),
), ),
error: error:
(error, stackTrace) => AppScaffold( (error, stackTrace) => AppScaffold(

View File

@@ -96,6 +96,66 @@ class CloudFileList extends HookConsumerWidget {
); );
} }
final allImages =
!files.any(
(e) => e.mimeType == null || !e.mimeType!.startsWith('image'),
);
if (allImages) {
return ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight, minWidth: maxWidth),
child: AspectRatio(
aspectRatio: calculateAspectRatio(),
child: CarouselView(
padding: padding,
itemSnapping: true,
itemExtent: math.min(
MediaQuery.of(context).size.width * 0.85,
maxWidth * 0.85,
),
shape: RoundedRectangleBorder(
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
children: [
for (var i = 0; i < files.length; i++)
Stack(
children: [
_CloudFileListEntry(
file: files[i],
heroTag: heroTags[i],
isImage: files[i].mimeType?.startsWith('image') ?? false,
disableZoomIn: disableZoomIn,
),
Positioned(
bottom: 12,
left: 16,
child: Text('${i + 1}/${files.length}')
.textColor(Colors.white)
.textShadow(
color: Colors.black54,
offset: Offset(1, 1),
blurRadius: 3,
),
),
],
),
],
onTap: (i) {
if (!(files[i].mimeType?.startsWith('image') ?? false)) {
return;
}
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
rootNavigator: true,
);
}
},
),
),
);
}
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight, minWidth: maxWidth), constraints: BoxConstraints(maxHeight: maxHeight, minWidth: maxWidth),
child: AspectRatio( child: AspectRatio(