diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index c96320d..7c33b45 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171AE2AB2116B00064C43 /* NetworkHandler.swift */; }; A70171B12AB211DF00064C43 /* CustomError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B02AB211DF00064C43 /* CustomError.swift */; }; A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B32AB2122900064C43 /* NetworkRequests.swift */; }; - A70171B92AB399FB00064C43 /* DateFormatterExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */; }; A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BD2AB4987900064C43 /* CategoryDetailView.swift */; }; A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BF2AB498A900064C43 /* RecipeDetailView.swift */; }; A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C12AB498C600064C43 /* RecipeCardView.swift */; }; @@ -35,6 +34,7 @@ A74D33C32AFCD1C300D06555 /* RecipeScraper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74D33C22AFCD1C300D06555 /* RecipeScraper.swift */; }; A76B8A6F2ADFFA8800096CEC /* SupportedLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */; }; A76B8A712AE002AE00096CEC /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76B8A702AE002AE00096CEC /* Alerts.swift */; }; + A79AA8E02AFF80E3007D25F2 /* DurationComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */; }; A7AEAE642AD5521400135378 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = A7AEAE632AD5521400135378 /* Localizable.xcstrings */; }; A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */; }; A7F3F8EA2ACC221C0076C227 /* CategoryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */; }; @@ -73,7 +73,6 @@ A70171AE2AB2116B00064C43 /* NetworkHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkHandler.swift; sourceTree = ""; }; A70171B02AB211DF00064C43 /* CustomError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomError.swift; sourceTree = ""; }; A70171B32AB2122900064C43 /* NetworkRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequests.swift; sourceTree = ""; }; - A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatterExtension.swift; sourceTree = ""; }; A70171BD2AB4987900064C43 /* CategoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailView.swift; sourceTree = ""; }; A70171BF2AB498A900064C43 /* RecipeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailView.swift; sourceTree = ""; }; A70171C12AB498C600064C43 /* RecipeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCardView.swift; sourceTree = ""; }; @@ -90,6 +89,7 @@ A74D33C22AFCD1C300D06555 /* RecipeScraper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeScraper.swift; sourceTree = ""; }; A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedLanguage.swift; sourceTree = ""; }; A76B8A702AE002AE00096CEC /* Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alerts.swift; sourceTree = ""; }; + A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DurationComponents.swift; sourceTree = ""; }; A7AEAE632AD5521400135378 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; A7F3F8E72ACBFC760076C227 /* KeywordPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeywordPickerView.swift; sourceTree = ""; }; A7F3F8E92ACC221C0076C227 /* CategoryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryPickerView.swift; sourceTree = ""; }; @@ -228,6 +228,7 @@ A70171C32AB4A31200064C43 /* DataStore.swift */, A70171C52AB4C43A00064C43 /* DataModels.swift */, A70171CA2AB4CD1700064C43 /* UserSettings.swift */, + A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */, ); path = Data; sourceTree = ""; @@ -235,7 +236,6 @@ A703226B2ABAF60D00D7C4ED /* Extensions */ = { isa = PBXGroup; children = ( - A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */, A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */, A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */, ); @@ -404,10 +404,10 @@ A70171C42AB4A31200064C43 /* DataStore.swift in Sources */, A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */, A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */, - A70171B92AB399FB00064C43 /* DateFormatterExtension.swift in Sources */, A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */, A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */, A7F3F8E82ACBFC760076C227 /* KeywordPickerView.swift in Sources */, + A79AA8E02AFF80E3007D25F2 /* DurationComponents.swift in Sources */, A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */, A7F3F8EA2ACC221C0076C227 /* CategoryPickerView.swift in Sources */, A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */, diff --git a/Nextcloud Cookbook iOS Client/Data/DataModels.swift b/Nextcloud Cookbook iOS Client/Data/DataModels.swift index 4040e98..819e556 100644 --- a/Nextcloud Cookbook iOS Client/Data/DataModels.swift +++ b/Nextcloud Cookbook iOS Client/Data/DataModels.swift @@ -91,6 +91,16 @@ struct RecipeDetail: Codable { recipeInstructions = [] nutrition = [:] } + + func getKeywordsArray() -> [String] { + return keywords.components(separatedBy: ",") + } + + mutating func setKeywordsFromArray(_ keywordsArray: [String]) { + if !self.keywords.isEmpty { + self.keywords = keywordsArray.joined(separator: ",") + } + } } extension RecipeDetail { diff --git a/Nextcloud Cookbook iOS Client/Data/DurationComponents.swift b/Nextcloud Cookbook iOS Client/Data/DurationComponents.swift new file mode 100644 index 0000000..7e9a74f --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Data/DurationComponents.swift @@ -0,0 +1,99 @@ +// +// Duration.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 11.11.23. +// + +import Foundation +import SwiftUI + + +class DurationComponents: ObservableObject { + @Published var minuteComponent: String = "00" { + didSet { + if minuteComponent.count > 2 { + minuteComponent = oldValue + } else if minuteComponent.count == 1 { + minuteComponent = "0\(minuteComponent)" + } else if minuteComponent.count == 0 { + minuteComponent = "00" + } + let filtered = minuteComponent.filter { $0.isNumber } + if minuteComponent != filtered { + minuteComponent = filtered + } + } + } + + @Published var hourComponent: String = "00" { + didSet { + if hourComponent.count > 2 { + hourComponent = oldValue + } else if hourComponent.count == 1 { + hourComponent = "0\(hourComponent)" + } else if hourComponent.count == 0 { + hourComponent = "00" + } + let filtered = hourComponent.filter { $0.isNumber } + if hourComponent != filtered { + hourComponent = filtered + } + } + } + + func fromPTString(_ PTRepresentation: String) { + let hourRegex = /([0-9]{1,2})H/ + let minuteRegex = /([0-9]{1,2})M/ + if let match = PTRepresentation.firstMatch(of: hourRegex) { + self.hourComponent = String(match.1) + } + if let match = PTRepresentation.firstMatch(of: minuteRegex) { + self.minuteComponent = String(match.1) + } + } + + func toPTString() -> String { + return "PT\(hourComponent)H\(minuteComponent)M00S" + } + + func toText() -> LocalizedStringKey { + let intHour = Int(hourComponent) ?? 0 + let intMinute = Int(minuteComponent) ?? 0 + if intHour != 0 && intMinute != 0 { + return "\(intHour) h, \(intMinute) min" + } else if intHour == 0 && intMinute != 0 { + return "\(intMinute) min" + } else if intHour != 0 && intMinute == 0 { + return "\(intHour) h" + } else { + return "-" + } + } + + static func ptToText(_ ptString: String) -> String { + let hourRegex = /([0-9]{1,2})H/ + let minuteRegex = /([0-9]{1,2})M/ + + var intHour = 0 + var intMinute = 0 + if let match = ptString.firstMatch(of: hourRegex) { + let hourComponent = String(match.1) + intHour = Int(hourComponent) ?? 0 + } + if let match = ptString.firstMatch(of: minuteRegex) { + let minuteComponent = String(match.1) + intMinute = Int(minuteComponent) ?? 0 + } + + if intHour != 0 && intMinute != 0 { + return "\(intHour) h, \(intMinute) min" + } else if intHour == 0 && intMinute != 0 { + return "\(intMinute) min" + } else if intHour != 0 && intMinute == 0 { + return "\(intHour) h" + } else { + return "-" + } + } +} diff --git a/Nextcloud Cookbook iOS Client/Extensions/DateFormatterExtension.swift b/Nextcloud Cookbook iOS Client/Extensions/DateFormatterExtension.swift deleted file mode 100644 index e596f8a..0000000 --- a/Nextcloud Cookbook iOS Client/Extensions/DateFormatterExtension.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// DateFormatterExtension.swift -// Nextcloud Cookbook iOS Client -// -// Created by Vincent Meilinger on 14.09.23. -// - -import Foundation - -extension Formatter { - static let positional: DateComponentsFormatter = { - let formatter = DateComponentsFormatter() - formatter.unitsStyle = .positional - return formatter - }() - - static func formatDate(duration: String) -> String { - var duration = duration - if duration.hasPrefix("PT") { duration.removeFirst(2) } - var hour: Int = 0, minute: Int = 0 - if let index = duration.firstIndex(of: "H") { - hour = Int(duration[.. (Int, Int) { - var duration = duration - if duration.hasPrefix("PT") { duration.removeFirst(2) } - var hour: Int = 0, minute: Int = 0 - if let index = duration.firstIndex(of: "H") { - hour = Int(duration[.. () = {} - @StateObject private var prepDuration: Duration = Duration() - @StateObject private var cookDuration: Duration = Duration() - @StateObject private var totalDuration: Duration = Duration() + @StateObject private var prepDuration: DurationComponents = DurationComponents() + @StateObject private var cookDuration: DurationComponents = DurationComponents() + @StateObject private var totalDuration: DurationComponents = DurationComponents() @State private var searchText: String = "" @State private var keywords: [String] = [] @State private var keywordSuggestions: [String] = [] @@ -94,6 +90,7 @@ struct RecipeEditView: View { let (scrapedRecipe, error) = try await RecipeScraper().scrape(url: importURL) if let scrapedRecipe = scrapedRecipe { self.recipe = scrapedRecipe + prepareView() } if let error = error { self.alertType = error @@ -200,13 +197,10 @@ struct RecipeEditView: View { } func createRecipe() { - self.recipe.prepTime = prepDuration.format() - self.recipe.cookTime = cookDuration.format() - self.recipe.totalTime = totalDuration.format() - - if !self.keywords.isEmpty { - self.recipe.keywords = self.keywords.joined(separator: ",") - } + self.recipe.prepTime = prepDuration.toPTString() + self.recipe.cookTime = cookDuration.toPTString() + self.recipe.totalTime = totalDuration.toPTString() + self.recipe.setKeywordsFromArray(keywords) } func recipeValid() -> Bool { @@ -316,19 +310,16 @@ struct RecipeEditView: View { } func prepareView() { - if uploadNew { return } if let prepTime = recipe.prepTime { - prepDuration.initFromPT(prepTime) + prepDuration.fromPTString(prepTime) } if let cookTime = recipe.cookTime { - cookDuration.initFromPT(cookTime) + cookDuration.fromPTString(cookTime) } if let totalTime = recipe.totalTime { - totalDuration.initFromPT(totalTime) - } - for keyword in self.recipe.keywords.components(separatedBy: ",") { - keywords.append(keyword) + totalDuration.fromPTString(totalTime) } + self.keywords = recipe.getKeywordsArray() } } @@ -381,7 +372,7 @@ fileprivate struct EditableListSection: View { fileprivate struct DurationPicker: View { @State var title: LocalizedStringKey - @ObservedObject var duration: Duration + @ObservedObject var duration: DurationComponents var body: some View { HStack { @@ -405,54 +396,7 @@ fileprivate struct DurationPicker: View { -fileprivate class Duration: ObservableObject { - @Published var minuteComponent: String = "00" { - didSet { - if minuteComponent.count > 2 { - minuteComponent = oldValue - } else if minuteComponent.count == 1 { - minuteComponent = "0\(minuteComponent)" - } else if minuteComponent.count == 0 { - minuteComponent = "00" - } - let filtered = minuteComponent.filter { $0.isNumber } - if minuteComponent != filtered { - minuteComponent = filtered - } - } - } - - @Published var hourComponent: String = "00" { - didSet { - if hourComponent.count > 2 { - hourComponent = oldValue - } else if hourComponent.count == 1 { - hourComponent = "0\(hourComponent)" - } else if hourComponent.count == 0 { - hourComponent = "00" - } - let filtered = hourComponent.filter { $0.isNumber } - if hourComponent != filtered { - hourComponent = filtered - } - } - } - - func initFromPT(_ PTRepresentation: String) { - let hourRegex = /([0-9]{1,2})H/ - let minuteRegex = /([0-9]{1,2})M/ - if let match = PTRepresentation.firstMatch(of: hourRegex) { - self.hourComponent = String(match.1) - } - if let match = PTRepresentation.firstMatch(of: minuteRegex) { - self.minuteComponent = String(match.1) - } - } - - func format() -> String { - return "PT\(hourComponent)H\(minuteComponent)M00S" - } -} +