Number formatting settings, recipe ingredient amount calculation
This commit is contained in:
@@ -57,6 +57,7 @@
|
|||||||
A977D0E22B60034E009783A9 /* GroceryListTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0E12B60034E009783A9 /* GroceryListTabView.swift */; };
|
A977D0E22B60034E009783A9 /* GroceryListTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0E12B60034E009783A9 /* GroceryListTabView.swift */; };
|
||||||
A97B4D322B80B3E900EC1A88 /* RecipeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */; };
|
A97B4D322B80B3E900EC1A88 /* RecipeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */; };
|
||||||
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D342B80B82A00EC1A88 /* ShareView.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 */; };
|
A9A43AE12B963150003D95CA /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = A9A43AE02B963150003D95CA /* SwipeActions */; };
|
||||||
A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */; };
|
A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */; };
|
||||||
A9BBB38E2B8E44B3002DA7FF /* BottomClipper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38D2B8E44B3002DA7FF /* BottomClipper.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 = "<group>"; };
|
A977D0E12B60034E009783A9 /* GroceryListTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryListTabView.swift; sourceTree = "<group>"; };
|
||||||
A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeModels.swift; sourceTree = "<group>"; };
|
A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeModels.swift; sourceTree = "<group>"; };
|
||||||
A97B4D342B80B82A00EC1A88 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; };
|
A97B4D342B80B82A00EC1A88 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; };
|
||||||
|
A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = "<group>"; };
|
||||||
A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeaderView.swift; sourceTree = "<group>"; };
|
A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeaderView.swift; sourceTree = "<group>"; };
|
||||||
A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomClipper.swift; sourceTree = "<group>"; };
|
A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomClipper.swift; sourceTree = "<group>"; };
|
||||||
A9BBB38F2B91BE31002DA7FF /* ObservableRecipeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableRecipeDetail.swift; sourceTree = "<group>"; };
|
A9BBB38F2B91BE31002DA7FF /* ObservableRecipeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableRecipeDetail.swift; sourceTree = "<group>"; };
|
||||||
@@ -375,6 +377,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A76B8A702AE002AE00096CEC /* Alerts.swift */,
|
A76B8A702AE002AE00096CEC /* Alerts.swift */,
|
||||||
|
A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */,
|
||||||
A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */,
|
A79AA8DF2AFF80E3007D25F2 /* DurationComponents.swift */,
|
||||||
A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */,
|
A76B8A6E2ADFFA8800096CEC /* SupportedLanguage.swift */,
|
||||||
);
|
);
|
||||||
@@ -570,6 +573,7 @@
|
|||||||
A97506192B920EC200E86029 /* RecipeIngredientSection.swift in Sources */,
|
A97506192B920EC200E86029 /* RecipeIngredientSection.swift in Sources */,
|
||||||
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */,
|
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */,
|
||||||
A975061F2B920FFC00E86029 /* RecipeToolSection.swift in Sources */,
|
A975061F2B920FFC00E86029 /* RecipeToolSection.swift in Sources */,
|
||||||
|
A9805BED2BAAC70E003B7231 /* NumberFormatter.swift in Sources */,
|
||||||
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */,
|
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */,
|
||||||
A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */,
|
A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */,
|
||||||
A79AA8E22AFF8C14007D25F2 /* RecipeEditViewModel.swift in Sources */,
|
A79AA8E22AFF8C14007D25F2 /* RecipeEditViewModel.swift in Sources */,
|
||||||
|
|||||||
Binary file not shown.
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
// Additional functionality
|
// Additional functionality
|
||||||
@Published var ingredientMultiplier: Double
|
@Published var ingredientMultiplier: Double
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
id = ""
|
id = ""
|
||||||
name = String(localized: "New Recipe")
|
name = String(localized: "New Recipe")
|
||||||
@@ -92,9 +94,34 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func adjustIngredient(_ ingredient: String, by factor: Double) -> AttributedString {
|
static func adjustIngredient(_ ingredient: String, by factor: Double) -> AttributedString {
|
||||||
var matches = ObservableRecipeDetail.matchPatternAndMultiply(.mixedFraction, in: ingredient, multFactor: factor)
|
if factor == 0 {
|
||||||
matches.append(contentsOf: ObservableRecipeDetail.matchPatternAndMultiply(.fraction, in: ingredient, multFactor: factor, excludedRanges: matches.map({ tuple in tuple.1 })))
|
return AttributedString(ingredient)
|
||||||
matches.append(contentsOf: ObservableRecipeDetail.matchPatternAndMultiply(.number, in: ingredient, multFactor: factor, excludedRanges: matches.map({ tuple in tuple.1 })))
|
}
|
||||||
|
// 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})
|
matches.sort(by: { a, b in a.1.lowerBound > b.1.lowerBound})
|
||||||
|
|
||||||
var attributedString = AttributedString(ingredient)
|
var attributedString = AttributedString(ingredient)
|
||||||
@@ -102,7 +129,8 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
print(newSubstring, matchRange)
|
print(newSubstring, matchRange)
|
||||||
guard let range = Range(matchRange, in: attributedString) else { continue }
|
guard let range = Range(matchRange, in: attributedString) else { continue }
|
||||||
var attributedSubString = AttributedString(newSubstring)
|
var attributedSubString = AttributedString(newSubstring)
|
||||||
attributedSubString.foregroundColor = .blue
|
//attributedSubString.foregroundColor = .ncTextHighlight
|
||||||
|
attributedSubString.font = .system(.body, weight: .bold)
|
||||||
attributedString.replaceSubrange(range, with: attributedSubString)
|
attributedString.replaceSubrange(range, with: attributedSubString)
|
||||||
print("\n", attributedString)
|
print("\n", attributedString)
|
||||||
}
|
}
|
||||||
@@ -130,8 +158,8 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
var adjustedValue: Double = 0
|
var adjustedValue: Double = 0
|
||||||
switch expr {
|
switch expr {
|
||||||
case .number:
|
case .number:
|
||||||
guard let number = Double(matchedString) else { continue }
|
guard let number = numberFormatter.number(from: matchedString) else { continue }
|
||||||
adjustedValue = number
|
adjustedValue = number.doubleValue
|
||||||
case .fraction:
|
case .fraction:
|
||||||
let fracComponents = matchedString.split(separator: "/")
|
let fracComponents = matchedString.split(separator: "/")
|
||||||
guard fracComponents.count == 2 else { continue }
|
guard fracComponents.count == 2 else { continue }
|
||||||
@@ -160,6 +188,9 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func formatNumber(_ value: Double) -> String {
|
static func formatNumber(_ value: Double) -> String {
|
||||||
|
if value <= 0.0001 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
let integerPart = value >= 1 ? Int(value) : 0
|
let integerPart = value >= 1 ? Int(value) : 0
|
||||||
let decimalPart = value - Double(integerPart)
|
let decimalPart = value - Double(integerPart)
|
||||||
|
|
||||||
@@ -183,7 +214,7 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
return "\(String(integerPart)) \(closest.fraction)"
|
return "\(String(integerPart)) \(closest.fraction)"
|
||||||
} else {
|
} else {
|
||||||
// If no close match is found, return the original value as a string
|
// 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
|
case mixedFraction, fraction, number
|
||||||
|
|
||||||
|
var id: String { self.rawValue }
|
||||||
|
|
||||||
var pattern: String {
|
var pattern: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .mixedFraction:
|
case .mixedFraction:
|
||||||
#"(\d+)\s+(\d+)/(\d+)"#
|
#"(\d+)\s+(\d+)/(\d+)"#
|
||||||
case .fraction:
|
case .fraction:
|
||||||
#"(?:[1-9][0-9]*|0)\/[1-9][0-9]*"#
|
#"(?:[1-9][0-9]*|0)\/([1-9][0-9]*)"#
|
||||||
case .number:
|
case .number:
|
||||||
#"(\d+(\.\d+)?)"#
|
#"(\d+([.,]\d+)?)"#
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var localizedDescription: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .mixedFraction:
|
||||||
|
"Mixed fraction"
|
||||||
|
case .fraction:
|
||||||
|
"Fraction"
|
||||||
|
case .number:
|
||||||
|
"Number"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ class UserSettings: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Published var decimalNumberSeparator: String {
|
||||||
|
didSet {
|
||||||
|
UserDefaults.standard.set(decimalNumberSeparator, forKey: "decimalNumberSeparator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.username = UserDefaults.standard.object(forKey: "username") as? String ?? ""
|
self.username = UserDefaults.standard.object(forKey: "username") as? String ?? ""
|
||||||
self.token = UserDefaults.standard.object(forKey: "token") 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.expandKeywordSection = UserDefaults.standard.object(forKey: "expandKeywordSection") as? Bool ?? false
|
||||||
self.expandInfoSection = UserDefaults.standard.object(forKey: "expandInfoSection") as? Bool ?? false
|
self.expandInfoSection = UserDefaults.standard.object(forKey: "expandInfoSection") as? Bool ?? false
|
||||||
self.keepScreenAwake = UserDefaults.standard.object(forKey: "keepScreenAwake") as? Bool ?? true
|
self.keepScreenAwake = UserDefaults.standard.object(forKey: "keepScreenAwake") as? Bool ?? true
|
||||||
|
self.decimalNumberSeparator = UserDefaults.standard.object(forKey: "decimalNumberSeparator") as? String ?? "."
|
||||||
|
|
||||||
if authString == "" {
|
if authString == "" {
|
||||||
if token != "" && username != "" {
|
if token != "" && username != "" {
|
||||||
|
|||||||
@@ -27,4 +27,7 @@ extension Color {
|
|||||||
public static var ncGradientLight: Color {
|
public static var ncGradientLight: Color {
|
||||||
return Color("ncgradientlightblue")
|
return Color("ncgradientlightblue")
|
||||||
}
|
}
|
||||||
|
public static var ncTextHighlight: Color {
|
||||||
|
return Color("textHighlight")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,6 +354,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"1,42 (Comma)" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"1.42 (Point)" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"A recipe with that name already exists." : {
|
"A recipe with that name already exists." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1094,6 +1100,12 @@
|
|||||||
},
|
},
|
||||||
"decilitre" : {
|
"decilitre" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Decimal number format" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Decimal Separator" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Delete" : {
|
"Delete" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1575,6 +1587,9 @@
|
|||||||
},
|
},
|
||||||
"fluid ounce" : {
|
"fluid ounce" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Fraction" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"g" : {
|
"g" : {
|
||||||
|
|
||||||
@@ -2309,6 +2324,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Mixed fraction" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"ml" : {
|
"ml" : {
|
||||||
|
|
||||||
@@ -2489,6 +2507,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Number" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Nutrition" : {
|
"Nutrition" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2577,9 +2598,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Only highlighted ingredient were adjusted!" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"Other" : {
|
"Other" : {
|
||||||
"localizations" : {
|
"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." : {
|
"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" : {
|
"localizations" : {
|
||||||
@@ -3864,6 +3885,9 @@
|
|||||||
},
|
},
|
||||||
"tsp" : {
|
"tsp" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Unable to adjust some ingredients!" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Unable to complete action." : {
|
"Unable to complete action." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
|
|||||||
22
Nextcloud Cookbook iOS Client/Util/NumberFormatter.swift
Normal file
22
Nextcloud Cookbook iOS Client/Util/NumberFormatter.swift
Normal file
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
@@ -47,14 +47,7 @@ struct RecipeIngredientSection: View {
|
|||||||
|
|
||||||
ServingPickerView(selectedServingSize: $viewModel.observableRecipeDetail.ingredientMultiplier)
|
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..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
|
ForEach(0..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
|
||||||
IngredientListItem(
|
IngredientListItem(
|
||||||
ingredient: $viewModel.observableRecipeDetail.recipeIngredient[ix],
|
ingredient: $viewModel.observableRecipeDetail.recipeIngredient[ix],
|
||||||
@@ -70,6 +63,16 @@ struct RecipeIngredientSection: View {
|
|||||||
}
|
}
|
||||||
.padding(4)
|
.padding(4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if viewModel.observableRecipeDetail.ingredientMultiplier != Double(viewModel.observableRecipeDetail.recipeYield) {
|
||||||
|
HStack() {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
Text("Unable to adjust some ingredients!")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if viewModel.editMode {
|
if viewModel.editMode {
|
||||||
Button {
|
Button {
|
||||||
viewModel.presentIngredientEditView.toggle()
|
viewModel.presentIngredientEditView.toggle()
|
||||||
@@ -78,7 +81,9 @@ struct RecipeIngredientSection: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
}
|
}
|
||||||
}.padding()
|
}
|
||||||
|
.padding()
|
||||||
|
.animation(.easeInOut, value: viewModel.observableRecipeDetail.ingredientMultiplier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +99,9 @@ fileprivate struct IngredientListItem: View {
|
|||||||
|
|
||||||
@State var modifiedIngredient: AttributedString = ""
|
@State var modifiedIngredient: AttributedString = ""
|
||||||
@State var isSelected: Bool = false
|
@State var isSelected: Bool = false
|
||||||
|
var unmodified: Bool {
|
||||||
|
servings == Double(recipeYield) || servings == 0
|
||||||
|
}
|
||||||
|
|
||||||
// Drag animation
|
// Drag animation
|
||||||
@State private var dragOffset: CGFloat = 0
|
@State private var dragOffset: CGFloat = 0
|
||||||
@@ -116,7 +124,11 @@ fileprivate struct IngredientListItem: View {
|
|||||||
} else {
|
} else {
|
||||||
Image(systemName: "circle")
|
Image(systemName: "circle")
|
||||||
}
|
}
|
||||||
if servings == Double(recipeYield) {
|
if !unmodified && String(modifiedIngredient.characters) == ingredient {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
if unmodified {
|
||||||
Text(ingredient)
|
Text(ingredient)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.lineLimit(5)
|
.lineLimit(5)
|
||||||
@@ -175,15 +187,6 @@ fileprivate struct IngredientListItem: View {
|
|||||||
|
|
||||||
struct ServingPickerView: View {
|
struct ServingPickerView: View {
|
||||||
@Binding var selectedServingSize: Double
|
@Binding var selectedServingSize: Double
|
||||||
@State var numberFormatter: NumberFormatter
|
|
||||||
|
|
||||||
init(selectedServingSize: Binding<Double>) {
|
|
||||||
_selectedServingSize = selectedServingSize
|
|
||||||
numberFormatter = NumberFormatter()
|
|
||||||
numberFormatter.usesSignificantDigits = true
|
|
||||||
numberFormatter.maximumFractionDigits = 2
|
|
||||||
numberFormatter.decimalSeparator = "."
|
|
||||||
}
|
|
||||||
|
|
||||||
// Computed property to handle the text field input and update the selectedServingSize
|
// Computed property to handle the text field input and update the selectedServingSize
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -207,7 +210,7 @@ struct ServingPickerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: selectedServingSize) { newValue in
|
.onChange(of: selectedServingSize) { newValue in
|
||||||
if newValue <= 0 { selectedServingSize = 1 }
|
if newValue < 0 { selectedServingSize = 0 }
|
||||||
else if newValue > 100 { selectedServingSize = 100 }
|
else if newValue > 100 { selectedServingSize = 100 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
Section {
|
||||||
Toggle(isOn: $userSettings.storeRecipes) {
|
Toggle(isOn: $userSettings.storeRecipes) {
|
||||||
Text("Offline recipes")
|
Text("Offline recipes")
|
||||||
@@ -170,12 +184,6 @@ struct SettingsView: View {
|
|||||||
} message: {
|
} message: {
|
||||||
Text(viewModel.alertType.getMessage())
|
Text(viewModel.alertType.getMessage())
|
||||||
}
|
}
|
||||||
.onDisappear {
|
|
||||||
Task {
|
|
||||||
userSettings.lastUpdate = .distantPast
|
|
||||||
await appState.updateAllRecipeDetails()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.task {
|
.task {
|
||||||
await viewModel.getUserData()
|
await viewModel.getUserData()
|
||||||
}
|
}
|
||||||
@@ -239,3 +247,4 @@ extension SettingsView {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user