Add Quick Connect sign in to tvOS (v2) (#487)
This commit is contained in:
parent
30cb980ed3
commit
ed519744f4
|
@ -89,7 +89,7 @@ final class MainCoordinator: NavigationCoordinatable {
|
||||||
else { fatalError("Need to have new current login state server") }
|
else { fatalError("Need to have new current login state server") }
|
||||||
guard SessionManager.main.currentLogin != nil else { return }
|
guard SessionManager.main.currentLogin != nil else { return }
|
||||||
if newCurrentServerState.id == SessionManager.main.currentLogin.server.id {
|
if newCurrentServerState.id == SessionManager.main.currentLogin.server.id {
|
||||||
SessionManager.main.loginUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user)
|
SessionManager.main.signInUser(server: newCurrentServerState, user: SessionManager.main.currentLogin.user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -270,6 +270,14 @@ internal enum L10n {
|
||||||
internal static var quickConnectCode: String { return L10n.tr("Localizable", "quickConnectCode") }
|
internal static var quickConnectCode: String { return L10n.tr("Localizable", "quickConnectCode") }
|
||||||
/// Invalid Quick Connect code
|
/// Invalid Quick Connect code
|
||||||
internal static var quickConnectInvalidError: String { return L10n.tr("Localizable", "quickConnectInvalidError") }
|
internal static var quickConnectInvalidError: String { return L10n.tr("Localizable", "quickConnectInvalidError") }
|
||||||
|
/// Note: Quick Connect not enabled
|
||||||
|
internal static var quickConnectNotEnabled: String { return L10n.tr("Localizable", "quickConnectNotEnabled") }
|
||||||
|
/// 1. Open the Jellyfin app on your phone or web browser and sign in with your account
|
||||||
|
internal static var quickConnectStep1: String { return L10n.tr("Localizable", "quickConnectStep1") }
|
||||||
|
/// 2. Open the user menu and go to the Quick Connect page
|
||||||
|
internal static var quickConnectStep2: String { return L10n.tr("Localizable", "quickConnectStep2") }
|
||||||
|
/// 3. Enter the following code:
|
||||||
|
internal static var quickConnectStep3: String { return L10n.tr("Localizable", "quickConnectStep3") }
|
||||||
/// Authorizing Quick Connect successful. Please continue on your other device.
|
/// Authorizing Quick Connect successful. Please continue on your other device.
|
||||||
internal static var quickConnectSuccessMessage: String { return L10n.tr("Localizable", "quickConnectSuccessMessage") }
|
internal static var quickConnectSuccessMessage: String { return L10n.tr("Localizable", "quickConnectSuccessMessage") }
|
||||||
/// Rated
|
/// Rated
|
||||||
|
|
|
@ -22,7 +22,7 @@ final class SessionManager {
|
||||||
|
|
||||||
// MARK: currentLogin
|
// MARK: currentLogin
|
||||||
|
|
||||||
private(set) var currentLogin: CurrentLogin!
|
fileprivate(set) var currentLogin: CurrentLogin!
|
||||||
|
|
||||||
// MARK: main
|
// MARK: main
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ final class SessionManager {
|
||||||
// MARK: init
|
// MARK: init
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
|
setAuthHeader(with: "")
|
||||||
|
|
||||||
if let lastUserID = Defaults[.lastServerUserID],
|
if let lastUserID = Defaults[.lastServerUserID],
|
||||||
let user = try? SwiftfinStore.dataStack.fetchOne(
|
let user = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
From<SwiftfinStore.Models.StoredUser>(),
|
From<SwiftfinStore.Models.StoredUser>(),
|
||||||
|
@ -195,86 +197,31 @@ final class SessionManager {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: loginUser publisher
|
// MARK: signInUser publisher
|
||||||
|
|
||||||
// Logs in a user with an associated server, storing if successful
|
// Logs in a user with an associated server, storing if successful
|
||||||
func loginUser(
|
func signInUser(
|
||||||
server: SwiftfinStore.State.Server,
|
server: SwiftfinStore.State.Server,
|
||||||
username: String,
|
username: String,
|
||||||
password: String
|
password: String
|
||||||
) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
||||||
setAuthHeader(with: "")
|
|
||||||
|
|
||||||
JellyfinAPIAPI.basePath = server.currentURI
|
JellyfinAPIAPI.basePath = server.currentURI
|
||||||
|
|
||||||
return UserAPI.authenticateUserByName(authenticateUserByNameRequest: .init(username: username, pw: password))
|
return UserAPI.authenticateUserByName(authenticateUserByNameRequest: .init(username: username, pw: password))
|
||||||
.tryMap { response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in
|
.processAuthenticationRequest(with: self, server: server)
|
||||||
|
|
||||||
guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") }
|
|
||||||
|
|
||||||
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
|
||||||
let newUser = transaction.create(Into<SwiftfinStore.Models.StoredUser>())
|
|
||||||
|
|
||||||
guard let username = response.user?.name,
|
|
||||||
let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") }
|
|
||||||
|
|
||||||
newUser.username = username
|
|
||||||
newUser.id = id
|
|
||||||
newUser.appleTVID = ""
|
|
||||||
|
|
||||||
// Check for existing user on device
|
|
||||||
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(
|
|
||||||
From<SwiftfinStore.Models.StoredUser>(),
|
|
||||||
[Where<SwiftfinStore.Models.StoredUser>(
|
|
||||||
"id == %@",
|
|
||||||
newUser.id
|
|
||||||
)]
|
|
||||||
) {
|
|
||||||
throw SwiftfinStore.Error.existingUser(existingUser.state)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>())
|
// Logs in a user with an associated server, storing if successful
|
||||||
newAccessToken.value = accessToken
|
func signInUser(server: SwiftfinStore.State.Server, quickConnectSecret: String) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
||||||
newUser.accessToken = newAccessToken
|
JellyfinAPIAPI.basePath = server.currentURI
|
||||||
|
|
||||||
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(
|
return UserAPI.authenticateWithQuickConnect(authenticateWithQuickConnectRequest: .init(secret: quickConnectSecret))
|
||||||
From<SwiftfinStore.Models.StoredServer>(),
|
.processAuthenticationRequest(with: self, server: server)
|
||||||
[
|
|
||||||
Where<SwiftfinStore.Models.StoredServer>(
|
|
||||||
"id == %@",
|
|
||||||
server.id
|
|
||||||
),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else { fatalError("No stored server associated with given state server?") }
|
|
||||||
|
|
||||||
guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
|
|
||||||
editUserServer.users.insert(newUser)
|
|
||||||
|
|
||||||
return (editUserServer, newUser, transaction)
|
|
||||||
}
|
|
||||||
.handleEvents(receiveOutput: { [unowned self] server, user, transaction in
|
|
||||||
setAuthHeader(with: user.accessToken?.value ?? "")
|
|
||||||
try? transaction.commitAndWait()
|
|
||||||
|
|
||||||
// Fetch for the right queue
|
|
||||||
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
|
|
||||||
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
|
|
||||||
|
|
||||||
Defaults[.lastServerUserID] = user.id
|
|
||||||
|
|
||||||
currentLogin = (server: currentServer.state, user: currentUser.state)
|
|
||||||
Notifications[.didSignIn].post()
|
|
||||||
})
|
|
||||||
.map { _, user, _ in
|
|
||||||
user.state
|
|
||||||
}
|
|
||||||
.eraseToAnyPublisher()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: loginUser
|
// MARK: signInUser
|
||||||
|
|
||||||
func loginUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
|
func signInUser(server: SwiftfinStore.State.Server, user: SwiftfinStore.State.User) {
|
||||||
JellyfinAPIAPI.basePath = server.currentURI
|
JellyfinAPIAPI.basePath = server.currentURI
|
||||||
Defaults[.lastServerUserID] = user.id
|
Defaults[.lastServerUserID] = user.id
|
||||||
setAuthHeader(with: user.accessToken)
|
setAuthHeader(with: user.accessToken)
|
||||||
|
@ -347,7 +294,7 @@ final class SessionManager {
|
||||||
try? transaction.commitAndWait()
|
try? transaction.commitAndWait()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setAuthHeader(with accessToken: String) {
|
fileprivate func setAuthHeader(with accessToken: String) {
|
||||||
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||||
var deviceName = UIDevice.current.name
|
var deviceName = UIDevice.current.name
|
||||||
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
deviceName = deviceName.folding(options: .diacriticInsensitive, locale: .current)
|
||||||
|
@ -370,3 +317,74 @@ final class SessionManager {
|
||||||
JellyfinAPIAPI.customHeaders["X-Emby-Authorization"] = header
|
JellyfinAPIAPI.customHeaders["X-Emby-Authorization"] = header
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension AnyPublisher where Output == AuthenticationResult {
|
||||||
|
func processAuthenticationRequest(
|
||||||
|
with sessionManager: SessionManager,
|
||||||
|
server: SwiftfinStore.State.Server
|
||||||
|
) -> AnyPublisher<SwiftfinStore.State.User, Error> {
|
||||||
|
self
|
||||||
|
.tryMap { response -> (SwiftfinStore.Models.StoredServer, SwiftfinStore.Models.StoredUser, UnsafeDataTransaction) in
|
||||||
|
|
||||||
|
guard let accessToken = response.accessToken else { throw JellyfinAPIError("Access token missing from network call") }
|
||||||
|
|
||||||
|
let transaction = SwiftfinStore.dataStack.beginUnsafe()
|
||||||
|
let newUser = transaction.create(Into<SwiftfinStore.Models.StoredUser>())
|
||||||
|
|
||||||
|
guard let username = response.user?.name,
|
||||||
|
let id = response.user?.id else { throw JellyfinAPIError("Missing user data from network call") }
|
||||||
|
|
||||||
|
newUser.username = username
|
||||||
|
newUser.id = id
|
||||||
|
newUser.appleTVID = ""
|
||||||
|
|
||||||
|
// Check for existing user on device
|
||||||
|
if let existingUser = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
|
From<SwiftfinStore.Models.StoredUser>(),
|
||||||
|
[Where<SwiftfinStore.Models.StoredUser>(
|
||||||
|
"id == %@",
|
||||||
|
newUser.id
|
||||||
|
)]
|
||||||
|
) {
|
||||||
|
throw SwiftfinStore.Error.existingUser(existingUser.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
let newAccessToken = transaction.create(Into<SwiftfinStore.Models.StoredAccessToken>())
|
||||||
|
newAccessToken.value = accessToken
|
||||||
|
newUser.accessToken = newAccessToken
|
||||||
|
|
||||||
|
guard let userServer = try? SwiftfinStore.dataStack.fetchOne(
|
||||||
|
From<SwiftfinStore.Models.StoredServer>(),
|
||||||
|
[
|
||||||
|
Where<SwiftfinStore.Models.StoredServer>(
|
||||||
|
"id == %@",
|
||||||
|
server.id
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else { fatalError("No stored server associated with given state server?") }
|
||||||
|
|
||||||
|
guard let editUserServer = transaction.edit(userServer) else { fatalError("Can't get proxy for existing object?") }
|
||||||
|
editUserServer.users.insert(newUser)
|
||||||
|
|
||||||
|
return (editUserServer, newUser, transaction)
|
||||||
|
}
|
||||||
|
.handleEvents(receiveOutput: { server, user, transaction in
|
||||||
|
sessionManager.setAuthHeader(with: user.accessToken?.value ?? "")
|
||||||
|
try? transaction.commitAndWait()
|
||||||
|
|
||||||
|
// Fetch for the right queue
|
||||||
|
let currentServer = SwiftfinStore.dataStack.fetchExisting(server)!
|
||||||
|
let currentUser = SwiftfinStore.dataStack.fetchExisting(user)!
|
||||||
|
|
||||||
|
Defaults[.lastServerUserID] = user.id
|
||||||
|
|
||||||
|
sessionManager.currentLogin = (server: currentServer.state, user: currentUser.state)
|
||||||
|
Notifications[.didSignIn].post()
|
||||||
|
})
|
||||||
|
.map { _, user, _ in
|
||||||
|
user.state
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -34,9 +34,9 @@ class UserListViewModel: ViewModel {
|
||||||
self.users = SessionManager.main.fetchUsers(for: server)
|
self.users = SessionManager.main.fetchUsers(for: server)
|
||||||
}
|
}
|
||||||
|
|
||||||
func login(user: SwiftfinStore.State.User) {
|
func signIn(user: SwiftfinStore.State.User) {
|
||||||
self.isLoading = true
|
self.isLoading = true
|
||||||
SessionManager.main.loginUser(server: server, user: user)
|
SessionManager.main.signInUser(server: server, user: user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(user: SwiftfinStore.State.User) {
|
func remove(user: SwiftfinStore.State.User) {
|
||||||
|
|
|
@ -15,14 +15,24 @@ final class UserSignInViewModel: ViewModel {
|
||||||
|
|
||||||
@RouterObject
|
@RouterObject
|
||||||
var router: UserSignInCoordinator.Router?
|
var router: UserSignInCoordinator.Router?
|
||||||
let server: SwiftfinStore.State.Server
|
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var publicUsers: [UserDto] = []
|
var publicUsers: [UserDto] = []
|
||||||
|
@Published
|
||||||
|
var quickConnectCode: String?
|
||||||
|
@Published
|
||||||
|
var quickConnectEnabled = false
|
||||||
|
|
||||||
|
let server: SwiftfinStore.State.Server
|
||||||
|
private var quickConnectTimer: Timer?
|
||||||
|
private var quickConnectSecret: String?
|
||||||
|
|
||||||
init(server: SwiftfinStore.State.Server) {
|
init(server: SwiftfinStore.State.Server) {
|
||||||
self.server = server
|
self.server = server
|
||||||
|
super.init()
|
||||||
|
|
||||||
JellyfinAPIAPI.basePath = server.currentURI
|
JellyfinAPIAPI.basePath = server.currentURI
|
||||||
|
checkQuickConnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
var alertTitle: String {
|
var alertTitle: String {
|
||||||
|
@ -34,10 +44,10 @@ final class UserSignInViewModel: ViewModel {
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
func login(username: String, password: String) {
|
func signIn(username: String, password: String) {
|
||||||
LogManager.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login")
|
LogManager.log.debug("Attempting to login to server at \"\(server.currentURI)\"", tag: "login")
|
||||||
|
|
||||||
SessionManager.main.loginUser(server: server, username: username, password: password)
|
SessionManager.main.signInUser(server: server, username: username, password: password)
|
||||||
.trackActivity(loading)
|
.trackActivity(loading)
|
||||||
.sink { completion in
|
.sink { completion in
|
||||||
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
|
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
|
||||||
|
@ -79,4 +89,65 @@ final class UserSignInViewModel: ViewModel {
|
||||||
let urlString = ImageAPI.getSplashscreenWithRequestBuilder().URLString
|
let urlString = ImageAPI.getSplashscreenWithRequestBuilder().URLString
|
||||||
return URL(string: urlString)
|
return URL(string: urlString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkQuickConnect() {
|
||||||
|
QuickConnectAPI.getEnabled()
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { enabled in
|
||||||
|
self.quickConnectEnabled = enabled
|
||||||
|
if enabled {
|
||||||
|
self.startQuickConnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startQuickConnect() {
|
||||||
|
QuickConnectAPI.initiate()
|
||||||
|
.sink(receiveCompletion: { completion in
|
||||||
|
self.handleAPIRequestError(completion: completion)
|
||||||
|
}, receiveValue: { response in
|
||||||
|
|
||||||
|
self.quickConnectSecret = response.secret
|
||||||
|
self.quickConnectCode = response.code
|
||||||
|
LogManager.log.debug("QuickConnect code: \(response.code ?? "--")")
|
||||||
|
|
||||||
|
self.quickConnectTimer = Timer.scheduledTimer(
|
||||||
|
timeInterval: 5,
|
||||||
|
target: self,
|
||||||
|
selector: #selector(self.checkAuthStatus),
|
||||||
|
userInfo: nil,
|
||||||
|
repeats: true
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc
|
||||||
|
private func checkAuthStatus() {
|
||||||
|
guard let quickConnectSecret = quickConnectSecret else { return }
|
||||||
|
|
||||||
|
QuickConnectAPI.connect(secret: quickConnectSecret)
|
||||||
|
.sink(receiveCompletion: { _ in
|
||||||
|
// Prefer not to handle error handling like normal as
|
||||||
|
// this is a repeated call
|
||||||
|
}, receiveValue: { value in
|
||||||
|
guard let authenticated = value.authenticated, authenticated else {
|
||||||
|
LogManager.log.debug("QuickConnect not authenticated yet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.quickConnectTimer?.invalidate()
|
||||||
|
|
||||||
|
SessionManager.main.signInUser(server: self.server, quickConnectSecret: quickConnectSecret)
|
||||||
|
.trackActivity(self.loading)
|
||||||
|
.sink { completion in
|
||||||
|
self.handleAPIRequestError(displayMessage: L10n.unableToConnectServer, completion: completion)
|
||||||
|
} receiveValue: { _ in
|
||||||
|
}
|
||||||
|
.store(in: &self.cancellables)
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ struct UserListView: View {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(viewModel.users, id: \.id) { user in
|
ForEach(viewModel.users, id: \.id) { user in
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(user: user)
|
viewModel.signIn(user: user)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text(user.username)
|
Text(user.username)
|
||||||
|
|
|
@ -27,7 +27,8 @@ struct UserSignInView: View {
|
||||||
.opacity(0.9)
|
.opacity(0.9)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
Form {
|
HStack(alignment: .top) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
Section {
|
Section {
|
||||||
TextField(L10n.username, text: $username)
|
TextField(L10n.username, text: $username)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
|
@ -38,11 +39,13 @@ struct UserSignInView: View {
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(username: username, password: password)
|
viewModel.signIn(username: username, password: password)
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
L10n.connect.text
|
L10n.connect.text
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
}
|
}
|
||||||
|
@ -52,8 +55,16 @@ struct UserSignInView: View {
|
||||||
|
|
||||||
} header: {
|
} header: {
|
||||||
L10n.signInToServer(viewModel.server.name).text
|
L10n.signInToServer(viewModel.server.name).text
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if !viewModel.quickConnectEnabled {
|
||||||
|
L10n.quickConnectNotEnabled.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
.alert(item: $viewModel.errorMessage) { _ in
|
.alert(item: $viewModel.errorMessage) { _ in
|
||||||
Alert(
|
Alert(
|
||||||
title: Text(viewModel.alertTitle),
|
title: Text(viewModel.alertTitle),
|
||||||
|
@ -62,6 +73,37 @@ struct UserSignInView: View {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.signIn)
|
.navigationTitle(L10n.signIn)
|
||||||
|
|
||||||
|
if viewModel.quickConnectEnabled {
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
L10n.quickConnect.text
|
||||||
|
.font(.title3)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
L10n.quickConnectStep1.text
|
||||||
|
|
||||||
|
L10n.quickConnectStep2.text
|
||||||
|
|
||||||
|
L10n.quickConnectStep3.text
|
||||||
|
}
|
||||||
|
.padding(.vertical)
|
||||||
|
|
||||||
|
Text(viewModel.quickConnectCode ?? "------")
|
||||||
|
.tracking(10)
|
||||||
|
.font(.title)
|
||||||
|
.monospacedDigit()
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UserSignInView_Preivews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UserSignInView(viewModel: .init(server: .sample))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ struct UserLoginCellView: View {
|
||||||
DisclosureGroup {
|
DisclosureGroup {
|
||||||
SecureField(L10n.password, text: $enteredPassword)
|
SecureField(L10n.password, text: $enteredPassword)
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(username: user.name ?? "--", password: enteredPassword)
|
viewModel.signIn(username: user.name ?? "--", password: enteredPassword)
|
||||||
} label: {
|
} label: {
|
||||||
L10n.signIn.text
|
L10n.signIn.text
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ struct UserListView: View {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(viewModel.users, id: \.id) { user in
|
ForEach(viewModel.users, id: \.id) { user in
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(user: user)
|
viewModel.signIn(user: user)
|
||||||
} label: {
|
} label: {
|
||||||
ZStack(alignment: Alignment.leading) {
|
ZStack(alignment: Alignment.leading) {
|
||||||
Rectangle()
|
Rectangle()
|
||||||
|
|
|
@ -37,7 +37,7 @@ struct UserSignInView: View {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button {
|
Button {
|
||||||
viewModel.login(username: username, password: password)
|
viewModel.signIn(username: username, password: password)
|
||||||
} label: {
|
} label: {
|
||||||
L10n.signIn.text
|
L10n.signIn.text
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Loading…
Reference in New Issue