diff --git a/lib/api/client.dart b/lib/api/client.dart index 2644158..b32776b 100644 --- a/lib/api/client.dart +++ b/lib/api/client.dart @@ -2,12 +2,12 @@ import 'dart:convert'; import 'package:cryptography/cryptography.dart'; import 'package:dio/dio.dart'; -import 'package:eh_downloader_flutter/api/file.dart'; import 'package:retrofit/retrofit.dart'; import 'api_result.dart'; import 'config.dart'; import 'eh.dart'; +import 'file.dart'; import 'gallery.dart'; import 'status.dart'; import 'tags.dart'; diff --git a/lib/api/user.dart b/lib/api/user.dart index 1eaa388..777a441 100644 --- a/lib/api/user.dart +++ b/lib/api/user.dart @@ -1,4 +1,6 @@ import 'package:enum_flag/enum_flag.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:json_annotation/json_annotation.dart'; part 'user.g.dart'; @@ -7,16 +9,35 @@ enum UserPermission with EnumFlag { readGallery, editGallery, deleteGallery, - manageTasks, + manageTasks; + + String localText(BuildContext context) { + final i18n = AppLocalizations.of(context)!; + switch (this) { + case UserPermission.readGallery: + return i18n.readGallery; + case UserPermission.editGallery: + return i18n.editGallery; + case UserPermission.deleteGallery: + return i18n.deleteGallery; + case UserPermission.manageTasks: + return i18n.manageTasks; + } + } } const userPermissionAll = 15; class UserPermissions { - const UserPermissions(this.code); - final int code; + UserPermissions(this.code); + int code; bool has(UserPermission permission) => code.hasFlag(permission); + bool get isAll => code == userPermissionAll; int toJson() => code; + void add(UserPermission flag) { + code |= flag.value; + } + static int toJson2(UserPermissions code) { return code.code; } @@ -25,6 +46,10 @@ class UserPermissions { return UserPermissions(code); } + void remove(UserPermission flag) { + code &= ~flag.value; + } + @override String toString() { if (code & userPermissionAll == userPermissionAll) return "all"; diff --git a/lib/components/gallery_info.dart b/lib/components/gallery_info.dart index 19ac3a5..058d11c 100644 --- a/lib/components/gallery_info.dart +++ b/lib/components/gallery_info.dart @@ -1,4 +1,4 @@ -import 'package:eh_downloader_flutter/globals.dart'; +import '../globals.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../api/file.dart'; diff --git a/lib/components/user_card.dart b/lib/components/user_card.dart index c66eb78..c011b7e 100644 --- a/lib/components/user_card.dart +++ b/lib/components/user_card.dart @@ -30,10 +30,12 @@ class UserCard extends StatelessWidget { style: TextStyle(color: cs.secondary)) ], )), - IconButton( - onPressed: () {}, - tooltip: i18n.edit, - icon: const Icon(Icons.edit)), + !user.isAdmin || (user.isAdmin && auth.isRoot == true) + ? IconButton( + onPressed: () {}, + tooltip: i18n.edit, + icon: const Icon(Icons.edit)) + : Container(), !user.isAdmin || (user.isAdmin && auth.isRoot == true && user.id != 0) ? IconButton( diff --git a/lib/components/user_permissions_chips.dart b/lib/components/user_permissions_chips.dart new file mode 100644 index 0000000..24cdc9a --- /dev/null +++ b/lib/components/user_permissions_chips.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../api/user.dart'; + +class UserPermissionsChips extends StatefulWidget { + const UserPermissionsChips({super.key, this.permissions, this.onChanged}); + final UserPermissions? permissions; + final ValueChanged? onChanged; + + @override + State createState() => _UserPermissionsChips(); +} + +class _UserPermissionsChips extends State { + late UserPermissions _permissions; + @override + void initState() { + _permissions = widget.permissions ?? UserPermissions(0); + super.initState(); + } + + void _onChanged() { + if (widget.onChanged != null) { + widget.onChanged!(_permissions); + } + } + + Widget _buildChips(BuildContext context) { + final i18n = AppLocalizations.of(context)!; + var list = [ + FilterChip( + label: Text(i18n.allPermissions), + selected: _permissions.isAll, + onSelected: (bool value) { + setState(() { + if (value) { + _permissions.code = userPermissionAll; + } else { + _permissions.code = 0; + } + }); + _onChanged(); + }, + ) + ]; + for (var flag in UserPermission.values) { + list.add(FilterChip( + label: Text(flag.localText(context)), + selected: _permissions.has(flag), + onSelected: (bool value) { + setState(() { + if (value) { + _permissions.add(flag); + } else { + _permissions.remove(flag); + } + }); + _onChanged(); + }, + )); + } + return Wrap(spacing: 5.0, children: list); + } + + @override + Widget build(BuildContext context) { + final i18n = AppLocalizations.of(context)!; + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ + const Icon(Icons.person), + Text(i18n.userPermissions, style: const TextStyle(fontSize: 20)), + ])), + _buildChips(context), + ]); + } +} diff --git a/lib/dialog/gallery_details_page.dart b/lib/dialog/gallery_details_page.dart index d1a815e..7fb0d8c 100644 --- a/lib/dialog/gallery_details_page.dart +++ b/lib/dialog/gallery_details_page.dart @@ -58,7 +58,7 @@ class _GalleryDetailsPage extends State { try { _cancel = CancelToken(); _isLoading = true; - final data = (await api.getGallery(widget.gid)).unwrap(); + final data = (await api.getGallery(widget.gid, cancel: _cancel)).unwrap(); if (!_cancel!.isCancelled) { setState(() { _meta = data.meta; diff --git a/lib/dialog/new_download_task_page.dart b/lib/dialog/new_download_task_page.dart index c70c063..807743f 100644 --- a/lib/dialog/new_download_task_page.dart +++ b/lib/dialog/new_download_task_page.dart @@ -1,10 +1,10 @@ import 'package:dio/dio.dart'; -import 'package:eh_downloader_flutter/api/task.dart'; -import 'package:eh_downloader_flutter/components/labeled_checkbox.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/task.dart'; +import '../components/labeled_checkbox.dart'; import '../components/number_field.dart'; import '../globals.dart'; import '../utils/parse_url.dart'; diff --git a/lib/dialog/new_user_page.dart b/lib/dialog/new_user_page.dart index 327e4c8..09e383f 100644 --- a/lib/dialog/new_user_page.dart +++ b/lib/dialog/new_user_page.dart @@ -1,10 +1,16 @@ +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/labeled_checkbox.dart'; +import '../components/user_permissions_chips.dart'; import '../globals.dart'; +final _log = Logger("NewUserPage"); + class NewUserPage extends StatefulWidget { const NewUserPage({super.key}); @@ -19,7 +25,12 @@ class _NewUserPage extends State { String _username = ""; String _password = ""; bool _isAdmin = false; + UserPermissions _permissions = + UserPermissions(UserPermission.readGallery.value); bool _passwordVisible = false; + CancelToken? _cancel; + bool _isRequesting = false; + int? _newUserId; Widget _buildWithVecticalPadding(Widget child) { return Container( @@ -28,6 +39,34 @@ class _NewUserPage extends State { ); } + Future _request() async { + setState(() { + _isRequesting = true; + }); + try { + _cancel = CancelToken(); + // 关闭对话框不中断连接 + _newUserId = (await api.createUser(_username, _password, + isAdmin: _isAdmin, + permissions: _isAdmin ? null : _permissions.code)) + .unwrap(); + if (!_cancel!.isCancelled) { + setState(() { + _isRequesting = false; + }); + } + listener.tryEmit("new_user", _newUserId); + } catch (e) { + _log.severe("Failed to create new user: $e"); + } + } + + @override + void dispose() { + _cancel?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { if (!tryInitApi(context)) { @@ -39,6 +78,12 @@ class _NewUserPage extends State { }); return Container(); } + if (_newUserId != null) { + SchedulerBinding.instance.addPostFrameCallback((_) { + context.canPop() ? context.pop() : context.go("/users"); + }); + return Container(); + } final i18n = AppLocalizations.of(context)!; final maxWidth = MediaQuery.of(context).size.width; return Container( @@ -51,6 +96,7 @@ class _NewUserPage extends State { child: Form( key: _formKey, child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( alignment: Alignment.center, @@ -117,6 +163,26 @@ class _NewUserPage extends State { } }, label: Text(i18n.admin))), + !_isAdmin + ? _buildWithVecticalPadding(UserPermissionsChips( + permissions: _permissions, + onChanged: (v) { + setState(() { + _permissions = v; + }); + })) + : Container(), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + ElevatedButton( + onPressed: !_isRequesting && + _username.isNotEmpty && + _password.isNotEmpty + ? () { + _request(); + } + : null, + child: Text(i18n.create)) + ]), ], ))), ); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 92fd4ab..39a4348 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -206,5 +206,11 @@ "user": "User", "edit": "Edit", "delete": "Delete", - "createNewUser": "Create new user" + "createNewUser": "Create new user", + "readGallery": "Read gallery", + "editGallery": "Edit gallery", + "deleteGallery": "Delete gallery", + "manageTasks": "Manage tasks", + "allPermissions": "All permissions", + "userPermissions": "User permissions" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 007be1e..727b0a0 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -206,5 +206,11 @@ "user": "用户", "edit": "编辑", "delete": "删除", - "createNewUser": "新建用户" + "createNewUser": "新建用户", + "readGallery": "查看画廊", + "editGallery": "编辑画廊", + "deleteGallery": "删除画廊", + "manageTasks": "管理任务", + "allPermissions": "所有权限", + "userPermissions": "用户权限" } diff --git a/lib/pages/gallery.dart b/lib/pages/gallery.dart index c57dedb..ad80c78 100644 --- a/lib/pages/gallery.dart +++ b/lib/pages/gallery.dart @@ -64,7 +64,7 @@ class _GalleryPage extends State try { _cancel = CancelToken(); _isLoading = true; - final data = (await api.getGallery(_gid)).unwrap(); + final data = (await api.getGallery(_gid, cancel: _cancel)).unwrap(); _data = data; final fileData = (await api.getFiles( data.pages.map((e) => e.token).toList(), diff --git a/lib/pages/users.dart b/lib/pages/users.dart index 7ebacf3..c66fc9d 100644 --- a/lib/pages/users.dart +++ b/lib/pages/users.dart @@ -23,12 +23,14 @@ class _UsersPage extends State with ThemeModeWidget, IsTopWidget2 { List? _users; bool _isLoading = false; CancelToken? _cancel; + CancelToken? _cancel2; Object? _error; Future _fetchData() async { + _cancel2?.cancel(); try { _cancel = CancelToken(); _isLoading = true; - final users = (await api.getUsers(all: true)).unwrap(); + final users = (await api.getUsers(all: true, cancel: _cancel)).unwrap(); if (!_cancel!.isCancelled) { setState(() { _users = users; @@ -153,4 +155,38 @@ class _UsersPage extends State with ThemeModeWidget, IsTopWidget2 { ), ); } + + @override + void dispose() { + _cancel?.cancel(); + _cancel2?.cancel(); + listener.removeEventListener("new_user", _onNewUser); + super.dispose(); + } + + @override + void initState() { + listener.on("new_user", _onNewUser); + super.initState(); + } + + void _onNewUser(dynamic arg) async { + int uid = arg as int; + try { + _cancel2 = CancelToken(); + final u = (await api.getUser(id: uid, cancel: _cancel2)).unwrap(); + if (!_cancel2!.isCancelled) { + setState(() { + _users?.add(u); + }); + } + } catch (e) { + if (!_cancel2!.isCancelled) { + _log.severe("Failed to load user $uid:", e); + setState(() { + _error = e; + }); + } + } + } } diff --git a/lib/viewer/single.dart b/lib/viewer/single.dart index 336438c..044c3b7 100644 --- a/lib/viewer/single.dart +++ b/lib/viewer/single.dart @@ -27,7 +27,11 @@ class SinglePageViewerExtra { class SinglePageViewer extends StatefulWidget { const SinglePageViewer( - {super.key, required this.gid, required this.index, this.data, this.files}); + {super.key, + required this.gid, + required this.index, + this.data, + this.files}); final GalleryData? data; final EhFiles? files; final int gid; @@ -89,7 +93,8 @@ class _SinglePageViewer extends State _cancel = CancelToken(); _isLoading = true; if (_data == null) { - final data = (await api.getGallery(widget.gid)).unwrap(); + final data = + (await api.getGallery(widget.gid, cancel: _cancel)).unwrap(); _data = data; } final fileData = (await api.getFiles(