Implement signIn flow
This commit is contained in:
parent
02fc87fe25
commit
fbb3756746
@ -19,6 +19,9 @@
|
||||
3D9063E72B279A320063DD2A /* JelUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063E62B279A320063DD2A /* JelUITests.swift */; };
|
||||
3D9063E92B279A320063DD2A /* JelUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9063E82B279A320063DD2A /* JelUITestsLaunchTests.swift */; };
|
||||
3D9064592B27E4C70063DD2A /* JellyfinKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D9064582B27E4C70063DD2A /* JellyfinKit */; };
|
||||
3D91FDC92B28C62800919017 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDC82B28C62800919017 /* SignInView.swift */; };
|
||||
3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */; };
|
||||
3D91FDCD2B2907E800919017 /* JellyfinDateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */; };
|
||||
3DF1ED3E2B282836000AD8EA /* JellyfinClientController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@ -69,6 +72,9 @@
|
||||
3D9063E22B279A320063DD2A /* JelUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JelUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3D9063E62B279A320063DD2A /* JelUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JelUITests.swift; sourceTree = "<group>"; };
|
||||
3D9063E82B279A320063DD2A /* JelUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JelUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||
3D91FDC82B28C62800919017 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = "<group>"; };
|
||||
3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInToServerView.swift; sourceTree = "<group>"; };
|
||||
3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinDateFormatter.swift; sourceTree = "<group>"; };
|
||||
3DC0E5802B2832B9001CCE96 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
3DF1ED3D2B282836000AD8EA /* JellyfinClientController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinClientController.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@ -102,7 +108,7 @@
|
||||
3D1015D72B27F54A00F5C29A /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D1015D82B27F57400F5C29A /* AddServerView.swift */,
|
||||
3D91FDC52B28C28900919017 /* SignIn */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -120,6 +126,7 @@
|
||||
3D1015E02B27FE5700F5C29A /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D91FDCC2B2907E800919017 /* JellyfinDateFormatter.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@ -187,6 +194,16 @@
|
||||
path = JelUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3D91FDC52B28C28900919017 /* SignIn */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3D91FDC82B28C62800919017 /* SignInView.swift */,
|
||||
3D1015D82B27F57400F5C29A /* AddServerView.swift */,
|
||||
3D91FDCA2B28CA2500919017 /* SignInToServerView.swift */,
|
||||
);
|
||||
path = SignIn;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -328,7 +345,10 @@
|
||||
3DF1ED3E2B282836000AD8EA /* JellyfinClientController.swift in Sources */,
|
||||
3D1015D92B27F57400F5C29A /* AddServerView.swift in Sources */,
|
||||
3D9063CB2B279A310063DD2A /* JelApp.swift in Sources */,
|
||||
3D91FDCD2B2907E800919017 /* JellyfinDateFormatter.swift in Sources */,
|
||||
3D1015DC2B27F5D300F5C29A /* Model.xcdatamodeld in Sources */,
|
||||
3D91FDC92B28C62800919017 /* SignInView.swift in Sources */,
|
||||
3D91FDCB2B28CA2500919017 /* SignInToServerView.swift in Sources */,
|
||||
3D1015E42B28000E00F5C29A /* AuthStateController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -9,11 +9,10 @@ import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@ObservedObject var authState: AuthStateController
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if !authState.loggedIn {
|
||||
AddServerView(authState: authState)
|
||||
SignInView(authState: authState)
|
||||
} else {
|
||||
Text("Logged in")
|
||||
Button("Log out") {
|
||||
|
@ -14,6 +14,8 @@ class AuthStateController: ObservableObject {
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
|
||||
static let shared = AuthStateController()
|
||||
|
||||
init(loggedIn: Bool = false, serverUrl: URL? = nil, authToken: String? = nil) {
|
||||
self.loggedIn = loggedIn
|
||||
self.serverUrl = serverUrl
|
||||
|
@ -9,22 +9,74 @@ import Foundation
|
||||
import Get
|
||||
import JellyfinKit
|
||||
|
||||
class JellyfinClientController {
|
||||
let api: APIClient
|
||||
struct AuthHeaders: Codable {
|
||||
var Client: String
|
||||
var Device: String
|
||||
var DeviceId: String
|
||||
var Version: String
|
||||
var Token: String
|
||||
}
|
||||
|
||||
enum JellyfinClientError: Error {
|
||||
case badResponseCode
|
||||
}
|
||||
|
||||
extension AuthHeaders {
|
||||
func format() -> String {
|
||||
return "MediaBrowser Client=\(self.Client), Device=\(self.Device), DeviceId=\(self.DeviceId), Version=\(self.Version), Token=\(self.Token)"
|
||||
}
|
||||
}
|
||||
|
||||
class JellyfinClientController: ObservableObject {
|
||||
private var api: APIClient
|
||||
|
||||
init(serverUrl: URL) {
|
||||
self.api = APIClient(
|
||||
baseURL: serverUrl
|
||||
)
|
||||
private var authHeaders: AuthHeaders
|
||||
private var authState: AuthStateController
|
||||
|
||||
init(authHeaders: AuthHeaders, serverUrl: URL? = nil, authState: AuthStateController = AuthStateController.shared) {
|
||||
self.authHeaders = authHeaders
|
||||
self.authState = authState
|
||||
|
||||
self.api = APIClient(baseURL: serverUrl)
|
||||
self.setUrl(url: serverUrl)
|
||||
}
|
||||
|
||||
func setToken(token: String) {
|
||||
self.authHeaders.Token = token
|
||||
}
|
||||
|
||||
func setUrl(url: URL?) {
|
||||
if url == nil {
|
||||
return
|
||||
}
|
||||
|
||||
self.api = APIClient(baseURL: url, {
|
||||
$0.sessionConfiguration.httpAdditionalHeaders = ["Authorization": self.authHeaders.format()]
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
|
||||
$0.decoder = decoder
|
||||
})
|
||||
}
|
||||
|
||||
func isJellyfinServer() async -> Bool {
|
||||
let request = Paths.getPublicUsers
|
||||
do {
|
||||
try await api.send(request)
|
||||
let res = try await api.send(request)
|
||||
if res.statusCode != 200 {
|
||||
throw JellyfinClientError.badResponseCode
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func signIn(username: String, pw: String) async throws {
|
||||
let request = Paths.authenticateUserByName(AuthenticateUserByName(pw: pw, username: username))
|
||||
let res = try await self.api.send(request)
|
||||
self.authState.loggedIn = true
|
||||
self.authState.authToken = res.value.accessToken
|
||||
self.authState.save()
|
||||
}
|
||||
}
|
||||
|
@ -10,14 +10,26 @@ import SwiftUI
|
||||
@main
|
||||
struct JelApp: App {
|
||||
let datamodelController = DatamodelController.shared
|
||||
let authStateController = AuthStateController()
|
||||
let authStateController = AuthStateController.shared
|
||||
|
||||
let jellyfinClientController = JellyfinClientController(authHeaders: AuthHeaders(
|
||||
Client: "Jel",
|
||||
Device: UIDevice.current.systemName,
|
||||
DeviceId: UIDevice.current.identifierForVendor!.uuidString,
|
||||
Version: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0",
|
||||
Token: ""))
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView(authState: authStateController)
|
||||
.environment(\.managedObjectContext,
|
||||
datamodelController.container.viewContext)
|
||||
.environmentObject(jellyfinClientController)
|
||||
.task {
|
||||
authStateController.load()
|
||||
if authStateController.serverUrl != nil {
|
||||
jellyfinClientController.setUrl(url: authStateController.serverUrl!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
33
Jel/Models/JellyfinDateFormatter.swift
Normal file
33
Jel/Models/JellyfinDateFormatter.swift
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// JellyfinDateFormatter.swift
|
||||
// Jel
|
||||
//
|
||||
// Created by zerocool on 12/12/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// from: https://stackoverflow.com/a/46458771
|
||||
extension Formatter {
|
||||
static let iso8601withFractionalSeconds: ISO8601DateFormatter = {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
return formatter
|
||||
}()
|
||||
static let iso8601: ISO8601DateFormatter = {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = [.withInternetDateTime]
|
||||
return formatter
|
||||
}()
|
||||
}
|
||||
|
||||
extension JSONDecoder.DateDecodingStrategy {
|
||||
static let iso8601withFractionalSeconds = custom {
|
||||
let container = try $0.singleValueContainer()
|
||||
let string = try container.decode(String.self)
|
||||
if let date = Formatter.iso8601withFractionalSeconds.date(from: string) ?? Formatter.iso8601.date(from: string) {
|
||||
return date
|
||||
}
|
||||
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(string)")
|
||||
}
|
||||
}
|
@ -8,12 +8,14 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AddServerView: View {
|
||||
@EnvironmentObject var jellyfinClient: JellyfinClientController
|
||||
@ObservedObject var authState: AuthStateController
|
||||
@Binding var serverUrlIsValid: Bool
|
||||
|
||||
@State var serverUrlString: String = ""
|
||||
@State var serverUrlString: String = "http://"
|
||||
@State var urlHasError: Bool = false
|
||||
@State var currentErrorMessage: String = ""
|
||||
@State var loading: Bool = false
|
||||
@State var isLoading: Bool = false
|
||||
|
||||
@FocusState var serverUrlIsFocused: Bool
|
||||
|
||||
@ -22,10 +24,8 @@ struct AddServerView: View {
|
||||
Text("Connect to a server")
|
||||
.font(.title)
|
||||
HStack {
|
||||
|
||||
TextField(text: $serverUrlString) {
|
||||
Text("http://jellyfin.example.com")
|
||||
.foregroundStyle(.placeholder)
|
||||
}
|
||||
.keyboardType(.URL)
|
||||
.textContentType(.URL)
|
||||
@ -33,11 +33,6 @@ struct AddServerView: View {
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.focused($serverUrlIsFocused)
|
||||
.onChange(of: serverUrlIsFocused) {
|
||||
if serverUrlIsFocused {
|
||||
urlHasError = false
|
||||
}
|
||||
}
|
||||
.onSubmit {
|
||||
Task {
|
||||
await checkServerUrl()
|
||||
@ -45,7 +40,7 @@ struct AddServerView: View {
|
||||
}
|
||||
|
||||
|
||||
if !loading {
|
||||
if !isLoading {
|
||||
Button(action: {
|
||||
Task {
|
||||
await checkServerUrl()
|
||||
@ -55,14 +50,14 @@ struct AddServerView: View {
|
||||
.labelStyle(.iconOnly)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(urlHasError)
|
||||
} else {
|
||||
ProgressView()
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.padding()
|
||||
.padding([.leading, .trailing])
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.disabled(isLoading)
|
||||
|
||||
if urlHasError {
|
||||
Text(currentErrorMessage)
|
||||
@ -73,13 +68,16 @@ struct AddServerView: View {
|
||||
}
|
||||
|
||||
func checkServerUrl() async {
|
||||
loading = true
|
||||
isLoading = true
|
||||
serverUrlIsFocused = false
|
||||
if isValidUrl(data: serverUrlString) {
|
||||
let url = URL(string: serverUrlString)!
|
||||
if await JellyfinClientController(serverUrl: url).isJellyfinServer() {
|
||||
jellyfinClient.setUrl(url: url)
|
||||
if await jellyfinClient.isJellyfinServer() {
|
||||
authState.serverUrl = url
|
||||
authState.save()
|
||||
urlHasError = false
|
||||
serverUrlIsValid = true
|
||||
} else {
|
||||
urlHasError = true
|
||||
currentErrorMessage = "Server not responding"
|
||||
@ -90,7 +88,7 @@ struct AddServerView: View {
|
||||
currentErrorMessage = "Invalid url"
|
||||
}
|
||||
|
||||
loading = false
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func isValidUrl(data: String) -> Bool {
|
||||
@ -105,5 +103,6 @@ struct AddServerView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AddServerView(authState: AuthStateController())
|
||||
AddServerView(authState: AuthStateController(), serverUrlIsValid: .constant(false))
|
||||
|
||||
}
|
79
Jel/Views/SignIn/SignInToServerView.swift
Normal file
79
Jel/Views/SignIn/SignInToServerView.swift
Normal file
@ -0,0 +1,79 @@
|
||||
//
|
||||
// SignInToServerView.swift
|
||||
// Jel
|
||||
//
|
||||
// Created by zerocool on 12/12/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SignInToServerView: View {
|
||||
@EnvironmentObject var jellyfinClient: JellyfinClientController
|
||||
@ObservedObject var authState: AuthStateController
|
||||
|
||||
@State var username: String = ""
|
||||
@State var password: String = ""
|
||||
|
||||
@State var isLoading: Bool = false
|
||||
@State var hasError: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Sign in")
|
||||
.font(.title)
|
||||
TextField(text: $username) {
|
||||
Text("Username")
|
||||
}
|
||||
.textContentType(.username)
|
||||
|
||||
SecureField(text: $password) {
|
||||
Text("Password")
|
||||
}
|
||||
.textContentType(.password)
|
||||
.onSubmit {
|
||||
Task {
|
||||
await logInToServer()
|
||||
}
|
||||
}
|
||||
|
||||
if !isLoading {
|
||||
Button {
|
||||
Task {
|
||||
await logInToServer()
|
||||
}
|
||||
} label: {
|
||||
Text("Sign in")
|
||||
}
|
||||
.disabled(username.isEmpty || password.isEmpty)
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
|
||||
if hasError {
|
||||
Text("Unable to sign in")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.textInputAutocapitalization(.never)
|
||||
.disabled(isLoading)
|
||||
}
|
||||
|
||||
func logInToServer() async {
|
||||
isLoading = true
|
||||
hasError = false
|
||||
do {
|
||||
try await jellyfinClient.signIn(username: username, pw: password)
|
||||
} catch {
|
||||
hasError = true
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SignInToServerView(authState: AuthStateController())
|
||||
}
|
42
Jel/Views/SignIn/SignInView.swift
Normal file
42
Jel/Views/SignIn/SignInView.swift
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// SignInView.swift
|
||||
// Jel
|
||||
//
|
||||
// Created by zerocool on 12/12/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SignInView: View {
|
||||
@EnvironmentObject var jellyfinClient: JellyfinClientController
|
||||
@ObservedObject var authState: AuthStateController
|
||||
@State var serverUrlIsValid: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
AddServerView(authState: authState, serverUrlIsValid: $serverUrlIsValid)
|
||||
.navigationDestination(isPresented: $serverUrlIsValid) {
|
||||
SignInToServerView(authState: authState)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
await checkLoadedServerUrl()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkLoadedServerUrl() async {
|
||||
if authState.serverUrl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if await jellyfinClient.isJellyfinServer() {
|
||||
serverUrlIsValid = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SignInView(authState: AuthStateController())
|
||||
}
|
Loading…
Reference in New Issue
Block a user