mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-19 18:34:17 +08:00
Update
This commit is contained in:
@@ -19,4 +19,18 @@ class ApiResult<T> {
|
||||
final String? error;
|
||||
Map<String, dynamic> toJson(Object? Function(T) toJsonT) =>
|
||||
_$ApiResultToJson(this, toJsonT);
|
||||
T unwrap() {
|
||||
if (ok) {
|
||||
return data!;
|
||||
} else {
|
||||
return throw error!;
|
||||
}
|
||||
}
|
||||
(int, String) unwrapErr() {
|
||||
if (ok) {
|
||||
return throw 'unwrap_err called on ok ApiResult';
|
||||
} else {
|
||||
return (status, error!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,11 @@ abstract class _EHApi {
|
||||
factory _EHApi(Dio dio, {required String baseUrl}) = __EHApi;
|
||||
|
||||
@PUT('/user')
|
||||
@MultiPart()
|
||||
Future<ApiResult<int>> createUser(
|
||||
@Query("name") String name, @Query("password") String password,
|
||||
{@Query("is_admin") bool? isAdmin,
|
||||
@Query("permissions") int? permissions});
|
||||
@Part(name: "name") String name, @Part(name: "password") String password,
|
||||
{@Part(name: "is_admin") bool? isAdmin,
|
||||
@Part(name: "permissions") int? permissions});
|
||||
@GET('/user')
|
||||
Future<ApiResult<BUser>> getUser(
|
||||
{@Query("id") int? id, @Query("username") String? username});
|
||||
@@ -42,19 +43,21 @@ abstract class _EHApi {
|
||||
Future<ApiResult<ServerStatus>> getStatus();
|
||||
|
||||
@PUT('/token')
|
||||
@MultiPart()
|
||||
// ignore: unused_element
|
||||
Future<ApiResult<Token>> _createToken(
|
||||
{@Query("username") required String username,
|
||||
@Query("password") required String password,
|
||||
@Query("t") required int t,
|
||||
{@Part(name: "username") required String username,
|
||||
@Part(name: "password") required String password,
|
||||
@Part(name: "t") required int t,
|
||||
// ignore: unused_element
|
||||
@Query("set_cookie") bool? setCookie,
|
||||
@Part(name: "set_cookie") bool? setCookie,
|
||||
// ignore: unused_element
|
||||
@Query("http_only") bool? httpOnly,
|
||||
@Part(name: "http_only") bool? httpOnly,
|
||||
// ignore: unused_element
|
||||
@Query("secure") bool? secure});
|
||||
@Part(name: "secure") bool? secure});
|
||||
@DELETE('/token')
|
||||
Future<ApiResult<bool>> deleteToken({@Query("token") String? token});
|
||||
@MultiPart()
|
||||
Future<ApiResult<bool>> deleteToken({@Part(name: "token") String? token});
|
||||
@GET('/token')
|
||||
Future<ApiResult<TokenWithUserInfo>> getToken(
|
||||
{@Query("token") String? token});
|
||||
|
||||
@@ -26,20 +26,36 @@ class __EHApi implements _EHApi {
|
||||
int? permissions,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{
|
||||
r'name': name,
|
||||
r'password': password,
|
||||
r'is_admin': isAdmin,
|
||||
r'permissions': permissions,
|
||||
};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _data = null;
|
||||
final _data = FormData();
|
||||
_data.fields.add(MapEntry(
|
||||
'name',
|
||||
name,
|
||||
));
|
||||
_data.fields.add(MapEntry(
|
||||
'password',
|
||||
password,
|
||||
));
|
||||
if (isAdmin != null) {
|
||||
_data.fields.add(MapEntry(
|
||||
'is_admin',
|
||||
isAdmin.toString(),
|
||||
));
|
||||
}
|
||||
if (permissions != null) {
|
||||
_data.fields.add(MapEntry(
|
||||
'permissions',
|
||||
permissions.toString(),
|
||||
));
|
||||
}
|
||||
final _result = await _dio
|
||||
.fetch<Map<String, dynamic>>(_setStreamType<ApiResult<int>>(Options(
|
||||
method: 'PUT',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
contentType: 'multipart/form-data',
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
@@ -136,22 +152,46 @@ class __EHApi implements _EHApi {
|
||||
bool? secure,
|
||||
}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{
|
||||
r'username': username,
|
||||
r'password': password,
|
||||
r't': t,
|
||||
r'set_cookie': setCookie,
|
||||
r'http_only': httpOnly,
|
||||
r'secure': secure,
|
||||
};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _data = null;
|
||||
final _data = FormData();
|
||||
_data.fields.add(MapEntry(
|
||||
'username',
|
||||
username,
|
||||
));
|
||||
_data.fields.add(MapEntry(
|
||||
'password',
|
||||
password,
|
||||
));
|
||||
_data.fields.add(MapEntry(
|
||||
't',
|
||||
t.toString(),
|
||||
));
|
||||
if (setCookie != null) {
|
||||
_data.fields.add(MapEntry(
|
||||
'set_cookie',
|
||||
setCookie.toString(),
|
||||
));
|
||||
}
|
||||
if (httpOnly != null) {
|
||||
_data.fields.add(MapEntry(
|
||||
'http_only',
|
||||
httpOnly.toString(),
|
||||
));
|
||||
}
|
||||
if (secure != null) {
|
||||
_data.fields.add(MapEntry(
|
||||
'secure',
|
||||
secure.toString(),
|
||||
));
|
||||
}
|
||||
final _result = await _dio
|
||||
.fetch<Map<String, dynamic>>(_setStreamType<ApiResult<Token>>(Options(
|
||||
method: 'PUT',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
contentType: 'multipart/form-data',
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
@@ -174,15 +214,22 @@ class __EHApi implements _EHApi {
|
||||
@override
|
||||
Future<ApiResult<bool>> deleteToken({String? token}) async {
|
||||
const _extra = <String, dynamic>{};
|
||||
final queryParameters = <String, dynamic>{r'token': token};
|
||||
final queryParameters = <String, dynamic>{};
|
||||
queryParameters.removeWhere((k, v) => v == null);
|
||||
final _headers = <String, dynamic>{};
|
||||
final Map<String, dynamic>? _data = null;
|
||||
final _data = FormData();
|
||||
if (token != null) {
|
||||
_data.fields.add(MapEntry(
|
||||
'token',
|
||||
token,
|
||||
));
|
||||
}
|
||||
final _result = await _dio
|
||||
.fetch<Map<String, dynamic>>(_setStreamType<ApiResult<bool>>(Options(
|
||||
method: 'DELETE',
|
||||
headers: _headers,
|
||||
extra: _extra,
|
||||
contentType: 'multipart/form-data',
|
||||
)
|
||||
.compose(
|
||||
_dio.options,
|
||||
|
||||
27
lib/auth.dart
Normal file
27
lib/auth.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'api/status.dart';
|
||||
import 'api/user.dart';
|
||||
import 'globals.dart';
|
||||
|
||||
class AuthInfo {
|
||||
AuthInfo();
|
||||
BUser? _user;
|
||||
BUser? get user => _user;
|
||||
ServerStatus? _status;
|
||||
ServerStatus? get status => _status;
|
||||
bool get isAuthed => (_user != null);
|
||||
|
||||
Future<void> getServerStatus() async {
|
||||
_status = (await api.getStatus()).unwrap();
|
||||
}
|
||||
|
||||
Future<bool> checkAuth() async {
|
||||
final re = await api.getUser();
|
||||
if (re.ok) {
|
||||
_user = re.data!;
|
||||
} else {
|
||||
_user = null;
|
||||
}
|
||||
await getServerStatus();
|
||||
return re.ok;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'api/client.dart';
|
||||
import 'auth.dart';
|
||||
|
||||
final dio = Dio()
|
||||
..options.validateStatus = (int? _) {
|
||||
@@ -48,3 +49,5 @@ EHApi get api {
|
||||
}
|
||||
return _api!;
|
||||
}
|
||||
|
||||
final AuthInfo auth = AuthInfo();
|
||||
|
||||
@@ -2,8 +2,11 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'globals.dart';
|
||||
|
||||
final _log = Logger("HomePage");
|
||||
|
||||
class HomePage extends HookWidget {
|
||||
const HomePage({Key? key}) : super(key: key);
|
||||
|
||||
@@ -20,6 +23,17 @@ class HomePage extends HookWidget {
|
||||
return;
|
||||
}
|
||||
initApi(baseUrl);
|
||||
if (!auth.isAuthed) {
|
||||
auth.checkAuth().then((re) {
|
||||
if (!re) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
context.go(auth.status!.noUser ? "/create_root_user" : "/login");
|
||||
});
|
||||
}
|
||||
}).catchError((err) {
|
||||
_log.log(Level.SEVERE, "Failed to check auth info:", err);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}, []);
|
||||
return const Scaffold(
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"title": "E-Hentai Downloader Dashboard",
|
||||
"serverHost": "EH Downloader Server Host",
|
||||
"apiPath": "API Path"
|
||||
"apiPath": "API Path",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"save": "Save",
|
||||
"login": "Login"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"title": "E-Hentai 下载器面板",
|
||||
"serverHost": "EH Downloader 服务器主机",
|
||||
"apiPath": "API 路径"
|
||||
"apiPath": "API 路径",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"save": "保存",
|
||||
"login": "登录"
|
||||
}
|
||||
|
||||
136
lib/login.dart
Normal file
136
lib/login.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'globals.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({Key? key}) : super(key: key);
|
||||
|
||||
static const String routeName = '/login';
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
String _username = "";
|
||||
String _password = "";
|
||||
bool _passwordVisible = false;
|
||||
bool _isValid = false;
|
||||
bool _isLogin = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_username = "";
|
||||
_password = "";
|
||||
_passwordVisible = false;
|
||||
_isValid = false;
|
||||
_isLogin = false;
|
||||
}
|
||||
|
||||
static bool _checkIsValid(String username, String password) {
|
||||
return (username.isNotEmpty && password.isNotEmpty);
|
||||
}
|
||||
|
||||
void _usernameChanged(String value) {
|
||||
bool isValid = _checkIsValid(value, _password);
|
||||
setState(() {
|
||||
_username = value;
|
||||
_isValid = isValid;
|
||||
});
|
||||
}
|
||||
|
||||
void _passwordChanged(String value) {
|
||||
bool isValid = _checkIsValid(_username, value);
|
||||
setState(() {
|
||||
_password = value;
|
||||
_isValid = isValid;
|
||||
});
|
||||
}
|
||||
|
||||
void _passwordVisibleChanged() {
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
}
|
||||
|
||||
static Future<bool> _login(String username, String password) async {
|
||||
String baseUrl = api.baseUrl!;
|
||||
final u = Uri.parse(baseUrl);
|
||||
final re = await api.createToken(
|
||||
username: username,
|
||||
password: password,
|
||||
setCookie: true,
|
||||
httpOnly: true,
|
||||
secure: u.scheme == 'https');
|
||||
return re.ok;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 100),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.username,
|
||||
),
|
||||
initialValue: _username,
|
||||
onChanged: _usernameChanged,
|
||||
)),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(
|
||||
border: const OutlineInputBorder(),
|
||||
labelText: AppLocalizations.of(context)!.password,
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Theme.of(context).primaryColorDark,
|
||||
),
|
||||
onPressed: _passwordVisibleChanged,
|
||||
),
|
||||
),
|
||||
initialValue: _password,
|
||||
onChanged: _passwordChanged,
|
||||
obscureText: !_passwordVisible,
|
||||
)),
|
||||
ElevatedButton(
|
||||
onPressed: _isValid && !_isLogin
|
||||
? () {
|
||||
setState(() {
|
||||
_isLogin = true;
|
||||
});
|
||||
_login(_username, _password).then((re) {
|
||||
if (re) {
|
||||
context.go("/");
|
||||
} else {
|
||||
setState(() {
|
||||
_isLogin = false;
|
||||
});
|
||||
}
|
||||
}).catchError((e) {
|
||||
setState(() {
|
||||
_isLogin = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context)!.login)),
|
||||
]))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'package:chinese_font_library/chinese_font_library.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 'package:window_manager/window_manager.dart';
|
||||
import 'globals.dart';
|
||||
import 'home.dart';
|
||||
import 'login.dart';
|
||||
import 'set_server.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
@@ -18,9 +21,25 @@ final _router = GoRouter(
|
||||
path: SetServerPage.routeName,
|
||||
builder: (context, state) => const SetServerPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: LoginPage.routeName,
|
||||
builder: (context, state) => const LoginPage(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Future<void> initLogger() async {
|
||||
var logLevel = prefs.getInt("logLevel");
|
||||
var logLevelName = prefs.getString("logLevelName");
|
||||
if (logLevel != null && logLevelName != null) {
|
||||
Logger.root.level = Level(logLevelName, logLevel);
|
||||
}
|
||||
Logger.root.onRecord.listen((record) {
|
||||
print('${record.level.name}: ${record.time}: ${record.message}');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
void main() async {
|
||||
if (!kIsWeb) await prepareJar();
|
||||
await preparePrefs();
|
||||
@@ -28,6 +47,7 @@ void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await windowManager.ensureInitialized();
|
||||
}
|
||||
await initLogger();
|
||||
runApp(const MainApp());
|
||||
}
|
||||
|
||||
@@ -36,6 +56,7 @@ class MainApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var theme = ThemeData();
|
||||
return MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
onGenerateTitle: (context) {
|
||||
@@ -47,6 +68,7 @@ class MainApp extends StatelessWidget {
|
||||
},
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
theme: (kIsWeb || isWindows) ? theme.useSystemChineseFont() : theme,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class _SetServerPageState extends State<SetServerPage> {
|
||||
context.go('/');
|
||||
}
|
||||
: null,
|
||||
child: const Text("Save")),
|
||||
child: Text(AppLocalizations.of(context)!.save)),
|
||||
],
|
||||
))),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
bool get isDesktop => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS);
|
||||
bool get isDesktop =>
|
||||
!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS);
|
||||
bool get isWindows => Platform.isWindows;
|
||||
|
||||
18
pubspec.lock
18
pubspec.lock
@@ -121,6 +121,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
chinese_font_library:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: chinese_font_library
|
||||
sha256: aec5b5ab290c873251a9f7772b2101a851f4f87bf3b544b515a864de569ebcc0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -309,6 +317,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -374,7 +390,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
logging:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logging
|
||||
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
|
||||
|
||||
@@ -7,6 +7,7 @@ environment:
|
||||
sdk: '>=3.0.5 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
chinese_font_library: ^1.0.2
|
||||
cryptography: ^2.5.0
|
||||
cryptography_flutter: ^2.3.0
|
||||
dio: ^5.3.2
|
||||
@@ -19,6 +20,7 @@ dependencies:
|
||||
go_router: ^10.1.0
|
||||
intl: any
|
||||
json_annotation: ^4.8.1
|
||||
logging: ^1.2.0
|
||||
path_provider: ^2.1.0
|
||||
retrofit: ^4.0.1
|
||||
shared_preferences: ^2.2.0
|
||||
|
||||
Reference in New Issue
Block a user