Add user managemant page

This commit is contained in:
2024-05-28 05:06:03 +00:00
committed by GitHub
parent 10a8e80762
commit f06dc34c7e
16 changed files with 435 additions and 12 deletions

View File

@@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:dio/dio.dart';
import 'package:eh_downloader_flutter/api/file.dart';
import 'package:retrofit/dio.dart';
import 'package:retrofit/retrofit.dart';
import 'api_result.dart';
@@ -82,6 +81,12 @@ abstract class _EHApi {
{@Query("id") int? id,
@Query("username") String? username,
@CancelRequest() CancelToken? cancel});
@GET('/user/list')
Future<ApiResult<List<BUser>>> getUsers(
{@Query("all") bool? all,
@Query("offset") int? offset,
@Query("limit") int? limit,
@CancelRequest() CancelToken? cancel});
@GET('/status')
Future<ApiResult<ServerStatus>> getStatus(

View File

@@ -116,6 +116,51 @@ class __EHApi implements _EHApi {
return value;
}
@override
Future<ApiResult<List<BUser>>> getUsers({
bool? all,
int? offset,
int? limit,
CancelToken? cancel,
}) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{
r'all': all,
r'offset': offset,
r'limit': limit,
};
queryParameters.removeWhere((k, v) => v == null);
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<ApiResult<List<BUser>>>(Options(
method: 'GET',
headers: _headers,
extra: _extra,
)
.compose(
_dio.options,
'/user/list',
queryParameters: queryParameters,
data: _data,
cancelToken: cancel,
)
.copyWith(
baseUrl: _combineBaseUrls(
_dio.options.baseUrl,
baseUrl,
))));
final value = ApiResult<List<BUser>>.fromJson(
_result.data!,
(json) => json is List<dynamic>
? json
.map<BUser>((i) => BUser.fromJson(i as Map<String, dynamic>))
.toList()
: List.empty(),
);
return value;
}
@override
Future<ApiResult<ServerStatus>> getStatus({CancelToken? cancel}) async {
final _extra = <String, dynamic>{};

View File

@@ -21,6 +21,7 @@ class AuthInfo {
bool _isChecking = false;
bool get isChecking => _isChecking;
bool? get isAdmin => _user?.isAdmin;
bool? get isRoot => _user != null ? _user!.id == 0 : null;
bool? get isDocker => _status?.isDocker;
bool? get canReadGallery =>
_user?.permissions.has(UserPermission.readGallery);

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../api/user.dart';
import '../globals.dart';
class UserCard extends StatelessWidget {
const UserCard(this.user, {super.key});
final BUser user;
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
final cs = Theme.of(context).colorScheme;
return Card.outlined(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
SelectableText(
user.username,
maxLines: 1,
style: TextStyle(
fontWeight: FontWeight.bold, color: cs.primary),
),
Text(user.isAdmin ? i18n.admin : i18n.user,
style: TextStyle(color: cs.secondary))
],
)),
IconButton(
onPressed: () {},
tooltip: i18n.edit,
icon: const Icon(Icons.edit)),
!user.isAdmin ||
(user.isAdmin && auth.isRoot == true && user.id != 0)
? IconButton(
onPressed: () {},
tooltip: i18n.delete,
icon: const Icon(Icons.delete))
: Container(),
])));
}
}

View File

@@ -13,6 +13,8 @@ class DownloadZipPage extends StatefulWidget {
const DownloadZipPage(this.gid, {super.key});
final int gid;
static const routeName = '/dialog/download/zip/:gid';
@override
State<DownloadZipPage> createState() => _DownloadZipPage();
}

View File

@@ -42,6 +42,8 @@ class GalleryDetailsPage extends StatefulWidget {
final int gid;
final GMeta? meta;
static const routeName = '/dialog/gallery/details/:gid';
@override
State<GalleryDetailsPage> createState() => _GalleryDetailsPage();
}

View File

@@ -16,6 +16,8 @@ class NewDownloadTaskPage extends StatefulWidget {
final int? gid;
final String? token;
static const routeName = "/dialog/new_download_task";
@override
State<NewDownloadTaskPage> createState() => _NewDownloadTaskPage();
}

View File

