mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-10 15:59:25 +08:00
Add session manage page
This commit is contained in:
@@ -187,9 +187,20 @@ abstract class _EHApi {
|
||||
Future<ApiResult<bool>> deleteToken(
|
||||
{@Part(name: "token") String? token,
|
||||
@CancelRequest() CancelToken? cancel});
|
||||
@DELETE('/token/manage')
|
||||
@MultiPart()
|
||||
Future<ApiResult<bool>> deleteTokenById(@Part(name: "id") int id,
|
||||
{@CancelRequest() CancelToken? cancel});
|
||||
@GET('/token')
|
||||
Future<ApiResult<TokenWithUserInfo>> getToken(
|
||||
{@Query("token") String? token, @CancelRequest() CancelToken? cancel});
|
||||
@GET('/token/manage')
|
||||
Future<ApiResult<List<TokenWithoutToken>>> getTokens(
|
||||
{@Query("uid") int? uid,
|
||||
@Query("offset") int? offset,
|
||||
@Query("limit") int? limit,
|
||||
@Query("all_user") bool? allUser,
|
||||
@CancelRequest() CancelToken? cancel});
|
||||
@GET('/shared_token')
|
||||
Future<ApiResult<SharedToken>> getSharedToken(
|
||||
{@CancelRequest() CancelToken? cancel});
|
||||
|
||||
@@ -689,6 +689,52 @@ class __EHApi implements _EHApi {
|
||||
return _value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResult<bool>> deleteTokenById(
|
||||
int id, {
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
final _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final _data = FormData();
|
||||
_data.fields.add(MapEntry(
|
||||
'id',
|
||||
id.toString(),
|
||||
));
|
||||
final _options = _setStreamType<ApiResult<bool>>(Options(
|
||||
method: 'DELETE',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
contentType: 'multipart/form-data',
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
'/token/manage',
|
||||
queryParameters: queryParameters,
|
||||
data: _data,
|
||||
cancelToken: cancel,
|
||||
)
|
||||
.copyWith(
|
||||
baseUrl: _combineBaseUrls(
|
||||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
)));
|
||||
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
|
||||
late ApiResult<bool> _value;
|
||||
try {
|
||||
_value = ApiResult<bool>.fromJson(
|
||||
_result.data!,
|
||||
(json) => json as bool,
|
||||
);
|
||||
} on Object catch (e, s) {
|
||||
errorLogger?.logError(e, s, _options);
|
||||
rethrow;
|
||||
}
|
||||
return _value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResult<TokenWithUserInfo>> getToken({
|
||||
String? token,
|
||||
@@ -730,6 +776,60 @@ class __EHApi implements _EHApi {
|
||||
return _value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResult<List<TokenWithoutToken>>> getTokens({
|
||||
int? uid,
|
||||
int? offset,
|
||||
int? limit,
|
||||
bool? allUser,
|
||||
CancelToken? cancel,
|
||||
}) async {
|
||||
final _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{
|
||||
r'uid': uid,
|
||||
r'offset': offset,
|
||||
r'limit': limit,
|
||||
r'all_user': allUser,
|
||||
};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
const Map<String, dynamic>? _data = null;
|
||||
final _options = _setStreamType<ApiResult<List<TokenWithoutToken>>>(Options(
|
||||
method: 'GET',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
'/token/manage',
|
||||
queryParameters: queryParameters,
|
||||
data: _data,
|
||||
cancelToken: cancel,
|
||||
)
|
||||
.copyWith(
|
||||
baseUrl: _combineBaseUrls(
|
||||
_dio.options.baseUrl,
|
||||
baseUrl,
|
||||
)));
|
||||
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
|
||||
late ApiResult<List<TokenWithoutToken>> _value;
|
||||
try {
|
||||
_value = ApiResult<List<TokenWithoutToken>>.fromJson(
|
||||
_result.data!,
|
||||
(json) => json is List<dynamic>
|
||||
? json
|
||||
.map<TokenWithoutToken>((i) =>
|
||||
TokenWithoutToken.fromJson(i as Map<String, dynamic>))
|
||||
.toList()
|
||||
: List.empty(),
|
||||
);
|
||||
} on Object catch (e, s) {
|
||||
errorLogger?.logError(e, s, _options);
|
||||
rethrow;
|
||||
}
|
||||
return _value;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ApiResult<SharedToken>> getSharedToken({CancelToken? cancel}) async {
|
||||
final _extra = <String, dynamic>{};
|
||||
|
||||
@@ -40,6 +40,42 @@ class Token {
|
||||
Map<String, dynamic> toJson() => _$TokenToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class TokenWithoutToken {
|
||||
const TokenWithoutToken({
|
||||
required this.id,
|
||||
required this.uid,
|
||||
required this.expired,
|
||||
required this.httpOnly,
|
||||
required this.secure,
|
||||
required this.lastUsed,
|
||||
this.client,
|
||||
this.device,
|
||||
this.clientVersion,
|
||||
this.clientPlatform,
|
||||
});
|
||||
final int id;
|
||||
final int uid;
|
||||
@JsonKey(fromJson: _fromJson, toJson: _toJson)
|
||||
final DateTime expired;
|
||||
@JsonKey(name: 'http_only')
|
||||
final bool httpOnly;
|
||||
final bool secure;
|
||||
@JsonKey(fromJson: _fromJson, toJson: _toJson, name: 'last_used')
|
||||
final DateTime lastUsed;
|
||||
final String? client;
|
||||
final String? device;
|
||||
@JsonKey(name: 'client_version')
|
||||
final String? clientVersion;
|
||||
@JsonKey(name: 'client_platform')
|
||||
final String? clientPlatform;
|
||||
static DateTime _fromJson(String d) => DateTime.parse(d);
|
||||
static String _toJson(DateTime d) => d.toIso8601String();
|
||||
factory TokenWithoutToken.fromJson(Map<String, dynamic> json) =>
|
||||
_$TokenWithoutTokenFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$TokenWithoutTokenToJson(this);
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
class TokenWithUserInfo {
|
||||
const TokenWithUserInfo({
|
||||
|
||||
@@ -34,6 +34,34 @@ Map<String, dynamic> _$TokenToJson(Token instance) => <String, dynamic>{
|
||||
'client_platform': instance.clientPlatform,
|
||||
};
|
||||
|
||||
TokenWithoutToken _$TokenWithoutTokenFromJson(Map<String, dynamic> json) =>
|
||||
TokenWithoutToken(
|
||||
id: (json['id'] as num).toInt(),
|
||||
uid: (json['uid'] as num).toInt(),
|
||||
expired: TokenWithoutToken._fromJson(json['expired'] as String),
|
||||
httpOnly: json['http_only'] as bool,
|
||||
secure: json['secure'] as bool,
|
||||
lastUsed: TokenWithoutToken._fromJson(json['last_used'] as String),
|
||||
client: json['client'] as String?,
|
||||
device: json['device'] as String?,
|
||||
clientVersion: json['client_version'] as String?,
|
||||
clientPlatform: json['client_platform'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$TokenWithoutTokenToJson(TokenWithoutToken instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'uid': instance.uid,
|
||||
'expired': TokenWithoutToken._toJson(instance.expired),
|
||||
'http_only': instance.httpOnly,
|
||||
'secure': instance.secure,
|
||||
'last_used': TokenWithoutToken._toJson(instance.lastUsed),
|
||||
'client': instance.client,
|
||||
'device': instance.device,
|
||||
'client_version': instance.clientVersion,
|
||||
'client_platform': instance.clientPlatform,
|
||||
};
|
||||
|
||||
TokenWithUserInfo _$TokenWithUserInfoFromJson(Map<String, dynamic> json) =>
|
||||
TokenWithUserInfo(
|
||||
token: Token.fromJson(json['token'] as Map<String, dynamic>),
|
||||
|
||||
@@ -60,6 +60,7 @@ class AuthInfo {
|
||||
Future<void> checkSessionInfo() async {
|
||||
final data = (await api.getToken()).unwrap();
|
||||
_token = data.token;
|
||||
listener.tryEmit("auth_token_updated", null);
|
||||
final d = await device;
|
||||
final cv = await clientVersion;
|
||||
final cp = clientPlatform;
|
||||
@@ -87,6 +88,7 @@ class AuthInfo {
|
||||
clientVersion: ecv,
|
||||
clientPlatform: ecp);
|
||||
_token = re.unwrap();
|
||||
listener.tryEmit("auth_token_updated", null);
|
||||
} catch (e) {
|
||||
_log.warning("Failed to update token:", e);
|
||||
}
|
||||
|
||||
117
lib/components/session_card.dart
Normal file
117
lib/components/session_card.dart
Normal file
@@ -0,0 +1,117 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import '../api/token.dart';
|
||||
import '../api/user.dart';
|
||||
import '../globals.dart';
|
||||
import '../main.dart';
|
||||
|
||||
final _log = Logger("SessionCard");
|
||||
|
||||
Future<void> _deleteSession(int id, String errmsg) async {
|
||||
try {
|
||||
(await api.deleteTokenById(id)).unwrap();
|
||||
listener.tryEmit("delete_session", id);
|
||||
} catch (e) {
|
||||
_log.severe("Failed to delete session $id: $e");
|
||||
final snack = SnackBar(content: Text("$errmsg$e"));
|
||||
rootScaffoldMessengerKey.currentState?.showSnackBar(snack);
|
||||
}
|
||||
}
|
||||
|
||||
class SessionCard extends StatelessWidget {
|
||||
const SessionCard(this.token, {this.user, super.key});
|
||||
final BUser? user;
|
||||
final TokenWithoutToken token;
|
||||
String get device {
|
||||
var s = "";
|
||||
if (token.device != null) {
|
||||
s = token.device!;
|
||||
}
|
||||
var c = "";
|
||||
if (token.client != null) {
|
||||
c = token.client!;
|
||||
}
|
||||
if (token.clientPlatform != null) {
|
||||
if (c.isNotEmpty) {
|
||||
c += " ${token.clientPlatform!}";
|
||||
}
|
||||
}
|
||||
if (token.clientVersion != null) {
|
||||
if (c.isNotEmpty) {
|
||||
c += " ${token.clientVersion!}";
|
||||
}
|
||||
}
|
||||
if (s.isEmpty) {
|
||||
s = c;
|
||||
} else if (c.isNotEmpty) {
|
||||
s = "$s($c)";
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
final expiredTime =
|
||||
DateFormat.yMd(MainApp.of(context).lang.toLocale().toString())
|
||||
.add_jms()
|
||||
.format(token.expired);
|
||||
final lastUsed =
|
||||
DateFormat.yMd(MainApp.of(context).lang.toLocale().toString())
|
||||
.add_jms()
|
||||
.format(token.lastUsed);
|
||||
return Card.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text("${i18n.sessionId}${i18n.colon}${token.id}"),
|
||||
Text("${i18n.expireTime}${i18n.colon}$expiredTime"),
|
||||
Text("${i18n.lastUsedTime}${i18n.colon}$lastUsed"),
|
||||
device.isEmpty
|
||||
? Container()
|
||||
: Text("${i18n.device}${i18n.colon}$device"),
|
||||
user != null
|
||||
? Text("${i18n.username}${i18n.colon}${user!.username}")
|
||||
: Container(),
|
||||
],
|
||||
)),
|
||||
IconButton(
|
||||
onPressed: token.id != auth.token?.id
|
||||
? () => showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(i18n.deleteSession),
|
||||
content: Text(user != null
|
||||
? i18n.deleteSessionForUserConfirm(
|
||||
user!.username)
|
||||
: i18n.deleteSessionConfirm),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_deleteSession(
|
||||
token.id, i18n.failedDeleteSession);
|
||||
context.pop();
|
||||
},
|
||||
child: Text(i18n.yes)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop();
|
||||
},
|
||||
child: Text(i18n.no)),
|
||||
]);
|
||||
})
|
||||
: null,
|
||||
tooltip: i18n.delete,
|
||||
icon: const Icon(Icons.delete))
|
||||
])));
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,7 @@ enum MoreVertSettings {
|
||||
markAsAd,
|
||||
markAsNonAd,
|
||||
shareGallery,
|
||||
sessions,
|
||||
}
|
||||
|
||||
void onMoreVertSettingsSelected(BuildContext context, MoreVertSettings value) {
|
||||
@@ -207,6 +208,9 @@ void onMoreVertSettingsSelected(BuildContext context, MoreVertSettings value) {
|
||||
context.push("/dialog/gallery/share/$gid");
|
||||
}
|
||||
break;
|
||||
case MoreVertSettings.sessions:
|
||||
context.push("/sessions");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -234,6 +238,10 @@ List<PopupMenuEntry<MoreVertSettings>> buildMoreVertSettings(
|
||||
list.add(PopupMenuItem(
|
||||
value: MoreVertSettings.taskManager, child: Text(i18n.taskManager)));
|
||||
}
|
||||
if (path != "/sessions") {
|
||||
list.add(PopupMenuItem(
|
||||
value: MoreVertSettings.sessions, child: Text(i18n.sessionManagemant)));
|
||||
}
|
||||
if (path == "/gallery/:gid" && auth.canShareGallery == true) {
|
||||
list.add(PopupMenuItem(
|
||||
value: MoreVertSettings.shareGallery, child: Text(i18n.shareGallery)));
|
||||
|
||||
@@ -339,5 +339,21 @@
|
||||
"ok": "Ok",
|
||||
"changeSettings": "Change settings",
|
||||
"createUpdateMeiliSearchDataTask": "Create sync meilisearch server's data task",
|
||||
"updateMeiliSearchDataGidHelp": "If gallery id is not empty, only specified gallery will sync to meilisearch server."
|
||||
"updateMeiliSearchDataGidHelp": "If gallery id is not empty, only specified gallery will sync to meilisearch server.",
|
||||
"sessionManagemant": "Session Management",
|
||||
"deleteSession": "Delete session",
|
||||
"deleteSessionConfirm": "Do you want to delete session?",
|
||||
"deleteSessionForUserConfirm": "Do you want to delete session for user {user}?",
|
||||
"@deleteSessionForUserConfirm": {
|
||||
"placeholders": {
|
||||
"user": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionId": "Session ID",
|
||||
"lastUsedTime": "Last used time",
|
||||
"device": "Device",
|
||||
"failedDeleteSession": "Failed to delete session: ",
|
||||
"allUser": "All users"
|
||||
}
|
||||
|
||||
@@ -339,5 +339,21 @@
|
||||
"ok": "确定",
|
||||
"changeSettings": "修改设置",
|
||||
"createUpdateMeiliSearchDataTask": "创建同步meilisearch服务器数据任务",
|
||||
"updateMeiliSearchDataGidHelp": "如果画廊ID非空,只有指定的画廊会同步至meilisearch服务器。"
|
||||
"updateMeiliSearchDataGidHelp": "如果画廊ID非空,只有指定的画廊会同步至meilisearch服务器。",
|
||||
"sessionManagemant": "会话管理",
|
||||
"deleteSession": "删除会话",
|
||||
"deleteSessionConfirm": "是否删除会话?",
|
||||
"deleteSessionForUserConfirm": "是否为用户 {user} 删除会话?",
|
||||
"@deleteSessionForUserConfirm": {
|
||||
"placeholders": {
|
||||
"user": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sessionId": "会话 ID",
|
||||
"lastUsedTime": "上次使用时间",
|
||||
"device": "设备",
|
||||
"failedDeleteSession": "删除会话失败:",
|
||||
"allUser": "所有用户"
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import 'pages/galleries.dart';
|
||||
import 'pages/gallery.dart';
|
||||
import 'pages/home.dart';
|
||||
import 'pages/login.dart';
|
||||
import 'pages/sessions.dart';
|
||||
import 'pages/settings.dart';
|
||||
import 'pages/settings/cache.dart';
|
||||
import 'pages/settings/display.dart';
|
||||
@@ -356,6 +357,10 @@ final _router = GoRouter(
|
||||
return NewUpdateMeiliSearchDataTaskPage(gid: gid);
|
||||
});
|
||||
}),
|
||||
GoRoute(
|
||||
path: SessionsPage.routeName,
|
||||
builder: (context, state) => SessionsPage(key: state.pageKey),
|
||||
),
|
||||
],
|
||||
observers: [
|
||||
_NavigatorObserver(),
|
||||
|
||||
312
lib/pages/sessions.dart
Normal file
312
lib/pages/sessions.dart
Normal file
@@ -0,0 +1,312 @@
|
||||
import 'dart:ui';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:eh_downloader_flutter/api/token.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import '../api/user.dart';
|
||||
import '../../components/labeled_checkbox.dart';
|
||||
import '../components/session_card.dart';
|
||||
import '../globals.dart';
|
||||
import '../platform/media_query.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
final _log = Logger("SessionsPage");
|
||||
|
||||
class SessionsPage extends StatefulWidget {
|
||||
const SessionsPage({super.key});
|
||||
|
||||
static const String routeName = '/sessions';
|
||||
|
||||
@override
|
||||
State<SessionsPage> createState() => _SessionsPage();
|
||||
}
|
||||
|
||||
class _SessionsPage extends State<SessionsPage>
|
||||
with ThemeModeWidget, IsTopWidget2 {
|
||||
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
|
||||
GlobalKey<RefreshIndicatorState>();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
int? _uid;
|
||||
bool _allUser = false;
|
||||
List<BUser>? _users;
|
||||
List<TokenWithoutToken>? _tokens;
|
||||
bool _isLoading = false;
|
||||
bool _isLoadingUsers = false;
|
||||
CancelToken? _cancel;
|
||||
CancelToken? _cancel2;
|
||||
Object? _error;
|
||||
Future<void> _fetchUserData() async {
|
||||
try {
|
||||
_cancel = CancelToken();
|
||||
_isLoadingUsers = true;
|
||||
final users = (await api.getUsers(all: true, cancel: _cancel)).unwrap();
|
||||
if (!_cancel!.isCancelled) {
|
||||
setState(() {
|
||||
_users = users;
|
||||
_isLoadingUsers = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!_cancel!.isCancelled) {
|
||||
_log.severe("Failed to load user list:", e);
|
||||
setState(() {
|
||||
_error = e;
|
||||
_isLoadingUsers = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchData() async {
|
||||
try {
|
||||
_cancel2 = CancelToken();
|
||||
_isLoading = true;
|
||||
final tokens =
|
||||
(await api.getTokens(uid: _uid, allUser: _allUser, cancel: _cancel2))
|
||||
.unwrap();
|
||||
if (!_cancel2!.isCancelled) {
|
||||
setState(() {
|
||||
_tokens = tokens;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!_cancel2!.isCancelled) {
|
||||
_log.severe("Failed to load token list:", e);
|
||||
setState(() {
|
||||
_error = e;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
listener.on("user_logined", _onStateChanged);
|
||||
listener.on("auth_token_updated", _onStateChanged);
|
||||
listener.on("delete_session", _onDeleteSession);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancel?.cancel();
|
||||
_cancel2?.cancel();
|
||||
listener.removeEventListener("user_logined", _onStateChanged);
|
||||
listener.removeEventListener("auth_token_updated", _onStateChanged);
|
||||
listener.removeEventListener("delete_session", _onDeleteSession);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!tryInitApi(context)) {
|
||||
return Container();
|
||||
}
|
||||
final isLoadingUsers =
|
||||
auth.isAdmin == true && _users == null && _error == null;
|
||||
if (isLoadingUsers && !_isLoadingUsers) _fetchUserData();
|
||||
final isLoading = _tokens == null && _error == null;
|
||||
if (isLoading && !_isLoading) _fetchData();
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
final th = Theme.of(context);
|
||||
if (isTop(context)) {
|
||||
setCurrentTitle(i18n.sessionManagemant, th.primaryColor.value);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: _tokens == null && (auth.isAdmin != true || _users == null)
|
||||
? AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
context.canPop() ? context.pop() : context.go("/");
|
||||
},
|
||||
),
|
||||
title: Text(i18n.sessionManagemant),
|
||||
actions: [
|
||||
buildThemeModeIcon(context),
|
||||
buildMoreVertSettingsButon(context),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
body: isLoading || isLoadingUsers
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _tokens != null
|
||||
? _buildMain(context)
|
||||
: Center(
|
||||
child: Text("Error: $_error"),
|
||||
));
|
||||
}
|
||||
|
||||
Widget _buildMain(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
return Stack(children: [
|
||||
RefreshIndicator(
|
||||
key: _refreshIndicatorKey,
|
||||
onRefresh: () async {
|
||||
return await _fetchData();
|
||||
},
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.trackpad,
|
||||
},
|
||||
),
|
||||
child: _buildTokenList(context))),
|
||||
Positioned(
|
||||
bottom: size.height / 10,
|
||||
right: size.width / 10,
|
||||
child: _buildIconList(context)),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildRefreshIcon(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
_refreshIndicatorKey.currentState?.show();
|
||||
},
|
||||
tooltip: i18n.refresh,
|
||||
icon: const Icon(Icons.refresh));
|
||||
}
|
||||
|
||||
Widget _buildIconList(BuildContext context) {
|
||||
return Row(children: [
|
||||
isDesktop || (kIsWeb && pointerIsMouse)
|
||||
? _buildRefreshIcon(context)
|
||||
: Container(),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildTokenList(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
return CustomScrollView(slivers: [
|
||||
SliverAppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
context.canPop() ? context.pop() : context.go("/");
|
||||
},
|
||||
),
|
||||
title: Text(i18n.sessionManagemant),
|
||||
actions: [
|
||||
buildThemeModeIcon(context),
|
||||
buildMoreVertSettingsButon(context),
|
||||
],
|
||||
floating: true,
|
||||
),
|
||||
_buildUserSelect(context),
|
||||
_buildSliverGrid(context),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildAllUserCheckbox(BuildContext context) {
|
||||
if (auth.isRoot != true) return Container();
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: LabeledCheckbox(
|
||||
label: Text(i18n.allUser),
|
||||
value: _allUser,
|
||||
onChanged: (v) {
|
||||
if (v != null) {
|
||||
setState(() {
|
||||
_allUser = v;
|
||||
_fetchData();
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Widget _buildUserSelectBox(BuildContext context) {
|
||||
var userList = _users!;
|
||||
if (auth.isRoot != true) {
|
||||
userList.removeWhere((e) => e.isAdmin);
|
||||
}
|
||||
var items = userList
|
||||
.map((e) => DropdownMenuItem(value: e.id, child: Text(e.username)))
|
||||
.toList();
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
constraints: const BoxConstraints(maxWidth: 500),
|
||||
child: DropdownButtonFormField<int>(
|
||||
items: items,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
_uid = v;
|
||||
_fetchData();
|
||||
});
|
||||
},
|
||||
value: _uid ?? auth.user?.id,
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: i18n.user,
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserSelect(BuildContext context) {
|
||||
if (auth.isAdmin != true || _users == null) {
|
||||
return SliverToBoxAdapter(child: Container());
|
||||
}
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
final maxWidth = MediaQuery.of(context).size.width;
|
||||
return PinnedHeaderSliver(
|
||||
child: Container(
|
||||
color: cs.surface,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: auth.isRoot != true || maxWidth >= 500
|
||||
? Row(children: [
|
||||
_buildAllUserCheckbox(context),
|
||||
Expanded(child: _buildUserSelectBox(context)),
|
||||
])
|
||||
: Column(children: [
|
||||
_buildAllUserCheckbox(context),
|
||||
_buildUserSelectBox(context),
|
||||
]))));
|
||||
}
|
||||
|
||||
Widget _buildSliverGrid(BuildContext context) {
|
||||
return SliverGrid(
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 370.0,
|
||||
mainAxisSpacing: 10.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
mainAxisExtent: 200.0,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) {
|
||||
final token = _tokens![index]!;
|
||||
if (_allUser || (_uid != null && _uid != auth.user?.id)) {
|
||||
final ind = _users?.indexWhere((e) => e.id == token.uid);
|
||||
if (ind != null && ind > -1) {
|
||||
return SessionCard(token, user: _users![ind!]);
|
||||
}
|
||||
}
|
||||
return SessionCard(token);
|
||||
},
|
||||
childCount: _tokens!.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onStateChanged(dynamic _) {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _onDeleteSession(dynamic arg) {
|
||||
final id = arg as int;
|
||||
setState(() {
|
||||
_tokens?.removeWhere((e) => e.id == id);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user