mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 12:57:24 +00:00
search users
This commit is contained in:
36
lib/components/grid.dart
Normal file
36
lib/components/grid.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class SliverGridViewWithFixedItemHeight extends StatelessWidget {
|
||||
const SliverGridViewWithFixedItemHeight(
|
||||
{required this.delegate,
|
||||
required this.maxCrossAxisExtent,
|
||||
required this.itemHeight,
|
||||
super.key});
|
||||
|
||||
final SliverChildDelegate delegate;
|
||||
|
||||
final double maxCrossAxisExtent;
|
||||
|
||||
final double itemHeight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverLayoutBuilder(
|
||||
builder: ((context, constraints) => SliverGrid(
|
||||
delegate: delegate,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||
childAspectRatio:
|
||||
calcChildAspectRatio(constraints.crossAxisExtent)),
|
||||
)));
|
||||
}
|
||||
|
||||
double calcChildAspectRatio(double width) {
|
||||
var crossItems = width ~/ maxCrossAxisExtent;
|
||||
if (width % maxCrossAxisExtent != 0) {
|
||||
crossItems += 1;
|
||||
}
|
||||
final itemWidth = width / crossItems;
|
||||
return itemWidth / itemHeight;
|
||||
}
|
||||
}
|
104
lib/components/user_preview.dart
Normal file
104
lib/components/user_preview.dart
Normal file
@@ -0,0 +1,104 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:pixes/components/animated_image.dart';
|
||||
import 'package:pixes/components/color_scheme.dart';
|
||||
import 'package:pixes/foundation/app.dart';
|
||||
import 'package:pixes/foundation/image_provider.dart';
|
||||
import 'package:pixes/pages/user_info_page.dart';
|
||||
import 'package:pixes/utils/translation.dart';
|
||||
|
||||
import '../network/network.dart';
|
||||
|
||||
class UserPreviewWidget extends StatefulWidget {
|
||||
const UserPreviewWidget(this.user, {super.key});
|
||||
|
||||
final UserPreview user;
|
||||
|
||||
@override
|
||||
State<UserPreviewWidget> createState() => _UserPreviewWidgetState();
|
||||
}
|
||||
|
||||
class _UserPreviewWidgetState extends State<UserPreviewWidget> {
|
||||
bool isFollowing = false;
|
||||
|
||||
void follow() async{
|
||||
if(isFollowing) return;
|
||||
setState(() {
|
||||
isFollowing = true;
|
||||
});
|
||||
var method = widget.user.isFollowed ? "delete" : "add";
|
||||
var res = await Network().follow(widget.user.id.toString(), method);
|
||||
if(res.error) {
|
||||
if(mounted) {
|
||||
context.showToast(message: "Network Error");
|
||||
}
|
||||
} else {
|
||||
widget.user.isFollowed = !widget.user.isFollowed;
|
||||
}
|
||||
setState(() {
|
||||
isFollowing = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 64,
|
||||
height: 64,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(64),
|
||||
child: ColoredBox(
|
||||
color: ColorScheme.of(context).secondaryContainer,
|
||||
child: AnimatedImage(
|
||||
image: CachedImageProvider(widget.user.avatar),
|
||||
fit: BoxFit.cover,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12,),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(widget.user.name, maxLines: 1, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
Button(
|
||||
onPressed: () => context.to(() => UserInfoPage(widget.user.id.toString())),
|
||||
child: Text("View".tl,),
|
||||
),
|
||||
const SizedBox(width: 8,),
|
||||
if(isFollowing)
|
||||
Button(onPressed: follow, child: const SizedBox(
|
||||
width: 42,
|
||||
height: 24,
|
||||
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).errorColor),),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
).paddingVertical(8),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -309,3 +309,36 @@ class SearchOptions {
|
||||
DateTime? endTime;
|
||||
AgeLimit ageLimit = AgeLimit.unlimited;
|
||||
}
|
||||
|
||||
/*
|
||||
json:
|
||||
{
|
||||
"id": 20542044,
|
||||
"name": "vocaloidhm01",
|
||||
"account": "vocaloidhm01",
|
||||
"profile_image_urls": {
|
||||
"medium": "https://i.pximg.net/user-profile/img/2023/04/28/00/21/54/24348957_c74a61e78ddccb467417be7c37b5d463_170.jpg"
|
||||
},
|
||||
"is_followed": false,
|
||||
"is_access_blocking_user": false
|
||||
}
|
||||
*/
|
||||
class UserPreview {
|
||||
final int id;
|
||||
final String name;
|
||||
final String account;
|
||||
final String avatar;
|
||||
bool isFollowed;
|
||||
final bool isBlocking;
|
||||
|
||||
UserPreview(this.id, this.name, this.account, this.avatar, this.isFollowed,
|
||||
this.isBlocking);
|
||||
|
||||
UserPreview.fromJson(Map<String, dynamic> json)
|
||||
: id = json['id'],
|
||||
name = json['name'],
|
||||
account = json['account'],
|
||||
avatar = json['profile_image_urls']['medium'],
|
||||
isFollowed = json['is_followed'],
|
||||
isBlocking = json['is_access_blocking_user'];
|
||||
}
|
||||
|
@@ -302,12 +302,12 @@ class Network {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Res<List<User>>> searchUsers(String keyword, [String? nextUrl]) async{
|
||||
Future<Res<List<UserPreview>>> searchUsers(String keyword, [String? nextUrl]) async{
|
||||
var path = nextUrl ?? "/v1/search/user?filter=for_android&word=${Uri.encodeComponent(keyword)}";
|
||||
var res = await apiGet(path);
|
||||
if (res.success) {
|
||||
return Res(
|
||||
(res.data["user_previews"] as List).map((e) => User.fromJson(e)).toList(),
|
||||
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList(),
|
||||
subData: res.data["next_url"]);
|
||||
} else {
|
||||
return Res.error(res.errorMessage);
|
||||
|
@@ -3,6 +3,7 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:pixes/appdata.dart';
|
||||
import 'package:pixes/components/loading.dart';
|
||||
import 'package:pixes/components/page_route.dart';
|
||||
import 'package:pixes/components/user_preview.dart';
|
||||
import 'package:pixes/foundation/app.dart';
|
||||
import 'package:pixes/network/network.dart';
|
||||
import 'package:pixes/pages/user_info_page.dart';
|
||||
@@ -10,6 +11,7 @@ import 'package:pixes/utils/translation.dart';
|
||||
|
||||
import '../components/animated_image.dart';
|
||||
import '../components/color_scheme.dart';
|
||||
import '../components/grid.dart';
|
||||
import '../components/illust_widget.dart';
|
||||
import '../foundation/image_provider.dart';
|
||||
|
||||
@@ -41,7 +43,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
case 1:
|
||||
// TODO: novel search
|
||||
case 2:
|
||||
// TODO: user search
|
||||
context.to(() => SearchUserResultPage(text));
|
||||
case 3:
|
||||
// TODO: artwork id
|
||||
throw UnimplementedError();
|
||||
@@ -87,6 +89,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
child: TextBox(
|
||||
placeholder: searchTypes[searchType].tl,
|
||||
onChanged: (s) => text = s,
|
||||
onSubmitted: (s) => search(),
|
||||
foregroundDecoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: ColorScheme.of(context)
|
||||
@@ -361,20 +364,29 @@ class SearchResultPage extends StatefulWidget {
|
||||
class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Illust> {
|
||||
@override
|
||||
Widget buildContent(BuildContext context, final List<Illust> data) {
|
||||
return LayoutBuilder(builder: (context, constrains){
|
||||
return MasonryGridView.builder(
|
||||
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 240,
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Text("${"Search".tl}: ${widget.keyword}",
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),)
|
||||
.paddingVertical(12).paddingHorizontal(16),
|
||||
),
|
||||
itemCount: data.length,
|
||||
itemBuilder: (context, index) {
|
||||
if(index == data.length - 1){
|
||||
nextPage();
|
||||
}
|
||||
return IllustWidget(data[index]);
|
||||
},
|
||||
);
|
||||
});
|
||||
SliverMasonryGrid(
|
||||
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 240,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if(index == data.length - 1){
|
||||
nextPage();
|
||||
}
|
||||
return IllustWidget(data[index]);
|
||||
},
|
||||
childCount: data.length,
|
||||
),
|
||||
).sliverPaddingHorizontal(8)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String? nextUrl;
|
||||
@@ -395,3 +407,55 @@ class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Ill
|
||||
}
|
||||
}
|
||||
|
||||
class SearchUserResultPage extends StatefulWidget {
|
||||
const SearchUserResultPage(this.keyword, {super.key});
|
||||
|
||||
final String keyword;
|
||||
|
||||
@override
|
||||
State<SearchUserResultPage> createState() => _SearchUserResultPageState();
|
||||
}
|
||||
|
||||
class _SearchUserResultPageState extends MultiPageLoadingState<SearchUserResultPage, UserPreview> {
|
||||
@override
|
||||
Widget buildContent(BuildContext context, final List<UserPreview> data) {
|
||||
return CustomScrollView(
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Text("${"Search".tl}: ${widget.keyword}",
|
||||
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),)
|
||||
.paddingVertical(12).paddingHorizontal(16),
|
||||
),
|
||||
SliverGridViewWithFixedItemHeight(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if(index == data.length - 1){
|
||||
nextPage();
|
||||
}
|
||||
return UserPreviewWidget(data[index]);
|
||||
},
|
||||
childCount: data.length
|
||||
),
|
||||
maxCrossAxisExtent: 520,
|
||||
itemHeight: 114,
|
||||
).sliverPaddingHorizontal(8)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String? nextUrl;
|
||||
|
||||
@override
|
||||
Future<Res<List<UserPreview>>> loadData(page) async{
|
||||
if(nextUrl == "end") {
|
||||
return Res.error("No more data");
|
||||
}
|
||||
var res = await Network().searchUsers(widget.keyword, nextUrl);
|
||||
if(!res.error) {
|
||||
nextUrl = res.subData;
|
||||
nextUrl ??= "end";
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user