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;
|
DateTime? endTime;
|
||||||
AgeLimit ageLimit = AgeLimit.unlimited;
|
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 path = nextUrl ?? "/v1/search/user?filter=for_android&word=${Uri.encodeComponent(keyword)}";
|
||||||
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) => User.fromJson(e)).toList(),
|
(res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e["user"])).toList(),
|
||||||
subData: res.data["next_url"]);
|
subData: res.data["next_url"]);
|
||||||
} else {
|
} else {
|
||||||
return Res.error(res.errorMessage);
|
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/appdata.dart';
|
||||||
import 'package:pixes/components/loading.dart';
|
import 'package:pixes/components/loading.dart';
|
||||||
import 'package:pixes/components/page_route.dart';
|
import 'package:pixes/components/page_route.dart';
|
||||||
|
import 'package:pixes/components/user_preview.dart';
|
||||||
import 'package:pixes/foundation/app.dart';
|
import 'package:pixes/foundation/app.dart';
|
||||||
import 'package:pixes/network/network.dart';
|
import 'package:pixes/network/network.dart';
|
||||||
import 'package:pixes/pages/user_info_page.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/animated_image.dart';
|
||||||
import '../components/color_scheme.dart';
|
import '../components/color_scheme.dart';
|
||||||
|
import '../components/grid.dart';
|
||||||
import '../components/illust_widget.dart';
|
import '../components/illust_widget.dart';
|
||||||
import '../foundation/image_provider.dart';
|
import '../foundation/image_provider.dart';
|
||||||
|
|
||||||
@@ -41,7 +43,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
case 1:
|
case 1:
|
||||||
// TODO: novel search
|
// TODO: novel search
|
||||||
case 2:
|
case 2:
|
||||||
// TODO: user search
|
context.to(() => SearchUserResultPage(text));
|
||||||
case 3:
|
case 3:
|
||||||
// TODO: artwork id
|
// TODO: artwork id
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
@@ -87,6 +89,7 @@ class _SearchPageState extends State<SearchPage> {
|
|||||||
child: TextBox(
|
child: TextBox(
|
||||||
placeholder: searchTypes[searchType].tl,
|
placeholder: searchTypes[searchType].tl,
|
||||||
onChanged: (s) => text = s,
|
onChanged: (s) => text = s,
|
||||||
|
onSubmitted: (s) => search(),
|
||||||
foregroundDecoration: BoxDecoration(
|
foregroundDecoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: ColorScheme.of(context)
|
color: ColorScheme.of(context)
|
||||||
@@ -361,20 +364,29 @@ class SearchResultPage extends StatefulWidget {
|
|||||||
class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Illust> {
|
class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, 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 CustomScrollView(
|
||||||
return MasonryGridView.builder(
|
slivers: [
|
||||||
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
SliverToBoxAdapter(
|
||||||
maxCrossAxisExtent: 240,
|
child: Text("${"Search".tl}: ${widget.keyword}",
|
||||||
|
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),)
|
||||||
|
.paddingVertical(12).paddingHorizontal(16),
|
||||||
),
|
),
|
||||||
itemCount: data.length,
|
SliverMasonryGrid(
|
||||||
itemBuilder: (context, index) {
|
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
|
||||||
if(index == data.length - 1){
|
maxCrossAxisExtent: 240,
|
||||||
nextPage();
|
),
|
||||||
}
|
delegate: SliverChildBuilderDelegate(
|
||||||
return IllustWidget(data[index]);
|
(context, index) {
|
||||||
},
|
if(index == data.length - 1){
|
||||||
);
|
nextPage();
|
||||||
});
|
}
|
||||||
|
return IllustWidget(data[index]);
|
||||||
|
},
|
||||||
|
childCount: data.length,
|
||||||
|
),
|
||||||
|
).sliverPaddingHorizontal(8)
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String? nextUrl;
|
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