feat: logout (#497)

* feat: logout

* update api type
This commit is contained in:
My-Responsitories
2025-03-23 13:46:26 +08:00
committed by GitHub
parent 7c3e3cb1f8
commit d6587cf3b6
11 changed files with 139 additions and 129 deletions

View File

@@ -13,6 +13,7 @@ import 'package:PiliPlus/models/github/latest.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import '../../utils/cache_manage.dart'; import '../../utils/cache_manage.dart';
import '../mine/controller.dart';
class AboutPage extends StatefulWidget { class AboutPage extends StatefulWidget {
const AboutPage({super.key, this.showAppBar}); const AboutPage({super.key, this.showAppBar});
@@ -302,6 +303,9 @@ Commit Hash: ${BuildConfig.commitHash}''',
.putAll(res) .putAll(res)
.then((_) => Accounts.refresh()) .then((_) => Accounts.refresh())
.then((_) { .then((_) {
MineController.anonymity.value =
!Accounts.get(AccountType.heartbeat)
.isLogin;
if (Accounts.main.isLogin) { if (Accounts.main.isLogin) {
return LoginUtils.onLoginMain(); return LoginUtils.onLoginMain();
} }

View File

@@ -646,7 +646,9 @@ class LoginPageController extends GetxController
LoginAccount(BiliCookieJar.fromList(cookieInfo), LoginAccount(BiliCookieJar.fromList(cookieInfo),
tokenInfo['access_token'], tokenInfo['refresh_token']) tokenInfo['access_token'], tokenInfo['refresh_token'])
.onChange(), .onChange(),
AnonymousAccount().logout().then((i) => Request.buvidActive(i)) AnonymousAccount()
.delete()
.then((_) => Request.buvidActive(AnonymousAccount()))
]); ]);
if (Accounts.main.isLogin) { if (Accounts.main.isLogin) {
SmartDialog.showToast('登录成功'); SmartDialog.showToast('登录成功');

View File

@@ -35,7 +35,7 @@ class MainController extends GetxController {
late int homeIndex = -1; late int homeIndex = -1;
late DynamicBadgeMode msgBadgeMode = GStorage.msgBadgeMode; late DynamicBadgeMode msgBadgeMode = GStorage.msgBadgeMode;
late List<MsgUnReadType> msgUnReadTypes = GStorage.msgUnReadTypeV2; late Set<MsgUnReadType> msgUnReadTypes = GStorage.msgUnReadTypeV2.toSet();
late final RxString msgUnReadCount = ''.obs; late final RxString msgUnReadCount = ''.obs;
late int lastCheckUnreadAt = 0; late int lastCheckUnreadAt = 0;
@@ -83,7 +83,7 @@ class MainController extends GetxController {
try { try {
bool shouldCheckPM = msgUnReadTypes.contains(MsgUnReadType.pm); bool shouldCheckPM = msgUnReadTypes.contains(MsgUnReadType.pm);
bool shouldCheckFeed = bool shouldCheckFeed =
([...msgUnReadTypes]..remove(MsgUnReadType.pm)).isNotEmpty; shouldCheckPM ? msgUnReadTypes.length > 1 : msgUnReadTypes.isNotEmpty;
List res = await Future.wait([ List res = await Future.wait([
if (shouldCheckPM) _queryPMUnread(), if (shouldCheckPM) _queryPMUnread(),
if (shouldCheckFeed) _queryMsgFeedUnread(), if (shouldCheckFeed) _queryMsgFeedUnread(),

View File

@@ -7,13 +7,15 @@ import 'package:PiliPlus/pages/setting/privacy_setting.dart';
import 'package:PiliPlus/pages/setting/recommend_setting.dart'; import 'package:PiliPlus/pages/setting/recommend_setting.dart';
import 'package:PiliPlus/pages/setting/style_setting.dart'; import 'package:PiliPlus/pages/setting/style_setting.dart';
import 'package:PiliPlus/pages/setting/video_setting.dart'; import 'package:PiliPlus/pages/setting/video_setting.dart';
import 'package:PiliPlus/utils/accounts/account.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/login.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'widgets/multi_select_dialog.dart';
class _SettingsModel { class _SettingsModel {
final String name; final String name;
final String title; final String title;
@@ -37,7 +39,7 @@ class SettingPage extends StatefulWidget {
class _SettingPageState extends State<SettingPage> { class _SettingPageState extends State<SettingPage> {
late String _type = 'privacySetting'; late String _type = 'privacySetting';
final RxBool _isLogin = Accounts.main.isLogin.obs; final RxBool _noAccount = Accounts.accountMode.isEmpty.obs;
TextStyle get _titleStyle => Theme.of(context).textTheme.titleMedium!; TextStyle get _titleStyle => Theme.of(context).textTheme.titleMedium!;
TextStyle get _subTitleStyle => Theme.of(context) TextStyle get _subTitleStyle => Theme.of(context)
.textTheme .textTheme
@@ -173,8 +175,15 @@ class _SettingPageState extends State<SettingPage> {
leading: const Icon(Icons.switch_account_outlined), leading: const Icon(Icons.switch_account_outlined),
title: const Text('设置账号模式'), title: const Text('设置账号模式'),
), ),
// TODO: 多账号登出 Obx(
_buildLoginItem, () => _noAccount.value
? const SizedBox.shrink()
: ListTile(
leading: const Icon(Icons.logout_outlined),
onTap: () => _logoutDialog(context),
title: Text('退出登录', style: _titleStyle),
),
),
ListTile( ListTile(
tileColor: _getTileColor(_items.last.name), tileColor: _getTileColor(_items.last.name),
onTap: () => _toPage(_items.last.name), onTap: () => _toPage(_items.last.name),
@@ -186,18 +195,30 @@ class _SettingPageState extends State<SettingPage> {
); );
} }
Widget get _buildLoginItem => Obx( Future<void> _logoutDialog(BuildContext context) async {
() => _isLogin.value.not final result = await showDialog<Set<LoginAccount>>(
? const SizedBox.shrink() context: context,
: ListTile( builder: (context) {
leading: const Icon(Icons.logout_outlined), return MultiSelectDialog<LoginAccount>(
onTap: () { title: '选择要登出的账号uid',
showDialog( initValues: Iterable.empty(),
values: {for (var i in Accounts.account.values) i: i.mid.toString()},
);
},
);
if (!context.mounted || result.isNullOrEmpty) return;
Future<void> logout() {
_noAccount.value = result!.length == Accounts.account.length;
return Accounts.deleteAll(result);
}
await showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: const Text('提示'), title: const Text('提示'),
content: const Text('确认要退出登录吗'), content: Text(
"确认要退出以下账号登录吗\n\n${result!.map((i) => i.mid.toString()).join('\n')}"),
actions: [ actions: [
TextButton( TextButton(
onPressed: Get.back, onPressed: Get.back,
@@ -211,18 +232,11 @@ class _SettingPageState extends State<SettingPage> {
TextButton( TextButton(
onPressed: () { onPressed: () {
Get.back(); Get.back();
_isLogin.value = false; logout();
LoginUtils.onLogoutMain();
final account = Accounts.main;
Accounts.accountMode
.removeWhere((_, a) => a == account);
account.logout().then((_) => Accounts.refresh());
}, },
child: Text( child: Text(
'仅登出', '仅登出',
style: TextStyle( style: TextStyle(color: Theme.of(context).colorScheme.error),
color: Theme.of(context).colorScheme.error,
),
), ),
), ),
TextButton( TextButton(
@@ -230,10 +244,8 @@ class _SettingPageState extends State<SettingPage> {
SmartDialog.showLoading(); SmartDialog.showLoading();
final res = await LoginHttp.logout(Accounts.main); final res = await LoginHttp.logout(Accounts.main);
if (res['status']) { if (res['status']) {
await Accounts.main.logout();
await LoginUtils.onLogoutMain();
_isLogin.value = false;
SmartDialog.dismiss(); SmartDialog.dismiss();
logout();
Get.back(); Get.back();
} else { } else {
SmartDialog.dismiss(); SmartDialog.dismiss();
@@ -244,12 +256,8 @@ class _SettingPageState extends State<SettingPage> {
) )
], ],
); );
}, });
); }
},
title: Text('退出登录', style: _titleStyle),
),
);
Widget get _buildSearchItem => Padding( Widget get _buildSearchItem => Padding(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 5), padding: const EdgeInsets.only(left: 16, right: 16, bottom: 5),

View File

@@ -344,15 +344,13 @@ List<SettingsModel> get styleSettings => [
SettingsModel( SettingsModel(
settingsType: SettingsType.normal, settingsType: SettingsType.normal,
onTap: (setState) async { onTap: (setState) async {
List<MsgUnReadType>? result = await showDialog( final result = await showDialog<Set<MsgUnReadType>>(
context: Get.context!, context: Get.context!,
builder: (context) { builder: (context) {
return MultiSelectDialog<MsgUnReadType>( return MultiSelectDialog<MsgUnReadType>(
title: '消息未读类型', title: '消息未读类型',
initValues: GStorage.msgUnReadTypeV2, initValues: GStorage.msgUnReadTypeV2,
values: MsgUnReadType.values.map((e) { values: {for (var i in MsgUnReadType.values) i: i.title},
return {'title': e.title, 'value': e};
}).toList(),
); );
}, },
); );

View File

@@ -2,16 +2,15 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class MultiSelectDialog<T> extends StatefulWidget { class MultiSelectDialog<T> extends StatefulWidget {
final List<T> initValues; final Iterable<T> initValues;
final String title; final String title;
final List<dynamic> values; final Map<T, String> values;
const MultiSelectDialog({ const MultiSelectDialog(
super.key, {super.key,
required this.initValues, required this.initValues,
required this.values, required this.values,
required this.title, required this.title});
});
@override @override
State<MultiSelectDialog<T>> createState() => _MultiSelectDialogState<T>(); State<MultiSelectDialog<T>> createState() => _MultiSelectDialogState<T>();
@@ -36,30 +35,24 @@ class _MultiSelectDialogState<T> extends State<MultiSelectDialog<T>> {
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: List.generate( children: widget.values.entries.map((i) {
widget.values.length, bool isChecked = _tempValues.contains(i.key);
(index) {
bool isChecked =
_tempValues.contains(widget.values[index]['value']);
return CheckboxListTile( return CheckboxListTile(
dense: true, dense: true,
value: isChecked, value: isChecked,
controlAffinity: ListTileControlAffinity.leading, controlAffinity: ListTileControlAffinity.leading,
title: Text( title: Text(
widget.values[index]['title'], i.value,
style: Theme.of(context).textTheme.titleMedium!, style: Theme.of(context).textTheme.titleMedium!,
), ),
onChanged: (value) { onChanged: (value) {
if (isChecked) { isChecked
_tempValues.remove(widget.values[index]['value']); ? _tempValues.remove(i.key)
} else { : _tempValues.add(i.key);
_tempValues.add(widget.values[index]['value']);
}
setState(() {}); setState(() {});
}, },
); );
}, }).toList(),
),
), ),
); );
}), }),
@@ -75,7 +68,7 @@ class _MultiSelectDialogState<T> extends State<MultiSelectDialog<T>> {
), ),
), ),
TextButton( TextButton(
onPressed: () => Get.back(result: _tempValues.toList()), onPressed: () => Get.back(result: _tempValues),
child: const Text('确定'), child: const Text('确定'),
), ),
], ],

View File

@@ -16,7 +16,7 @@ abstract class Account {
bool activited = false; bool activited = false;
Future<AnonymousAccount> logout(); Future<void> delete();
Future<void> onChange(); Future<void> onChange();
Map<String, dynamic>? toJson(); Map<String, dynamic>? toJson();
@@ -55,10 +55,7 @@ class LoginAccount implements Account {
bool activited = false; bool activited = false;
@override @override
Future<AnonymousAccount> logout() async { Future<void> delete() => _box.delete(_midStr);
await Future.wait([cookieJar.deleteAll(), _box.delete(_midStr)]);
return AnonymousAccount();
}
@override @override
Future<void> onChange() => _box.put(_midStr, this); Future<void> onChange() => _box.put(_midStr, this);
@@ -121,10 +118,9 @@ class AnonymousAccount implements Account {
bool activited = false; bool activited = false;
@override @override
Future<AnonymousAccount> logout() async { Future<void> delete() async {
await cookieJar.deleteAll(); await cookieJar.deleteAll();
activited = false; activited = false;
return this;
} }
@override @override

View File

@@ -19,23 +19,22 @@ final _setCookieReg = RegExp('(?<=)(,)(?=[^;]+?=)');
class AccountManager extends Interceptor { class AccountManager extends Interceptor {
static final Map<AccountType, Set<String>> apiTypeSet = { static final Map<AccountType, Set<String>> apiTypeSet = {
AccountType.heartbeat: { AccountType.heartbeat: {
Api.videoUrl,
Api.videoIntro, Api.videoIntro,
Api.relatedList,
Api.replyList, Api.replyList,
Api.replyReplyList, Api.replyReplyList,
Api.searchSuggest,
Api.searchByType,
Api.heartBeat, Api.heartBeat,
Api.ab2c, Api.ab2c,
Api.bangumiInfo, Api.bangumiInfo,
Api.liveRoomInfo, Api.liveRoomInfo,
Api.liveRoomInfoH5,
Api.onlineTotal, Api.onlineTotal,
Api.dynamicDetail, Api.dynamicDetail,
Api.aiConclusion, Api.aiConclusion,
Api.getSeasonDetailApi, Api.getSeasonDetailApi,
Api.liveRoomDmToken, Api.liveRoomDmToken,
Api.liveRoomDmPrefetch, Api.liveRoomDmPrefetch,
Api.searchByType,
Api.memberDynamicSearch
}, },
AccountType.recommend: { AccountType.recommend: {
Api.recommendListWeb, Api.recommendListWeb,
@@ -43,10 +42,11 @@ class AccountManager extends Interceptor {
Api.feedDislike, Api.feedDislike,
Api.feedDislikeCancel, Api.feedDislikeCancel,
Api.hotList, Api.hotList,
Api.relatedList,
Api.hotSearchList, // 不同账号搜索结果可能不一样 Api.hotSearchList, // 不同账号搜索结果可能不一样
Api.searchDefault, Api.searchDefault,
Api.searchSuggest, Api.searchSuggest,
Api.searchByType Api.liveList,
}, },
AccountType.video: {Api.videoUrl, Api.bangumiVideoUrl} AccountType.video: {Api.videoUrl, Api.bangumiVideoUrl}
}; };

View File

@@ -21,9 +21,11 @@ extension ScrollControllerExt on ScrollController {
} }
} }
extension ListExt<T> on List<T>? { extension IterableExt<T> on Iterable<T>? {
bool get isNullOrEmpty => this == null || this!.isEmpty; bool get isNullOrEmpty => this == null || this!.isEmpty;
}
extension ListExt<T> on List<T>? {
T? getOrNull(int index) { T? getOrNull(int index) {
if (isNullOrEmpty) { if (isNullOrEmpty) {
return null; return null;

View File

@@ -105,7 +105,7 @@ class LoginUtils {
} catch (_) {} } catch (_) {}
} else { } else {
// 获取用户信息失败 // 获取用户信息失败
await Accounts.set(AccountType.main, await account.logout()); await Accounts.deleteAll({account});
SmartDialog.showNotify( SmartDialog.showNotify(
msg: '登录失败请检查cookie是否正确${result['message']}', msg: '登录失败请检查cookie是否正确${result['message']}',
notifyType: NotifyType.warning); notifyType: NotifyType.warning);

View File

@@ -862,16 +862,25 @@ class Accounts {
for (var i in AccountType.values) { for (var i in AccountType.values) {
accountMode[i] = AnonymousAccount(); accountMode[i] = AnonymousAccount();
} }
if (!AnonymousAccount().activited) { await AnonymousAccount().delete();
Request.buvidActive(AnonymousAccount()); Request.buvidActive(AnonymousAccount());
} }
}
static Future<void> close() async { static Future<void> close() async {
account.compact(); account.compact();
account.close(); account.close();
} }
static Future<void> deleteAll(Set<Account> accounts) async {
var isloginMain = Accounts.main.isLogin;
Accounts.accountMode
.updateAll((_, a) => accounts.contains(a) ? AnonymousAccount() : a);
await Future.wait(accounts.map((i) => i.delete()));
if (isloginMain && !Accounts.main.isLogin) {
await LoginUtils.onLogoutMain();
}
}
static Future<void> set(AccountType key, Account account) async { static Future<void> set(AccountType key, Account account) async {
await (accountMode[key]?..type.remove(key))?.onChange(); await (accountMode[key]?..type.remove(key))?.onChange();
accountMode[key] = account..type.add(key); accountMode[key] = account..type.add(key);
@@ -879,11 +888,9 @@ class Accounts {
if (!account.activited) await Request.buvidActive(account); if (!account.activited) await Request.buvidActive(account);
switch (key) { switch (key) {
case AccountType.main: case AccountType.main:
if (account.isLogin) { await (account.isLogin
await LoginUtils.onLoginMain(); ? LoginUtils.onLoginMain()
} else { : LoginUtils.onLogoutMain());
await LoginUtils.onLogoutMain();
}
break; break;
case AccountType.heartbeat: case AccountType.heartbeat:
MineController.anonymity.value = !account.isLogin; MineController.anonymity.value = !account.isLogin;