calculator allows manual entries too
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
import Foundation
|
||||
|
||||
struct CalculatorView: View {
|
||||
@EnvironmentObject var unitSettings: UnitSystemSettings
|
||||
@@ -17,6 +18,10 @@ struct CalculatorView: View {
|
||||
@State private var showingLibrary = false
|
||||
@State private var isWattMode = false
|
||||
@State private var editingValue: EditingValue? = nil
|
||||
@State private var voltageInput: String = ""
|
||||
@State private var currentInput: String = ""
|
||||
@State private var powerInput: String = ""
|
||||
@State private var lengthInput: String = ""
|
||||
@State private var showingLoadEditor = false
|
||||
@State private var presentedAffiliateLink: AffiliateLinkInfo?
|
||||
@State private var completedItemIDs: Set<String>
|
||||
@@ -32,6 +37,15 @@ struct CalculatorView: View {
|
||||
case voltage, current, power, length
|
||||
}
|
||||
|
||||
private static let editFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = .current
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.minimumFractionDigits = 0
|
||||
formatter.maximumFractionDigits = 1
|
||||
return formatter
|
||||
}()
|
||||
|
||||
struct AffiliateLinkInfo: Identifiable, Equatable {
|
||||
let id: String
|
||||
let affiliateURL: URL?
|
||||
@@ -123,13 +137,34 @@ struct CalculatorView: View {
|
||||
}
|
||||
.alert("Edit Length", isPresented: Binding(
|
||||
get: { editingValue == .length },
|
||||
set: { if !$0 { editingValue = nil } }
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
editingValue = nil
|
||||
lengthInput = ""
|
||||
}
|
||||
}
|
||||
)) {
|
||||
TextField("Length", value: $calculator.length, format: .number)
|
||||
TextField("Length", text: $lengthInput)
|
||||
.keyboardType(.decimalPad)
|
||||
Button("Cancel", role: .cancel) { editingValue = nil }
|
||||
Button("Save") {
|
||||
.onAppear {
|
||||
if lengthInput.isEmpty {
|
||||
lengthInput = formattedValue(calculator.length)
|
||||
}
|
||||
}
|
||||
.onChange(of: lengthInput) { newValue in
|
||||
guard editingValue == .length, let parsed = parseInput(newValue) else { return }
|
||||
calculator.length = roundToTenth(parsed)
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
editingValue = nil
|
||||
lengthInput = ""
|
||||
}
|
||||
Button("Save") {
|
||||
if let parsed = parseInput(lengthInput) {
|
||||
calculator.length = roundToTenth(parsed)
|
||||
}
|
||||
editingValue = nil
|
||||
lengthInput = ""
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
@@ -138,18 +173,39 @@ struct CalculatorView: View {
|
||||
}
|
||||
.alert("Edit Voltage", isPresented: Binding(
|
||||
get: { editingValue == .voltage },
|
||||
set: { if !$0 { editingValue = nil } }
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
editingValue = nil
|
||||
voltageInput = ""
|
||||
}
|
||||
}
|
||||
)) {
|
||||
TextField("Voltage", value: $calculator.voltage, format: .number)
|
||||
TextField("Voltage", text: $voltageInput)
|
||||
.keyboardType(.decimalPad)
|
||||
Button("Cancel", role: .cancel) { editingValue = nil }
|
||||
.onAppear {
|
||||
if voltageInput.isEmpty {
|
||||
voltageInput = formattedValue(calculator.voltage)
|
||||
}
|
||||
}
|
||||
.onChange(of: voltageInput) { newValue in
|
||||
guard editingValue == .voltage, let parsed = parseInput(newValue) else { return }
|
||||
calculator.voltage = roundToTenth(parsed)
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
editingValue = nil
|
||||
voltageInput = ""
|
||||
}
|
||||
Button("Save") {
|
||||
if let parsed = parseInput(voltageInput) {
|
||||
calculator.voltage = roundToTenth(parsed)
|
||||
}
|
||||
editingValue = nil
|
||||
if isWattMode {
|
||||
calculator.updateFromPower()
|
||||
} else {
|
||||
calculator.updateFromCurrent()
|
||||
}
|
||||
voltageInput = ""
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
@@ -158,14 +214,35 @@ struct CalculatorView: View {
|
||||
}
|
||||
.alert("Edit Current", isPresented: Binding(
|
||||
get: { editingValue == .current },
|
||||
set: { if !$0 { editingValue = nil } }
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
editingValue = nil
|
||||
currentInput = ""
|
||||
}
|
||||
}
|
||||
)) {
|
||||
TextField("Current", value: $calculator.current, format: .number)
|
||||
TextField("Current", text: $currentInput)
|
||||
.keyboardType(.decimalPad)
|
||||
Button("Cancel", role: .cancel) { editingValue = nil }
|
||||
.onAppear {
|
||||
if currentInput.isEmpty {
|
||||
currentInput = formattedValue(calculator.current)
|
||||
}
|
||||
}
|
||||
.onChange(of: currentInput) { newValue in
|
||||
guard editingValue == .current, let parsed = parseInput(newValue) else { return }
|
||||
calculator.current = roundToTenth(parsed)
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
editingValue = nil
|
||||
currentInput = ""
|
||||
}
|
||||
Button("Save") {
|
||||
if let parsed = parseInput(currentInput) {
|
||||
calculator.current = roundToTenth(parsed)
|
||||
}
|
||||
editingValue = nil
|
||||
calculator.updateFromCurrent()
|
||||
currentInput = ""
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
@@ -174,14 +251,35 @@ struct CalculatorView: View {
|
||||
}
|
||||
.alert("Edit Power", isPresented: Binding(
|
||||
get: { editingValue == .power },
|
||||
set: { if !$0 { editingValue = nil } }
|
||||
set: { isPresented in
|
||||
if !isPresented {
|
||||
editingValue = nil
|
||||
powerInput = ""
|
||||
}
|
||||
}
|
||||
)) {
|
||||
TextField("Power", value: $calculator.power, format: .number)
|
||||
TextField("Power", text: $powerInput)
|
||||
.keyboardType(.decimalPad)
|
||||
Button("Cancel", role: .cancel) { editingValue = nil }
|
||||
.onAppear {
|
||||
if powerInput.isEmpty {
|
||||
powerInput = formattedValue(calculator.power)
|
||||
}
|
||||
}
|
||||
.onChange(of: powerInput) { newValue in
|
||||
guard editingValue == .power, let parsed = parseInput(newValue) else { return }
|
||||
calculator.power = roundToNearestFive(parsed)
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
editingValue = nil
|
||||
powerInput = ""
|
||||
}
|
||||
Button("Save") {
|
||||
if let parsed = parseInput(powerInput) {
|
||||
calculator.power = roundToNearestFive(parsed)
|
||||
}
|
||||
editingValue = nil
|
||||
calculator.updateFromPower()
|
||||
powerInput = ""
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
@@ -331,7 +429,7 @@ struct CalculatorView: View {
|
||||
VStack() {
|
||||
HStack {
|
||||
Button(action: {
|
||||
editingValue = .voltage
|
||||
beginVoltageEditing()
|
||||
}) {
|
||||
Text(String(format: "%.1fV", calculator.voltage))
|
||||
.font(.caption)
|
||||
@@ -405,7 +503,11 @@ struct CalculatorView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
Button(action: {
|
||||
editingValue = isWattMode ? .power : .current
|
||||
if isWattMode {
|
||||
beginPowerEditing()
|
||||
} else {
|
||||
beginCurrentEditing()
|
||||
}
|
||||
}) {
|
||||
Text(String(format: "%.1fA", calculator.current))
|
||||
.fontWeight(.semibold)
|
||||
@@ -414,7 +516,11 @@ struct CalculatorView: View {
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button(action: {
|
||||
editingValue = isWattMode ? .current : .power
|
||||
if isWattMode {
|
||||
beginCurrentEditing()
|
||||
} else {
|
||||
beginPowerEditing()
|
||||
}
|
||||
}) {
|
||||
Text(String(format: "%.0fW", calculator.calculatedPower))
|
||||
.font(.caption)
|
||||
@@ -459,7 +565,7 @@ struct CalculatorView: View {
|
||||
|
||||
Text("•").foregroundColor(.secondary)
|
||||
|
||||
Button(action: { editingValue = .length }) {
|
||||
Button(action: { beginLengthEditing() }) {
|
||||
Text(String(format: "%.1f %@", calculator.length, unitSettings.unitSystem.lengthUnit))
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.primary)
|
||||
@@ -660,14 +766,64 @@ struct CalculatorView: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private var voltageSliderRange: ClosedRange<Double> {
|
||||
let upperBound = max(48, calculator.voltage)
|
||||
return 0...upperBound
|
||||
}
|
||||
|
||||
private var powerSliderRange: ClosedRange<Double> {
|
||||
let upperBound = max(2000, calculator.power)
|
||||
return 0...upperBound
|
||||
}
|
||||
|
||||
private var currentSliderRange: ClosedRange<Double> {
|
||||
let upperBound = max(100, calculator.current)
|
||||
return 0...upperBound
|
||||
}
|
||||
|
||||
private var lengthSliderRange: ClosedRange<Double> {
|
||||
let baseMax = unitSettings.unitSystem == .metric ? 20.0 : 60.0
|
||||
let upperBound = max(baseMax, calculator.length)
|
||||
return 0...upperBound
|
||||
}
|
||||
|
||||
private var voltageSnapValues: [Double] {
|
||||
[3.3, 5.0, 6.0, 9.0, 12.0, 13.8, 24.0, 28.0, 48.0]
|
||||
}
|
||||
|
||||
private var powerSnapValues: [Double] {
|
||||
[5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 100, 125, 150, 200, 250, 300, 400, 500, 600, 750, 1000, 1250, 1500, 2000]
|
||||
}
|
||||
|
||||
private var currentSnapValues: [Double] {
|
||||
[0.5, 1, 2, 3, 4, 5, 7.5, 10, 12, 15, 20, 25, 30, 40, 50, 60, 75, 80, 100]
|
||||
}
|
||||
|
||||
private var lengthSnapValues: [Double] {
|
||||
if unitSettings.unitSystem == .metric {
|
||||
return [0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 7.5, 10, 12, 15, 20]
|
||||
} else {
|
||||
return [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 25, 30, 40, 50, 60]
|
||||
}
|
||||
}
|
||||
|
||||
private var voltageSlider: some View {
|
||||
SliderSection(title: String(localized: "slider.voltage.title", comment: "Title for the voltage slider"),
|
||||
value: $calculator.voltage,
|
||||
range: 3...48,
|
||||
unit: "V",
|
||||
tapAction: { editingValue = .voltage },
|
||||
snapValues: [3.3, 5.0, 6.0, 9.0, 12.0, 13.8, 24.0, 28.0, 48.0])
|
||||
SliderSection(title: String(localized: "slider.voltage.title", comment: "Title for the voltage slider"),
|
||||
value: Binding(
|
||||
get: { calculator.voltage },
|
||||
set: { newValue in
|
||||
if editingValue == .voltage {
|
||||
calculator.voltage = roundToTenth(newValue)
|
||||
} else {
|
||||
calculator.voltage = normalizedVoltage(for: newValue)
|
||||
}
|
||||
}
|
||||
),
|
||||
range: voltageSliderRange,
|
||||
unit: "V",
|
||||
tapAction: beginVoltageEditing,
|
||||
snapValues: editingValue == .voltage ? nil : voltageSnapValues)
|
||||
.onChange(of: calculator.voltage) {
|
||||
if isWattMode {
|
||||
calculator.updateFromPower()
|
||||
@@ -683,8 +839,17 @@ struct CalculatorView: View {
|
||||
private var currentPowerSlider: some View {
|
||||
if isWattMode {
|
||||
SliderSection(title: String(localized: "slider.power.title", comment: "Title for the power slider"),
|
||||
value: $calculator.power,
|
||||
range: 0...2000,
|
||||
value: Binding(
|
||||
get: { calculator.power },
|
||||
set: { newValue in
|
||||
if editingValue == .power {
|
||||
calculator.power = roundToNearestFive(newValue)
|
||||
} else {
|
||||
calculator.power = normalizedPower(for: newValue)
|
||||
}
|
||||
}
|
||||
),
|
||||
range: powerSliderRange,
|
||||
unit: "W",
|
||||
buttonText: String(localized: "slider.button.watt", comment: "Button label when showing power control"),
|
||||
buttonAction: {
|
||||
@@ -692,8 +857,8 @@ struct CalculatorView: View {
|
||||
calculator.updateFromPower()
|
||||
autoUpdateSavedLoad()
|
||||
},
|
||||
tapAction: { editingValue = .power },
|
||||
snapValues: [5, 10, 15, 20, 25, 30, 40, 50, 60, 75, 100, 125, 150, 200, 250, 300, 400, 500, 600, 750, 1000, 1250, 1500, 2000])
|
||||
tapAction: beginPowerEditing,
|
||||
snapValues: editingValue == .power ? nil : powerSnapValues)
|
||||
.onChange(of: calculator.power) {
|
||||
calculator.updateFromPower()
|
||||
calculator.objectWillChange.send()
|
||||
@@ -701,8 +866,17 @@ struct CalculatorView: View {
|
||||
}
|
||||
} else {
|
||||
SliderSection(title: String(localized: "slider.current.title", comment: "Title for the current slider"),
|
||||
value: $calculator.current,
|
||||
range: 0...100,
|
||||
value: Binding(
|
||||
get: { calculator.current },
|
||||
set: { newValue in
|
||||
if editingValue == .current {
|
||||
calculator.current = roundToTenth(newValue)
|
||||
} else {
|
||||
calculator.current = normalizedCurrent(for: newValue)
|
||||
}
|
||||
}
|
||||
),
|
||||
range: currentSliderRange,
|
||||
unit: "A",
|
||||
buttonText: String(localized: "slider.button.ampere", comment: "Button label when showing current control"),
|
||||
buttonAction: {
|
||||
@@ -710,8 +884,8 @@ struct CalculatorView: View {
|
||||
calculator.updateFromCurrent()
|
||||
autoUpdateSavedLoad()
|
||||
},
|
||||
tapAction: { editingValue = .current },
|
||||
snapValues: [0.5, 1, 2, 3, 4, 5, 7.5, 10, 12, 15, 20, 25, 30, 40, 50, 60, 75, 80, 100])
|
||||
tapAction: beginCurrentEditing,
|
||||
snapValues: editingValue == .current ? nil : currentSnapValues)
|
||||
.onChange(of: calculator.current) {
|
||||
calculator.updateFromCurrent()
|
||||
calculator.objectWillChange.send()
|
||||
@@ -726,17 +900,104 @@ struct CalculatorView: View {
|
||||
comment: "Title format for the cable length slider"
|
||||
)
|
||||
return SliderSection(title: String(format: lengthTitleFormat, locale: Locale.current, unitSettings.unitSystem.lengthUnit),
|
||||
value: $calculator.length,
|
||||
range: 0...20,
|
||||
value: Binding(
|
||||
get: { calculator.length },
|
||||
set: { newValue in
|
||||
if editingValue == .length {
|
||||
calculator.length = roundToTenth(newValue)
|
||||
} else {
|
||||
calculator.length = normalizedLength(for: newValue)
|
||||
}
|
||||
}
|
||||
),
|
||||
range: lengthSliderRange,
|
||||
unit: unitSettings.unitSystem.lengthUnit,
|
||||
tapAction: { editingValue = .length },
|
||||
snapValues: unitSettings.unitSystem == .metric ?
|
||||
[0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 7.5, 10, 12, 15, 20] :
|
||||
[1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 25, 30, 40, 50, 60])
|
||||
.onChange(of: calculator.length) {
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
tapAction: beginLengthEditing,
|
||||
snapValues: editingValue == .length ? nil : lengthSnapValues)
|
||||
.onChange(of: calculator.length) {
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
}
|
||||
|
||||
private func normalizedVoltage(for value: Double) -> Double {
|
||||
let rounded = roundToTenth(value)
|
||||
if let snap = nearestValue(to: rounded, in: voltageSnapValues, tolerance: 0.3) {
|
||||
return snap
|
||||
}
|
||||
return rounded
|
||||
}
|
||||
|
||||
private func normalizedCurrent(for value: Double) -> Double {
|
||||
let rounded = roundToTenth(value)
|
||||
if let snap = nearestValue(to: rounded, in: currentSnapValues, tolerance: 0.3) {
|
||||
return snap
|
||||
}
|
||||
return rounded
|
||||
}
|
||||
|
||||
private func normalizedPower(for value: Double) -> Double {
|
||||
if let snap = nearestValue(to: value, in: powerSnapValues, tolerance: 2.5) {
|
||||
return snap
|
||||
}
|
||||
return roundToNearestFive(value)
|
||||
}
|
||||
|
||||
private func normalizedLength(for value: Double) -> Double {
|
||||
let rounded = roundToTenth(value)
|
||||
if let snap = nearestValue(to: rounded, in: lengthSnapValues, tolerance: 0.5) {
|
||||
return snap
|
||||
}
|
||||
return rounded
|
||||
}
|
||||
|
||||
private func nearestValue(to value: Double, in options: [Double], tolerance: Double) -> Double? {
|
||||
guard let closest = options.min(by: { abs($0 - value) < abs($1 - value) }) else { return nil }
|
||||
return abs(closest - value) <= tolerance ? closest : nil
|
||||
}
|
||||
|
||||
private func roundToTenth(_ value: Double) -> Double {
|
||||
max(0, (value * 10).rounded() / 10)
|
||||
}
|
||||
|
||||
private func roundToNearestFive(_ value: Double) -> Double {
|
||||
max(0, (value / 5).rounded() * 5)
|
||||
}
|
||||
|
||||
private func formattedValue(_ value: Double) -> String {
|
||||
Self.editFormatter.string(from: NSNumber(value: roundToTenth(value))) ?? String(format: "%.1f", value)
|
||||
}
|
||||
|
||||
private func parseInput(_ text: String) -> Double? {
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
if let number = Self.editFormatter.number(from: trimmed)?.doubleValue {
|
||||
return number
|
||||
}
|
||||
let decimalSeparator = Locale.current.decimalSeparator ?? "."
|
||||
let altSeparator = decimalSeparator == "." ? "," : "."
|
||||
let normalized = trimmed.replacingOccurrences(of: altSeparator, with: decimalSeparator)
|
||||
return Self.editFormatter.number(from: normalized)?.doubleValue
|
||||
}
|
||||
|
||||
private func beginVoltageEditing() {
|
||||
voltageInput = formattedValue(calculator.voltage)
|
||||
editingValue = .voltage
|
||||
}
|
||||
|
||||
private func beginCurrentEditing() {
|
||||
currentInput = formattedValue(calculator.current)
|
||||
editingValue = .current
|
||||
}
|
||||
|
||||
private func beginPowerEditing() {
|
||||
powerInput = formattedValue(calculator.power)
|
||||
editingValue = .power
|
||||
}
|
||||
|
||||
private func beginLengthEditing() {
|
||||
lengthInput = formattedValue(calculator.length)
|
||||
editingValue = .length
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user