mirror of
https://github.com/simon-ding/polaris.git
synced 2026-02-06 23:21:00 +08:00
login feature
This commit is contained in:
@@ -8,6 +8,12 @@ const (
|
||||
SettingDownloadDir = "download_dir"
|
||||
)
|
||||
|
||||
const (
|
||||
SettingAuthEnabled = "auth_enbled"
|
||||
SettingUsername = "auth_username"
|
||||
SettingPassword = "auth_password"
|
||||
)
|
||||
|
||||
const (
|
||||
IndexerTorznabImpl = "torznab"
|
||||
)
|
||||
|
||||
2
go.mod
2
go.mod
@@ -10,6 +10,8 @@ require (
|
||||
golang.org/x/net v0.25.0
|
||||
)
|
||||
|
||||
require github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 // indirect
|
||||
github.com/agext/levenshtein v1.2.1 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -52,6 +52,8 @@ github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package utils
|
||||
|
||||
import "unicode"
|
||||
import (
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func isASCII(s string) bool {
|
||||
for _, c := range s {
|
||||
@@ -11,3 +15,14 @@ func isASCII(s string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// HashPassword generates a bcrypt hash for the given password.
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// VerifyPassword verifies if the given password matches the stored hash.
|
||||
func VerifyPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
141
server/auth.go
Normal file
141
server/auth.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"polaris/db"
|
||||
"polaris/log"
|
||||
"polaris/pkg/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *Server) isAuthEnabled() bool {
|
||||
authEnabled := s.db.GetSetting(db.SettingAuthEnabled)
|
||||
return authEnabled == "true"
|
||||
}
|
||||
|
||||
func (s *Server) authModdleware(c *gin.Context) {
|
||||
if !s.isAuthEnabled() {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
auth := c.GetHeader("Authorization")
|
||||
if auth == "" {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
auth = strings.TrimPrefix(auth, "Bearer ")
|
||||
log.Infof("current token: %v", auth)
|
||||
token, err := jwt.ParseWithClaims(auth, &jwt.RegisteredClaims{}, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(secretKey), nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("parse token error: %v", err)
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !token.Valid {
|
||||
log.Errorf("token is not valid: %v", auth)
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
claim := token.Claims.(*jwt.RegisteredClaims)
|
||||
|
||||
if time.Until(claim.ExpiresAt.Time) <= 0 {
|
||||
log.Infof("token is no longer valid: %s", auth)
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
|
||||
}
|
||||
|
||||
type LoginIn struct {
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
const secretKey = "r1OF7nhpNjnYiGKtTLuKEVq7YznzT"
|
||||
|
||||
func (s *Server) Login(c *gin.Context) (interface{}, error) {
|
||||
var in LoginIn
|
||||
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
|
||||
if !s.isAuthEnabled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
user := s.db.GetSetting(db.SettingUsername)
|
||||
if user != in.User {
|
||||
return nil, errors.New("login fail")
|
||||
}
|
||||
password := s.db.GetSetting(db.SettingPassword)
|
||||
if !utils.VerifyPassword(in.Password, password) {
|
||||
return nil, errors.New("login fail")
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
|
||||
Issuer: "system",
|
||||
Subject: in.User,
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
})
|
||||
sig, err := token.SignedString([]byte(secretKey))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "sign")
|
||||
}
|
||||
return gin.H{
|
||||
"token": sig,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type EnableAuthIn struct {
|
||||
Enable bool `json:"enable"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (s *Server) EnableAuth(c *gin.Context) (interface{}, error) {
|
||||
var in EnableAuthIn
|
||||
if err := c.ShouldBindJSON(&in); err != nil {
|
||||
return nil, errors.Wrap(err, "bind json")
|
||||
}
|
||||
|
||||
if in.Enable && (in.User == "" || in.Password == "") {
|
||||
return nil, errors.New("user password should not empty")
|
||||
}
|
||||
if !in.Enable {
|
||||
log.Infof("disable auth")
|
||||
s.db.SetSetting(db.SettingAuthEnabled, "false")
|
||||
} else {
|
||||
log.Info("enable auth")
|
||||
s.db.SetSetting(db.SettingAuthEnabled, "true")
|
||||
s.db.SetSetting(db.SettingUsername, in.User)
|
||||
|
||||
hash, err := utils.HashPassword(in.Password)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "hash password")
|
||||
}
|
||||
s.db.SetSetting(db.SettingPassword, hash)
|
||||
}
|
||||
return "success", nil
|
||||
}
|
||||
|
||||
func (s *Server) GetAuthSetting(c *gin.Context) (interface{}, error) {
|
||||
enabled := s.db.GetSetting(db.SettingAuthEnabled)
|
||||
user := s.db.GetSetting(db.SettingUsername)
|
||||
|
||||
return EnableAuthIn{
|
||||
Enable: enabled == "true",
|
||||
User: user,
|
||||
}, nil
|
||||
}
|
||||
@@ -38,12 +38,17 @@ func (s *Server) Serve() error {
|
||||
//st, _ := fs.Sub(ui.Web, "build/web")
|
||||
s.r.Use(static.Serve("/", static.EmbedFolder(ui.Web, "build/web")))
|
||||
|
||||
s.r.POST("/api/login", HttpHandler(s.Login))
|
||||
|
||||
api := s.r.Group("/api/v1")
|
||||
api.Use(s.authModdleware)
|
||||
|
||||
setting := api.Group("/setting")
|
||||
{
|
||||
setting.POST("/do", HttpHandler(s.SetSetting))
|
||||
setting.GET("/do", HttpHandler(s.GetSetting))
|
||||
setting.POST("/auth", HttpHandler(s.EnableAuth))
|
||||
setting.GET("/auth", HttpHandler(s.GetAuthSetting))
|
||||
}
|
||||
|
||||
tv := api.Group("/tv")
|
||||
|
||||
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],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
306
ui/pubspec.lock
306
ui/pubspec.lock
@@ -1,6 +1,22 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
another_flushbar:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: another_flushbar
|
||||
sha256: "19bf9520230ec40b300aaf9dd2a8fefcb277b25ecd1c4838f530566965befc2a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.12.30"
|
||||
another_transformer_page_view:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: another_transformer_page_view
|
||||
sha256: a7cd46ede62d621c5abe7e58c7cb2745abe67b3bfec64f59b8889c93d7be7a8e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -57,6 +73,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.4.3+1"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.5"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -65,6 +89,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
ffi:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -78,6 +118,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
flutter_login:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_login
|
||||
sha256: "1f7c46d0d76081cf4c5180e3a265b1f5b1d7e48c81859f58f03a8dcd27338b85"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -96,6 +144,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
font_awesome_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: font_awesome_flutter
|
||||
sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "10.7.0"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -112,6 +168,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
intl_phone_number_input:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: intl_phone_number_input
|
||||
sha256: "1c4328713a9503ab26a1fdbb6b00b4cada68c18aac922b35bedbc72eff1297c3"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -136,6 +208,30 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
libphonenumber_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: libphonenumber_platform_interface
|
||||
sha256: f801f6c65523f56504b83f0890e6dad584ab3a7507dca65fec0eed640afea40f
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.4.2"
|
||||
libphonenumber_plugin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: libphonenumber_plugin
|
||||
sha256: c615021d9816fbda2b2587881019ed595ecdf54d999652d7e4cce0e1f026368c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.3"
|
||||
libphonenumber_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: libphonenumber_web
|
||||
sha256: "8186f420dbe97c3132283e52819daff1e55d60d6db46f7ea5ac42f42a28cc2ef"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -176,6 +272,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.12.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -184,6 +288,62 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
phone_numbers_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: phone_numbers_parser
|
||||
sha256: "62451b689d842791ed1fd5dc9eacf36ffa8bad23a78ad6cde732dc2fb222fae2"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
quiver:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -200,6 +360,70 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
sign_in_button:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sign_in_button
|
||||
sha256: "977b9b0415d2f3909e642275dfabba7919ba8e111324641b76cae6d1acbd183e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
@@ -269,6 +493,70 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.3.2"
|
||||
url_launcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.3.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -285,6 +573,22 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "14.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.5.1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
sdks:
|
||||
dart: ">=3.4.3 <4.0.0"
|
||||
flutter: ">=3.18.0-18.0.pre.54"
|
||||
flutter: ">=3.22.0"
|
||||
|
||||
@@ -39,6 +39,8 @@ dependencies:
|
||||
go_router: ^14.2.0
|
||||
flutter_riverpod: ^2.5.1
|
||||
quiver: ^3.2.1
|
||||
flutter_login: ^5.0.0
|
||||
shared_preferences: ^2.2.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user