@@ -0,0 +1,124 @@
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import '../components/labeled_checkbox.dart';
import '../globals.dart';
class NewUserPage extends StatefulWidget {
const NewUserPage({super.key});
static const routeName = "/dialog/user/new";
@override
State<StatefulWidget> createState() => _NewUserPage();
}
class _NewUserPage extends State<NewUserPage> {
final _formKey = GlobalKey<FormState>();
String _username = "";
String _password = "";
bool _isAdmin = false;
bool _passwordVisible = false;
Widget _buildWithVecticalPadding(Widget child) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: child,
);
}
@override
Widget build(BuildContext context) {
if (!tryInitApi(context)) {
return Container();
}
if (auth.isAdmin == false) {
SchedulerBinding.instance.addPostFrameCallback((_) {
context.go("/");
});
return Container();
}
final i18n = AppLocalizations.of(context)!;
final maxWidth = MediaQuery.of(context).size.width;
return Container(
padding: maxWidth < 400
? const EdgeInsets.symmetric(vertical: 20, horizontal: 5)
: const EdgeInsets.all(20),
width: maxWidth < 810 ? null : 800,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(10)),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
children: [
Stack(
alignment: Alignment.center,
children: [
Text(
i18n.createNewUser,
style: Theme.of(context).textTheme.headlineSmall,
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: () => context.canPop()
? context.pop()
: context.go("/users"),
icon: const Icon(Icons.close),
)),
],
),
_buildWithVecticalPadding(TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.username,
),
initialValue: _username,
onChanged: (value) {
setState(() {
_username = value;
});
},
)),
_buildWithVecticalPadding(TextFormField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: i18n.password,
suffixIcon: IconButton(
icon: Icon(
_passwordVisible
? Icons.visibility
: Icons.visibility_off,
color: Theme.of(context).primaryColorDark,
),
onPressed: () {
setState(() {
_passwordVisible = !_passwordVisible;
});
},
),
),
initialValue: _password,
onChanged: (value) {
setState(() {
_password = value;
});
},
obscureText: !_passwordVisible,
)),
_buildWithVecticalPadding(LabeledCheckbox(
value: _isAdmin,
onChanged: (b) {
if (b != null) {
setState(() {
_isAdmin = b;
});
}
},
label: Text(i18n.admin))),
],
))),
);
}
}

View File

@@ -39,6 +39,8 @@ class TaskPage extends StatefulWidget {
const TaskPage(this.id, {super.key});
final int id;
static const routeName = "/dialog/task/:id";
@override
State<StatefulWidget> createState() => _TaskPage();
}

View File

