diff --git a/lib/api/base_api.dart b/lib/api/base_api.dart index dcfb4c6..87f7352 100644 --- a/lib/api/base_api.dart +++ b/lib/api/base_api.dart @@ -3,20 +3,21 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; -import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio_http2_adapter/dio_http2_adapter.dart'; +import 'package:cookie_jar/cookie_jar.dart'; import 'package:flutter/foundation.dart'; import 'package:isekai_wiki/global.dart'; -import 'package:isekai_wiki/utils/api_utils.dart'; +import 'package:path_provider/path_provider.dart'; -class HttpResponseCodeError extends Error { - int? statusCode; +class HttpResponseException implements Exception { + int statusCode; + String? statusText; - HttpResponseCodeError(this.statusCode); + HttpResponseException(this.statusCode, {this.statusText}); @override String toString() { - return "Http error: $statusCode"; + return "Http error: $statusCode $statusText"; } } @@ -24,26 +25,26 @@ class BaseApi { static Dio? _dioInstance; static CookieJar? cookieJar; - static Dio createClient() { + static Future createClient() async { var dio = Dio(); - return dio; - } - - static Dio getClient() { - _dioInstance ??= createClient(); - if (!kIsWeb) { // HTTP2 - _dioInstance!.httpClientAdapter = Http2Adapter( + /* + dio.httpClientAdapter = Http2Adapter( ConnectionManager( idleTimeout: 10000, ), ); + */ // Cookie - cookieJar = PersistCookieJar(); - _dioInstance!.interceptors.add(CookieManager(cookieJar!)); + var tempDir = await getTemporaryDirectory(); + + cookieJar = PersistCookieJar( + storage: FileStorage("${tempDir.path}/cookies"), + ); + dio.interceptors.add(CookieManager(cookieJar!)); // 缓存 final cacheOptions = CacheOptions( @@ -55,11 +56,10 @@ class BaseApi { keyBuilder: CacheOptions.defaultCacheKeyBuilder, allowPostMethod: false, ); - _dioInstance!.interceptors - .add(DioCacheInterceptor(options: cacheOptions)); + dio.interceptors.add(DioCacheInterceptor(options: cacheOptions)); // 自定义Header - _dioInstance!.interceptors.add( + dio.interceptors.add( InterceptorsWrapper(onRequest: (options, handler) { options.headers["X-IsekaiWikiApp-Version"] = Global.packageInfo?.version ?? "unknow"; @@ -69,24 +69,37 @@ class BaseApi { ); } + return dio; + } + + static Future getClient() async { + _dioInstance ??= await createClient(); + return _dioInstance!; } + static Future clearCookie() async { + await cookieJar?.deleteAll(); + } + static Future get(Uri uri, {Map? search}) async { - var res = await getClient().get( + var client = await getClient(); + var res = await client.get( uri.toString(), + queryParameters: search, options: Options(responseType: ResponseType.plain), ); - if (res.statusCode != 200) { - throw HttpResponseCodeError(res.statusCode); + if (res.statusCode != null && res.statusCode != 200) { + throw HttpResponseException(res.statusCode!, + statusText: res.statusMessage!); } return res.data ?? ""; } - static Future getJson(Uri uri) async { - var resText = await get(uri); + static Future getJson(Uri uri, {Map? search}) async { + var resText = await get(uri, search: search); var resData = jsonDecode(resText); if (resData is Map) { diff --git a/lib/api/mw/list.dart b/lib/api/mw/list.dart index 31f1839..87bd5a1 100755 --- a/lib/api/mw/list.dart +++ b/lib/api/mw/list.dart @@ -19,21 +19,9 @@ class MWApiList { } var mwRes = await MWApi.get("query", query: query); + var rcRes = RecentChangesResponse.fromJson(mwRes.data); - if (!mwRes.ok) { - return MWResponse(errorList: mwRes.errorList); - } - if (mwRes.data != null) { - var rcRes = RecentChangesResponse.fromJson(mwRes.data!); - - return MWResponse( - ok: true, - data: rcRes.recentchanges, - continueInfo: mwRes.continueInfo); - } else { - return MWResponse( - errorList: [MWError(code: 'response_data_empty', info: '加载的数据为空')]); - } + return mwRes.replaceData(rcRes.recentchanges); } /// 获取合并的最近更改列表(包括新页面和最近更新) @@ -61,13 +49,11 @@ class MWApiList { var rcResList = await Future.wait([ ignoreRcNew - ? Future.value( - MWResponse>(ok: true, data: [])) + ? Future.value(MWResponse>([])) : getRecentChanges("new", limit: limit, continueInfo: continueInfoNew), ignoreRcEdit - ? Future.value( - MWResponse>(ok: true, data: [])) + ? Future.value(MWResponse>([])) : getRecentChanges("edit", limit: limit, continueInfo: continueInfoEdit), ]); @@ -75,16 +61,7 @@ class MWApiList { var rcNewRes = rcResList[0]; var rcEditRes = rcResList[1]; - if (!rcNewRes.ok) { - return MWResponse(errorList: rcNewRes.errorList); - } else if (!rcEditRes.ok) { - return MWResponse(errorList: rcNewRes.errorList); - } - - List mergedList = [ - ...rcNewRes.data!, - ...rcEditRes.data! - ]; + List mergedList = [...rcNewRes.data, ...rcEditRes.data]; mergedList.sort((a, b) { // 为新页面添加7天的保护期 @@ -120,8 +97,7 @@ class MWApiList { } return MWResponse( - ok: true, - data: uniqueMergedList, + uniqueMergedList, continueInfo: mergedContinueInfo.isNotEmpty ? mergedContinueInfo : null, ); } @@ -165,54 +141,44 @@ class MWApiList { var mwRes = await MWApi.get("query", query: query); - if (!mwRes.ok) { - return MWResponse(errorList: mwRes.errorList); - } - if (mwRes.data != null) { - var pagesRes = PagesResponse.fromJson(mwRes.data!); + var pagesRes = PagesResponse.fromJson(mwRes.data); - var pageList = pagesRes.pages.map((pageInfo) { - if (pageInfo.description != null) { - pageInfo.description = - pageInfo.description!.replaceAll(RegExp(r"\n\n"), "\n"); - } - if (pageInfo.title.contains("/")) { - var splitPos = pageInfo.title.lastIndexOf("/"); - pageInfo.displayTitle = pageInfo.title.substring(splitPos + 1); - pageInfo.subtitle = pageInfo.title.substring(0, splitPos); - } else { - pageInfo.displayTitle = pageInfo.title; - } - return pageInfo; - }).toList(); - // 按照传入顺序对页面进行排序 - List sortedPages = []; - - if (pageids != null) { - for (var pageid in pageids) { - var index = - pageList.indexWhere((element) => element.pageid == pageid); - if (index != -1) { - sortedPages.add(pageList[index]); - } - } + var pageList = pagesRes.pages.map((pageInfo) { + if (pageInfo.description != null) { + pageInfo.description = + pageInfo.description!.replaceAll(RegExp(r"\n\n"), "\n"); } + if (pageInfo.title.contains("/")) { + var splitPos = pageInfo.title.lastIndexOf("/"); + pageInfo.displayTitle = pageInfo.title.substring(splitPos + 1); + pageInfo.subtitle = pageInfo.title.substring(0, splitPos); + } else { + pageInfo.displayTitle = pageInfo.title; + } + return pageInfo; + }).toList(); + // 按照传入顺序对页面进行排序 + List sortedPages = []; - if (titles != null) { - for (var title in titles) { - var index = - pagesRes.pages.indexWhere((element) => element.title == title); - if (index != -1) { - sortedPages.add(pagesRes.pages[index]); - } + if (pageids != null) { + for (var pageid in pageids) { + var index = pageList.indexWhere((element) => element.pageid == pageid); + if (index != -1) { + sortedPages.add(pageList[index]); } } + } - return MWResponse( - ok: true, data: sortedPages, continueInfo: mwRes.continueInfo); - } else { - return MWResponse( - errorList: [MWError(code: 'response_data_empty', info: '加载的数据为空')]); + if (titles != null) { + for (var title in titles) { + var index = + pagesRes.pages.indexWhere((element) => element.title == title); + if (index != -1) { + sortedPages.add(pagesRes.pages[index]); + } + } } + + return mwRes.replaceData(sortedPages); } } diff --git a/lib/api/mw/mw_api.dart b/lib/api/mw/mw_api.dart index 50d67cd..5e1c603 100755 --- a/lib/api/mw/mw_api.dart +++ b/lib/api/mw/mw_api.dart @@ -1,88 +1,64 @@ import 'dart:convert'; -import 'dart:io'; -import 'package:flutter/foundation.dart'; -import 'package:http/http.dart' as http; +import 'package:dio/dio.dart'; import 'package:isekai_wiki/api/base_api.dart'; import 'package:isekai_wiki/global.dart'; +import 'package:json_annotation/json_annotation.dart'; -import '../../utils/api_utils.dart'; +part 'mw_api.g.dart'; + +@JsonSerializable() +class MWApiErrorException implements Exception { + String code; -class MWError { - String? code; String? info; + + @JsonKey(name: "*") String? detail; - MWError({this.code, this.info, this.detail}); + MWApiErrorException({required this.code, this.info, this.detail}); - static MWError fromMap(Map errorMap) { - var mwError = MWError(); - if (errorMap.containsKey("code")) { - mwError.code = errorMap["code"].toString(); - } + factory MWApiErrorException.fromJson(Map json) => + _$MWApiExceptionFromJson(json); - if (errorMap.containsKey("info")) { - mwError.info = errorMap["info"].toString(); - } + Map toJson() => _$MWApiExceptionToJson(this); - if (errorMap.containsKey("*")) { - mwError.detail = errorMap["*"].toString(); - } - - return mwError; + @override + String toString() { + return "MediaWiki Api Error: ${info ?? code}"; } } -class MWMultiError extends Error { - List errorList; +class MWApiEmptyBodyException implements Exception { + dynamic resData; - MWMultiError(this.errorList); + MWApiEmptyBodyException(this.resData); @override String toString() { - return errorList.map((e) => e.info).join("\n"); + return "MediaWiki Api Response body is empty"; } } class MWResponse { - bool ok = false; - List? errorList; - T? data; + List? warnList; + T data; Map? continueInfo; - MWResponse({this.ok = false, this.errorList, this.data, this.continueInfo}); -} - -class MWApiClient extends http.BaseClient { - final http.Client _inner; - - MWApiClient(this._inner); + MWResponse(this.data, {this.warnList, this.continueInfo}); - Future send(http.BaseRequest request) async { - request.headers['user-agent'] = await ApiUtils.getUserAgent(); - return await _inner.send(request); + MWResponse replaceData(N data) { + return MWResponse( + data, + warnList: warnList, + continueInfo: continueInfo, + ); } } class MWApi { static Uri apiBaseUri = Uri.parse(Global.wikiApiUrl); - static HttpClient getHttpClient() { - return HttpClient(); - } - - static Future> _getHeaders() async { - var headers = { - "X-IsekaiWikiApp-Version": Global.packageInfo?.version ?? "unknow", - }; - - if (!kIsWeb) { - headers["User-Agent"] = await ApiUtils.getUserAgent(); - } - - return headers; - } - static Future>> get(String action, {Map? query}) async { Map queryStr = @@ -97,58 +73,62 @@ class MWApi { queryStr["origin"] = Global.webOrigin!; } - Uri requestUri = apiBaseUri.replace(queryParameters: queryStr); - - var res = await http.get(requestUri, headers: await _getHeaders()); + var resText = ""; + try { + resText = await BaseApi.get(apiBaseUri, search: queryStr); + } on DioError catch (err) { + if (err.type == DioErrorType.response) { + if (err.response != null) { + var response = err.response!; + var contentType = response.headers[Headers.contentTypeHeader]; + if (contentType != null && + contentType.contains("application/json") && + response.data is String) { + resText = response.data as String; + } + } + } - if (res.statusCode != 200) { - throw HttpResponseCodeError(res.statusCode); + if (resText.isEmpty) { + // 没有捕获到服务器返回的错误,则抛给上一层 + rethrow; + } } - var responseBody = res.body; - - return parseMWResponse(action, responseBody); + return parseMWResponse(action, resText); } - static MWResponse> parseMWResponse(String action, String resJson) { - var mwRes = MWResponse>(); - List errorList = []; - + static MWResponse> parseMWResponse( + String action, String resJson) { var resData = jsonDecode(resJson); - if (resData is Map) { - // 处理请求错误 - var resError = resData["error"]; - if (resError is Map) { - errorList.add(MWError.fromMap(resError)); - } else if (resError is List) { - for (var errItem in resError) { - if (errItem is Map) { - errorList.add(MWError.fromMap(errItem)); - } - } - } - if (errorList.isNotEmpty) { - mwRes.errorList = errorList; - return mwRes; - } + if (resData is! Map) { + throw MWApiEmptyBodyException(resData); + } - // 请求结果 - if (resData.containsKey(action) && resData[action] is Map) { - mwRes.data = resData[action] as Map; - mwRes.ok = true; - } + // 处理请求错误 + if (resData.containsKey("error")) { + throw MWApiErrorException.fromJson(resData["error"]!); + } - // 继续查询参数 - var batchcomplete = resData["batchcomplete"]; - if (batchcomplete is bool && batchcomplete) { - var continueInfo = resData["continue"]; - if (continueInfo is Map) { - mwRes.continueInfo = {}; - continueInfo.forEach((key, value) { - var keyStr = key.toString(); - mwRes.continueInfo![keyStr] = value.toString(); - }); - } + // 请求结果 + if (!resData.containsKey(action) || + resData[action] is! Map) { + throw MWApiEmptyBodyException(resData); + } + + MWResponse> mwRes = + MWResponse(resData[action] as Map); + + // continue参数 + var batchcomplete = resData["batchcomplete"]; + if (batchcomplete is bool && batchcomplete) { + var continueInfo = resData["continue"]; + if (continueInfo is Map) { + mwRes.continueInfo = {}; + continueInfo.forEach((key, value) { + var keyStr = key; + mwRes.continueInfo![keyStr] = value.toString(); + }); } } diff --git a/lib/api/mw/mw_api.g.dart b/lib/api/mw/mw_api.g.dart new file mode 100644 index 0000000..f00ddc1 --- /dev/null +++ b/lib/api/mw/mw_api.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'mw_api.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MWApiErrorException _$MWApiExceptionFromJson(Map json) => + MWApiErrorException( + code: json['code'] as String, + info: json['info'] as String?, + detail: json['*'] as String?, + ); + +Map _$MWApiExceptionToJson(MWApiErrorException instance) => + { + 'code': instance.code, + 'info': instance.info, + '*': instance.detail, + }; diff --git a/lib/api/mw/token.dart b/lib/api/mw/token.dart new file mode 100644 index 0000000..0f7802d --- /dev/null +++ b/lib/api/mw/token.dart @@ -0,0 +1 @@ +class MWApiToken {} diff --git a/lib/api/mw/user.dart b/lib/api/mw/user.dart index 4a34d89..f602893 100644 --- a/lib/api/mw/user.dart +++ b/lib/api/mw/user.dart @@ -11,18 +11,9 @@ class MWApiUser { var mwRes = await MWApi.get("mugenapp", query: query); - if (!mwRes.ok) { - return MWResponse(errorList: mwRes.errorList); - } - if (mwRes.data != null) { - var authRes = MugenAppStartAuthResponse.fromJson(mwRes.data!); + var data = MugenAppStartAuthResponse.fromJson(mwRes.data); - return MWResponse( - ok: true, data: authRes.startauth, continueInfo: mwRes.continueInfo); - } else { - return MWResponse( - errorList: [MWError(code: 'response_data_empty', info: '加载的数据为空')]); - } + return mwRes.replaceData(data.startauth); } static Future> attemptAuth( @@ -34,20 +25,9 @@ class MWApiUser { var mwRes = await MWApi.get("mugenapp", query: query); - if (!mwRes.ok) { - return MWResponse(errorList: mwRes.errorList); - } - if (mwRes.data != null) { - var authRes = MugenAppAttemptAuthResponse.fromJson(mwRes.data!); + var data = MugenAppAttemptAuthResponse.fromJson(mwRes.data); - return MWResponse( - ok: true, - data: authRes.attemptauth, - continueInfo: mwRes.continueInfo); - } else { - return MWResponse( - errorList: [MWError(code: 'response_data_empty', info: '加载的数据为空')]); - } + return mwRes.replaceData(data.attemptauth); } static Future> getCurrentUserInfo() async { @@ -58,16 +38,8 @@ class MWApiUser { var mwRes = await MWApi.get("query", query: query); - if (!mwRes.ok) { - return MWResponse(errorList: mwRes.errorList); - } - if (mwRes.data != null) { - var userInfoRes = MetaUserInfoResponse.fromJson(mwRes.data!); + var data = MetaUserInfoResponse.fromJson(mwRes.data); - return MWResponse(ok: true, data: userInfoRes); - } else { - return MWResponse( - errorList: [MWError(code: 'response_data_empty', info: '加载的数据为空')]); - } + return mwRes.replaceData(data); } } diff --git a/lib/api/response/parse.dart b/lib/api/response/parse.dart index f2f8774..8a63f25 100644 --- a/lib/api/response/parse.dart +++ b/lib/api/response/parse.dart @@ -8,8 +8,8 @@ class MWParseCategoryInfo { String category; MWParseCategoryInfo({ + required this.category, this.sortkey = "", - this.category = "", }); factory MWParseCategoryInfo.fromJson(Map json) => @@ -27,11 +27,11 @@ class MWParseLangLinkInfo { String title; MWParseLangLinkInfo({ - this.lang = "", + required this.lang, this.url = "", this.langname = "", this.autonym = "", - this.title = "", + required this.title, }); factory MWParseLangLinkInfo.fromJson(Map json) => @@ -47,8 +47,8 @@ class MWParsePageLinkInfo { bool exists; MWParsePageLinkInfo({ - this.ns = -1, - this.title = "", + required this.ns, + required this.title, this.exists = false, }); @@ -109,8 +109,8 @@ class MWParseInfo { Map properties; MWParseInfo({ - this.title = "", - this.pageid = -1, + required this.title, + required this.pageid, this.revid = -1, this.text = "", this.langlink = const [], @@ -130,7 +130,8 @@ class MWParseInfo { this.properties = const {}, }); - factory MWParseInfo.fromJson(Map json) => _$MWParseInfoFromJson(json); + factory MWParseInfo.fromJson(Map json) => + _$MWParseInfoFromJson(json); Map toJson() => _$MWParseInfoToJson(this); } @@ -141,7 +142,8 @@ class MWParseResponse { MWParseResponse({required this.parse}); - factory MWParseResponse.fromJson(Map json) => _$MWParseResponseFromJson(json); + factory MWParseResponse.fromJson(Map json) => + _$MWParseResponseFromJson(json); Map toJson() => _$MWParseResponseToJson(this); } diff --git a/lib/components/recent_page_list.dart b/lib/components/recent_page_list.dart index 850b1b2..3345487 100755 --- a/lib/components/recent_page_list.dart +++ b/lib/components/recent_page_list.dart @@ -18,7 +18,7 @@ class RecentPageListController extends GetxController { var pageList = RxList(); var continueInfo = RxMap(); - var errorList = RxList(); + var errorList = RxList(); var hasNextPage = true.obs; var isLoading = false.obs; @@ -28,7 +28,8 @@ class RecentPageListController extends GetxController { @override void onInit() { scrollController?.addListener(() { - if (scrollController!.position.pixels > scrollController!.position.maxScrollExtent - 10) { + if (scrollController!.position.pixels > + scrollController!.position.maxScrollExtent - 10) { // 滚动到底部 if (hasNextPage.value && !isLoading.value) { loadNextPages(); @@ -70,49 +71,38 @@ class RecentPageListController extends GetxController { isLoading.value = true; - var rcListRes = await MWApiList.getMixedRecentChanges( - limit: 10, continueInfo: continueInfo.isNotEmpty ? continueInfo : null); - if (!rcListRes.ok) { - // 发生错误,展示错误列表 - errorList.value = rcListRes.errorList ?? []; - isLoading.value = false; - hasNextPage.value = false; - if (shouldRefresh) { - pageList.clear(); + try { + var rcListRes = await MWApiList.getMixedRecentChanges( + limit: 10, + continueInfo: continueInfo.isNotEmpty ? continueInfo : null); + + var pageIds = rcListRes.data.map((rcInfo) => rcInfo.pageid).toList(); + var pageListRes = await MWApiList.getPageInfoList( + pageids: pageIds, extractChars: 515, getInWatchlist: uc!.isLoggedIn); + + if (uc!.isLoggedIn) { + // 更新收藏列表信息 + flc!.updateFromPageList(pageListRes.data); } - return; - } - var pageIds = rcListRes.data!.map((rcInfo) => rcInfo.pageid).toList(); - var pageListRes = await MWApiList.getPageInfoList( - pageids: pageIds, extractChars: 515, getInWatchlist: uc!.isLoggedIn); - if (!pageListRes.ok) { - // 发生错误,展示错误列表 - errorList.value = pageListRes.errorList ?? []; - isLoading.value = false; + // 加载完成,设置信息 + if (shouldRefresh) { + // 初次加载,替换全部数据 + pageList.value = pageListRes.data; + shouldRefresh = false; + } else { + pageList.addAll(pageListRes.data); + } + hasNextPage.value = rcListRes.continueInfo != null; + continueInfo.value = rcListRes.continueInfo ?? {}; + } catch (err) { hasNextPage.value = false; if (shouldRefresh) { pageList.clear(); } - return; - } - - if (uc!.isLoggedIn) { - // 更新收藏列表信息 - flc!.updateFromPageList(pageListRes.data!); - } - - // 加载完成,设置信息 - if (shouldRefresh) { - // 初次加载,替换全部数据 - pageList.value = pageListRes.data!; - shouldRefresh = false; - } else { - pageList.addAll(pageListRes.data!); + } finally { + isLoading.value = false; } - hasNextPage.value = rcListRes.continueInfo != null; - continueInfo.value = rcListRes.continueInfo ?? {}; - isLoading.value = false; } Future reload() async { @@ -138,13 +128,14 @@ class RecentPageList extends StatelessWidget { return Column( key: key, children: [ - for (var i = 0; i < 6; i++) PageCard(key: ValueKey("rpl-card-$i"), isLoading: true), + for (var i = 0; i < 6; i++) + PageCard(key: ValueKey("rpl-card-$i"), isLoading: true), ], ); } - Widget _buildPageCard( - int index, PageInfo pageInfo, RecentPageListController c, FavoriteListController fc) { + Widget _buildPageCard(int index, PageInfo pageInfo, + RecentPageListController c, FavoriteListController fc) { return PageCard( key: ValueKey("rpl-card-$index"), pageInfo: c.pageList[index], diff --git a/lib/models/user.dart b/lib/models/user.dart index 7bf10b6..ff12541 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -4,9 +4,13 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_web_browser/flutter_web_browser.dart'; import 'package:get/get.dart'; +import 'package:isekai_wiki/api/base_api.dart'; +import 'package:isekai_wiki/api/mw/mw_api.dart'; import 'package:isekai_wiki/api/mw/user.dart'; +import 'package:isekai_wiki/api/response/mugenapp.dart'; import 'package:isekai_wiki/global.dart'; import 'package:isekai_wiki/utils/dialog.dart'; +import 'package:isekai_wiki/utils/error.dart'; import 'package:json_annotation/json_annotation.dart'; part 'user.g.dart'; @@ -66,18 +70,29 @@ class UserController extends GetxController { /// 更新用户资料,并检测登录状态 Future updateProfile() async { - var userInfoMWRes = await MWApiUser.getCurrentUserInfo(); - if (!userInfoMWRes.ok) { - if (kDebugMode) { - print("Cannot update profile of current user"); - print(userInfoMWRes.errorList); + try { + var userInfoMWRes = await MWApiUser.getCurrentUserInfo(); + + var userInfoRes = userInfoMWRes.data; + + if (userInfoRes.userinfo.id == 0) { + // 登录状态已失效,提示用户 + // 由于刷新用户信息可能出现在首页加载完成之前,这里应该需要一个消息列表 + // 把加载中的错误放在消息列表中,首页加载完成后弹出 + logout(logoutRemote: false); } - } - var userInfoRes = userInfoMWRes.data!; - nickName.value = userInfoRes.userinfo.realname ?? ""; - if (userInfoRes.useravatar != null) { - avatarUrlSet.value = userInfoRes.useravatar!; + userId.value = userInfoRes.userinfo.id; + userName.value = userInfoRes.userinfo.name; + nickName.value = userInfoRes.userinfo.realname ?? ""; + if (userInfoRes.useravatar != null) { + avatarUrlSet.value = userInfoRes.useravatar!; + } + } catch (err, stack) { + if (kDebugMode) { + printError(info: "Cannot update profile of current user: $err"); + stack.printError(); + } } } @@ -158,29 +173,34 @@ class UserController extends GetxController { Future startAuthFlow() async { authProcessing.value = true; - var startAuthRes = await MWApiUser.startAuth(); - if (!startAuthRes.ok) { + try { + var startAuthRes = await MWApiUser.startAuth(); + + var startAuthInfo = startAuthRes.data; + loginRequestToken.value = startAuthInfo.loginRequestKey; + + await FlutterWebBrowser.openWebPage( + url: startAuthInfo.loginUrl, + customTabsOptions: const CustomTabsOptions( + defaultColorSchemeParams: CustomTabsColorSchemeParams( + toolbarColor: Color.fromRGBO(33, 37, 41, 1), + ), + shareState: CustomTabsShareState.off, + showTitle: true, + ), + safariVCOptions: const SafariViewControllerOptions( + barCollapsingEnabled: true, + )); + } catch (err, stack) { authProcessing.value = false; - alert(Get.overlayContext!, startAuthRes.errorList?[0].info ?? "未知错误", - title: "错误"); - return; - } - - var startAuthInfo = startAuthRes.data!; - loginRequestToken.value = startAuthInfo.loginRequestKey; + loginRequestToken.value = ""; - await FlutterWebBrowser.openWebPage( - url: startAuthInfo.loginUrl, - customTabsOptions: const CustomTabsOptions( - defaultColorSchemeParams: CustomTabsColorSchemeParams( - toolbarColor: Color.fromRGBO(33, 37, 41, 1), - ), - shareState: CustomTabsShareState.off, - showTitle: true, - ), - safariVCOptions: const SafariViewControllerOptions( - barCollapsingEnabled: true, - )); + alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err), title: "错误"); + if (kDebugMode) { + print("Exception in startAuthFlow: $err"); + stack.printError(); + } + } } // 尝试通过登录请求Token完成登录 @@ -190,16 +210,21 @@ class UserController extends GetxController { return; } - var attemptAuthRes = await MWApiUser.attemptAuth(loginRequestToken.value); + MWResponse attemptAuthRes; - if (!attemptAuthRes.ok) { + try { + attemptAuthRes = await MWApiUser.attemptAuth(loginRequestToken.value); + } catch (err, stack) { authProcessing.value = false; - alert(Get.overlayContext!, attemptAuthRes.errorList?[0].info ?? "未知错误", - title: "错误"); + alert(Get.overlayContext!, ErrorUtils.getErrorMessage(err), title: "错误"); + if (kDebugMode) { + print("Exception in startAuthFlow: $err"); + stack.printError(); + } return; } - var attemptAuthInfo = attemptAuthRes.data!; + var attemptAuthInfo = attemptAuthRes.data; if (attemptAuthInfo.status == "pending") { authProcessing.value = false; loginRequestToken.value = ""; @@ -208,19 +233,28 @@ class UserController extends GetxController { return; } else if (attemptAuthInfo.status == "success") { + loginRequestToken.value = ""; + userId.value = attemptAuthInfo.userid!; userName.value = attemptAuthInfo.username!; - try { - await updateProfile(); - } catch (err) { - err.printError(info: 'Cannot update profile after auth'); - } + await updateProfile(); authProcessing.value = false; saveToStorage(); } } - Future logout() async {} + Future logout({bool logoutRemote = true}) async { + // 清除Cookie + await BaseApi.clearCookie(); + + // 清除所有用户信息 + userId.value = 0; + userName.value = ""; + nickName.value = ""; + avatarUrlSet.clear(); + + saveToStorage(); + } } diff --git a/lib/pages/own_profile.dart b/lib/pages/own_profile.dart index 05ccd4b..7651d62 100755 --- a/lib/pages/own_profile.dart +++ b/lib/pages/own_profile.dart @@ -6,6 +6,7 @@ import 'package:isekai_wiki/components/isekai_nav_bar.dart'; import 'package:isekai_wiki/models/user.dart'; import 'package:isekai_wiki/pages/about.dart'; import 'package:isekai_wiki/styles.dart'; +import 'package:isekai_wiki/utils/dialog.dart'; import '../components/dummy_icon.dart'; import '../components/follow_scale.dart'; @@ -20,17 +21,28 @@ class OwnProfileController extends GetxController { } Future handleLoginClick() { - handleStartAuth(); - return Future.delayed(const Duration(milliseconds: 100)); + if (uc.authProcessing.isFalse) { + handleStartAuth(); + return Future.delayed(const Duration(milliseconds: 100)); + } else { + return Future.value(); + } } Future handleStartAuth() async { await uc.startAuthFlow(); } - Future handleLogoutClick() { - handleLogout(); - return Future.delayed(const Duration(milliseconds: 100)); + Future handleLogoutClick() async { + if (await confirm( + Get.overlayContext!, + "你想要退出登录吗?", + title: "退出登录", + positiveText: "确定", + isDanger: true, + )) { + handleLogout(); + } } Future handleLogout() async { @@ -41,21 +53,20 @@ class OwnProfileController extends GetxController { class OwnProfileTab extends StatelessWidget { const OwnProfileTab({super.key}); - Widget _buildUserAvatar(UserController uc, {double size = 56}) { + Widget _buildUserAvatar(UserController uc, {double size = 64}) { return Obx(() { var avatarUrl = uc.getAvatar(128); if (avatarUrl != null && avatarUrl.isNotEmpty) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(size / 2)), - ), + return ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(size / 2)), child: CachedNetworkImage( width: size, height: size, placeholder: (_, __) => const CupertinoActivityIndicator(radius: 12), imageUrl: avatarUrl, + fit: BoxFit.cover, ), ); } else { @@ -75,58 +86,65 @@ class OwnProfileTab extends StatelessWidget { return FollowTextScale( child: Obx( - () => !uc.isLoggedIn - ? CupertinoListSection.insetGrouped( - backgroundColor: Styles.themePageBackgroundColor, - children: [ - CupertinoListTile.notched( - title: - const Text('登录/注册', style: Styles.listTileLargeTitle), - padding: const EdgeInsets.only( - left: 6, right: 14, top: 0, bottom: 0), - leading: const DummyIcon( - color: CupertinoColors.systemGrey, - icon: CupertinoIcons.person_fill, - size: 60, - rounded: true, + () => CupertinoListSection.insetGrouped( + backgroundColor: Styles.themePageBackgroundColor, + dividerMargin: uc.isLoggedIn ? 14 : double.infinity, + children: [ + Obx( + () => uc.isLoggedIn + ? CupertinoListTile.notched( + title: Text(uc.getDisplayName, + style: Styles.listTileLargeTitle), + padding: const EdgeInsets.only( + left: 6, right: 14, top: 0, bottom: 0), + leading: _buildUserAvatar(uc), + leadingSize: 80, + leadingToTitle: 4, + trailing: const CupertinoListTileChevron(), + onTap: () {}, + ) + : CupertinoListTile.notched( + title: + const Text('登录/注册', style: Styles.listTileLargeTitle), + padding: const EdgeInsets.only( + left: 6, right: 14, top: 0, bottom: 0), + leading: const DummyIcon( + color: CupertinoColors.systemGrey, + icon: CupertinoIcons.person_fill, + size: 64, + rounded: true, + ), + leadingSize: 80, + leadingToTitle: 4, + trailing: uc.authProcessing.value + ? const Padding( + padding: EdgeInsets.only(right: 5), + child: CupertinoActivityIndicator( + radius: 12, + ), + ) + : const CupertinoListTileChevron(), + onTap: c.handleLoginClick, ), - leadingSize: 80, - leadingToTitle: 4, - trailing: uc.authProcessing.value - ? const Padding( - padding: EdgeInsets.only(right: 5), - child: CupertinoActivityIndicator( - radius: 12, - ), - ) - : const CupertinoListTileChevron(), - onTap: c.handleLoginClick, - ), - ], - ) - : CupertinoListSection.insetGrouped( - backgroundColor: Styles.themePageBackgroundColor, - children: [ - CupertinoListTile.notched( - title: Text(uc.getDisplayName, - style: Styles.listTileLargeTitle), - leading: _buildUserAvatar(uc), - leadingSize: 80, - leadingToTitle: 4, - trailing: const CupertinoListTileChevron(), - onTap: () {}, + ), + AnimatedSize( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: SizedBox( + height: uc.isLoggedIn ? null : 0, + child: CupertinoListTile.notched( + title: const Text('退出登录'), + leading: const DummyIcon( + color: CupertinoColors.systemRed, + icon: CupertinoIcons.arrow_right_square, ), - CupertinoListTile.notched( - title: const Text('退出登录'), - leading: const DummyIcon( - color: CupertinoColors.systemRed, - icon: CupertinoIcons.arrow_right_square, - ), - trailing: const CupertinoListTileChevron(), - onTap: () {}, - ), - ], + trailing: const CupertinoListTileChevron(), + onTap: c.handleLogoutClick, + ), ), + ), + ], + ), ), ); } diff --git a/lib/utils/dialog.dart b/lib/utils/dialog.dart index 56ff799..15d9e89 100644 --- a/lib/utils/dialog.dart +++ b/lib/utils/dialog.dart @@ -26,7 +26,10 @@ Future alert(BuildContext context, String content, {String? title}) { } Future confirm(BuildContext context, String content, - {String? title, String? positiveText, String? negativeText}) { + {String? title, + String? positiveText, + String? negativeText, + bool isDanger = false}) { var c = Completer(); positiveText ??= "好"; @@ -46,7 +49,8 @@ Future confirm(BuildContext context, String content, child: Text(negativeText!), ), CupertinoDialogAction( - isDefaultAction: true, + isDestructiveAction: isDanger, + isDefaultAction: !isDanger, onPressed: () { Navigator.of(context).pop(); c.complete(true); diff --git a/lib/utils/error.dart b/lib/utils/error.dart new file mode 100644 index 0000000..1cae860 --- /dev/null +++ b/lib/utils/error.dart @@ -0,0 +1,34 @@ +import 'package:dio/dio.dart'; +import 'package:http2/http2.dart'; +import 'package:isekai_wiki/api/base_api.dart'; +import 'package:isekai_wiki/api/mw/mw_api.dart'; + +class ErrorUtils { + static String getErrorMessage(dynamic err) { + if (err is MWApiEmptyBodyException) { + return "加载的数据为空"; + } else if (err is MWApiErrorException) { + return err.info ?? err.code; + } else if (err is HttpResponseException) { + return "服务器错误:${err.statusCode} ${err.statusText}"; + } else if (err is TypeError) { + // JSON解析错误 + return "服务器返回的数据不正确"; + } else if (err is DioError) { + if (err.type == DioErrorType.connectTimeout || + err.type == DioErrorType.sendTimeout || + err.type == DioErrorType.receiveTimeout) { + return "链接超时,请重试"; + } else if (err.type == DioErrorType.response) { + return "HTTP错误:${err.response?.statusCode} ${err.response?.statusMessage}"; + } else if (err.type == DioErrorType.other) { + var innerErr = err.error! as Exception; + if (innerErr is StreamTransportException) { + return innerErr.message; + } + } + return err.message; + } + return err.toString(); + } +} diff --git a/pubspec.lock b/pubspec.lock index 48cf61a..9690ce4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -428,7 +428,7 @@ packages: source: hosted version: "0.13.5" http2: - dependency: transitive + dependency: "direct main" description: name: http2 url: "https://pub.dartlang.org" @@ -603,7 +603,7 @@ packages: source: hosted version: "0.2.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index aa754f8..c37515d 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -58,6 +58,8 @@ dependencies: package_info_plus: ^3.0.2 pull_down_button: ^0.4.1 cached_network_image: ^3.2.3 + path_provider: ^2.0.11 + http2: ^2.0.1 dio: ^4.0.6 dio_cookie_manager: ^2.0.0 dio_http2_adapter: ^2.0.0