mirror of
https://github.com/simon-ding/polaris.git
synced 2026-06-09 11:39:46 +08:00
login feature
This commit is contained in:
82
ui/lib/login_page.dart
Normal file
82
ui/lib/login_page.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart' show timeDilation;
|
||||
import 'package:flutter_login/flutter_login.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:quiver/strings.dart';
|
||||
import 'package:ui/providers/login.dart';
|
||||
import 'package:ui/weclome.dart';
|
||||
|
||||
class LoginScreen extends ConsumerWidget {
|
||||
static const route = '/login';
|
||||
|
||||
const LoginScreen({super.key});
|
||||
|
||||
Duration get loginTime => Duration(milliseconds: timeDilation.ceil() * 2250);
|
||||
|
||||
|
||||
Future<String?> _recoverPassword(String name) {
|
||||
return Future.delayed(loginTime).then((_) {
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return FlutterLogin(
|
||||
title: 'Polaris',
|
||||
onLogin: (data) {
|
||||
ref.read(authSettingProvider.notifier).login(data.name, data.password);
|
||||
},
|
||||
onSubmitAnimationCompleted: () {
|
||||
context.go(WelcomePage.route);
|
||||
},
|
||||
onRecoverPassword: _recoverPassword,
|
||||
userValidator: (value) => isBlank(value)? "不能为空":null,
|
||||
userType: LoginUserType.name,
|
||||
hideForgotPasswordButton: true,
|
||||
messages: LoginMessages(
|
||||
userHint: '用户名',
|
||||
passwordHint: '密码',
|
||||
loginButton: '登录',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class IntroWidget extends StatelessWidget {
|
||||
const IntroWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Column(
|
||||
children: [
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "You are trying to login/sign up on server hosted on ",
|
||||
),
|
||||
TextSpan(
|
||||
text: "example.com",
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(child: Divider()),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text("Authenticate"),
|
||||
),
|
||||
Expanded(child: Divider()),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:ui/login_page.dart';
|
||||
import 'package:ui/navdrawer.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/search.dart';
|
||||
import 'package:ui/system_settings.dart';
|
||||
import 'package:ui/tv_details.dart';
|
||||
@@ -74,15 +76,19 @@ class MyApp extends StatelessWidget {
|
||||
path: TvDetailsPage.route,
|
||||
builder: (context, state) =>
|
||||
TvDetailsPage(seriesId: state.pathParameters['id']!),
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final _router = GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
navigatorKey: APIs.navigatorKey,
|
||||
initialLocation: WelcomePage.route,
|
||||
routes: [
|
||||
_shellRoute,
|
||||
GoRoute(
|
||||
path: LoginScreen.route,
|
||||
builder: (context, state) =>const LoginScreen(),
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class APIs {
|
||||
static final _baseUrl = baseUrl();
|
||||
@@ -14,16 +18,50 @@ class APIs {
|
||||
static final addDownloadClientUrl = "$_baseUrl/api/v1/downloader/add";
|
||||
static final delDownloadClientUrl = "$_baseUrl/api/v1/downloader/del/";
|
||||
static final storageUrl = "$_baseUrl/api/v1/storage/";
|
||||
static final loginUrl = "$_baseUrl/api/login";
|
||||
static final loginSettingUrl = "$_baseUrl/api/v1/setting/auth";
|
||||
|
||||
static const tmdbImgBaseUrl = "https://image.tmdb.org/t/p/w500/";
|
||||
|
||||
static const tmdbApiKey = "tmdb_api_key";
|
||||
static const downloadDirKey = "download_dir";
|
||||
|
||||
static final GlobalKey<NavigatorState> navigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
|
||||
static String baseUrl() {
|
||||
if (kReleaseMode) {
|
||||
return "";
|
||||
}
|
||||
return "http://127.0.0.1:8080";
|
||||
}
|
||||
|
||||
static Dio? dio1;
|
||||
|
||||
static Future<Dio> getDio() async {
|
||||
if (dio1 != null) {
|
||||
return dio1!;
|
||||
}
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
var token = prefs.getString("token");
|
||||
var dio = Dio();
|
||||
dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
options.headers['Authorization'] = "Bearer $token";
|
||||
return handler.next(options);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
if (error.response?.statusCode != null &&
|
||||
error.response?.statusCode! == 403) {
|
||||
final context = navigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
context.go('/login');
|
||||
}
|
||||
}
|
||||
return handler.next(error);
|
||||
},
|
||||
));
|
||||
dio1 = dio;
|
||||
return dio;
|
||||
}
|
||||
}
|
||||
|
||||
61
ui/lib/providers/login.dart
Normal file
61
ui/lib/providers/login.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/server_response.dart';
|
||||
|
||||
var authSettingProvider =
|
||||
AsyncNotifierProvider.autoDispose<AuthSettingData, AuthSetting>(
|
||||
AuthSettingData.new);
|
||||
|
||||
class AuthSettingData extends AutoDisposeAsyncNotifier<AuthSetting> {
|
||||
@override
|
||||
FutureOr<AuthSetting> build() async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.loginSettingUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
var as = AuthSetting.fromJson(sp.data);
|
||||
return as;
|
||||
}
|
||||
|
||||
Future<void> updateAuthSetting(
|
||||
bool enable, String user, String password) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.post(APIs.loginSettingUrl,
|
||||
data: {"enable": enable, "user": user, "password": password});
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
|
||||
Future<void> login(String user, String password) async {
|
||||
var resp = await Dio()
|
||||
.post(APIs.loginUrl, data: {"user": user, "password": password});
|
||||
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
|
||||
if (sp.code != 0) {
|
||||
throw sp.message;
|
||||
}
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString("token", sp.data["token"]);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthSetting {
|
||||
bool enable;
|
||||
String user;
|
||||
|
||||
AuthSetting({required this.enable, required this.user});
|
||||
|
||||
factory AuthSetting.fromJson(Map<String, dynamic> json) {
|
||||
return AuthSetting(enable: json["enable"], user: json["user"]);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,8 @@ class SeriesDetailData
|
||||
extends AutoDisposeFamilyAsyncNotifier<SeriesDetails, String> {
|
||||
@override
|
||||
FutureOr<SeriesDetails> build(String arg) async {
|
||||
var resp = await Dio().get("${APIs.seriesDetailUrl}$arg");
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get("${APIs.seriesDetailUrl}$arg");
|
||||
var rsp = ServerResponse.fromJson(resp.data);
|
||||
if (rsp.code != 0) {
|
||||
throw rsp.message;
|
||||
@@ -22,7 +23,8 @@ class SeriesDetailData
|
||||
|
||||
Future<String> searchAndDownload(
|
||||
String seriesId, int seasonNum, int episodeNum) async {
|
||||
var resp = await Dio().post(APIs.searchAndDownloadUrl, data: {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.post(APIs.searchAndDownloadUrl, data: {
|
||||
"id": int.parse(seriesId),
|
||||
"season": seasonNum,
|
||||
"episode": episodeNum,
|
||||
|
||||
@@ -22,11 +22,11 @@ var storageSettingProvider =
|
||||
StorageSettingData.new);
|
||||
|
||||
class EditSettingData extends FamilyAsyncNotifier<String, String> {
|
||||
final dio = Dio();
|
||||
String? key;
|
||||
|
||||
@override
|
||||
FutureOr<String> build(String arg) async {
|
||||
final dio = await APIs.getDio();
|
||||
key = arg;
|
||||
var resp = await dio.get(APIs.settingsUrl, queryParameters: {"key": arg});
|
||||
var rrr = ServerResponse.fromJson(resp.data);
|
||||
@@ -40,6 +40,7 @@ class EditSettingData extends FamilyAsyncNotifier<String, String> {
|
||||
}
|
||||
|
||||
Future<void> updateSettings(String v) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.post(APIs.settingsUrl, data: {
|
||||
"key": key,
|
||||
"value": v,
|
||||
@@ -53,10 +54,9 @@ class EditSettingData extends FamilyAsyncNotifier<String, String> {
|
||||
}
|
||||
|
||||
class IndexerSetting extends AsyncNotifier<List<Indexer>> {
|
||||
final dio = Dio();
|
||||
|
||||
@override
|
||||
FutureOr<List<Indexer>> build() async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.allIndexersUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
@@ -75,6 +75,7 @@ class IndexerSetting extends AsyncNotifier<List<Indexer>> {
|
||||
isBlank(indexer.apiKey)) {
|
||||
return;
|
||||
}
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.post(APIs.addIndexerUrl, data: indexer.toJson());
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
@@ -84,6 +85,7 @@ class IndexerSetting extends AsyncNotifier<List<Indexer>> {
|
||||
}
|
||||
|
||||
Future<void> deleteIndexer(int id) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.delete("${APIs.delIndexerUrl}$id");
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
@@ -117,10 +119,9 @@ class Indexer {
|
||||
}
|
||||
|
||||
class DownloadClientSetting extends AsyncNotifier<List<DownloadClient>> {
|
||||
final dio = Dio();
|
||||
|
||||
@override
|
||||
FutureOr<List<DownloadClient>> build() async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.allDownloadClientsUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
@@ -137,7 +138,7 @@ class DownloadClientSetting extends AsyncNotifier<List<DownloadClient>> {
|
||||
if (name.isEmpty || url.isEmpty) {
|
||||
return;
|
||||
}
|
||||
var dio = Dio();
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.post(APIs.addDownloadClientUrl, data: {
|
||||
"name": name,
|
||||
"url": url,
|
||||
@@ -150,7 +151,7 @@ class DownloadClientSetting extends AsyncNotifier<List<DownloadClient>> {
|
||||
}
|
||||
|
||||
Future<void> deleteDownloadClients(int id) async {
|
||||
var dio = Dio();
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.delete("${APIs.delDownloadClientUrl}$id");
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
@@ -202,9 +203,10 @@ class DownloadClient {
|
||||
}
|
||||
|
||||
class StorageSettingData extends AsyncNotifier<List<Storage>> {
|
||||
final dio = Dio();
|
||||
|
||||
@override
|
||||
FutureOr<List<Storage>> build() async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.storageUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
@@ -219,6 +221,7 @@ class StorageSettingData extends AsyncNotifier<List<Storage>> {
|
||||
}
|
||||
|
||||
Future<void> deleteStorage(int id) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.delete("${APIs.storageUrl}$id");
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
@@ -228,6 +231,7 @@ class StorageSettingData extends AsyncNotifier<List<Storage>> {
|
||||
}
|
||||
|
||||
Future<void> addStorage(Storage s) async {
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.post(APIs.storageUrl, data: s.toJson());
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
if (sp.code != 0) {
|
||||
|
||||
@@ -6,7 +6,8 @@ import 'package:ui/providers/APIs.dart';
|
||||
import 'package:ui/providers/server_response.dart';
|
||||
|
||||
final welcomePageDataProvider = FutureProvider((ref) async {
|
||||
var resp = await Dio().get(APIs.watchlistUrl);
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.watchlistUrl);
|
||||
var sp = ServerResponse.fromJson(resp.data);
|
||||
List<TvSeries> favList = List.empty(growable: true);
|
||||
for (var item in sp.data as List) {
|
||||
@@ -39,7 +40,7 @@ class SearchPageData extends AutoDisposeAsyncNotifier<List<SearchResult>> {
|
||||
}
|
||||
|
||||
void queryResults(String q) async {
|
||||
final dio = Dio();
|
||||
final dio = await APIs.getDio();
|
||||
var resp = await dio.get(APIs.searchUrl, queryParameters: {"query": q});
|
||||
//var dy = jsonDecode(resp.data.toString());
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:quiver/strings.dart';
|
||||
import 'package:ui/providers/login.dart';
|
||||
import 'package:ui/providers/settings.dart';
|
||||
import 'package:ui/utils.dart';
|
||||
|
||||
@@ -24,6 +25,8 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
Future<void>? _pendingStorage;
|
||||
final _tmdbApiController = TextEditingController();
|
||||
final _downloadDirController = TextEditingController();
|
||||
bool? _enableAuth;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var tmdbKey = ref.watch(settingProvider(APIs.tmdbApiKey));
|
||||
@@ -238,6 +241,60 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
loading: () => const CircularProgressIndicator());
|
||||
});
|
||||
|
||||
var authData = ref.watch(authSettingProvider);
|
||||
TextEditingController _userController = TextEditingController();
|
||||
TextEditingController _passController = TextEditingController();
|
||||
var authSetting = authData.when(
|
||||
data: (data) {
|
||||
if (_enableAuth == null) {
|
||||
setState(() {
|
||||
_enableAuth = data.enable;
|
||||
});
|
||||
}
|
||||
_userController.text = data.user;
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text("开启认证"),
|
||||
value: _enableAuth!,
|
||||
onChanged: (v) {
|
||||
setState(() {
|
||||
_enableAuth = v;
|
||||
});
|
||||
}),
|
||||
_enableAuth!
|
||||
? Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _userController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "用户名",
|
||||
icon: Icon(Icons.verified_user),
|
||||
)),
|
||||
TextFormField(
|
||||
controller: _passController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: "密码",
|
||||
icon: Icon(Icons.verified_user),
|
||||
))
|
||||
],
|
||||
)
|
||||
: const Column(),
|
||||
Center(
|
||||
child: ElevatedButton(
|
||||
child: const Text("保存"),
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(authSettingProvider.notifier)
|
||||
.updateAuthSetting(_enableAuth!,
|
||||
_userController.text, _passController.text);
|
||||
}))
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (err, trace) => Text("$err"),
|
||||
loading: () => const CircularProgressIndicator());
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
ExpansionTile(
|
||||
@@ -268,6 +325,13 @@ class _SystemSettingsPageState extends ConsumerState<SystemSettingsPage> {
|
||||
title: const Text("存储设置"),
|
||||
children: [storageSetting],
|
||||
),
|
||||
ExpansionTile(
|
||||
tilePadding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
|
||||
childrenPadding: const EdgeInsets.fromLTRB(50, 0, 50, 0),
|
||||
initiallyExpanded: true,
|
||||
title: const Text("认证设置"),
|
||||
children: [authSetting],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user