@@ -355,8 +355,10 @@ void checkAuth(BuildContext context) {
if (!auth.isAuthed && !auth.checked && !auth.isChecking) {
auth.checkAuth().then((re) {
if (!re) {
if (auth.status!.noUser && prefs.getBool("skipCreateRootUser") == true)
if (auth.status!.noUser &&
prefs.getBool("skipCreateRootUser") == true) {
return;
}
final loc = auth.status!.noUser ? "/create_root_user" : "/login";
final path = GoRouterState.of(context).path;
if (path != loc) {

View File

@@ -200,5 +200,11 @@
"cachedFileSize": "Cached file size",
"update": "Update",
"updateFileSize": "Update file size",
"clearCaches": "Clear caches"
"clearCaches": "Clear caches",
"userManagemant": "User Management",
"admin": "Administrator",
"user": "User",
"edit": "Edit",
"delete": "Delete",
"createNewUser": "Create new user"
}

View File

@@ -200,5 +200,11 @@
"cachedFileSize": "已缓存文件大小",
"update": "更新",
"updateFileSize": "更新文件大小",
"clearCaches": "清除缓存"
"clearCaches": "清除缓存",
"userManagemant": "用户管理",
"admin": "管理员",
"user": "用户",
"edit": "编辑",
"delete": "删除",
"createNewUser": "新建用户"
}

View File

@@ -11,6 +11,7 @@ import 'dialog/dialog_page.dart';
import 'dialog/download_zip_page.dart';
import 'dialog/gallery_details_page.dart';
import 'dialog/new_download_task_page.dart';
import 'dialog/new_user_page.dart';
import 'dialog/task_page.dart';
import 'globals.dart';
import 'logs/file.dart';
@@ -23,6 +24,7 @@ import 'pages/server_settings.dart';
import 'pages/set_server.dart';
import 'pages/settings.dart';
import 'pages/task_manager.dart';
import 'pages/users.dart';
import 'utils.dart';
import 'viewer/single.dart';
@@ -97,7 +99,7 @@ final _router = GoRouter(
redirect: (context, state) => "/galleries",
),
GoRoute(
path: '/dialog/download/zip/:gid',
path: DownloadZipPage.routeName,
pageBuilder: (context, state) => DialogPage(
key: state.pageKey,
builder: (context) {
@@ -140,7 +142,7 @@ final _router = GoRouter(
}
}),
GoRoute(
path: '/dialog/gallery/details/:gid',
path: GalleryDetailsPage.routeName,
pageBuilder: (context, state) {
final extra = state.extra as GalleryDetailsPageExtra?;
return DialogPage(
@@ -169,7 +171,7 @@ final _router = GoRouter(
builder: (context, state) => TaskManagerPage(key: state.pageKey),
),
GoRoute(
path: "/dialog/new_download_task",
path: NewDownloadTaskPage.routeName,
pageBuilder: (context, state) {
int? gid;
String? token;
@@ -186,7 +188,7 @@ final _router = GoRouter(
});
}),
GoRoute(
path: "/dialog/task/:id",
path: TaskPage.routeName,
pageBuilder: (context, state) {
return DialogPage(
key: state.pageKey,
@@ -203,6 +205,19 @@ final _router = GoRouter(
return "/task_manager";
}
}),
GoRoute(
path: UsersPage.routeName,
builder: (context, state) => const UsersPage(),
),
GoRoute(
path: NewUserPage.routeName,
pageBuilder: (context, state) {
return DialogPage(
key: state.pageKey,
builder: (context) {
return const NewUserPage();
});
}),
],
);

View File

@@ -13,6 +13,7 @@ class HomeDrawer extends StatelessWidget {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return Drawer(
child: ListView(
children: <Widget>[
@@ -26,7 +27,7 @@ class HomeDrawer extends StatelessWidget {
),
ListTile(
leading: const Icon(Icons.collections),
title: Text(AppLocalizations.of(context)!.galleries),
title: Text(i18n.galleries),
onTap: () {
Scaffold.of(context).closeDrawer();
context.push("/galleries");
@@ -35,7 +36,7 @@ class HomeDrawer extends StatelessWidget {
auth.isAdmin == true
? ListTile(
leading: const Icon(Icons.admin_panel_settings),
title: Text(AppLocalizations.of(context)!.serverSettings),
title: Text(i18n.serverSettings),
onTap: () {
Scaffold.of(context).closeDrawer();
context.push("/server_settings");
@@ -45,13 +46,22 @@ class HomeDrawer extends StatelessWidget {
auth.canManageTasks == true
? ListTile(
leading: const Icon(Icons.task),
title: Text(AppLocalizations.of(context)!.taskManager),
title: Text(i18n.taskManager),
onTap: () {
Scaffold.of(context).closeDrawer();
context.push("/task_manager");
},
)
: Container(),
auth.isAdmin == true
? ListTile(
leading: const Icon(Icons.manage_accounts),
title: Text(i18n.userManagemant),
onTap: () {
Scaffold.of(context).closeDrawer();
context.push("/users");
})
: Container(),
ListTile(
leading: const Icon(Icons.settings),
title: Text(AppLocalizations.of(context)!.settings),

156
lib/pages/users.dart Normal file
View File

@@ -0,0 +1,156 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.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/user_card.dart';
import '../globals.dart';
final _log = Logger("UsersPage");
class UsersPage extends StatefulWidget {
const UsersPage({super.key});
static const String routeName = '/users';
@override
State<UsersPage> createState() => _UsersPage();
}
class _UsersPage extends State<UsersPage> with ThemeModeWidget, IsTopWidget2 {
List<BUser>? _users;
bool _isLoading = false;
CancelToken? _cancel;
Object? _error;
Future<void> _fetchData() async {
try {
_cancel = CancelToken();
_isLoading = true;
final users = (await api.getUsers(all: true)).unwrap();
if (!_cancel!.isCancelled) {
setState(() {
_users = users;
_isLoading = false;
});
}
} catch (e) {
if (!_cancel!.isCancelled) {
_log.severe("Failed to load user list:", e);
setState(() {
_error = e;
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
if (!tryInitApi(context)) {
return Container();
}
if (auth.isAdmin == false) {
SchedulerBinding.instance.addPostFrameCallback((_) {
context.go("/");
});
return Container();
}
final isLoading = _users == null && _error == null;
if (isLoading && !_isLoading) _fetchData();
final i18n = AppLocalizations.of(context)!;
final th = Theme.of(context);
if (isTop(context)) {
setCurrentTitle(i18n.userManagemant, th.primaryColor.value);
}
return Scaffold(
appBar: _users == null
? AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.canPop() ? context.pop() : context.go("/");
},
),
title: Text(i18n.userManagemant),
actions: [
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
],
)
: null,
body: isLoading
? const Center(child: CircularProgressIndicator())
: _users != null
? _buildMain(context)
: Center(
child: Text("Error: $_error"),
));
}
Widget _buildMain(BuildContext context) {
final size = MediaQuery.of(context).size;
return Stack(children: [
_buildUserList(context),
Positioned(
bottom: size.height / 10,
right: size.width / 10,
child: _buildIconList(context)),
]);
}
Widget _buildAddIcon(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
return IconButton(
onPressed: () {
context.push("/dialog/user/new");
},
tooltip: i18n.create,
icon: const Icon(Icons.add));
}
Widget _buildIconList(BuildContext context) {
return Row(children: [
_buildAddIcon(context),
]);
}
Widget _buildUserList(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.userManagemant),
actions: [
buildThemeModeIcon(context),
buildMoreVertSettingsButon(context),
],
floating: true,
),
_buildSliverGrid(context),
]);
}
Widget _buildSliverGrid(BuildContext context) {
return SliverGrid(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 360.0,
mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0,
childAspectRatio: 4.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return UserCard(_users![index]!);
},
childCount: _users!.length,
),
);
}
}

View File

@@ -62,7 +62,6 @@ dev_dependencies:
build_runner: ^2.4.6
retrofit_generator: ^8.1.0
json_serializable: ^6.7.1
sqflite_common_ffi: ^2.3.3
flutter:
generate: true