diff --git a/lib/components/loading.dart b/lib/components/loading.dart index 7b92e71..6521ba4 100644 --- a/lib/components/loading.dart +++ b/lib/components/loading.dart @@ -77,7 +77,14 @@ abstract class MultiPageLoadingState _data!.addAll(value.data); }); } 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); } }); } diff --git a/lib/network/models.dart b/lib/network/models.dart index 7e5e376..4d01e0b 100644 --- a/lib/network/models.dart +++ b/lib/network/models.dart @@ -227,6 +227,12 @@ enum KeywordMatchType { @override 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 { @@ -246,6 +252,8 @@ enum FavoriteNumber { @override toString() => this == FavoriteNumber.unlimited ? "Unlimited" : "$number Bookmarks"; + + String toParam() => this == FavoriteNumber.unlimited ? "" : " ${number}users入り"; } enum SearchSort { @@ -253,16 +261,25 @@ enum SearchSort { oldToNew, popular; + bool get isPremium => appdata.account?.user.isPremium == true; + @override toString() { if(this == SearchSort.popular) { - return appdata.account?.user.isPremium == true ? "Popular" : "Popular(limited)"; + return isPremium ? "Popular" : "Popular(limited)"; } else if(this == SearchSort.newToOld) { return "New to old"; } else { return "Old to new"; } } + + String toParam() => switch(this) { + SearchSort.newToOld => "date_desc", + SearchSort.oldToNew => "date_asc", + // TODO: 等我开个会员 + SearchSort.popular => "", + }; } enum AgeLimit { @@ -276,6 +293,12 @@ enum AgeLimit { @override toString() => text; + + String toParam() => switch(this) { + AgeLimit.unlimited => "", + AgeLimit.allAges => " -R-18", + AgeLimit.r18 => "R-18", + }; } class SearchOptions { diff --git a/lib/network/network.dart b/lib/network/network.dart index 9fd94e6..68fc1a1 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -111,64 +111,28 @@ class Network { appdata.account = account; appdata.writeData(); return const Res(true); - } - catch(e, s){ - Log.error("Network", "$e\n$s"); - return Res.error(e); - } - } - - Future>> apiGet(String path, {Map? query}) async { - try { - if(!path.startsWith("http")) { - path = "$baseUrl$path"; - } - final res = await dio.get>(path, - queryParameters: query, options: Options(headers: _headers, validateStatus: (status) => true)); - if (res.statusCode == 200) { - return Res(res.data!); - } else if(res.statusCode == 400) { - if(res.data.toString().contains("Access Token")) { - var refresh = await refreshToken(); - if(refresh.success) { - return apiGet(path, query: query); - } else { - return Res.error(refresh.errorMessage); - } - } else { - return Res.error("Invalid Status Code: ${res.statusCode}"); - } - } else if((res.statusCode??500) < 500){ - return Res.error(res.data?["error"]?["message"] ?? "Invalid Status code ${res.statusCode}"); - } else { - return Res.error("Invalid Status Code: ${res.statusCode}"); - } - } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e); } } - - Future>> apiPost(String path, {Map? query, Map? data}) async { + + Future>> apiGet(String path, + {Map? query}) async { try { - if(!path.startsWith("http")) { + if (!path.startsWith("http")) { path = "$baseUrl$path"; } - final res = await dio.post>(path, - queryParameters: query, - data: data, - options: Options( - headers: _headers, - validateStatus: (status) => true, - contentType: Headers.formUrlEncodedContentType - )); + final res = await dio.get>(path, + queryParameters: query, + options: + Options(headers: _headers, validateStatus: (status) => true)); if (res.statusCode == 200) { return Res(res.data!); - } else if(res.statusCode == 400) { - if(res.data.toString().contains("Access Token")) { + } else if (res.statusCode == 400) { + if (res.data.toString().contains("Access Token")) { var refresh = await refreshToken(); - if(refresh.success) { + if (refresh.success) { return apiGet(path, query: query); } else { return Res.error(refresh.errorMessage); @@ -176,12 +140,50 @@ class Network { } else { return Res.error("Invalid Status Code: ${res.statusCode}"); } - } else if((res.statusCode??500) < 500){ - return Res.error(res.data?["error"]?["message"] ?? "Invalid Status code ${res.statusCode}"); + } else if ((res.statusCode ?? 500) < 500) { + return Res.error(res.data?["error"]?["message"] ?? + "Invalid Status code ${res.statusCode}"); } else { return Res.error("Invalid Status Code: ${res.statusCode}"); } + } catch (e, s) { + Log.error("Network", "$e\n$s"); + return Res.error(e); + } + } + Future>> apiPost(String path, + {Map? query, Map? data}) async { + try { + if (!path.startsWith("http")) { + path = "$baseUrl$path"; + } + final res = await dio.post>(path, + queryParameters: query, + data: data, + options: Options( + headers: _headers, + validateStatus: (status) => true, + contentType: Headers.formUrlEncodedContentType)); + if (res.statusCode == 200) { + return Res(res.data!); + } else if (res.statusCode == 400) { + if (res.data.toString().contains("Access Token")) { + var refresh = await refreshToken(); + if (refresh.success) { + return apiGet(path, query: query); + } else { + return Res.error(refresh.errorMessage); + } + } else { + return Res.error("Invalid Status Code: ${res.statusCode}"); + } + } else if ((res.statusCode ?? 500) < 500) { + return Res.error(res.data?["error"]?["message"] ?? + "Invalid Status code ${res.statusCode}"); + } else { + return Res.error("Invalid Status Code: ${res.statusCode}"); + } } catch (e, s) { Log.error("Network", "$e\n$s"); return Res.error(e); @@ -189,8 +191,9 @@ class Network { } /// get user details - Future> getUserDetails(Object userId) async{ - var res = await apiGet("/v1/user/detail", query: {"user_id": userId, "filter": "for_android"}); + Future> getUserDetails(Object userId) async { + var res = await apiGet("/v1/user/detail", + query: {"user_id": userId, "filter": "for_android"}); if (res.success) { return Res(UserDetails.fromJson(res.data)); } else { @@ -199,60 +202,115 @@ class Network { } Future>> 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) { - 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 { return Res.error(res.errorMessage); } } - Future>> getBookmarkedIllusts(String restrict, [String? nextUrl]) async { - var res = await apiGet(nextUrl ?? "/v1/user/bookmarks/illust?user_id=49258688&restrict=$restrict"); + Future>> getBookmarkedIllusts(String restrict, + [String? nextUrl]) async { + var res = await apiGet(nextUrl ?? + "/v1/user/bookmarks/illust?user_id=49258688&restrict=$restrict"); 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 { return Res.error(res.errorMessage); } } - Future> addBookmark(String id, String method, [String type = "public"]) async { - var res = method == "add" ? await apiPost("/v2/illust/bookmark/$method", data: { - "illust_id": id, - "restrict": type - }) : await apiPost("/v1/illust/bookmark/$method", data: { - "illust_id": id, - }); - if(!res.error) { + Future> addBookmark(String id, String method, + [String type = "public"]) async { + var res = method == "add" + ? await apiPost("/v2/illust/bookmark/$method", + data: {"illust_id": id, "restrict": type}) + : await apiPost("/v1/illust/bookmark/$method", data: { + "illust_id": id, + }); + if (!res.error) { return const Res(true); } else { return Res.fromErrorRes(res); } } - Future> follow(String uid, String method, [String type = "public"]) async { - var res = method == "add" ? await apiPost("/v1/user/follow/add", data: { - "user_id": uid, - "restrict": type - }) : await apiPost("/v1/user/follow/delete", data: { - "user_id": uid, - }); - if(!res.error) { + Future> follow(String uid, String method, + [String type = "public"]) async { + var res = method == "add" + ? await apiPost("/v1/user/follow/add", + data: {"user_id": uid, "restrict": type}) + : await apiPost("/v1/user/follow/delete", data: { + "user_id": uid, + }); + if (!res.error) { return const Res(true); } else { return Res.fromErrorRes(res); } } - + Future>> getHotTags() async { - var res = await apiGet("/v1/trending-tags/illust?filter=for_android&include_translated_tag_results=true"); - if(res.error) { + var res = await apiGet( + "/v1/trending-tags/illust?filter=for_android&include_translated_tag_results=true"); + if (res.error) { return Res.fromErrorRes(res); } else { return Res(List.from(res.data["trend_tags"].map((e) => TrendingTag( - Tag(e["tag"], e["translated_name"]), - Illust.fromJson(e["illust"]) - )))); + Tag(e["tag"], e["translated_name"]), Illust.fromJson(e["illust"]))))); + } + } + + Future>> 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>> 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>> 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); } } } diff --git a/lib/pages/bookmarks.dart b/lib/pages/bookmarks.dart index 52eabee..0289964 100644 --- a/lib/pages/bookmarks.dart +++ b/lib/pages/bookmarks.dart @@ -86,7 +86,7 @@ class _OneBookmarkedPageState extends MultiPageLoadingState<_OneBookmarkedPage, var res = await Network().getBookmarkedIllusts(widget.restrict, nextUrl); if(!res.error) { nextUrl = res.subData; - nextUrl ?? "end"; + nextUrl ??= "end"; } return res; } diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 0182fe5..51976ce 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -1,6 +1,7 @@ import "dart:async"; import "package:fluent_ui/fluent_ui.dart"; +import "package:flutter/foundation.dart"; import "package:pixes/appdata.dart"; import "package:pixes/components/color_scheme.dart"; import "package:pixes/components/md.dart"; @@ -433,7 +434,7 @@ class UserPane extends PaneItem { fontSize: 16, fontWeight: FontWeight.w500), ), Text( - appdata.account!.user.email, + kDebugMode ? "" : appdata.account!.user.email, style: const TextStyle(fontSize: 12), ) ], diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 3f5b377..c890be3 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -10,6 +10,7 @@ import 'package:pixes/utils/translation.dart'; import '../components/animated_image.dart'; import '../components/color_scheme.dart'; +import '../components/illust_widget.dart'; import '../foundation/image_provider.dart'; class SearchPage extends StatefulWidget { @@ -24,17 +25,30 @@ class _SearchPageState extends State { int searchType = 0; + static const searchTypes = [ + "Search artwork", + "Search novel", + "Search user", + "Artwork ID", + "Artist ID", + "Novel ID" + ]; + void search() { switch(searchType) { case 0: context.to(() => SearchResultPage(text)); case 1: - // TODO: artwork by id - throw UnimplementedError(); + // TODO: novel search case 2: - context.to(() => UserInfoPage(text)); + // TODO: user search case 3: - // TODO: novel page + // TODO: artwork id + throw UnimplementedError(); + case 4: + context.to(() => UserInfoPage(text)); + case 5: + // TODO: novel id throw UnimplementedError(); } } @@ -132,13 +146,6 @@ class _SearchPageState extends State { ); } - static const searchTypes = [ - "Keyword search", - "Artwork ID", - "Artist ID", - "Novel ID" - ]; - Widget buildSearchOption(BuildContext context) { return MenuFlyout( items: List.generate( @@ -351,10 +358,40 @@ class SearchResultPage extends StatefulWidget { State createState() => _SearchResultPageState(); } -class _SearchResultPageState extends State { +class _SearchResultPageState extends MultiPageLoadingState { @override - Widget build(BuildContext context) { - return const ScaffoldPage(); + Widget buildContent(BuildContext context, final List data) { + 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>> 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; } }