improve user preview

This commit is contained in:
wgh19
2024-05-18 16:46:56 +08:00
parent b0d740a174
commit 2a1a668c25
8 changed files with 473 additions and 310 deletions

View File

@@ -1,48 +1,44 @@
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/app.dart';
class SliverGridViewWithFixedItemHeight extends StatelessWidget { class SliverGridViewWithFixedItemHeight extends StatelessWidget {
const SliverGridViewWithFixedItemHeight( const SliverGridViewWithFixedItemHeight(
{required this.delegate, {required this.delegate,
required this.maxCrossAxisExtent, this.maxCrossAxisExtent = double.infinity,
required this.itemHeight, this.minCrossAxisExtent = 0,
super.key}); required this.itemHeight,
super.key});
final SliverChildDelegate delegate; final SliverChildDelegate delegate;
final double maxCrossAxisExtent; final double maxCrossAxisExtent;
final double minCrossAxisExtent;
final double itemHeight; final double itemHeight;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverLayoutBuilder( return SliverLayoutBuilder(
builder: ((context, constraints) => SliverGrid( builder: ((context, constraints) => SliverGrid(
delegate: delegate, delegate: delegate,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: SliverGridDelegateWithFixedHeight(
maxCrossAxisExtent: maxCrossAxisExtent, itemHeight: itemHeight,
childAspectRatio: maxCrossAxisExtent: maxCrossAxisExtent,
calcChildAspectRatio(constraints.crossAxisExtent)), minCrossAxisExtent: minCrossAxisExtent),
).sliverPadding(EdgeInsets.only(bottom: context.padding.bottom)))); ).sliverPadding(EdgeInsets.only(bottom: context.padding.bottom))));
}
double calcChildAspectRatio(double width) {
var crossItems = width ~/ maxCrossAxisExtent;
if (width % maxCrossAxisExtent != 0) {
crossItems += 1;
}
final itemWidth = width / crossItems;
return itemWidth / itemHeight;
} }
} }
class GridViewWithFixedItemHeight extends StatelessWidget { class GridViewWithFixedItemHeight extends StatelessWidget {
const GridViewWithFixedItemHeight( const GridViewWithFixedItemHeight(
{ required this.builder, {required this.builder,
required this.itemCount, required this.itemCount,
required this.maxCrossAxisExtent, this.maxCrossAxisExtent = double.infinity,
required this.itemHeight, this.minCrossAxisExtent = 0,
super.key}); required this.itemHeight,
super.key});
final Widget Function(BuildContext, int) builder; final Widget Function(BuildContext, int) builder;
@@ -50,28 +46,69 @@ class GridViewWithFixedItemHeight extends StatelessWidget {
final double maxCrossAxisExtent; final double maxCrossAxisExtent;
final double minCrossAxisExtent;
final double itemHeight; final double itemHeight;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: ((context, constraints) => GridView.builder( builder: ((context, constraints) => GridView.builder(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: SliverGridDelegateWithFixedHeight(
maxCrossAxisExtent: maxCrossAxisExtent, itemHeight: itemHeight,
childAspectRatio: maxCrossAxisExtent: maxCrossAxisExtent,
calcChildAspectRatio(constraints.maxWidth)), minCrossAxisExtent: minCrossAxisExtent),
itemBuilder: builder, itemBuilder: builder,
itemCount: itemCount, itemCount: itemCount,
padding: EdgeInsets.only(bottom: context.padding.bottom), padding: EdgeInsets.only(bottom: context.padding.bottom),
))); )));
}
}
class SliverGridDelegateWithFixedHeight extends SliverGridDelegate {
const SliverGridDelegateWithFixedHeight({
this.maxCrossAxisExtent = double.infinity,
this.minCrossAxisExtent = 0,
required this.itemHeight,
});
final double maxCrossAxisExtent;
final double minCrossAxisExtent;
final double itemHeight;
@override
SliverGridLayout getLayout(SliverConstraints constraints) {
var crossItemsCount = calcCrossItemsCount(constraints.crossAxisExtent);
return SliverGridRegularTileLayout(
crossAxisCount: crossItemsCount,
mainAxisStride: itemHeight,
childMainAxisExtent: itemHeight,
crossAxisStride: constraints.crossAxisExtent / crossItemsCount,
childCrossAxisExtent: constraints.crossAxisExtent / crossItemsCount,
reverseCrossAxis: false);
} }
double calcChildAspectRatio(double width) { int calcCrossItemsCount(double width) {
var crossItems = width ~/ maxCrossAxisExtent; int count = 20;
if (width % maxCrossAxisExtent != 0) { var itemWidth = width / 20;
crossItems += 1; while (
!(itemWidth > minCrossAxisExtent && itemWidth < maxCrossAxisExtent)) {
count--;
itemWidth = width / count;
if (count == 1) {
return 1;
}
} }
final itemWidth = width / crossItems; return count;
return itemWidth / itemHeight;
} }
}
@override
bool shouldRelayout(covariant SliverGridDelegate oldDelegate) {
return oldDelegate is! SliverGridDelegateWithFixedHeight ||
oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent ||
oldDelegate.minCrossAxisExtent != minCrossAxisExtent ||
oldDelegate.itemHeight != itemHeight;
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:pixes/components/animated_image.dart'; import 'package:pixes/components/animated_image.dart';
import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/app.dart';
@@ -20,15 +22,15 @@ class UserPreviewWidget extends StatefulWidget {
class _UserPreviewWidgetState extends State<UserPreviewWidget> { class _UserPreviewWidgetState extends State<UserPreviewWidget> {
bool isFollowing = false; bool isFollowing = false;
void follow() async{ void follow() async {
if(isFollowing) return; if (isFollowing) return;
setState(() { setState(() {
isFollowing = true; isFollowing = true;
}); });
var method = widget.user.isFollowed ? "delete" : "add"; var method = widget.user.isFollowed ? "delete" : "add";
var res = await Network().follow(widget.user.id.toString(), method); var res = await Network().follow(widget.user.id.toString(), method);
if(res.error) { if (res.error) {
if(mounted) { if (mounted) {
context.showToast(message: "Network Error"); context.showToast(message: "Network Error");
} }
} else { } else {
@@ -43,67 +45,120 @@ class _UserPreviewWidgetState extends State<UserPreviewWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Row( child: GestureDetector(
children: [ onTap: () => context.to(() => UserInfoPage(widget.user.id.toString())),
SizedBox( behavior: HitTestBehavior.translucent,
width: 64, child: SizedBox.expand(
height: 64, child: Row(
child: ClipRRect( children: [
borderRadius: BorderRadius.circular(64), SizedBox(
child: ColoredBox( width: 64,
color: ColorScheme.of(context).secondaryContainer, height: 64,
child: AnimatedImage( child: ClipRRect(
image: CachedImageProvider(widget.user.avatar), borderRadius: BorderRadius.circular(64),
fit: BoxFit.cover, child: ColoredBox(
filterQuality: FilterQuality.medium, color: ColorScheme.of(context).secondaryContainer,
child: AnimatedImage(
image: CachedImageProvider(widget.user.avatar),
fit: BoxFit.cover,
filterQuality: FilterQuality.medium,
),
),
), ),
), ),
), const SizedBox(
), width: 12,
const SizedBox(width: 12,), ),
Expanded( SizedBox(
child: Column( width: 96,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
const Spacer(),
Text(widget.user.name, maxLines: 1, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 12,),
Row(
children: [ children: [
Button( const Spacer(),
onPressed: () => context.to(() => UserInfoPage(widget.user.id.toString(), followCallback: (v){ Text(widget.user.name,
setState(() { maxLines: 1,
widget.user.isFollowed = v; style: const TextStyle(
}); fontSize: 16, fontWeight: FontWeight.bold)),
},)), const SizedBox(
child: Text("View".tl,), height: 12,
), ),
const SizedBox(width: 8,), Row(
if(isFollowing) children: [
Button(onPressed: follow, child: const SizedBox( if (isFollowing)
width: 42, Button(
height: 24, onPressed: follow,
child: Center( child: const SizedBox(
child: SizedBox.square( width: 42,
dimension: 18, height: 24,
child: ProgressRing(strokeWidth: 2,), child: Center(
child: SizedBox.square(
dimension: 18,
child: ProgressRing(
strokeWidth: 2,
),
),
),
))
else if (!widget.user.isFollowed)
Button(onPressed: follow, child: Text("Follow".tl))
else
Button(
onPressed: follow,
child: Text(
"Unfollow".tl,
style: TextStyle(
color: ColorScheme.of(context).error),
),
), ),
), ],
)) ),
else if (!widget.user.isFollowed) const Spacer(),
Button(onPressed: follow, child: Text("Follow".tl))
else
Button(
onPressed: follow,
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),),
),
], ],
), ),
const Spacer(), ),
], Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
var count = constraints.maxWidth.toInt() ~/ 96;
var images = List.generate(
min(count, widget.user.artworks.length),
(index) => buildIllust(widget.user.artworks[index]));
return Row(
children: images,
);
},
),
),
const Icon(
FluentIcons.chevron_right,
size: 14,
)
],
),
),
),
);
}
Widget buildIllust(Illust illust) {
return SizedBox(
width: 96,
height: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: ColoredBox(
color: ColorScheme.of(context).secondaryContainer,
child: AnimatedImage(
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
filterQuality: FilterQuality.medium,
image: CachedImageProvider(illust.images.first.medium),
), ),
) ),
], ),
), ),
); );
} }

