mirror of
https://github.com/wgh136/pixes.git
synced 2025-09-27 21:07:24 +00:00
search artworks
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user