users can now choose between http and https

This commit is contained in:
Vincent Meilinger
2024-01-07 17:10:47 +01:00
parent 5c7ff578c7
commit 2ed664857b
10 changed files with 105 additions and 176 deletions

View File

@@ -37,6 +37,12 @@ class UserSettings: ObservableObject {
}
}
@Published var serverProtocol: String {
didSet {
UserDefaults.standard.set(serverProtocol, forKey: "serverProtocol")
}
}
@Published var onboarding: Bool {
didSet {
UserDefaults.standard.set(onboarding, forKey: "onboarding")
@@ -102,6 +108,7 @@ class UserSettings: ObservableObject {
self.token = UserDefaults.standard.object(forKey: "token") as? String ?? ""
self.authString = UserDefaults.standard.object(forKey: "authString") as? String ?? ""
self.serverAddress = UserDefaults.standard.object(forKey: "serverAddress") as? String ?? ""
self.serverProtocol = UserDefaults.standard.object(forKey: "serverProtocol") as? String ?? "https://"
self.onboarding = UserDefaults.standard.object(forKey: "onboarding") as? Bool ?? true
self.defaultCategory = UserDefaults.standard.object(forKey: "defaultCategory") as? String ?? ""
self.language = UserDefaults.standard.object(forKey: "language") as? String ?? SupportedLanguage.DEVICE.rawValue

View File

@@ -710,6 +710,9 @@
}
}
}
},
"Copy Link" : {
},
"Could not establish a connection to the server. The action will be retried upon reconnection." : {
"localizations" : {
@@ -952,6 +955,9 @@
}
}
}
},
"e.g.: example.com" : {
},
"Edit" : {
"localizations" : {
@@ -1152,6 +1158,7 @@
}
},
"If the login button does not open your browser, copy the following link and paste it in your browser manually:" : {
"extractionState" : "stale",
"localizations" : {
"de" : {
"stringUnit" : {
@@ -1172,6 +1179,9 @@
}
}
}
},
"If the login button does not open your browser, use the 'Copy Link' button and paste the link in your browser manually." : {
},
"If you are interested in contributing to this project or simply wish to review its source code, we encourage you to visit the GitHub repository for this application." : {
"localizations" : {
@@ -1574,8 +1584,12 @@
}
}
}
},
"Make sure to enter the server address in the form 'example.com', or \n'<server address>:<port>'\n when a non-standard port is used." : {
},
"Make sure to enter the server address in the form 'example.com'. Currently, only servers using the 'https' protocol are supported." : {
"extractionState" : "stale",
"localizations" : {
"de" : {
"stringUnit" : {
@@ -2828,9 +2842,6 @@
}
}
}
},
"Use a non-standard port" : {
},
"Validate" : {
"localizations" : {

View File

@@ -9,8 +9,6 @@ import Foundation
import OSLog
struct ApiRequest {
/// The server address, e.g. https://example.com
let serverAddress: String
let path: String
let method: RequestMethod
let authString: String?
@@ -21,14 +19,12 @@ struct ApiRequest {
let cookbookPath = "/index.php/apps/cookbook"
init(
serverAdress: String,
path: String,
method: RequestMethod,
authString: String? = nil,
headerFields: [HeaderField] = [],
body: Data? = nil
) {
self.serverAddress = serverAdress
self.method = method
self.path = path
self.headerFields = headerFields
@@ -40,7 +36,7 @@ struct ApiRequest {
Logger.network.debug("\(method.rawValue) \(path) sending ...")
// Prepare URL
let urlString = "https://" + serverAddress + cookbookPath + path
let urlString = UserSettings.shared.serverProtocol + UserSettings.shared.serverAddress + cookbookPath + path
print("Full path: \(urlString)")
//Logger.network.debug("Full path: \(urlString)")
guard let urlStringSanitized = urlString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return (nil, .unknownError) }

View File

@@ -13,20 +13,17 @@ import UIKit
protocol CookbookApi {
/// Not implemented yet.
static func importRecipe(
from serverAdress: String,
auth: String,
data: Data
) async -> (RecipeDetail?, NetworkError?)
/// Get either the full image or a thumbnail sized version.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - id: The according recipe id.
/// - size: The size of the image.
/// - Returns: The image of the recipe with the specified id. A NetworkError if the request fails, otherwise nil.
static func getImage(
from serverAdress: String,
auth: String,
id: Int,
size: RecipeImage.RecipeImageSize
@@ -34,91 +31,75 @@ protocol CookbookApi {
/// Get all recipes.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A list of all recipes.
static func getRecipes(
from serverAdress: String,
auth: String
) async -> ([Recipe]?, NetworkError?)
/// Create a new recipe.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A NetworkError if the request fails. Nil otherwise.
static func createRecipe(
from serverAdress: String,
auth: String,
recipe: RecipeDetail
) async -> (NetworkError?)
/// Get the recipe with the specified id.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - id: The recipe id.
/// - Returns: The recipe if it exists. A NetworkError if the request fails.
static func getRecipe(
from serverAdress: String,
auth: String, id: Int
) async -> (RecipeDetail?, NetworkError?)
/// Update an existing recipe with new entries.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - id: The recipe id.
/// - Returns: A NetworkError if the request fails. Nil otherwise.
static func updateRecipe(
from serverAdress: String,
auth: String,
auth: String,
recipe: RecipeDetail
) async -> (NetworkError?)
/// Delete the recipe with the specified id.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - id: The recipe id.
/// - Returns: A NetworkError if the request fails. Nil otherwise.
static func deleteRecipe(
from serverAdress: String,
auth: String,
id: Int
) async -> (NetworkError?)
/// Get all categories.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A list of categories. A NetworkError if the request fails.
static func getCategories(
from serverAdress: String,
auth: String
) async -> ([Category]?, NetworkError?)
/// Get all recipes of a specified category.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - categoryName: The category name.
/// - Returns: A list of recipes. A NetworkError if the request fails.
static func getCategory(
from serverAdress: String,
auth: String,
named categoryName: String
) async -> ([Recipe]?, NetworkError?)
/// Rename an existing category.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - categoryName: The name of the category to be renamed.
/// - newName: The new category name.
/// - Returns: A NetworkError if the request fails.
static func renameCategory(
from serverAdress: String,
auth: String,
named categoryName: String,
newName: String
@@ -126,63 +107,51 @@ protocol CookbookApi {
/// Get all keywords/tags.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A list of tag strings. A NetworkError if the request fails.
static func getTags(
from serverAdress: String,
auth: String
) async -> ([RecipeKeyword]?, NetworkError?)
/// Get all recipes tagged with the specified keyword.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - keyword: The keyword.
/// - Returns: A list of recipes tagged with the specified keyword. A NetworkError if the request fails.
static func getRecipesTagged(
from serverAdress: String,
auth: String,
keyword: String
) async -> ([Recipe]?, NetworkError?)
/// Get the servers api version.
/// - Parameters:
/// - serverAdress: Server address in the format https://example.com.
/// - auth: Server authentication string.
/// - Returns: A NetworkError if the request fails.
static func getApiVersion(
from serverAdress: String,
auth: String
) async -> (NetworkError?)
/// Trigger a reindexing action on the server.
/// - Parameters:
/// - serverAdress: Server address in the format. https://example.com
/// - auth: Server authentication string
/// - Returns: A NetworkError if the request fails.
static func postReindex(
from serverAdress: String,
auth: String
) async -> (NetworkError?)
/// Get the current configuration of the Cookbook server application.
/// - Parameters:
/// - serverAdress: Server address in the format. https://example.com
/// - auth: Server authentication string
/// - Returns: A NetworkError if the request fails.
static func getConfig(
from serverAdress: String,
auth: String
) async -> (NetworkError?)
/// Set the current configuration of the Cookbook server application.
/// - Parameters:
/// - serverAdress: Server address in the format. https://example.com
/// - auth: Server authentication string
/// - Returns: A NetworkError if the request fails.
static func postConfig(
from serverAdress: String,
auth: String
) async -> (NetworkError?)
}

View File

@@ -10,9 +10,8 @@ import UIKit
class CookbookApiV1: CookbookApi {
static func importRecipe(from serverAdress: String, auth: String, data: Data) async -> (RecipeDetail?, NetworkError?) {
static func importRecipe(auth: String, data: Data) async -> (RecipeDetail?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/import",
method: .POST,
authString: auth,
@@ -24,10 +23,9 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil)
}
static func getImage(from serverAdress: String, auth: String, id: Int, size: RecipeImage.RecipeImageSize) async -> (UIImage?, NetworkError?) {
static func getImage(auth: String, id: Int, size: RecipeImage.RecipeImageSize) async -> (UIImage?, NetworkError?) {
let imageSize = (size == .FULL ? "full" : "thumb")
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes/\(id)/image?size=\(imageSize)",
method: .GET,
authString: auth,
@@ -39,9 +37,8 @@ class CookbookApiV1: CookbookApi {
return (UIImage(data: data), error)
}
static func getRecipes(from serverAdress: String, auth: String) async -> ([Recipe]?, NetworkError?) {
static func getRecipes(auth: String) async -> ([Recipe]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes",
method: .GET,
authString: auth,
@@ -53,13 +50,12 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil)
}
static func createRecipe(from serverAdress: String, auth: String, recipe: RecipeDetail) async -> (NetworkError?) {
static func createRecipe(auth: String, recipe: RecipeDetail) async -> (NetworkError?) {
guard let recipeData = JSONEncoder.safeEncode(recipe) else {
return .dataError
}
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes",
method: .POST,
authString: auth,
@@ -82,9 +78,8 @@ class CookbookApiV1: CookbookApi {
return nil
}
static func getRecipe(from serverAdress: String, auth: String, id: Int) async -> (RecipeDetail?, NetworkError?) {
static func getRecipe(auth: String, id: Int) async -> (RecipeDetail?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes/\(id)",
method: .GET,
authString: auth,
@@ -96,12 +91,11 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil)
}
static func updateRecipe(from serverAdress: String, auth: String, recipe: RecipeDetail) async -> (NetworkError?) {
static func updateRecipe(auth: String, recipe: RecipeDetail) async -> (NetworkError?) {
guard let recipeData = JSONEncoder.safeEncode(recipe) else {
return .dataError
}
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes/\(recipe.id)",
method: .PUT,
authString: auth,
@@ -124,9 +118,8 @@ class CookbookApiV1: CookbookApi {
return nil
}
static func deleteRecipe(from serverAdress: String, auth: String, id: Int) async -> (NetworkError?) {
static func deleteRecipe(auth: String, id: Int) async -> (NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/recipes/\(id)",
method: .DELETE,
authString: auth,
@@ -138,9 +131,8 @@ class CookbookApiV1: CookbookApi {
return nil
}
static func getCategories(from serverAdress: String, auth: String) async -> ([Category]?, NetworkError?) {
static func getCategories(auth: String) async -> ([Category]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/categories",
method: .GET,
authString: auth,
@@ -152,9 +144,8 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil)
}
static func getCategory(from serverAdress: String, auth: String, named categoryName: String) async -> ([Recipe]?, NetworkError?) {
static func getCategory(auth: String, named categoryName: String) async -> ([Recipe]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/category/\(categoryName)",
method: .GET,
authString: auth,
@@ -166,9 +157,8 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil)
}
static func renameCategory(from serverAdress: String, auth: String, named categoryName: String, newName: String) async -> (NetworkError?) {
static func renameCategory(auth: String, named categoryName: String, newName: String) async -> (NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/category/\(categoryName)",
method: .PUT,
authString: auth,
@@ -180,9 +170,8 @@ class CookbookApiV1: CookbookApi {
return nil
}
static func getTags(from serverAdress: String, auth: String) async -> ([RecipeKeyword]?, NetworkError?) {
static func getTags(auth: String) async -> ([RecipeKeyword]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/keywords",
method: .GET,
authString: auth,
@@ -194,9 +183,8 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil)
}
static func getRecipesTagged(from serverAdress: String, auth: String, keyword: String) async -> ([Recipe]?, NetworkError?) {
static func getRecipesTagged(auth: String, keyword: String) async -> ([Recipe]?, NetworkError?) {
let request = ApiRequest(
serverAdress: serverAdress,
path: "/api/v1/tags/\(keyword)",
method: .GET,
authString: auth,
@@ -208,19 +196,19 @@ class CookbookApiV1: CookbookApi {
return (JSONDecoder.safeDecode(data), nil)
}
static func getApiVersion(from serverAdress: String, auth: String) async -> (NetworkError?) {
static func getApiVersion(auth: String) async -> (NetworkError?) {
return .none
}
static func postReindex(from serverAdress: String, auth: String) async -> (NetworkError?) {
static func postReindex(auth: String) async -> (NetworkError?) {
return .none
}
static func getConfig(from serverAdress: String, auth: String) async -> (NetworkError?) {
static func getConfig(auth: String) async -> (NetworkError?) {
return .none
}
static func postConfig(from serverAdress: String, auth: String) async -> (NetworkError?) {
static func postConfig(auth: String) async -> (NetworkError?) {
return .none
}
}

View File

@@ -49,7 +49,6 @@ import UIKit
*/
func getCategories() async {
let (categories, _) = await api.getCategories(
from: userSettings.serverAddress,
auth: userSettings.authString
)
if let categories = categories {
@@ -93,7 +92,6 @@ import UIKit
func getServer(store: Bool = false) async -> Bool {
let (recipes, _) = await api.getCategory(
from: userSettings.serverAddress,
auth: userSettings.authString,
named: categoryString
)
@@ -156,7 +154,6 @@ import UIKit
*/
func getRecipes() async -> [Recipe] {
let (recipes, error) = await api.getRecipes(
from: userSettings.serverAddress,
auth: userSettings.authString
)
if let recipes = recipes {
@@ -197,7 +194,6 @@ import UIKit
func getServer() async -> RecipeDetail? {
let (recipe, error) = await api.getRecipe(
from: userSettings.serverAddress,
auth: userSettings.authString,
id: id
)
@@ -291,7 +287,6 @@ import UIKit
func getServer() async -> UIImage? {
let (image, _) = await api.getImage(
from: userSettings.serverAddress,
auth: userSettings.authString,
id: id,
size: size
@@ -368,7 +363,6 @@ import UIKit
func getServer() async -> [RecipeKeyword]? {
let (tags, _) = await api.getTags(
from: userSettings.serverAddress,
auth: userSettings.authString
)
return tags
@@ -423,7 +417,6 @@ import UIKit
*/
func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert? {
let (error) = await api.deleteRecipe(
from: userSettings.serverAddress,
auth: userSettings.authString,
id: id
)
@@ -455,7 +448,6 @@ import UIKit
*/
func checkServerConnection() async -> Bool {
let (categories, _) = await api.getCategories(
from: userSettings.serverAddress,
auth: userSettings.authString
)
if let categories = categories {
@@ -485,13 +477,11 @@ import UIKit
var error: NetworkError? = nil
if createNew {
error = await api.createRecipe(
from: userSettings.serverAddress,
auth: userSettings.authString,
recipe: recipeDetail
)
} else {
error = await api.updateRecipe(
from: userSettings.serverAddress,
auth: userSettings.authString,
recipe: recipeDetail
)
@@ -505,7 +495,6 @@ import UIKit
func importRecipe(url: String) async -> (RecipeDetail?, RequestAlert?) {
guard let data = JSONEncoder.safeEncode(RecipeImportRequest(url: url)) else { return (nil, .REQUEST_DROPPED) }
let (recipeDetail, error) = await api.importRecipe(
from: userSettings.serverAddress,
auth: userSettings.authString,
data: data
)

View File

@@ -181,11 +181,8 @@ struct LoginTextField: View {
struct ServerAddressField: View {
@Binding var addressString: String
@State var serverAddress: String = ""
@ObservedObject var userSettings = UserSettings.shared
@State var serverProtocol: ServerProtocol = .https
@State var serverPort: String = ""
@State var useNonStandardPort: Bool = false
enum ServerProtocol: String {
case https="https://", http="http://"
@@ -205,22 +202,26 @@ struct ServerAddressField: View {
}.pickerStyle(.menu)
.tint(.white)
.font(.headline)
.onChange(of: serverProtocol) { color in
userSettings.serverProtocol = color.rawValue
}
TextField("e.g.: example.com", text: $userSettings.serverAddress)
.textFieldStyle(.plain)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.foregroundStyle(.white)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.foregroundColor(Color.white.opacity(0.2))
)
LoginTextField(example: "e.g.: example.com", text: $serverAddress)
}
Toggle("Use a non-standard port", isOn: $useNonStandardPort)
.tint(.white.opacity(0.2))
.foregroundStyle(.white)
.font(.headline)
.padding(.top)
if useNonStandardPort {
LoginTextField(example: "e.g.: 80", text: $serverPort)
}
LoginLabel(text: "Full server address")
.padding(.top)
Text(createServerAddressString())
Text(userSettings.serverProtocol + userSettings.serverAddress)
.foregroundColor(.white)
.padding(.vertical, 5)
}
@@ -230,26 +231,13 @@ struct ServerAddressField: View {
.stroke(.white, lineWidth: 2)
.foregroundColor(.clear)
)
}.animation(.easeInOut, value: useNonStandardPort)
}
func createServerAddressString() -> String {
if useNonStandardPort && serverPort != "" {
addressString = serverProtocol.rawValue + serverAddress + ":" + serverPort
} else {
addressString = serverProtocol.rawValue + serverAddress
}
return addressString
}
}
struct ServerAddressField_Preview: PreviewProvider {
static var previews: some View {
ServerAddressField(addressString: .constant(""),
serverAddress: "example.com",
serverProtocol: .https,
serverPort: "80",
useNonStandardPort: true)
ServerAddressField()
.previewLayout(.sizeThatFits)
.padding()
.background(Color.nextcloudBlue)

View File

@@ -26,14 +26,9 @@ struct TokenLoginView: View {
var body: some View {
VStack(alignment: .leading) {
/*LoginLabel(text: "Server address")
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress)
.focused($focusedField, equals: .server)
.textContentType(.URL)
.submitLabel(.next)
ServerAddressField()
.padding(.bottom)
*/
ServerAddressField(addressString: $userSettings.serverAddress)
LoginLabel(text: "User name")
BorderedLoginTextField(example: "username", text: $userSettings.username)
.focused($focusedField, equals: .username)

View File

@@ -9,11 +9,10 @@ import Foundation
import SwiftUI
enum V2LoginStage: LoginStage {
case serverAddress, login, validate
case login, validate
func next() -> V2LoginStage {
switch self {
case .serverAddress: return .login
case .login: return .validate
case .validate: return .validate
}
@@ -21,8 +20,7 @@ enum V2LoginStage: LoginStage {
func previous() -> V2LoginStage {
switch self {
case .serverAddress: return .serverAddress
case .login: return .serverAddress
case .login: return .login
case .validate: return .login
}
}
@@ -34,9 +32,8 @@ struct V2LoginView: View {
@Binding var showAlert: Bool
@Binding var alertMessage: String
@State var loginStage: V2LoginStage = .serverAddress
@State var loginStage: V2LoginStage = .login
@State var loginRequest: LoginV2Request? = nil
@FocusState private var focusedField: Field?
@State var userSettings = UserSettings.shared
@@ -50,27 +47,14 @@ struct V2LoginView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
/*LoginLabel(text: "Server address")
.padding()
LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress, color: loginStage == .serverAddress ? .white : .secondary)
.focused($focusedField, equals: .server)
.textContentType(.URL)
.submitLabel(.done)
.padding([.bottom, .horizontal])
.onSubmit {
withAnimation(.easeInOut) {
loginStage = loginStage.next()
}
}
*/
ServerAddressField(addressString: $userSettings.serverAddress)
ServerAddressField()
CollapsibleView {
VStack(alignment: .leading) {
Text("Make sure to enter the server address in the form 'example.com'. Currently, only servers using the 'https' protocol are supported.")
if let loginRequest = loginRequest {
Text("If the login button does not open your browser, copy the following link and paste it in your browser manually:")
Text(loginRequest.login)
}
Text("Make sure to enter the server address in the form 'example.com', or \n'<server address>:<port>'\n when a non-standard port is used.")
.padding(.bottom)
Text("The 'Login' button will open a web browser. Please follow the login instructions provided there.\nAfter a successful login, return to this application and press 'Validate'.")
.padding(.bottom)
Text("If the login button does not open your browser, use the 'Copy Link' button and paste the link in your browser manually.")
}
} title: {
Text("Show help")
@@ -78,43 +62,45 @@ struct V2LoginView: View {
.font(.headline)
}.padding()
if loginStage == .login || loginStage == .validate {
Text("The 'Login' button will open a web browser. Please follow the login instructions provided there.\nAfter a successful login, return to this application and press 'Validate'.")
.font(.subheadline)
.foregroundStyle(.white)
.padding()
}
HStack {
if loginStage == .login || loginStage == .validate {
Button {
if userSettings.serverAddress == "" {
alertMessage = "Please enter a valid server address."
showAlert = true
return
}
Task {
await sendLoginV2Request()
if let loginRequest = loginRequest {
await UIApplication.shared.open(URL(string: loginRequest.login)!)
} else {
alertMessage = "Unable to reach server. Please check your server address and internet connection."
showAlert = true
}
}
loginStage = loginStage.next()
} label: {
Text("Login")
.foregroundColor(.white)
.font(.headline)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white, lineWidth: 2)
.foregroundColor(.clear)
)
}.padding()
if loginRequest != nil {
Button("Copy Link") {
UIPasteboard.general.string = loginRequest!.login
}
.font(.headline)
.foregroundStyle(.white)
.padding()
}
HStack {
Button {
if userSettings.serverAddress == "" {
alertMessage = "Please enter a valid server address."
showAlert = true
return
}
Task {
await sendLoginV2Request()
if let loginRequest = loginRequest {
await UIApplication.shared.open(URL(string: loginRequest.login)!)
} else {
alertMessage = "Unable to reach server. Please check your server address and internet connection."
showAlert = true
}
}
loginStage = loginStage.next()
} label: {
Text("Login")
.foregroundColor(.white)
.font(.headline)
.padding()
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.white, lineWidth: 2)
.foregroundColor(.clear)
)
}.padding()
if loginStage == .validate {
Spacer()