View File

@@ -128,8 +128,7 @@ class IllustAuthor {
final String avatar; final String avatar;
bool isFollowed; bool isFollowed;
IllustAuthor( IllustAuthor(this.id, this.name, this.account, this.avatar, this.isFollowed);
this.id, this.name, this.account, this.avatar, this.isFollowed);
} }
class Tag { class Tag {
@@ -250,11 +249,11 @@ enum KeywordMatchType {
@override @override
toString() => text; toString() => text;
String toParam() => switch(this) { String toParam() => switch (this) {
KeywordMatchType.tagsPartialMatches => "partial_match_for_tags", KeywordMatchType.tagsPartialMatches => "partial_match_for_tags",
KeywordMatchType.tagsExactMatch => "exact_match_for_tags", KeywordMatchType.tagsExactMatch => "exact_match_for_tags",
KeywordMatchType.titleOrDescriptionSearch => "title_and_caption" KeywordMatchType.titleOrDescriptionSearch => "title_and_caption"
}; };
} }
enum FavoriteNumber { enum FavoriteNumber {
@@ -273,9 +272,11 @@ enum FavoriteNumber {
const FavoriteNumber(this.number); const FavoriteNumber(this.number);
@override @override
toString() => this == FavoriteNumber.unlimited ? "Unlimited" : "$number Bookmarks"; toString() =>
this == FavoriteNumber.unlimited ? "Unlimited" : "$number Bookmarks";
String toParam() => this == FavoriteNumber.unlimited ? "" : " ${number}users入り"; String toParam() =>
this == FavoriteNumber.unlimited ? "" : " ${number}users入り";
} }
enum SearchSort { enum SearchSort {
@@ -288,37 +289,35 @@ enum SearchSort {
bool get isPremium => appdata.account?.user.isPremium == true; bool get isPremium => appdata.account?.user.isPremium == true;
static List<SearchSort> get availableValues => [ static List<SearchSort> get availableValues => [
SearchSort.newToOld, SearchSort.newToOld,
SearchSort.oldToNew, SearchSort.oldToNew,
SearchSort.popular, SearchSort.popular,
if(appdata.account?.user.isPremium == true) if (appdata.account?.user.isPremium == true) SearchSort.popularMale,
SearchSort.popularMale, if (appdata.account?.user.isPremium == true) SearchSort.popularFemale
if(appdata.account?.user.isPremium == true) ];
SearchSort.popularFemale
];
@override @override
toString() { toString() {
if(this == SearchSort.popular) { if (this == SearchSort.popular) {
return isPremium ? "Popular" : "Popular(limited)"; return isPremium ? "Popular" : "Popular(limited)";
} else if(this == SearchSort.newToOld) { } else if (this == SearchSort.newToOld) {
return "New to old"; return "New to old";
} else if(this == SearchSort.oldToNew){ } else if (this == SearchSort.oldToNew) {
return "Old to new"; return "Old to new";
} else if(this == SearchSort.popularMale){ } else if (this == SearchSort.popularMale) {
return "Popular(Male)"; return "Popular(Male)";
} else { } else {
return "Popular(Female)"; return "Popular(Female)";
} }
} }
String toParam() => switch(this) { String toParam() => switch (this) {
SearchSort.newToOld => "date_desc", SearchSort.newToOld => "date_desc",
SearchSort.oldToNew => "date_asc", SearchSort.oldToNew => "date_asc",
SearchSort.popular => "popular_desc", SearchSort.popular => "popular_desc",
SearchSort.popularMale => "popular_male_desc", SearchSort.popularMale => "popular_male_desc",
SearchSort.popularFemale => "popular_female_desc", SearchSort.popularFemale => "popular_female_desc",
}; };
} }
enum AgeLimit { enum AgeLimit {
@@ -333,11 +332,11 @@ enum AgeLimit {
@override @override
toString() => text; toString() => text;
String toParam() => switch(this) { String toParam() => switch (this) {
AgeLimit.unlimited => "", AgeLimit.unlimited => "",
AgeLimit.allAges => " -R-18", AgeLimit.allAges => " -R-18",
AgeLimit.r18 => "R-18", AgeLimit.r18 => "R-18",
}; };
} }
class SearchOptions { class SearchOptions {
@@ -369,17 +368,19 @@ class UserPreview {
final String avatar; final String avatar;
bool isFollowed; bool isFollowed;
final bool isBlocking; final bool isBlocking;
final List<Illust> artworks;
UserPreview(this.id, this.name, this.account, this.avatar, this.isFollowed, UserPreview(this.id, this.name, this.account, this.avatar, this.isFollowed,
this.isBlocking); this.isBlocking, this.artworks);
UserPreview.fromJson(Map<String, dynamic> json) UserPreview.fromJson(Map<String, dynamic> json)
: id = json['id'], : id = json['user']['id'],
name = json['name'], name = json['user']['name'],
account = json['account'], account = json['user']['account'],
avatar = json['profile_image_urls']['medium'], avatar = json['user']['profile_image_urls']['medium'],
isFollowed = json['is_followed'], isFollowed = json['user']['is_followed'],
isBlocking = json['is_access_blocking_user'] ?? false; isBlocking = json['user']['is_access_blocking_user'] ?? false,
artworks = (json['illusts'] as List).map((e) => Illust.fromJson(e)).toList();
} }
/* /*
@@ -402,7 +403,7 @@ class UserPreview {
} }
} }
*/ */
class Comment{ class Comment {
final String id; final String id;
final String comment; final String comment;
final DateTime date; final DateTime date;

View File

@@ -327,7 +327,7 @@ class Network {
var res = await apiGet(path); var res = await apiGet(path);
if (res.success) { if (res.success) {
return Res( return Res(
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList(), (res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e)).toList(),
subData: res.data["next_url"]); subData: res.data["next_url"]);
} else { } else {
return Res.error(res.errorMessage); return Res.error(res.errorMessage);
@@ -350,7 +350,7 @@ class Network {
var res = await apiGet(path); var res = await apiGet(path);
if (res.success) { if (res.success) {
return Res( return Res(
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList(), (res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e)).toList(),
subData: res.data["next_url"]); subData: res.data["next_url"]);
} else { } else {
return Res.error(res.errorMessage); return Res.error(res.errorMessage);
@@ -372,7 +372,7 @@ class Network {
var res = await apiGet("/v1/user/recommended?filter=for_android"); var res = await apiGet("/v1/user/recommended?filter=for_android");
if (res.success) { if (res.success) {
return Res( return Res(
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList(), (res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e)).toList(),
subData: res.data["next_url"]); subData: res.data["next_url"]);
} else { } else {
return Res.error(res.errorMessage); return Res.error(res.errorMessage);
@@ -473,7 +473,7 @@ class Network {
var res = await apiGet("/v1/user/related?filter=for_android&seed_user_id=$id"); var res = await apiGet("/v1/user/related?filter=for_android&seed_user_id=$id");
if (res.success) { if (res.success) {
return Res( return Res(
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList()); (res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e)).toList());
} else { } else {
return Res.error(res.errorMessage); return Res.error(res.errorMessage);
} }

View File

@@ -18,7 +18,8 @@ class FollowingUsersPage extends StatefulWidget {
State<FollowingUsersPage> createState() => _FollowingUsersPageState(); State<FollowingUsersPage> createState() => _FollowingUsersPageState();
} }
class _FollowingUsersPageState extends MultiPageLoadingState<FollowingUsersPage, UserPreview> { class _FollowingUsersPageState
extends MultiPageLoadingState<FollowingUsersPage, UserPreview> {
String type = "public"; String type = "public";
@override @override
@@ -28,11 +29,13 @@ class _FollowingUsersPageState extends MultiPageLoadingState<FollowingUsersPage,
SliverToBoxAdapter( SliverToBoxAdapter(
child: Row( child: Row(
children: [ children: [
Text("Following".tl, Text(
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),) "Following".tl,
.paddingVertical(12).paddingLeft(16), style:
const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
).paddingVertical(12).paddingLeft(16),
const Spacer(), const Spacer(),
if(widget.uid == appdata.account?.user.id) if (widget.uid == appdata.account?.user.id)
SegmentedButton( SegmentedButton(
value: type, value: type,
options: [ options: [
@@ -44,22 +47,21 @@ class _FollowingUsersPageState extends MultiPageLoadingState<FollowingUsersPage,
reset(); reset();
}, },
), ),
const SizedBox(width: 16,) const SizedBox(
width: 16,
)
], ],
), ),
), ),
SliverGridViewWithFixedItemHeight( SliverGridViewWithFixedItemHeight(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate((context, index) {
(context, index) { if (index == data.length - 1) {
if(index == data.length - 1){ nextPage();
nextPage(); }
} return UserPreviewWidget(data[index]);
return UserPreviewWidget(data[index]); }, childCount: data.length),
}, minCrossAxisExtent: 440,
childCount: data.length itemHeight: 136,
),
maxCrossAxisExtent: 520,
itemHeight: 114,
).sliverPaddingHorizontal(8) ).sliverPaddingHorizontal(8)
], ],
); );
@@ -68,12 +70,12 @@ class _FollowingUsersPageState extends MultiPageLoadingState<FollowingUsersPage,
String? nextUrl; String? nextUrl;
@override @override
Future<Res<List<UserPreview>>> loadData(page) async{ Future<Res<List<UserPreview>>> loadData(page) async {
if(nextUrl == "end") { if (nextUrl == "end") {
return Res.error("No more data"); return Res.error("No more data");
} }
var res = await Network().getFollowing(widget.uid, type, nextUrl); var res = await Network().getFollowing(widget.uid, type, nextUrl);
if(!res.error) { if (!res.error) {
nextUrl = res.subData; nextUrl = res.subData;
nextUrl ??= "end"; nextUrl ??= "end";
} }

View File

@@ -29,8 +29,11 @@ class _RecommendationPageState extends State<RecommendationPage> {
buildTab(), buildTab(),
Expanded( Expanded(
child: type != 2 child: type != 2
? _RecommendationArtworksPage(type, key: Key(type.toString()),) ? _RecommendationArtworksPage(
: const _RecommendationUsersPage(), type,
key: Key(type.toString()),
)
: const _RecommendationUsersPage(),
) )
], ],
); );
@@ -46,7 +49,7 @@ class _RecommendationPageState extends State<RecommendationPage> {
SegmentedButtonOption(2, "Users".tl), SegmentedButtonOption(2, "Users".tl),
], ],
onPressed: (key) { onPressed: (key) {
if(key != type) { if (key != type) {
setState(() { setState(() {
type = key; type = key;
}); });
@@ -58,35 +61,42 @@ class _RecommendationPageState extends State<RecommendationPage> {
} }
} }
class _RecommendationArtworksPage extends StatefulWidget { class _RecommendationArtworksPage extends StatefulWidget {
const _RecommendationArtworksPage(this.type, {super.key}); const _RecommendationArtworksPage(this.type, {super.key});
final int type; final int type;
@override @override
State<_RecommendationArtworksPage> createState() => _RecommendationArtworksPageState(); State<_RecommendationArtworksPage> createState() =>
_RecommendationArtworksPageState();
} }
class _RecommendationArtworksPageState extends MultiPageLoadingState<_RecommendationArtworksPage, Illust> { class _RecommendationArtworksPageState
extends MultiPageLoadingState<_RecommendationArtworksPage, Illust> {
@override @override
Widget buildContent(BuildContext context, final List<Illust> data) { Widget buildContent(BuildContext context, final List<Illust> data) {
return LayoutBuilder(builder: (context, constrains){ return LayoutBuilder(builder: (context, constrains) {
return MasonryGridView.builder( return MasonryGridView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8) padding: const EdgeInsets.symmetric(horizontal: 8) +
+ EdgeInsets.only(bottom: context.padding.bottom), EdgeInsets.only(bottom: context.padding.bottom),
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent( gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 240, maxCrossAxisExtent: 240,
), ),
itemCount: data.length, itemCount: data.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if(index == data.length - 1){ if (index == data.length - 1) {
nextPage(); nextPage();
} }
return IllustWidget(data[index], onTap: () { return IllustWidget(
context.to(() => IllustGalleryPage(illusts: data, data[index],
initialPage: index, nextUrl: Network.recommendationUrl,)); onTap: () {
},); context.to(() => IllustGalleryPage(
illusts: data,
initialPage: index,
nextUrl: Network.recommendationUrl,
));
},
);
}, },
); );
}); });
@@ -104,33 +114,32 @@ class _RecommendationUsersPage extends StatefulWidget {
const _RecommendationUsersPage(); const _RecommendationUsersPage();
@override @override
State<_RecommendationUsersPage> createState() => _RecommendationUsersPageState(); State<_RecommendationUsersPage> createState() =>
_RecommendationUsersPageState();
} }
class _RecommendationUsersPageState extends MultiPageLoadingState<_RecommendationUsersPage, UserPreview> { class _RecommendationUsersPageState
extends MultiPageLoadingState<_RecommendationUsersPage, UserPreview> {
@override @override
Widget buildContent(BuildContext context, List<UserPreview> data) { Widget buildContent(BuildContext context, List<UserPreview> data) {
return CustomScrollView( return CustomScrollView(
slivers: [ slivers: [
SliverGridViewWithFixedItemHeight( SliverGridViewWithFixedItemHeight(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate((context, index) {
(context, index) { if (index == data.length - 1) {
if(index == data.length - 1){ nextPage();
nextPage(); }
} return UserPreviewWidget(data[index]);
return UserPreviewWidget(data[index]); }, childCount: data.length),
}, minCrossAxisExtent: 440,
childCount: data.length itemHeight: 136,
),
maxCrossAxisExtent: 520,
itemHeight: 114,
).sliverPaddingHorizontal(8) ).sliverPaddingHorizontal(8)
], ],
); );
} }
@override @override
Future<Res<List<UserPreview>>> loadData(page) async{ Future<Res<List<UserPreview>>> loadData(page) async {
var res = await Network().getRecommendationUsers(); var res = await Network().getRecommendationUsers();
return res; return res;
} }

View File

@@ -557,8 +557,8 @@ class _SearchUserResultPageState extends MultiPageLoadingState<SearchUserResultP
}, },
childCount: data.length childCount: data.length
), ),
maxCrossAxisExtent: 520, minCrossAxisExtent: 440,
itemHeight: 114, itemHeight: 136,
).sliverPaddingHorizontal(8), ).sliverPaddingHorizontal(8),
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom),) SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom),)
], ],

