diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index 378573c..9e3f364 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -308,6 +308,9 @@ } } } + }, + "Bad URL" : { + }, "Cancel" : { "localizations" : { @@ -396,6 +399,9 @@ } } } + }, + "Connection error" : { + }, "Cookbook Client" : { "localizations" : { @@ -1326,9 +1332,15 @@ } } } + }, + "Parsing error" : { + }, "Paste the url of a recipe you would like to import in the above, and we will try to fill in the fields for you. This feature does not work with every website. If your favourite website is not supported, feel free to reach out for help. You can find the contact details in the app settings." : { + }, + "Please check the entered URL." : { + }, "Please check your credentials and internet connection." : { "localizations" : { @@ -1703,6 +1715,9 @@ } } } + }, + "This website might not be currently supported. If this appears incorrect, you can use the support options in the app settings to raise awareness about this issue." : { + }, "Title" : { "localizations" : { @@ -1813,6 +1828,9 @@ } } } + }, + "Unable to load website content. Please check your internet connection." : { + }, "Unable to upload your recipe. Please check your internet connection." : { "localizations" : { diff --git a/Nextcloud Cookbook iOS Client/RecipeImport/RecipeScraper.swift b/Nextcloud Cookbook iOS Client/RecipeImport/RecipeScraper.swift index 455c45f..edc5628 100644 --- a/Nextcloud Cookbook iOS Client/RecipeImport/RecipeScraper.swift +++ b/Nextcloud Cookbook iOS Client/RecipeImport/RecipeScraper.swift @@ -7,25 +7,28 @@ import Foundation import SwiftSoup +import SwiftUI + class RecipeScraper { - func scrape(url: String) async throws -> RecipeDetail? { + func scrape(url: String) async throws -> (RecipeDetail?, RecipeImportError?) { var contents: String? = nil if let url = URL(string: url) { do { contents = try String(contentsOf: url) } catch { print("ERROR: Could not load url content.") + return (nil, .CHECK_CONNECTION) } } else { print("ERROR: Bad url.") - return nil + return (nil, .BAD_URL) } guard let html = contents else { print("ERROR: no contents") - return nil + return (nil, .WEBSITE_NOT_SUPPORTED) } let doc = try SwiftSoup.parse(html) @@ -34,11 +37,13 @@ class RecipeScraper { for attr in elem.getAttributes()!.asList() { if attr.getValue() == "application/ld+json" { guard let dict = toDict(elem) else { continue } - return getRecipe(fromDict: dict) + if let recipe = getRecipe(fromDict: dict) { + return (recipe, nil) + } } } } - return nil + return (nil, .WEBSITE_NOT_SUPPORTED) } @@ -46,7 +51,6 @@ class RecipeScraper { var recipeDict: [String: Any]? = nil do { let jsonString = try elem.html() - //print(json) let json = try JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!, options: .fragmentsAllowed) if let recipe = json as? [String : Any] { recipeDict = recipe @@ -78,7 +82,7 @@ class RecipeScraper { var recipeDetail = RecipeDetail() recipeDetail.name = recipe["name"] as? String ?? "New Recipe" recipeDetail.recipeCategory = recipe["recipeCategory"] as? String ?? "" - recipeDetail.keywords = recipe["keywords"] as? String ?? "" + recipeDetail.keywords = joinedStringForKey("keywords", dict: recipe) recipeDetail.description = recipe["description"] as? String ?? "" recipeDetail.dateCreated = recipe["dateCreated"] as? String ?? "" recipeDetail.dateModified = recipe["dateModified"] as? String ?? "" @@ -90,14 +94,16 @@ class RecipeScraper { recipeDetail.recipeInstructions = stringArrayForKey("recipeInstructions", dict: recipe) recipeDetail.recipeYield = recipe["recipeYield"] as? Int ?? 0 recipeDetail.recipeIngredient = recipe["recipeIngredient"] as? [String] ?? [] - recipeDetail.tool = recipe["tool"] as? [String] ?? [] + recipeDetail.tool = stringArrayForKey("tool", dict: recipe) recipeDetail.nutrition = recipe["nutrition"] as? [String:String] ?? [:] - + print(recipeDetail) return recipeDetail } private func stringArrayForKey(_ key: String, dict: Dictionary) -> [String] { - if let value = dict[key] as? [String] { + if let text = dict[key] as? String { + return [text] + } else if let value = dict[key] as? [String] { return value } else if let orderedList = dict[key] as? [Any] { var entries: [String] = [] @@ -107,8 +113,6 @@ class RecipeScraper { entries.append(text) } return entries - } else if let text = dict[key] as? String { - return [text] } return [] } diff --git a/Nextcloud Cookbook iOS Client/Views/Alerts.swift b/Nextcloud Cookbook iOS Client/Views/Alerts.swift index f3e79f0..90c9ea2 100644 --- a/Nextcloud Cookbook iOS Client/Views/Alerts.swift +++ b/Nextcloud Cookbook iOS Client/Views/Alerts.swift @@ -9,6 +9,12 @@ import Foundation import SwiftUI +protocol UserAlert: Error { + var localizedTitle: LocalizedStringKey { get } + var localizedDescription: LocalizedStringKey { get } + var alertButtons: [AlertButton] { get } +} + enum AlertButton: LocalizedStringKey, Identifiable { var id: Self { return self @@ -19,7 +25,7 @@ enum AlertButton: LocalizedStringKey, Identifiable { -enum AlertType: Error { +enum RecipeCreationError: UserAlert { case NO_TITLE, DUPLICATE, @@ -77,3 +83,29 @@ enum AlertType: Error { } } + +enum RecipeImportError: UserAlert { + case BAD_URL, + CHECK_CONNECTION, + WEBSITE_NOT_SUPPORTED + + var localizedDescription: LocalizedStringKey { + switch self { + case .BAD_URL: return "Please check the entered URL." + case .CHECK_CONNECTION: return "Unable to load website content. Please check your internet connection." + case .WEBSITE_NOT_SUPPORTED: return "This website might not be currently supported. If this appears incorrect, you can use the support options in the app settings to raise awareness about this issue." + } + } + + var localizedTitle: LocalizedStringKey { + switch self { + case .BAD_URL: return "Bad URL" + case .CHECK_CONNECTION: return "Connection error" + case .WEBSITE_NOT_SUPPORTED: return "Parsing error" + } + } + + var alertButtons: [AlertButton] { + return [.OK] + } +} diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift index dc09144..d17a996 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift @@ -13,12 +13,16 @@ import PhotosUI struct RecipeEditView: View { @ObservedObject var viewModel: MainViewModel - @State var recipe: RecipeDetail = RecipeDetail() + @State var recipe: RecipeDetail = RecipeDetail() { + didSet { + prepareView() + } + } @Binding var isPresented: Bool @State var uploadNew: Bool = true @State private var presentAlert = false - @State private var alertType: AlertType = .GENERIC + @State private var alertType: UserAlert = RecipeCreationError.GENERIC @State private var alertAction: () -> () = {} @StateObject private var prepDuration: Duration = Duration() @@ -46,7 +50,7 @@ struct RecipeEditView: View { Menu { Button { print("Delete recipe.") - alertType = .CONFIRM_DELETE + alertType = RecipeCreationError.CONFIRM_DELETE alertAction = deleteRecipe presentAlert = true } label: { @@ -87,8 +91,14 @@ struct RecipeEditView: View { .onSubmit { Task { do { - if let recipe = try await RecipeScraper().scrape(url: importURL) { - self.recipe = recipe + let (scrapedRecipe, error) = try await RecipeScraper().scrape(url: importURL) + if let scrapedRecipe = scrapedRecipe { + self.recipe = scrapedRecipe + } + if let error = error { + self.alertType = error + self.alertAction = {} + self.presentAlert = true } } catch { print("Error") @@ -168,19 +178,7 @@ struct RecipeEditView: View { self.keywordSuggestions = await viewModel.getKeywords() } .onAppear { - if uploadNew { return } - if let prepTime = recipe.prepTime { - prepDuration.initFromPT(prepTime) - } - if let cookTime = recipe.cookTime { - cookDuration.initFromPT(cookTime) - } - if let totalTime = recipe.totalTime { - totalDuration.initFromPT(totalTime) - } - for keyword in self.recipe.keywords.components(separatedBy: ",") { - keywords.append(keyword) - } + prepareView() } .alert(alertType.localizedTitle, isPresented: $presentAlert) { ForEach(alertType.alertButtons) { buttonType in @@ -214,7 +212,7 @@ struct RecipeEditView: View { func recipeValid() -> Bool { // Check if the recipe has a name if recipe.name.replacingOccurrences(of: " ", with: "") == "" { - alertType = .NO_TITLE + alertType = RecipeCreationError.NO_TITLE alertAction = {} presentAlert = true return false @@ -229,7 +227,7 @@ struct RecipeEditView: View { .replacingOccurrences(of: " ", with: "") .lowercased() { - alertType = .DUPLICATE + alertType = RecipeCreationError.DUPLICATE alertAction = {} presentAlert = true return false @@ -316,6 +314,22 @@ struct RecipeEditView: View { } self.isPresented = false } + + func prepareView() { + if uploadNew { return } + if let prepTime = recipe.prepTime { + prepDuration.initFromPT(prepTime) + } + if let cookTime = recipe.cookTime { + cookDuration.initFromPT(cookTime) + } + if let totalTime = recipe.totalTime { + totalDuration.initFromPT(totalTime) + } + for keyword in self.recipe.keywords.components(separatedBy: ",") { + keywords.append(keyword) + } + } }