diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index f1eb67e..7a7f909 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ A977D0E22B60034E009783A9 /* GroceryListTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0E12B60034E009783A9 /* GroceryListTabView.swift */; }; A97B4D322B80B3E900EC1A88 /* RecipeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */; }; A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D342B80B82A00EC1A88 /* ShareView.swift */; }; + A9805BED2BAAC70E003B7231 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */; }; A9A43AE12B963150003D95CA /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = A9A43AE02B963150003D95CA /* SwipeActions */; }; A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */; }; A9BBB38E2B8E44B3002DA7FF /* BottomClipper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */; }; @@ -139,6 +140,7 @@ A977D0E12B60034E009783A9 /* GroceryListTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryListTabView.swift; sourceTree = ""; }; A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeModels.swift; sourceTree = ""; }; A97B4D342B80B82A00EC1A88 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = ""; }; + A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = ""; }; A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeaderView.swift; sourceTree = ""; }; A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomClipper.swift; sourceTree = ""; }; A9BBB38F2B91BE31002DA7FF /* ObservableRecipeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableRecipeDetail.swift; sourceTree = ""; }; @@ -375,6 +377,7 @@ isa = PBXGroup; children = ( A76B8A702AE002AE00096CEC /* Alerts.swift */, + A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */, A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */, A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */, ); @@ -570,6 +573,7 @@ A97506192B920EC200E86029 /* RecipeIngredientSection.swift in Sources */, A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */, A975061F2B920FFC00E86029 /* RecipeToolSection.swift in Sources */, + A9805BED2BAAC70E003B7231 /* NumberFormatter.swift in Sources */, A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */, A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */, A79AA8E22AFF8C14007D25F2 /* RecipeEditViewModel.swift in Sources */, diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate index 7e65337..54a9314 100644 Binary files a/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate and b/Nextcloud Cookbook iOS Client.xcodeproj/project.xcworkspace/xcuserdata/vincie.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/textHighlight.colorset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/textHighlight.colorset/Contents.json new file mode 100644 index 0000000..8385deb --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Assets.xcassets/textHighlight.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xC9", + "green" : "0x82", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xC9", + "green" : "0x82", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xC9", + "green" : "0x82", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift b/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift index dd2b0eb..a91c10a 100644 --- a/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift +++ b/Nextcloud Cookbook iOS Client/Data/ObservableRecipeDetail.swift @@ -29,6 +29,8 @@ class ObservableRecipeDetail: ObservableObject { // Additional functionality @Published var ingredientMultiplier: Double + + init() { id = "" name = String(localized: "New Recipe") @@ -92,9 +94,34 @@ class ObservableRecipeDetail: ObservableObject { } static func adjustIngredient(_ ingredient: String, by factor: Double) -> AttributedString { - var matches = ObservableRecipeDetail.matchPatternAndMultiply(.mixedFraction, in: ingredient, multFactor: factor) - matches.append(contentsOf: ObservableRecipeDetail.matchPatternAndMultiply(.fraction, in: ingredient, multFactor: factor, excludedRanges: matches.map({ tuple in tuple.1 }))) - matches.append(contentsOf: ObservableRecipeDetail.matchPatternAndMultiply(.number, in: ingredient, multFactor: factor, excludedRanges: matches.map({ tuple in tuple.1 }))) + if factor == 0 { + return AttributedString(ingredient) + } + // Match mixed fractions first + var matches = ObservableRecipeDetail.matchPatternAndMultiply( + .mixedFraction, + in: ingredient, + multFactor: factor + ) + // Then match fractions, exclude mixed fraction ranges + matches.append(contentsOf: + ObservableRecipeDetail.matchPatternAndMultiply( + .fraction, + in: ingredient, + multFactor: factor, + excludedRanges: matches.map({ tuple in tuple.1 }) + ) + ) + // Match numbers at last, exclude all prior matches + matches.append(contentsOf: + ObservableRecipeDetail.matchPatternAndMultiply( + .number, + in: ingredient, + multFactor: factor, + excludedRanges: matches.map({ tuple in tuple.1 }) + ) + ) + // Sort matches by match range lower bound, descending. matches.sort(by: { a, b in a.1.lowerBound > b.1.lowerBound}) var attributedString = AttributedString(ingredient) @@ -102,7 +129,8 @@ class ObservableRecipeDetail: ObservableObject { print(newSubstring, matchRange) guard let range = Range(matchRange, in: attributedString) else { continue } var attributedSubString = AttributedString(newSubstring) - attributedSubString.foregroundColor = .blue + //attributedSubString.foregroundColor = .ncTextHighlight + attributedSubString.font = .system(.body, weight: .bold) attributedString.replaceSubrange(range, with: attributedSubString) print("\n", attributedString) } @@ -130,8 +158,8 @@ class ObservableRecipeDetail: ObservableObject { var adjustedValue: Double = 0 switch expr { case .number: - guard let number = Double(matchedString) else { continue } - adjustedValue = number + guard let number = numberFormatter.number(from: matchedString) else { continue } + adjustedValue = number.doubleValue case .fraction: let fracComponents = matchedString.split(separator: "/") guard fracComponents.count == 2 else { continue } @@ -160,6 +188,9 @@ class ObservableRecipeDetail: ObservableObject { } static func formatNumber(_ value: Double) -> String { + if value <= 0.0001 { + return "0" + } let integerPart = value >= 1 ? Int(value) : 0 let decimalPart = value - Double(integerPart) @@ -183,7 +214,7 @@ class ObservableRecipeDetail: ObservableObject { return "\(String(integerPart)) \(closest.fraction)" } else { // If no close match is found, return the original value as a string - return String(format: "%.2f", value) + return numberFormatter.string(from: NSNumber(value: value)) ?? "0"//String(format: "%.2f", value) } } @@ -192,17 +223,30 @@ class ObservableRecipeDetail: ObservableObject { } } -enum RegexPattern { +enum RegexPattern: String, CaseIterable, Identifiable { case mixedFraction, fraction, number + var id: String { self.rawValue } + var pattern: String { switch self { case .mixedFraction: #"(\d+)\s+(\d+)/(\d+)"# case .fraction: - #"(?:[1-9][0-9]*|0)\/[1-9][0-9]*"# + #"(?:[1-9][0-9]*|0)\/([1-9][0-9]*)"# case .number: - #"(\d+(\.\d+)?)"# + #"(\d+([.,]\d+)?)"# + } + } + + var localizedDescription: LocalizedStringKey { + switch self { + case .mixedFraction: + "Mixed fraction" + case .fraction: + "Fraction" + case .number: + "Number" } } } diff --git a/Nextcloud Cookbook iOS Client/Data/UserSettings.swift b/Nextcloud Cookbook iOS Client/Data/UserSettings.swift index de942c4..cddb099 100644 --- a/Nextcloud Cookbook iOS Client/Data/UserSettings.swift +++ b/Nextcloud Cookbook iOS Client/Data/UserSettings.swift @@ -115,6 +115,12 @@ class UserSettings: ObservableObject { } } + @Published var decimalNumberSeparator: String { + didSet { + UserDefaults.standard.set(decimalNumberSeparator, forKey: "decimalNumberSeparator") + } + } + init() { self.username = UserDefaults.standard.object(forKey: "username") as? String ?? "" self.token = UserDefaults.standard.object(forKey: "token") as? String ?? "" @@ -133,6 +139,7 @@ class UserSettings: ObservableObject { self.expandKeywordSection = UserDefaults.standard.object(forKey: "expandKeywordSection") as? Bool ?? false self.expandInfoSection = UserDefaults.standard.object(forKey: "expandInfoSection") as? Bool ?? false self.keepScreenAwake = UserDefaults.standard.object(forKey: "keepScreenAwake") as? Bool ?? true + self.decimalNumberSeparator = UserDefaults.standard.object(forKey: "decimalNumberSeparator") as? String ?? "." if authString == "" { if token != "" && username != "" { diff --git a/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift b/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift index 439e045..d2a2699 100644 --- a/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift +++ b/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift @@ -27,4 +27,7 @@ extension Color { public static var ncGradientLight: Color { return Color("ncgradientlightblue") } + public static var ncTextHighlight: Color { + return Color("textHighlight") + } } diff --git a/Nextcloud Cookbook iOS Client/Localizable.xcstrings b/Nextcloud Cookbook iOS Client/Localizable.xcstrings index f2c0a01..d9d6eed 100644 --- a/Nextcloud Cookbook iOS Client/Localizable.xcstrings +++ b/Nextcloud Cookbook iOS Client/Localizable.xcstrings @@ -354,6 +354,12 @@ } } } + }, + "1,42 (Comma)" : { + + }, + "1.42 (Point)" : { + }, "A recipe with that name already exists." : { "localizations" : { @@ -1094,6 +1100,12 @@ }, "decilitre" : { + }, + "Decimal number format" : { + + }, + "Decimal Separator" : { + }, "Delete" : { "localizations" : { @@ -1575,6 +1587,9 @@ }, "fluid ounce" : { + }, + "Fraction" : { + }, "g" : { @@ -2309,6 +2324,9 @@ } } } + }, + "Mixed fraction" : { + }, "ml" : { @@ -2489,6 +2507,9 @@ } } } + }, + "Number" : { + }, "Nutrition" : { "localizations" : { @@ -2577,9 +2598,6 @@ } } } - }, - "Only highlighted ingredient were adjusted!" : { - }, "Other" : { "localizations" : { @@ -3682,6 +3700,9 @@ } } } + }, + "This setting will take effect after the app is restarted. It affects the adjustment of ingredient quantities." : { + }, "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." : { "localizations" : { @@ -3864,6 +3885,9 @@ }, "tsp" : { + }, + "Unable to adjust some ingredients!" : { + }, "Unable to complete action." : { "localizations" : { diff --git a/Nextcloud Cookbook iOS Client/Util/NumberFormatter.swift b/Nextcloud Cookbook iOS Client/Util/NumberFormatter.swift new file mode 100644 index 0000000..213a003 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Util/NumberFormatter.swift @@ -0,0 +1,22 @@ +// +// Locale.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 20.03.24. +// + +import Foundation + + +// Ingredient number formatting +func getNumberFormatter() -> NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 2 + formatter.decimalSeparator = UserSettings.shared.decimalNumberSeparator + return formatter +} + +let numberFormatter = getNumberFormatter() + + diff --git a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift index f556976..c23daac 100644 --- a/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift +++ b/Nextcloud Cookbook iOS Client/Views/Recipes/RecipeViewSections/RecipeIngredientSection.swift @@ -47,14 +47,7 @@ struct RecipeIngredientSection: View { ServingPickerView(selectedServingSize: $viewModel.observableRecipeDetail.ingredientMultiplier) } - if viewModel.observableRecipeDetail.ingredientMultiplier != Double(viewModel.observableRecipeDetail.recipeYield) { - HStack() { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundStyle(.red) - Text("Only highlighted ingredient were adjusted!") - .foregroundStyle(.secondary) - } - } + ForEach(0..) { - _selectedServingSize = selectedServingSize - numberFormatter = NumberFormatter() - numberFormatter.usesSignificantDigits = true - numberFormatter.maximumFractionDigits = 2 - numberFormatter.decimalSeparator = "." - } // Computed property to handle the text field input and update the selectedServingSize var body: some View { @@ -207,7 +210,7 @@ struct ServingPickerView: View { } } .onChange(of: selectedServingSize) { newValue in - if newValue <= 0 { selectedServingSize = 1 } + if newValue < 0 { selectedServingSize = 0 } else if newValue > 100 { selectedServingSize = 100 } } } diff --git a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift index cc49369..a7a396e 100644 --- a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift +++ b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift @@ -76,6 +76,20 @@ struct SettingsView: View { } } + Section { + HStack { + Text("Decimal number format") + Spacer() + Picker("Decimal Separator", selection: $userSettings.decimalNumberSeparator) { + Text("1.42 (Point)").tag(".") + Text("1,42 (Comma)").tag(",") + } + .pickerStyle(.menu) + } + } footer: { + Text("This setting will take effect after the app is restarted. It affects the adjustment of ingredient quantities.") + } + Section { Toggle(isOn: $userSettings.storeRecipes) { Text("Offline recipes") @@ -170,12 +184,6 @@ struct SettingsView: View { } message: { Text(viewModel.alertType.getMessage()) } - .onDisappear { - Task { - userSettings.lastUpdate = .distantPast - await appState.updateAllRecipeDetails() - } - } .task { await viewModel.getUserData() } @@ -239,3 +247,4 @@ extension SettingsView { +