完成登录和本地登出

main
落雨楓 2 years ago
parent 28d9d832c3
commit a9e70fccac

@ -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<Dio> 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<Dio> getClient() async {
_dioInstance ??= await createClient();
return _dioInstance!;
}
static Future<void> clearCookie() async {
await cookieJar?.deleteAll();
}
static Future<String> get(Uri uri, {Map<String, dynamic>? search}) async {
var res = await getClient().get<String>(
var client = await getClient();
var res = await client.get<String>(
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<Map> getJson(Uri uri) async {
var resText = await get(uri);
static Future<Map> getJson(Uri uri, {Map<String, dynamic>? search}) async {
var resText = await get(uri, search: search);
var resData = jsonDecode(resText);
if (resData is Map) {

@ -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<List<RecentChangesItem>>(ok: true, data: []))
? Future.value(MWResponse<List<RecentChangesItem>>([]))
: getRecentChanges("new",
limit: limit, continueInfo: continueInfoNew),
ignoreRcEdit
? Future.value(
MWResponse<List<RecentChangesItem>>(ok: true, data: []))
? Future.value(MWResponse<List<RecentChangesItem>>([]))
: 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<RecentChangesItem> mergedList = [
...rcNewRes.data!,
...rcEditRes.data!
];
List<RecentChangesItem> 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<PageInfo> 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<PageInfo> 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);
}
}

@ -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<String, dynamic> json) =>
_$MWApiExceptionFromJson(json);
if (errorMap.containsKey("info")) {
mwError.info = errorMap["info"].toString();
}
Map<String, dynamic> 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<MWError> 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<T> {
bool ok = false;
List<MWError>? errorList;
T? data;
List<dynamic>? warnList;
T data;
Map<String, String>? 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<http.StreamedResponse> send(http.BaseRequest request) async {
request.headers['user-agent'] = await ApiUtils.getUserAgent();
return await _inner.send(request);
MWResponse<N> replaceData<N>(N data) {
return MWResponse<N>(
data,
warnList: warnList,
continueInfo: continueInfo,
);
}
}
class MWApi {
static Uri apiBaseUri = Uri.parse(Global.wikiApiUrl);
static HttpClient getHttpClient() {
return HttpClient();
}
static Future<Map<String, String>> _getHeaders() async {
var headers = {
"X-IsekaiWikiApp-Version": Global.packageInfo?.version ?? "unknow",
};
if (!kIsWeb) {
headers["User-Agent"] = await ApiUtils.getUserAgent();
}
return headers;
}
static Future<MWResponse<Map<String, dynamic>>> get(String action,
{Map<String, dynamic>? query}) async {
Map<String, String> 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<Map<String, dynamic>> parseMWResponse(String action, String resJson) {
var mwRes = MWResponse<Map<String, dynamic>>();
List<MWError> errorList = [];
static MWResponse<Map<String, dynamic>> 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<String, dynamic>) {
throw MWApiEmptyBodyException(resData);
}
//
if (resData.containsKey(action) && resData[action] is Map) {
mwRes.data = resData[action] as Map<String, dynamic>;
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<String, dynamic>) {
throw MWApiEmptyBodyException(resData);
}
MWResponse<Map<String, dynamic>> mwRes =
MWResponse(resData[action] as Map<String, dynamic>);
// continue
var batchcomplete = resData["batchcomplete"];
if (batchcomplete is bool && batchcomplete) {
var continueInfo = resData["continue"];
if (continueInfo is Map<String, dynamic>) {
mwRes.continueInfo = {};
continueInfo.forEach((key, value) {
var keyStr = key;
mwRes.continueInfo![keyStr] = value.toString();
});
}
}

@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'mw_api.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
MWApiErrorException _$MWApiExceptionFromJson(Map<String, dynamic> json) =>
MWApiErrorException(
code: json['code'] as String,
info: json['info'] as String?,
detail: json['*'] as String?,
);
Map<String, dynamic> _$MWApiExceptionToJson(MWApiErrorException instance) =>
<String, dynamic>{
'code': instance.code,
'info': instance.info,
'*': instance.detail,
};

@ -0,0 +1 @@
class MWApiToken {}

@ -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<MWResponse<MugenAppAttemptAuthInfo>> 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<MWResponse<MetaUserInfoResponse>> 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);
}
}

@ -8,8 +8,8 @@ class MWParseCategoryInfo {
String category;
MWParseCategoryInfo({
required this.category,
this.sortkey = "",
this.category = "",
});
factory MWParseCategoryInfo.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) => _$MWParseInfoFromJson(json);
factory MWParseInfo.fromJson(Map<String, dynamic> json) =>
_$MWParseInfoFromJson(json);
Map<String, dynamic> toJson() => _$MWParseInfoToJson(this);
}
@ -141,7 +142,8 @@ class MWParseResponse {
MWParseResponse({required this.parse});
factory MWParseResponse.fromJson(Map<String, dynamic> json) => _$MWParseResponseFromJson(json);
factory MWParseResponse.fromJson(Map<String, dynamic> json) =>
_$MWParseResponseFromJson(json);
Map<String, dynamic> toJson() => _$MWParseResponseToJson(this);
}

@ -18,7 +18,7 @@ class RecentPageListController extends GetxController {
var pageList = RxList<PageInfo>();
var continueInfo = RxMap<String, String>();
var errorList = RxList<MWError>();
var errorList = RxList<MWApiErrorException>();
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<void> 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],

@ -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<void> 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<void> 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<MugenAppAttemptAuthInfo> 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<void> logout() async {}
Future<void> logout({bool logoutRemote = true}) async {
// Cookie
await BaseApi.clearCookie();
//
userId.value = 0;
userName.value = "";
nickName.value = "";
avatarUrlSet.clear();
saveToStorage();
}
}

@ -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<void> 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<void> handleStartAuth() async {
await uc.startAuthFlow();
}
Future<void> handleLogoutClick() {
handleLogout();
return Future.delayed(const Duration(milliseconds: 100));
Future<void> handleLogoutClick() async {
if (await confirm(
Get.overlayContext!,
"你想要退出登录吗?",
title: "退出登录",
positiveText: "确定",
isDanger: true,
)) {
handleLogout();
}
}
Future<void> 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>[
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: <Widget>[
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>[
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,
),
),
),
],
),
),
);
}

@ -26,7 +26,10 @@ Future<void> alert(BuildContext context, String content, {String? title}) {
}
Future<bool> confirm(BuildContext context, String content,
{String? title, String? positiveText, String? negativeText}) {
{String? title,
String? positiveText,
String? negativeText,
bool isDanger = false}) {
var c = Completer<bool>();
positiveText ??= "";
@ -46,7 +49,8 @@ Future<bool> confirm(BuildContext context, String content,
child: Text(negativeText!),
),
CupertinoDialogAction(
isDefaultAction: true,
isDestructiveAction: isDanger,
isDefaultAction: !isDanger,
onPressed: () {
Navigator.of(context).pop();
c.complete(true);

@ -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();
}
}

@ -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"

@ -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

Loading…
Cancel
Save