View File

@@ -43,8 +43,13 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
_RelatedUsers(widget.id), _RelatedUsers(widget.id),
buildInformation(), buildInformation(),
buildArtworkHeader(), buildArtworkHeader(),
_UserArtworks(data.id.toString(), page, key: ValueKey(data.id + page),), _UserArtworks(
SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)), data.id.toString(),
page,
key: ValueKey(data.id + page),
),
SliverPadding(
padding: EdgeInsets.only(bottom: context.padding.bottom)),
], ],
), ),
); );
@@ -52,23 +57,24 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
bool isFollowing = false; bool isFollowing = false;
void follow() async{ void follow() async {
if(isFollowing) return; if (isFollowing) return;
String type = ""; String type = "";
if(!data!.isFollowed) { if (!data!.isFollowed) {
await flyoutController.showFlyout( await flyoutController.showFlyout(
navigatorKey: App.rootNavigatorKey.currentState, navigatorKey: App.rootNavigatorKey.currentState,
builder: (context) => builder: (context) => MenuFlyout(
MenuFlyout(
items: [ items: [
MenuFlyoutItem(text: Text("Public".tl), MenuFlyoutItem(
text: Text("Public".tl),
onPressed: () => type = "public"), onPressed: () => type = "public"),
MenuFlyoutItem(text: Text("Private".tl), MenuFlyoutItem(
text: Text("Private".tl),
onPressed: () => type = "private"), onPressed: () => type = "private"),
], ],
)); ));
} }
if(type.isEmpty && !data!.isFollowed) { if (type.isEmpty && !data!.isFollowed) {
return; return;
} }
setState(() { setState(() {
@@ -76,8 +82,8 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
}); });
var method = data!.isFollowed ? "delete" : "add"; var method = data!.isFollowed ? "delete" : "add";
var res = await Network().follow(data!.id.toString(), method, type); var res = await Network().follow(data!.id.toString(), method, type);
if(res.error) { if (res.error) {
if(mounted) { if (mounted) {
context.showToast(message: "Network Error"); context.showToast(message: "Network Error");
} }
} else { } else {
@@ -100,7 +106,8 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
height: 64, height: 64,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(64), borderRadius: BorderRadius.circular(64),
border: Border.all(color: ColorScheme.of(context).outlineVariant, width: 0.6)), border: Border.all(
color: ColorScheme.of(context).outlineVariant, width: 0.6)),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(64), borderRadius: BorderRadius.circular(64),
child: Image( child: Image(
@@ -109,47 +116,60 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
height: 64, height: 64,
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
),), ),
),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(data!.name, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), Text(data!.name,
style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text.rich( Text.rich(
TextSpan( TextSpan(
children: [ children: [
TextSpan(text: 'Follows: '.tl), TextSpan(text: 'Follows: '.tl),
TextSpan( TextSpan(
text: '${data!.totalFollowUsers}', text: '${data!.totalFollowUsers}',
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = (() => context.to(() => FollowingUsersPage(widget.id))), ..onTap = (() =>
style: TextStyle(fontWeight: FontWeight.bold, color: FluentTheme.of(context).accentColor) context.to(() => FollowingUsersPage(widget.id))),
), style: TextStyle(
fontWeight: FontWeight.bold,
color: FluentTheme.of(context).accentColor)),
], ],
), ),
style: const TextStyle(fontSize: 14), style: const TextStyle(fontSize: 14),
), ),
if(widget.id != appdata.account?.user.id) if (widget.id != appdata.account?.user.id)
const SizedBox(height: 8,), const SizedBox(
if(widget.id != appdata.account?.user.id) height: 8,
if(isFollowing) ),
Button(onPressed: follow, child: const SizedBox( if (widget.id != appdata.account?.user.id)
width: 42, if (isFollowing)
height: 24, Button(
child: Center( onPressed: follow,
child: SizedBox.square( child: const SizedBox(
dimension: 18, width: 42,
child: ProgressRing(strokeWidth: 2,), height: 24,
), child: Center(
), child: SizedBox.square(
)) dimension: 18,
child: ProgressRing(
strokeWidth: 2,
),
),
),
))
else if (!data!.isFollowed) else if (!data!.isFollowed)
FlyoutTarget( FlyoutTarget(
controller: flyoutController, controller: flyoutController,
child: Button(onPressed: follow, child: Text("Follow".tl)) child: Button(onPressed: follow, child: Text("Follow".tl)))
)
else else
Button( Button(
onPressed: follow, onPressed: follow,
child: Text("Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error),), child: Text(
"Unfollow".tl,
style: TextStyle(color: ColorScheme.of(context).error),
),
), ),
], ],
), ),
@@ -158,63 +178,75 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
Widget buildHeader(String title, {Widget? action}) { Widget buildHeader(String title, {Widget? action}) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: 38, height: 38,
child: Row( child: Row(
children: [ children: [
Text( Text(
title, title,
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
).toAlign(Alignment.centerLeft), ).toAlign(Alignment.centerLeft),
const Spacer(), const Spacer(),
if(action != null) if (action != null) action.toAlign(Alignment.centerRight)
action.toAlign(Alignment.centerRight) ],
], ).paddingHorizontal(16))
).paddingHorizontal(16)).paddingTop(8); .paddingTop(8);
} }
Widget buildArtworkHeader() { Widget buildArtworkHeader() {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
height: 38, height: 38,
child: Row( child: Row(
children: [ children: [
SegmentedButton<int>( SegmentedButton<int>(
options: [ options: [
SegmentedButtonOption(0, "Artworks".tl), SegmentedButtonOption(0, "Artworks".tl),
SegmentedButtonOption(1, "Bookmarks".tl), SegmentedButtonOption(1, "Bookmarks".tl),
],
value: page,
onPressed: (value) {
setState(() {
page = value;
});
},
),
const Spacer(),
BatchDownloadButton(
request: () {
if (page == 0) {
return Network().getUserIllusts(data!.id.toString());
} else {
return Network().getUserBookmarks(data!.id.toString());
}
},
),
], ],
value: page, ).paddingHorizontal(16))
onPressed: (value) { .paddingTop(12),
setState(() {
page = value;
});
},
),
const Spacer(),
BatchDownloadButton(request: () {
if(page == 0) {
return Network().getUserIllusts(data!.id.toString());
} else {
return Network().getUserBookmarks(data!.id.toString());
}
},),
],
).paddingHorizontal(16)).paddingTop(12),
); );
} }
Widget buildInformation() { Widget buildInformation() {
Widget buildItem({IconData? icon, required String title, required String? content, Widget? trailing}) { Widget buildItem(
if(content == null || content.isEmpty) { {IconData? icon,
required String title,
required String? content,
Widget? trailing}) {
if (content == null || content.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
child: ListTile( child: ListTile(
leading: icon == null ? null : Icon(icon, size: 20,), leading: icon == null
? null
: Icon(
icon,
size: 20,
),
title: Text(title), title: Text(title),
subtitle: SelectableText(content), subtitle: SelectableText(content),
trailing: trailing, trailing: trailing,
@@ -226,30 +258,46 @@ class _UserInfoPageState extends LoadingState<UserInfoPage, UserDetails> {
child: Column( child: Column(
children: [ children: [
buildHeader("Information".tl), buildHeader("Information".tl),
buildItem(icon: MdIcons.comment_outlined, title: "Introduction".tl, content: data!.comment), buildItem(
buildItem(icon: MdIcons.cake_outlined, title: "Birthday".tl, content: data!.birth), icon: MdIcons.comment_outlined,
buildItem(icon: MdIcons.location_city_outlined, title: "Region", content: data!.region), title: "Introduction".tl,
buildItem(icon: MdIcons.work_outline, title: "Job".tl, content: data!.job), content: data!.comment),
buildItem(icon: MdIcons.person_2_outlined, title: "Gender".tl, content: data!.gender), buildItem(
icon: MdIcons.cake_outlined,
title: "Birthday".tl,
content: data!.birth),
buildItem(
icon: MdIcons.location_city_outlined,
title: "Region",
content: data!.region),
buildItem(
icon: MdIcons.work_outline, title: "Job".tl, content: data!.job),
buildItem(
icon: MdIcons.person_2_outlined,
title: "Gender".tl,
content: data!.gender),
buildHeader("Social Network".tl), buildHeader("Social Network".tl),
buildItem(title: "Webpage", buildItem(
title: "Webpage",
content: data!.webpage, content: data!.webpage,
trailing: IconButton( trailing: IconButton(
icon: const Icon(MdIcons.open_in_new, size: 18), icon: const Icon(MdIcons.open_in_new, size: 18),
onPressed: () => launchUrlString(data!.twitterUrl!) onPressed: () => launchUrlString(data!.twitterUrl!))),
)), buildItem(
buildItem(title: "Twitter", title: "Twitter",
content: data!.twitterUrl, content: data!.twitterUrl,
trailing: IconButton( trailing: IconButton(
icon: const Icon(MdIcons.open_in_new, size: 18), icon: const Icon(MdIcons.open_in_new, size: 18),
onPressed: () => launchUrlString(data!.twitterUrl!) onPressed: () => launchUrlString(data!.twitterUrl!))),
)), buildItem(
buildItem(title: "pawoo", title: "pawoo",
content: data!.pawooUrl, content: data!.pawooUrl,
trailing: IconButton( trailing: IconButton(
icon: const Icon(MdIcons.open_in_new, size: 18,), icon: const Icon(
onPressed: () => launchUrlString(data!.pawooUrl!) MdIcons.open_in_new,
)), size: 18,
),
onPressed: () => launchUrlString(data!.pawooUrl!))),
], ],
), ),
); );
@@ -292,7 +340,9 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
child: Row( child: Row(
children: [ children: [
const Icon(FluentIcons.info), const Icon(FluentIcons.info),
const SizedBox(width: 4,), const SizedBox(
width: 4,
),
Text(error) Text(error)
], ],
), ),
@@ -308,16 +358,13 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
maxCrossAxisExtent: 240, maxCrossAxisExtent: 240,
), ),
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, index) { (context, index) {
if(index == data.length - 1){ if (index == data.length - 1) {
nextPage(); nextPage();
} }
return IllustWidget(data[index], onTap: () { return IllustWidget(data[index], onTap: () {
context.to(() => IllustGalleryPage( context.to(() => IllustGalleryPage(
illusts: data, illusts: data, initialPage: index, nextUrl: nextUrl));
initialPage: index,
nextUrl: nextUrl
));
}); });
}, },
childCount: data.length, childCount: data.length,
@@ -328,16 +375,16 @@ class _UserArtworksState extends MultiPageLoadingState<_UserArtworks, Illust> {
String? nextUrl; String? nextUrl;
@override @override
Future<Res<List<Illust>>> loadData(page) async{ Future<Res<List<Illust>>> loadData(page) async {
if(nextUrl == "end") { if (nextUrl == "end") {
return Res.error("No more data"); return Res.error("No more data");
} }
var res = nextUrl == null var res = nextUrl == null
? (widget.type == 0 ? (widget.type == 0
? await Network().getUserIllusts(widget.uid) ? await Network().getUserIllusts(widget.uid)
: await Network().getUserBookmarks(widget.uid)) : await Network().getUserBookmarks(widget.uid))
: await Network().getIllustsWithNextUrl(nextUrl!); : await Network().getIllustsWithNextUrl(nextUrl!);
if(!res.error) { if (!res.error) {
nextUrl = res.subData; nextUrl = res.subData;
nextUrl ??= "end"; nextUrl ??= "end";
} }
@@ -354,12 +401,13 @@ class _RelatedUsers extends StatefulWidget {
State<_RelatedUsers> createState() => _RelatedUsersState(); State<_RelatedUsers> createState() => _RelatedUsersState();
} }
class _RelatedUsersState extends LoadingState<_RelatedUsers, List<UserPreview>> { class _RelatedUsersState
extends LoadingState<_RelatedUsers, List<UserPreview>> {
@override @override
Widget buildFrame(BuildContext context, Widget child) { Widget buildFrame(BuildContext context, Widget child) {
return SliverToBoxAdapter( return SliverToBoxAdapter(
child: SizedBox( child: SizedBox(
height: 108, height: 146,
width: double.infinity, width: double.infinity,
child: child, child: child,
), ),
@@ -370,18 +418,30 @@ class _RelatedUsersState extends LoadingState<_RelatedUsers, List<UserPreview>>
@override @override
Widget buildContent(BuildContext context, List<UserPreview> data) { Widget buildContent(BuildContext context, List<UserPreview> data) {
return Scrollbar( Widget content = Scrollbar(
controller: _controller,
child: ListView.builder(
controller: _controller, controller: _controller,
padding: const EdgeInsets.only(bottom: 8, left: 8), child: ListView.builder(
primary: false, controller: _controller,
scrollDirection: Axis.horizontal, padding: const EdgeInsets.only(bottom: 8, left: 8),
itemCount: data.length, primary: false,
itemBuilder: (context, index) { scrollDirection: Axis.horizontal,
return UserPreviewWidget(data[index]).fixWidth(264); itemCount: data.length,
}, itemBuilder: (context, index) {
)); return UserPreviewWidget(data[index]).fixWidth(342);
},
));
if (MediaQuery.of(context).size.width > 500) {
content = ScrollbarTheme.merge(
data: const ScrollbarThemeData(
thickness: 6,
hoveringThickness: 6,
mainAxisMargin: 4,
hoveringPadding: EdgeInsets.zero,
padding: EdgeInsets.zero,
hoveringMainAxisMargin: 4),
child: content);
}
return content;
} }
@override @override
@@ -389,4 +449,3 @@ class _RelatedUsersState extends LoadingState<_RelatedUsers, List<UserPreview>>
return Network().relatedUsers(widget.uid); return Network().relatedUsers(widget.uid);
} }
} }