search artworks

This commit is contained in:
wgh19
2024-05-13 15:38:57 +08:00
parent 63ddb97183
commit 4b0ffa8b68
6 changed files with 222 additions and 96 deletions

View File

@@ -77,7 +77,14 @@ abstract class MultiPageLoadingState<T extends StatefulWidget, S extends Object>
_data!.addAll(value.data); _data!.addAll(value.data);
}); });
} else { } else {
context.showToast(message: "Network Error"); var message = value.errorMessage ?? "Network Error";
if(message == "No more data") {
return;
}
if(message.length > 20) {
message = "${message.substring(0, 20)}...";
}
context.showToast(message: message);
} }
}); });
} }

View File

@@ -227,6 +227,12 @@ enum KeywordMatchType {
@override @override
toString() => text; toString() => text;
String toParam() => switch(this) {
KeywordMatchType.tagsPartialMatches => "partial_match_for_tags",
KeywordMatchType.tagsExactMatch => "exact_match_for_tags",
KeywordMatchType.titleOrDescriptionSearch => "title_and_caption"
};
} }
enum FavoriteNumber { enum FavoriteNumber {
@@ -246,6 +252,8 @@ enum FavoriteNumber {
@override @override
toString() => this == FavoriteNumber.unlimited ? "Unlimited" : "$number Bookmarks"; toString() => this == FavoriteNumber.unlimited ? "Unlimited" : "$number Bookmarks";
String toParam() => this == FavoriteNumber.unlimited ? "" : " ${number}users入り";
} }
enum SearchSort { enum SearchSort {
@@ -253,16 +261,25 @@ enum SearchSort {
oldToNew, oldToNew,
popular; popular;
bool get isPremium => appdata.account?.user.isPremium == true;
@override @override
toString() { toString() {
if(this == SearchSort.popular) { if(this == SearchSort.popular) {
return appdata.account?.user.isPremium == true ? "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 { } else {
return "Old to new"; return "Old to new";
} }
} }
String toParam() => switch(this) {
SearchSort.newToOld => "date_desc",
SearchSort.oldToNew => "date_asc",
// TODO: 等我开个会员
SearchSort.popular => "",
};
} }
enum AgeLimit { enum AgeLimit {
@@ -276,6 +293,12 @@ enum AgeLimit {
@override @override
toString() => text; toString() => text;
String toParam() => switch(this) {
AgeLimit.unlimited => "",
AgeLimit.allAges => " -R-18",
AgeLimit.r18 => "R-18",
};
} }
class SearchOptions { class SearchOptions {

View File

@@ -111,20 +111,22 @@ class Network {
appdata.account = account; appdata.account = account;
appdata.writeData(); appdata.writeData();
return const Res(true); return const Res(true);
} } catch (e, s) {
catch(e, s){
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e); return Res.error(e);
} }
} }
Future<Res<Map<String, dynamic>>> apiGet(String path, {Map<String, dynamic>? query}) async { Future<Res<Map<String, dynamic>>> apiGet(String path,
{Map<String, dynamic>? query}) async {
try { try {
if (!path.startsWith("http")) { if (!path.startsWith("http")) {
path = "$baseUrl$path"; path = "$baseUrl$path";
} }
final res = await dio.get<Map<String, dynamic>>(path, final res = await dio.get<Map<String, dynamic>>(path,
queryParameters: query, options: Options(headers: _headers, validateStatus: (status) => true)); queryParameters: query,
options:
Options(headers: _headers, validateStatus: (status) => true));
if (res.statusCode == 200) { if (res.statusCode == 200) {
return Res(res.data!); return Res(res.data!);
} else if (res.statusCode == 400) { } else if (res.statusCode == 400) {
@@ -139,18 +141,19 @@ class Network {
return Res.error("Invalid Status Code: ${res.statusCode}"); return Res.error("Invalid Status Code: ${res.statusCode}");
} }
} else if ((res.statusCode ?? 500) < 500) { } else if ((res.statusCode ?? 500) < 500) {
return Res.error(res.data?["error"]?["message"] ?? "Invalid Status code ${res.statusCode}"); return Res.error(res.data?["error"]?["message"] ??
"Invalid Status code ${res.statusCode}");
} else { } else {
return Res.error("Invalid Status Code: ${res.statusCode}"); return Res.error("Invalid Status Code: ${res.statusCode}");
} }
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e); return Res.error(e);
} }
} }
Future<Res<Map<String, dynamic>>> apiPost(String path, {Map<String, dynamic>? query, Map<String, dynamic>? data}) async { Future<Res<Map<String, dynamic>>> apiPost(String path,
{Map<String, dynamic>? query, Map<String, dynamic>? data}) async {
try { try {
if (!path.startsWith("http")) { if (!path.startsWith("http")) {
path = "$baseUrl$path"; path = "$baseUrl$path";
@@ -161,8 +164,7 @@ class Network {
options: Options( options: Options(
headers: _headers, headers: _headers,
validateStatus: (status) => true, validateStatus: (status) => true,
contentType: Headers.formUrlEncodedContentType contentType: Headers.formUrlEncodedContentType));
));
if (res.statusCode == 200) { if (res.statusCode == 200) {
return Res(res.data!); return Res(res.data!);
} else if (res.statusCode == 400) { } else if (res.statusCode == 400) {
@@ -177,11 +179,11 @@ class Network {
return Res.error("Invalid Status Code: ${res.statusCode}"); return Res.error("Invalid Status Code: ${res.statusCode}");
} }
} else if ((res.statusCode ?? 500) < 500) { } else if ((res.statusCode ?? 500) < 500) {
return Res.error(res.data?["error"]?["message"] ?? "Invalid Status code ${res.statusCode}"); return Res.error(res.data?["error"]?["message"] ??
"Invalid Status code ${res.statusCode}");
} else { } else {
return Res.error("Invalid Status Code: ${res.statusCode}"); return Res.error("Invalid Status Code: ${res.statusCode}");
} }
} catch (e, s) { } catch (e, s) {
Log.error("Network", "$e\n$s"); Log.error("Network", "$e\n$s");
return Res.error(e); return Res.error(e);
@@ -190,7 +192,8 @@ class Network {
/// get user details /// get user details
Future<Res<UserDetails>> getUserDetails(Object userId) async { Future<Res<UserDetails>> getUserDetails(Object userId) async {
var res = await apiGet("/v1/user/detail", query: {"user_id": userId, "filter": "for_android"}); var res = await apiGet("/v1/user/detail",
query: {"user_id": userId, "filter": "for_android"});
if (res.success) { if (res.success) {
return Res(UserDetails.fromJson(res.data)); return Res(UserDetails.fromJson(res.data));
} else { } else {
@@ -199,28 +202,36 @@ class Network {
} }
Future<Res<List<Illust>>> getRecommendedIllusts() async { Future<Res<List<Illust>>> getRecommendedIllusts() async {
var res = await apiGet("/v1/illust/recommended?include_privacy_policy=true&filter=for_android&include_ranking_illusts=true"); var res = await apiGet(
"/v1/illust/recommended?include_privacy_policy=true&filter=for_android&include_ranking_illusts=true");
if (res.success) { if (res.success) {
return Res((res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList()); return Res((res.data["illusts"] as List)
.map((e) => Illust.fromJson(e))
.toList());
} else { } else {
return Res.error(res.errorMessage); return Res.error(res.errorMessage);
} }
} }
Future<Res<List<Illust>>> getBookmarkedIllusts(String restrict, [String? nextUrl]) async { Future<Res<List<Illust>>> getBookmarkedIllusts(String restrict,
var res = await apiGet(nextUrl ?? "/v1/user/bookmarks/illust?user_id=49258688&restrict=$restrict"); [String? nextUrl]) async {
var res = await apiGet(nextUrl ??
"/v1/user/bookmarks/illust?user_id=49258688&restrict=$restrict");
if (res.success) { if (res.success) {
return Res((res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(), subData: res.data["next_url"]); return Res(
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
subData: res.data["next_url"]);
} else { } else {
return Res.error(res.errorMessage); return Res.error(res.errorMessage);
} }
} }
Future<Res<bool>> addBookmark(String id, String method, [String type = "public"]) async { Future<Res<bool>> addBookmark(String id, String method,
var res = method == "add" ? await apiPost("/v2/illust/bookmark/$method", data: { [String type = "public"]) async {
"illust_id": id, var res = method == "add"
"restrict": type ? await apiPost("/v2/illust/bookmark/$method",
}) : await apiPost("/v1/illust/bookmark/$method", data: { data: {"illust_id": id, "restrict": type})
: await apiPost("/v1/illust/bookmark/$method", data: {
"illust_id": id, "illust_id": id,
}); });
if (!res.error) { if (!res.error) {
@@ -230,11 +241,12 @@ class Network {
} }
} }
Future<Res<bool>> follow(String uid, String method, [String type = "public"]) async { Future<Res<bool>> follow(String uid, String method,
var res = method == "add" ? await apiPost("/v1/user/follow/add", data: { [String type = "public"]) async {
"user_id": uid, var res = method == "add"
"restrict": type ? await apiPost("/v1/user/follow/add",
}) : await apiPost("/v1/user/follow/delete", data: { data: {"user_id": uid, "restrict": type})
: await apiPost("/v1/user/follow/delete", data: {
"user_id": uid, "user_id": uid,
}); });
if (!res.error) { if (!res.error) {
@@ -245,14 +257,60 @@ class Network {
} }
Future<Res<List<TrendingTag>>> getHotTags() async { Future<Res<List<TrendingTag>>> getHotTags() async {
var res = await apiGet("/v1/trending-tags/illust?filter=for_android&include_translated_tag_results=true"); var res = await apiGet(
"/v1/trending-tags/illust?filter=for_android&include_translated_tag_results=true");
if (res.error) { if (res.error) {
return Res.fromErrorRes(res); return Res.fromErrorRes(res);
} else { } else {
return Res(List.from(res.data["trend_tags"].map((e) => TrendingTag( return Res(List.from(res.data["trend_tags"].map((e) => TrendingTag(
Tag(e["tag"], e["translated_name"]), Tag(e["tag"], e["translated_name"]), Illust.fromJson(e["illust"])))));
Illust.fromJson(e["illust"]) }
)))); }
Future<Res<List<Illust>>> search(
String keyword, SearchOptions options) async {
String path = "";
final encodedKeyword = Uri.encodeComponent(keyword +
options.favoriteNumber.toParam() +
options.ageLimit.toParam());
if (options.sort == SearchSort.popular && !options.sort.isPremium) {
path =
"/v1/search/popular-preview/illust?filter=for_android&include_translated_tag_results=true&merge_plain_keyword_results=true&word=$encodedKeyword&search_target=${options.matchType.toParam()}";
} else {
path =
"/v1/search/illust?filter=for_android&include_translated_tag_results=true&merge_plain_keyword_results=true&word=$encodedKeyword&sort=${options.sort.toParam()}&search_target=${options.matchType.toParam()}";
}
var res = await apiGet(path);
if (res.success) {
return Res(
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
subData: res.data["next_url"]);
} else {
return Res.error(res.errorMessage);
}
}
Future<Res<List<Illust>>> getIllustsWithNextUrl(String nextUrl) async{
var res = await apiGet(nextUrl);
if (res.success) {
return Res(
(res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(),
subData: res.data["next_url"]);
} else {
return Res.error(res.errorMessage);
}
}
Future<Res<List<User>>> 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(),
subData: res.data["next_url"]);
} else {
return Res.error(res.errorMessage);
} }
} }
} }

View File

@@ -86,7 +86,7 @@ class _OneBookmarkedPageState extends MultiPageLoadingState<_OneBookmarkedPage,
var res = await Network().getBookmarkedIllusts(widget.restrict, nextUrl); var res = await Network().getBookmarkedIllusts(widget.restrict, nextUrl);
if(!res.error) { if(!res.error) {
nextUrl = res.subData; nextUrl = res.subData;
nextUrl ?? "end"; nextUrl ??= "end";
} }
return res; return res;
} }

View File

@@ -1,6 +1,7 @@
import "dart:async"; import "dart:async";
import "package:fluent_ui/fluent_ui.dart"; import "package:fluent_ui/fluent_ui.dart";
import "package:flutter/foundation.dart";
import "package:pixes/appdata.dart"; import "package:pixes/appdata.dart";
import "package:pixes/components/color_scheme.dart"; import "package:pixes/components/color_scheme.dart";
import "package:pixes/components/md.dart"; import "package:pixes/components/md.dart";
@@ -433,7 +434,7 @@ class UserPane extends PaneItem {
fontSize: 16, fontWeight: FontWeight.w500), fontSize: 16, fontWeight: FontWeight.w500),
), ),
Text( Text(
appdata.account!.user.email, kDebugMode ? "<hide due to debug>" : appdata.account!.user.email,
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
) )
], ],

View File

@@ -10,6 +10,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/illust_widget.dart';
import '../foundation/image_provider.dart'; import '../foundation/image_provider.dart';
class SearchPage extends StatefulWidget { class SearchPage extends StatefulWidget {
@@ -24,17 +25,30 @@ class _SearchPageState extends State<SearchPage> {
int searchType = 0; int searchType = 0;
static const searchTypes = [
"Search artwork",
"Search novel",
"Search user",
"Artwork ID",
"Artist ID",
"Novel ID"
];
void search() { void search() {
switch(searchType) { switch(searchType) {
case 0: case 0:
context.to(() => SearchResultPage(text)); context.to(() => SearchResultPage(text));
case 1: case 1:
// TODO: artwork by id // TODO: novel search
throw UnimplementedError();
case 2: case 2:
context.to(() => UserInfoPage(text)); // TODO: user search
case 3: case 3:
// TODO: novel page // TODO: artwork id
throw UnimplementedError();
case 4:
context.to(() => UserInfoPage(text));
case 5:
// TODO: novel id
throw UnimplementedError(); throw UnimplementedError();
} }
} }
@@ -132,13 +146,6 @@ class _SearchPageState extends State<SearchPage> {
); );
} }
static const searchTypes = [
"Keyword search",
"Artwork ID",
"Artist ID",
"Novel ID"
];
Widget buildSearchOption(BuildContext context) { Widget buildSearchOption(BuildContext context) {
return MenuFlyout( return MenuFlyout(
items: List.generate( items: List.generate(
@@ -351,10 +358,40 @@ class SearchResultPage extends StatefulWidget {
State<SearchResultPage> createState() => _SearchResultPageState(); State<SearchResultPage> createState() => _SearchResultPageState();
} }
class _SearchResultPageState extends State<SearchResultPage> { class _SearchResultPageState extends MultiPageLoadingState<SearchResultPage, Illust> {
@override @override
Widget build(BuildContext context) { Widget buildContent(BuildContext context, final List<Illust> data) {
return const ScaffoldPage(); return LayoutBuilder(builder: (context, constrains){
return MasonryGridView.builder(
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 240,
),
itemCount: data.length,
itemBuilder: (context, index) {
if(index == data.length - 1){
nextPage();
}
return IllustWidget(data[index]);
},
);
});
}
String? nextUrl;
@override
Future<Res<List<Illust>>> loadData(page) async{
if(nextUrl == "end") {
return Res.error("No more data");
}
var res = nextUrl == null
? await Network().search(widget.keyword, appdata.searchOptions)
: await Network().getIllustsWithNextUrl(nextUrl!);
if(!res.error) {
nextUrl = res.subData;
nextUrl ??= "end";
}
return res;
} }
} }