完成基础的登录流程

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

@ -7,7 +7,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
@ -24,6 +24,14 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- Deep Links -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="isekaiwiki" android:host="*" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@ -31,4 +39,10 @@
android:name="flutterEmbedding"
android:value="2" />
</application>
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>

@ -1,9 +1,19 @@
PODS:
- Flutter (1.0.0)
- flutter_inappwebview (0.0.1):
- Flutter
- flutter_inappwebview/Core (= 0.0.1)
- OrderedSet (~> 5.0)
- flutter_inappwebview/Core (0.0.1):
- Flutter
- OrderedSet (~> 5.0)
- flutter_web_browser (0.17.1):
- Flutter
- OrderedSet (5.0.0)
- package_info_plus (0.4.5):
- Flutter
- shared_preferences_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- wakelock (0.0.1):
@ -13,19 +23,29 @@ PODS:
DEPENDENCIES:
- Flutter (from `Flutter`)
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
- flutter_web_browser (from `.symlinks/plugins/flutter_web_browser/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
SPEC REPOS:
trunk:
- OrderedSet
EXTERNAL SOURCES:
Flutter:
:path: Flutter
flutter_inappwebview:
:path: ".symlinks/plugins/flutter_inappwebview/ios"
flutter_web_browser:
:path: ".symlinks/plugins/flutter_web_browser/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/ios"
wakelock:
@ -35,8 +55,11 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
flutter_web_browser: 7bccaafbb0c5b8862afe7bcd158f15557109f61f
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f

@ -1,9 +1,13 @@
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:flutter/foundation.dart';
import 'package:isekai_wiki/global.dart';
import 'package:isekai_wiki/utils/api_utils.dart';
import 'package:http/http.dart' as http;
class HttpResponseCodeError extends Error {
int? statusCode;
@ -17,25 +21,68 @@ class HttpResponseCodeError extends Error {
}
class BaseApi {
static Future<Map<String, String>> _getHeaders() async {
Map<String, String> headers = {};
static Dio? _dioInstance;
static CookieJar? cookieJar;
static Dio createClient() {
var dio = Dio();
return dio;
}
static Dio getClient() {
_dioInstance ??= createClient();
if (!kIsWeb) {
headers["X-IsekaiWikiApp-Version"] = Global.packageInfo?.version ?? "unknow";
headers["User-Agent"] = await ApiUtils.getUserAgent();
// HTTP2
_dioInstance!.httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: 10000,
),
);
// Cookie
cookieJar = PersistCookieJar();
_dioInstance!.interceptors.add(CookieManager(cookieJar!));
//
final cacheOptions = CacheOptions(
store: MemCacheStore(),
policy: CachePolicy.request,
maxStale: const Duration(days: 7),
priority: CachePriority.normal,
cipher: null,
keyBuilder: CacheOptions.defaultCacheKeyBuilder,
allowPostMethod: false,
);
_dioInstance!.interceptors
.add(DioCacheInterceptor(options: cacheOptions));
// Header
_dioInstance!.interceptors.add(
InterceptorsWrapper(onRequest: (options, handler) {
options.headers["X-IsekaiWikiApp-Version"] =
Global.packageInfo?.version ?? "unknow";
options.headers["User-Agent"] = "";
return handler.next(options);
}),
);
}
return headers;
return _dioInstance!;
}
static Future<String> get(Uri uri, {Map<String, dynamic>? search}) async {
var res = await http.get(uri, headers: await _getHeaders());
var res = await getClient().get<String>(
uri.toString(),
options: Options(responseType: ResponseType.plain),
);
if (res.statusCode != 200) {
throw HttpResponseCodeError(res.statusCode);
}
return res.body;
return res.data ?? "";
}
static Future<Map> getJson(Uri uri) async {

@ -0,0 +1,73 @@
import 'package:isekai_wiki/api/response/userinfo.dart';
import '../response/mugenapp.dart';
import 'mw_api.dart';
class MWApiUser {
static Future<MWResponse<MugenAppStartAuthInfo>> startAuth() async {
var query = {
"method": "startauth",
};
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!);
return MWResponse(
ok: true, data: authRes.startauth, continueInfo: mwRes.continueInfo);
} else {
return MWResponse(
errorList: [MWError(code: 'response_data_empty', info: '加载的数据为空')]);
}
}
static Future<MWResponse<MugenAppAttemptAuthInfo>> attemptAuth(
String loginRequestKey) async {
var query = {
"method": "attemptauth",
"requestkey": loginRequestKey,
};
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!);
return MWResponse(
ok: true,
data: authRes.attemptauth,
continueInfo: mwRes.continueInfo);
} else {
return MWResponse(
errorList: [MWError(code: 'response_data_empty', info: '加载的数据为空')]);
}
}
static Future<MWResponse<MetaUserInfoResponse>> getCurrentUserInfo() async {
var query = {
"meta": "userinfo|useravatar",
"uiprop": "blockinfo|groups|rights|options|email|realname|latestcontrib",
};
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!);
return MWResponse(ok: true, data: userInfoRes);
} else {
return MWResponse(
errorList: [MWError(code: 'response_data_empty', info: '加载的数据为空')]);
}
}
}

@ -0,0 +1,65 @@
import 'package:json_annotation/json_annotation.dart';
part 'mugenapp.g.dart';
@JsonSerializable()
class MugenAppStartAuthInfo {
String loginUrl;
String loginRequestKey;
int ttl;
MugenAppStartAuthInfo({
required this.loginUrl,
required this.loginRequestKey,
this.ttl = 0,
});
factory MugenAppStartAuthInfo.fromJson(Map<String, dynamic> json) =>
_$MugenAppStartAuthInfoFromJson(json);
Map<String, dynamic> toJson() => _$MugenAppStartAuthInfoToJson(this);
}
@JsonSerializable()
class MugenAppStartAuthResponse {
MugenAppStartAuthInfo startauth;
MugenAppStartAuthResponse({required this.startauth});
factory MugenAppStartAuthResponse.fromJson(Map<String, dynamic> json) =>
_$MugenAppStartAuthResponseFromJson(json);
Map<String, dynamic> toJson() => _$MugenAppStartAuthResponseToJson(this);
}
@JsonSerializable()
class MugenAppAttemptAuthInfo {
String status;
int? userid;
String? username;
MugenAppAttemptAuthInfo({
required this.status,
this.userid,
this.username,
});
factory MugenAppAttemptAuthInfo.fromJson(Map<String, dynamic> json) =>
_$MugenAppAttemptAuthInfoFromJson(json);
Map<String, dynamic> toJson() => _$MugenAppAttemptAuthInfoToJson(this);
}
@JsonSerializable()
class MugenAppAttemptAuthResponse {
MugenAppAttemptAuthInfo attemptauth;
MugenAppAttemptAuthResponse({
required this.attemptauth,
});
factory MugenAppAttemptAuthResponse.fromJson(Map<String, dynamic> json) =>
_$MugenAppAttemptAuthResponseFromJson(json);
Map<String, dynamic> toJson() => _$MugenAppAttemptAuthResponseToJson(this);
}

@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'mugenapp.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
MugenAppStartAuthInfo _$MugenAppStartAuthInfoFromJson(
Map<String, dynamic> json) =>
MugenAppStartAuthInfo(
loginUrl: json['loginUrl'] as String,
loginRequestKey: json['loginRequestKey'] as String,
ttl: json['ttl'] as int? ?? 0,
);
Map<String, dynamic> _$MugenAppStartAuthInfoToJson(
MugenAppStartAuthInfo instance) =>
<String, dynamic>{
'loginUrl': instance.loginUrl,
'loginRequestKey': instance.loginRequestKey,
'ttl': instance.ttl,
};
MugenAppStartAuthResponse _$MugenAppStartAuthResponseFromJson(
Map<String, dynamic> json) =>
MugenAppStartAuthResponse(
startauth: MugenAppStartAuthInfo.fromJson(
json['startauth'] as Map<String, dynamic>),
);
Map<String, dynamic> _$MugenAppStartAuthResponseToJson(
MugenAppStartAuthResponse instance) =>
<String, dynamic>{
'startauth': instance.startauth,
};
MugenAppAttemptAuthInfo _$MugenAppAttemptAuthInfoFromJson(
Map<String, dynamic> json) =>
MugenAppAttemptAuthInfo(
status: json['status'] as String,
userid: json['userid'] as int?,
username: json['username'] as String?,
);
Map<String, dynamic> _$MugenAppAttemptAuthInfoToJson(
MugenAppAttemptAuthInfo instance) =>
<String, dynamic>{
'status': instance.status,
'userid': instance.userid,
'username': instance.username,
};
MugenAppAttemptAuthResponse _$MugenAppAttemptAuthResponseFromJson(
Map<String, dynamic> json) =>
MugenAppAttemptAuthResponse(
attemptauth: MugenAppAttemptAuthInfo.fromJson(
json['attemptauth'] as Map<String, dynamic>),
);
Map<String, dynamic> _$MugenAppAttemptAuthResponseToJson(
MugenAppAttemptAuthResponse instance) =>
<String, dynamic>{
'attemptauth': instance.attemptauth,
};

@ -0,0 +1,101 @@
import 'package:json_annotation/json_annotation.dart';
part 'userinfo.g.dart';
@JsonSerializable()
class UserGroupMembership {
String group;
String expiry;
UserGroupMembership({
required this.group,
required this.expiry,
});
factory UserGroupMembership.fromJson(Map<String, dynamic> json) =>
_$UserGroupMembershipFromJson(json);
Map<String, dynamic> toJson() => _$UserGroupMembershipToJson(this);
}
@JsonSerializable()
class UserAcceptLang {
double q;
@JsonKey(name: '*')
String langCode;
UserAcceptLang({
required this.q,
required this.langCode,
});
factory UserAcceptLang.fromJson(Map<String, dynamic> json) =>
_$UserAcceptLangFromJson(json);
Map<String, dynamic> toJson() => _$UserAcceptLangToJson(this);
}
@JsonSerializable()
class MetaUserInfo {
int id;
String name;
List<String>? groups;
List<UserGroupMembership>? groupmemberships;
List<String>? implicitgroups;
List<String>? rights;
Map<String, List<String>>? changeablegroups;
Map<String, dynamic>? options;
int? editcount;
String? realname;
String? email;
DateTime? emailauthenticated;
DateTime? registrationdate;
List<UserAcceptLang>? acceptlang;
int? unreadcount;
Map<String, int>? centralids;
Map<String, String>? attachedlocal;
DateTime? latestcontrib;
MetaUserInfo({
required this.id,
required this.name,
this.groups,
this.groupmemberships,
this.implicitgroups,
this.rights,
this.changeablegroups,
this.options,
this.editcount,
this.realname,
this.email,
this.emailauthenticated,
this.registrationdate,
this.acceptlang,
this.unreadcount,
this.centralids,
this.attachedlocal,
this.latestcontrib,
});
factory MetaUserInfo.fromJson(Map<String, dynamic> json) =>
_$MetaUserInfoFromJson(json);
Map<String, dynamic> toJson() => _$MetaUserInfoToJson(this);
}
@JsonSerializable()
class MetaUserInfoResponse {
MetaUserInfo userinfo;
Map<int, String>? useravatar;
MetaUserInfoResponse({
required this.userinfo,
this.useravatar,
});
factory MetaUserInfoResponse.fromJson(Map<String, dynamic> json) =>
_$MetaUserInfoResponseFromJson(json);
Map<String, dynamic> toJson() => _$MetaUserInfoResponseToJson(this);
}

@ -0,0 +1,114 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'userinfo.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserGroupMembership _$UserGroupMembershipFromJson(Map<String, dynamic> json) =>
UserGroupMembership(
group: json['group'] as String,
expiry: json['expiry'] as String,
);
Map<String, dynamic> _$UserGroupMembershipToJson(
UserGroupMembership instance) =>
<String, dynamic>{
'group': instance.group,
'expiry': instance.expiry,
};
UserAcceptLang _$UserAcceptLangFromJson(Map<String, dynamic> json) =>
UserAcceptLang(
q: (json['q'] as num).toDouble(),
langCode: json['*'] as String,
);
Map<String, dynamic> _$UserAcceptLangToJson(UserAcceptLang instance) =>
<String, dynamic>{
'q': instance.q,
'*': instance.langCode,
};
MetaUserInfo _$MetaUserInfoFromJson(Map<String, dynamic> json) => MetaUserInfo(
id: json['id'] as int,
name: json['name'] as String,
groups:
(json['groups'] as List<dynamic>?)?.map((e) => e as String).toList(),
groupmemberships: (json['groupmemberships'] as List<dynamic>?)
?.map((e) => UserGroupMembership.fromJson(e as Map<String, dynamic>))
.toList(),
implicitgroups: (json['implicitgroups'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
rights:
(json['rights'] as List<dynamic>?)?.map((e) => e as String).toList(),
changeablegroups:
(json['changeablegroups'] as Map<String, dynamic>?)?.map(
(k, e) =>
MapEntry(k, (e as List<dynamic>).map((e) => e as String).toList()),
),
options: json['options'] as Map<String, dynamic>?,
editcount: json['editcount'] as int?,
realname: json['realname'] as String?,
email: json['email'] as String?,
emailauthenticated: json['emailauthenticated'] == null
? null
: DateTime.parse(json['emailauthenticated'] as String),
registrationdate: json['registrationdate'] == null
? null
: DateTime.parse(json['registrationdate'] as String),
acceptlang: (json['acceptlang'] as List<dynamic>?)
?.map((e) => UserAcceptLang.fromJson(e as Map<String, dynamic>))
.toList(),
unreadcount: json['unreadcount'] as int?,
centralids: (json['centralids'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as int),
),
attachedlocal: (json['attachedlocal'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, e as String),
),
latestcontrib: json['latestcontrib'] == null
? null
: DateTime.parse(json['latestcontrib'] as String),
);
Map<String, dynamic> _$MetaUserInfoToJson(MetaUserInfo instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'groups': instance.groups,
'groupmemberships': instance.groupmemberships,
'implicitgroups': instance.implicitgroups,
'rights': instance.rights,
'changeablegroups': instance.changeablegroups,
'options': instance.options,
'editcount': instance.editcount,
'realname': instance.realname,
'email': instance.email,
'emailauthenticated': instance.emailauthenticated?.toIso8601String(),
'registrationdate': instance.registrationdate?.toIso8601String(),
'acceptlang': instance.acceptlang,
'unreadcount': instance.unreadcount,
'centralids': instance.centralids,
'attachedlocal': instance.attachedlocal,
'latestcontrib': instance.latestcontrib?.toIso8601String(),
};
MetaUserInfoResponse _$MetaUserInfoResponseFromJson(
Map<String, dynamic> json) =>
MetaUserInfoResponse(
userinfo: MetaUserInfo.fromJson(json['userinfo'] as Map<String, dynamic>),
useravatar: (json['useravatar'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(int.parse(k), e as String),
),
);
Map<String, dynamic> _$MetaUserInfoResponseToJson(
MetaUserInfoResponse instance) =>
<String, dynamic>{
'userinfo': instance.userinfo,
'useravatar':
instance.useravatar?.map((k, e) => MapEntry(k.toString(), e)),
};

@ -9,6 +9,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
/// Standard iOS navigation bar height without the status bar.
///
@ -420,7 +421,8 @@ class IsekaiNavigationBar extends StatefulWidget
@override
Size get preferredSize {
return const Size.fromHeight(_kNavBarPersistentHeight);
double scaleFactor = MediaQuery.of(Get.context!).textScaleFactor;
return Size.fromHeight(_kNavBarPersistentHeight * scaleFactor);
}
@override
@ -727,6 +729,7 @@ class _IsekaiSliverNavigationBarState extends State<IsekaiSliverNavigationBar> {
large: true,
);
double scaleFactor = MediaQuery.of(context).textScaleFactor;
return SliverPersistentHeader(
pinned: true, // iOS navigation bars are always pinned.
delegate: _LargeTitleNavigationBarSliverDelegate(
@ -742,8 +745,8 @@ class _IsekaiSliverNavigationBarState extends State<IsekaiSliverNavigationBar> {
actionsForegroundColor: CupertinoTheme.of(context).primaryColor,
transitionBetweenRoutes: widget.transitionBetweenRoutes,
heroTag: widget.heroTag,
persistentHeight:
_kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
persistentHeight: (_kNavBarPersistentHeight * scaleFactor) +
MediaQuery.of(context).padding.top,
alwaysShowMiddle: widget.middle != null,
stretchConfiguration:
widget.stretch ? OverScrollHeaderStretchConfiguration() : null,
@ -789,7 +792,10 @@ class _LargeTitleNavigationBarSliverDelegate
double get minExtent => persistentHeight;
@override
double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;
double get maxExtent =>
persistentHeight +
(_kNavBarLargeTitleHeightExtension *
MediaQuery.of(Get.context!).textScaleFactor);
@override
OverScrollHeaderStretchConfiguration? stretchConfiguration;
@ -985,8 +991,11 @@ class _PersistentNavigationBar extends StatelessWidget {
);
}
double scaleFactor = MediaQuery.of(context).textScaleFactor;
return SizedBox(
height: _kNavBarPersistentHeight + MediaQuery.of(context).padding.top,
height: (_kNavBarPersistentHeight * scaleFactor) +
MediaQuery.of(context).padding.top,
child: SafeArea(
bottom: false,
child: paddedToolbar,

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:isekai_wiki/models/user.dart';
@ -17,11 +18,8 @@ class LifeCycleController extends SuperController {
@override
void onResumed() {
debugPrint("onResume");
try {
var uc = Get.find<UserController>();
uc.attemptFinishAuth().catchError((err) {
err.printError(info: 'attemptFinishAuth');
});
} catch (_) {}
var uc = Get.find<UserController>();
uc.attemptFinishAuth();
}
}

@ -1,8 +1,12 @@
import 'dart:convert';
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/mw/user.dart';
import 'package:isekai_wiki/global.dart';
import 'package:isekai_wiki/utils/dialog.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@ -12,11 +16,17 @@ class UserInfo {
int userId;
String userName;
String? nickName;
String? avatarUrl;
Map<int, String>? avatarUrlSet;
UserInfo({required this.userId, required this.userName, this.nickName, this.avatarUrl});
UserInfo({
required this.userId,
required this.userName,
this.nickName,
this.avatarUrlSet,
});
factory UserInfo.fromJson(Map<String, dynamic> json) => _$UserInfoFromJson(json);
factory UserInfo.fromJson(Map<String, dynamic> json) =>
_$UserInfoFromJson(json);
Map<String, dynamic> toJson() => _$UserInfoToJson(this);
}
@ -24,13 +34,15 @@ class UserInfo {
class UserController extends GetxController {
bool _ignoreSave = false;
var authProcessing = false.obs;
var loginRequestToken = "".obs;
var userId = 0.obs;
var userName = "".obs;
var nickName = "".obs;
var avatarUrl = "".obs;
var avatarUrlSet = RxMap<int, String>({});
bool get isLoggedIn {
return userId.value > 0;
@ -45,6 +57,7 @@ class UserController extends GetxController {
super.onInit();
loadFromStorage();
updateProfile();
ever(loginRequestToken, (String token) {
saveToStorage();
@ -52,7 +65,35 @@ class UserController extends GetxController {
}
///
Future<void> updateProfile() async {}
Future<void> updateProfile() async {
var userInfoMWRes = await MWApiUser.getCurrentUserInfo();
if (!userInfoMWRes.ok) {
if (kDebugMode) {
print("Cannot update profile of current user");
print(userInfoMWRes.errorList);
}
}
var userInfoRes = userInfoMWRes.data!;
nickName.value = userInfoRes.userinfo.realname ?? "";
if (userInfoRes.useravatar != null) {
avatarUrlSet.value = userInfoRes.useravatar!;
}
}
String? getAvatar(int size) {
if (avatarUrlSet.isEmpty) {
return null;
}
for (var imgSize in avatarUrlSet.keys) {
if (size < imgSize) {
return avatarUrlSet[size];
}
}
return avatarUrlSet.values.last;
}
///
void loadFromStorage() {
@ -70,7 +111,7 @@ class UserController extends GetxController {
userId.value = userInfo.userId;
userName.value = userInfo.userName;
nickName.value = userInfo.nickName ?? "";
avatarUrl.value = userInfo.avatarUrl ?? "";
avatarUrlSet.value = userInfo.avatarUrlSet ?? {};
_ignoreSave = false;
var savedLoginRequestToken = prefs.getString("loginRequestToken");
@ -98,7 +139,7 @@ class UserController extends GetxController {
userId: userId.value,
userName: userName.value,
nickName: nickName.isNotEmpty ? nickName.value : null,
avatarUrl: avatarUrl.isNotEmpty ? avatarUrl.value : null,
avatarUrlSet: avatarUrlSet.isNotEmpty ? avatarUrlSet : null,
);
var userInfoJson = jsonEncode(userInfo.toJson());
@ -114,12 +155,72 @@ class UserController extends GetxController {
///
/// loginRequestToken
Future<bool> startAuthFlow() async {
return false;
Future<void> startAuthFlow() async {
authProcessing.value = true;
var startAuthRes = await MWApiUser.startAuth();
if (!startAuthRes.ok) {
authProcessing.value = false;
alert(Get.overlayContext!, startAuthRes.errorList?[0].info ?? "未知错误",
title: "错误");
return;
}
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,
));
}
// Token
Future<void> attemptFinishAuth() async {
if (loginRequestToken.isEmpty) return;
if (loginRequestToken.isEmpty) {
authProcessing.value = false;
return;
}
var attemptAuthRes = await MWApiUser.attemptAuth(loginRequestToken.value);
if (!attemptAuthRes.ok) {
authProcessing.value = false;
alert(Get.overlayContext!, attemptAuthRes.errorList?[0].info ?? "未知错误",
title: "错误");
return;
}
var attemptAuthInfo = attemptAuthRes.data!;
if (attemptAuthInfo.status == "pending") {
authProcessing.value = false;
loginRequestToken.value = "";
alert(Get.overlayContext!, "已取消登录", title: "提示");
return;
} else if (attemptAuthInfo.status == "success") {
userId.value = attemptAuthInfo.userid!;
userName.value = attemptAuthInfo.username!;
try {
await updateProfile();
} catch (err) {
err.printError(info: 'Cannot update profile after auth');
}
authProcessing.value = false;
saveToStorage();
}
}
Future<void> logout() async {}
}

@ -10,12 +10,15 @@ UserInfo _$UserInfoFromJson(Map<String, dynamic> json) => UserInfo(
userId: json['userId'] as int,
userName: json['userName'] as String,
nickName: json['nickName'] as String?,
avatarUrl: json['avatarUrl'] as String?,
avatarUrlSet: (json['avatarUrlSet'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(int.parse(k), e as String),
),
);
Map<String, dynamic> _$UserInfoToJson(UserInfo instance) => <String, dynamic>{
'userId': instance.userId,
'userName': instance.userName,
'nickName': instance.nickName,
'avatarUrl': instance.avatarUrl,
'avatarUrlSet':
instance.avatarUrlSet?.map((k, e) => MapEntry(k.toString(), e)),
};

@ -33,7 +33,10 @@ class AboutPage extends StatelessWidget {
var c = Get.put(AboutPageController());
return IsekaiPageScaffold(
navigationBar: const IsekaiNavigationBar(middle: Text('关于'), previousPageTitle: "设置"),
navigationBar: const IsekaiNavigationBar(
middle: Text('关于'),
previousPageTitle: "我的",
),
child: ListView(
children: [
const SizedBox(height: 18),
@ -43,7 +46,8 @@ class AboutPage extends StatelessWidget {
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Column(
children: const <Widget>[
Text("异世界百科APP", style: Styles.articleTitle),
@ -59,7 +63,8 @@ class AboutPage extends StatelessWidget {
backgroundColor: Styles.themePageBackgroundColor,
children: <CupertinoListTile>[
CupertinoListTile.notched(
title: const Text('异世界百科', style: TextStyle(color: Styles.linkColor)),
title: const Text('异世界百科',
style: TextStyle(color: Styles.linkColor)),
leading: const DummyIcon(
color: CupertinoColors.systemBlue,
icon: CupertinoIcons.globe,

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:isekai_wiki/components/isekai_nav_bar.dart';
import 'package:isekai_wiki/components/recent_page_list.dart';
import 'package:isekai_wiki/models/user.dart';
import 'package:isekai_wiki/pages/tab_page.dart';
@ -15,7 +16,10 @@ import '../styles.dart';
enum HomeTabs { newest, followed }
class HomeController extends GetxController with GetSingleTickerProviderStateMixin {
class HomeController extends GetxController
with GetSingleTickerProviderStateMixin {
double _navSearchButtonOffset = 90;
var showNavSearchButton = false.obs;
var isScrolling = false.obs;
@ -32,10 +36,15 @@ class HomeController extends GetxController with GetSingleTickerProviderStateMix
void onInit() {
tabController = TabController(length: 2, vsync: this);
_navSearchButtonOffset =
48 * MediaQuery.of(Get.context!).textScaleFactor + 48;
scrollController.addListener(() {
if (scrollController.offset >= 90 && !showNavSearchButton.value) {
if (scrollController.offset >= _navSearchButtonOffset &&
!showNavSearchButton.value) {
showNavSearchButton.value = true;
} else if (scrollController.offset < 90 && showNavSearchButton.value) {
} else if (scrollController.offset < _navSearchButtonOffset &&
showNavSearchButton.value) {
showNavSearchButton.value = false;
}
});
@ -93,7 +102,8 @@ class HomeTab extends StatelessWidget {
duration: const Duration(milliseconds: 100),
child: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.search, size: 26, color: Styles.themeNavTitleColor),
child: const Icon(CupertinoIcons.search,
size: 26, color: Styles.themeNavTitleColor),
onPressed: () {
onSearchClick?.call();
},
@ -111,7 +121,8 @@ class HomeTab extends StatelessWidget {
duration: const Duration(milliseconds: 100),
child: CupertinoButton(
padding: EdgeInsets.zero,
child: const Icon(CupertinoIcons.search, size: 26, color: Styles.themeNavTitleColor),
child: const Icon(CupertinoIcons.search,
size: 26, color: Styles.themeNavTitleColor),
onPressed: () {
onSearchClick?.call();
},
@ -132,11 +143,12 @@ class HomeTab extends StatelessWidget {
parent: AlwaysScrollableScrollPhysics(),
),
slivers: <Widget>[
CupertinoSliverNavigationBar(
IsekaiSliverNavigationBar(
leading: _buildNotificationIconButton(),
backgroundColor: Styles.themeMainColor,
brightness: Brightness.dark,
largeTitle: const Text('首页', style: TextStyle(color: Styles.themeNavTitleColor)),
largeTitle: const Text('首页',
style: TextStyle(color: Styles.themeNavTitleColor)),
border: Border.all(style: BorderStyle.none),
trailing: _buildSearchIconButton(),
),
@ -169,10 +181,12 @@ class HomeTab extends StatelessWidget {
children: [
Container(
padding: const EdgeInsets.all(1),
child: const Icon(CupertinoIcons.search, color: Colors.black54),
child: const Icon(CupertinoIcons.search,
color: Colors.black54),
),
const Text("搜索页面...",
textAlign: TextAlign.center, style: TextStyle(color: Colors.black54))
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black54))
],
),
),
@ -202,7 +216,10 @@ class HomeTab extends StatelessWidget {
indicatorColor: Styles.themeMainColor,
labelColor: Styles.themeMainColor,
unselectedLabelColor: Colors.black45,
tabs: const [CollapsedTabText('最新'), CollapsedTabText('关注')],
tabs: const [
CollapsedTabText('最新'),
CollapsedTabText('关注')
],
onTap: (int selected) {},
),
),
@ -247,7 +264,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
double get maxExtent => max(maxHeight, minHeight);
@override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return SizedBox.expand(child: child);
}

@ -1,6 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cupertino_lists/cupertino_lists.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
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';
@ -11,8 +13,6 @@ import '../components/follow_scale.dart';
class OwnProfileController extends GetxController {
late UserController uc;
var loginLoading = false.obs;
@override
void onInit() {
super.onInit();
@ -25,16 +25,51 @@ class OwnProfileController extends GetxController {
}
Future<void> handleStartAuth() async {
loginLoading.value = true;
await uc.startAuthFlow();
loginLoading.value = false;
}
Future<void> handleLogoutClick() {
handleLogout();
return Future.delayed(const Duration(milliseconds: 100));
}
Future<void> handleLogout() async {
await uc.logout();
}
}
class OwnProfileTab extends StatelessWidget {
const OwnProfileTab({super.key});
Widget _buildUserSection() {
Widget _buildUserAvatar(UserController uc, {double size = 56}) {
return Obx(() {
var avatarUrl = uc.getAvatar(128);
if (avatarUrl != null && avatarUrl.isNotEmpty) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(size / 2)),
),
child: CachedNetworkImage(
width: size,
height: size,
placeholder: (_, __) =>
const CupertinoActivityIndicator(radius: 12),
imageUrl: avatarUrl,
),
);
} else {
return DummyIcon(
color: CupertinoColors.systemGrey,
icon: CupertinoIcons.person_fill,
size: size,
rounded: true,
);
}
});
}
Widget _buildUserSection(BuildContext context) {
var c = Get.find<OwnProfileController>();
var uc = Get.find<UserController>();
@ -57,7 +92,7 @@ class OwnProfileTab extends StatelessWidget {
),
leadingSize: 80,
leadingToTitle: 4,
trailing: c.loginLoading.value
trailing: uc.authProcessing.value
? const Padding(
padding: EdgeInsets.only(right: 5),
child: CupertinoActivityIndicator(
@ -75,12 +110,7 @@ class OwnProfileTab extends StatelessWidget {
CupertinoListTile.notched(
title: Text(uc.getDisplayName,
style: Styles.listTileLargeTitle),
leading: const DummyIcon(
color: CupertinoColors.systemGrey,
icon: CupertinoIcons.person_fill,
size: 56,
rounded: true,
),
leading: _buildUserAvatar(uc),
leadingSize: 80,
leadingToTitle: 4,
trailing: const CupertinoListTileChevron(),
@ -90,7 +120,7 @@ class OwnProfileTab extends StatelessWidget {
title: const Text('退出登录'),
leading: const DummyIcon(
color: CupertinoColors.systemRed,
icon: CupertinoIcons.selection_pin_in_out,
icon: CupertinoIcons.arrow_right_square,
),
trailing: const CupertinoListTileChevron(),
onTap: () {},
@ -101,7 +131,7 @@ class OwnProfileTab extends StatelessWidget {
);
}
Widget _buildArticleListsSection() {
Widget _buildArticleListsSection(BuildContext context) {
return FollowTextScale(
child: CupertinoListSection.insetGrouped(
backgroundColor: Styles.themePageBackgroundColor,
@ -137,7 +167,7 @@ class OwnProfileTab extends StatelessWidget {
));
}
Widget _buildSettingsSection() {
Widget _buildSettingsSection(BuildContext context) {
return FollowTextScale(
child: CupertinoListSection.insetGrouped(
backgroundColor: Styles.themePageBackgroundColor,
@ -159,24 +189,28 @@ class OwnProfileTab extends StatelessWidget {
),
trailing: const CupertinoListTileChevron(),
onTap: () async {
await Navigator.of(Get.context!, rootNavigator: true)
.push(CupertinoPageRoute(builder: (_) => const AboutPage()));
await Navigator.of(context, rootNavigator: false).push(
CupertinoPageRoute(
builder: (_) => const AboutPage(),
),
);
},
),
],
));
}
SliverChildBuilderDelegate _buildSliverChildBuilderDelegate() {
SliverChildBuilderDelegate _buildSliverChildBuilderDelegate(
BuildContext context) {
return SliverChildBuilderDelegate(
(context, index) {
switch (index) {
case 0:
return _buildUserSection();
return _buildUserSection(context);
case 1:
return _buildArticleListsSection();
return _buildArticleListsSection(context);
case 2:
return _buildSettingsSection();
return _buildSettingsSection(context);
default:
// Do nothing. For now.
}
@ -191,14 +225,14 @@ class OwnProfileTab extends StatelessWidget {
return CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
const IsekaiSliverNavigationBar(
largeTitle: Text('我的'),
),
SliverSafeArea(
top: false,
minimum: const EdgeInsets.only(top: 4),
sliver: SliverList(
delegate: _buildSliverChildBuilderDelegate(),
delegate: _buildSliverChildBuilderDelegate(context),
),
)
],

@ -1,7 +1,6 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:isekai_wiki/global.dart';
import 'package:package_info_plus/package_info_plus.dart';
import '../extension/string.dart';
@ -15,9 +14,7 @@ class ApiUtils {
String osName = Platform.operatingSystem.capitalize();
String osVersion = Platform.operatingSystemVersion;
Global.packageInfo ??= await PackageInfo.fromPlatform();
String appVersion = Global.packageInfo!.version;
String appVersion = Global.packageInfo?.version ?? "0.0";
return "IsekaiWikiApp/$appVersion ($osName $osVersion)";
}

@ -6,11 +6,15 @@ import FlutterMacOS
import Foundation
import package_info_plus
import path_provider_macos
import shared_preferences_macos
import sqflite
import wakelock_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
}

@ -99,6 +99,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "8.4.2"
cached_network_image:
dependency: "direct main"
description:
name: cached_network_image
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.3"
cached_network_image_platform_interface:
dependency: transitive
description:
name: cached_network_image_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
cached_network_image_web:
dependency: transitive
description:
name: cached_network_image_web
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
characters:
dependency: transitive
description:
@ -155,6 +176,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.1"
cookie_jar:
dependency: transitive
description:
name: cookie_jar
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
crypto:
dependency: transitive
description:
@ -190,6 +218,34 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.4"
dio:
dependency: "direct main"
description:
name: dio
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.6"
dio_cache_interceptor:
dependency: "direct main"
description:
name: dio_cache_interceptor
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.1"
dio_cookie_manager:
dependency: "direct main"
description:
name: dio_cookie_manager
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
dio_http2_adapter:
dependency: "direct main"
description:
name: dio_http2_adapter
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
equatable:
dependency: transitive
description:
@ -230,6 +286,20 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_blurhash:
dependency: transitive
description:
name: flutter_blurhash
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.0"
flutter_cache_manager:
dependency: transitive
description:
name: flutter_cache_manager
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.0"
flutter_displaymode:
dependency: "direct main"
description:
@ -351,12 +421,19 @@ packages:
source: hosted
version: "0.15.1"
http:
dependency: "direct main"
dependency: transitive
description:
name: http
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.5"
http2:
dependency: transitive
description:
name: http2
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
http_multi_server:
dependency: transitive
description:
@ -476,6 +553,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
octo_image:
dependency: transitive
description:
name: octo_image
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
package_config:
dependency: transitive
description:
@ -518,6 +602,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
path_provider:
dependency: transitive
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.22"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.11"
path_provider_linux:
dependency: transitive
description:
@ -525,6 +630,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.7"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
path_provider_platform_interface:
dependency: transitive
description:
@ -539,6 +651,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.3"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.11.1"
petitparser:
dependency: transitive
description:
@ -616,6 +735,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
rxdart:
dependency: transitive
description:
name: rxdart
url: "https://pub.dartlang.org"
source: hosted
version: "0.27.7"
shared_preferences:
dependency: "direct main"
description:
@ -719,6 +845,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
sqflite:
dependency: transitive
description:
name: sqflite
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0+2"
stack_trace:
dependency: transitive
description:
@ -747,6 +887,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.1"
synchronized:
dependency: transitive
description:
name: synchronized
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0+3"
term_glyph:
dependency: transitive
description:
@ -782,6 +929,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
uuid:
dependency: transitive
description:
name: uuid
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.7"
vector_math:
dependency: transitive
description:
@ -938,4 +1092,4 @@ packages:
version: "3.1.1"
sdks:
dart: ">=2.18.0 <3.0.0"
flutter: ">=3.0.0"
flutter: ">=3.3.0"

@ -57,7 +57,11 @@ dependencies:
ruby_text: ^3.0.1
package_info_plus: ^3.0.2
pull_down_button: ^0.4.1
http: ^0.13.5
cached_network_image: ^3.2.3
dio: ^4.0.6
dio_cookie_manager: ^2.0.0
dio_http2_adapter: ^2.0.0
dio_cache_interceptor: ^3.3.1
get:
dev_dependencies:

Loading…
Cancel
Save