systems first
This commit is contained in:
@@ -294,6 +294,7 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Cable/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -304,7 +305,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.Cable;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.CableApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -324,6 +325,7 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Cable/Info.plist;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -334,7 +336,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.Cable;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.voltplan.CableApp;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "ios-marketing.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
|
||||
BIN
Cable/Assets.xcassets/AppIcon.appiconset/ios-marketing.png
Normal file
BIN
Cable/Assets.xcassets/AppIcon.appiconset/ios-marketing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
20
Cable/Assets.xcassets/PoweredByVoltplan.imageset/Contents.json
vendored
Normal file
20
Cable/Assets.xcassets/PoweredByVoltplan.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,7 @@
|
||||
<key>com.apple.developer.icloud-container-identifiers</key>
|
||||
<array/>
|
||||
<key>com.apple.developer.icloud-services</key>
|
||||
<array>
|
||||
<string>CloudKit</string>
|
||||
</array>
|
||||
<array/>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
|
||||
@@ -10,22 +10,30 @@ import SwiftData
|
||||
|
||||
@main
|
||||
struct CableApp: App {
|
||||
@StateObject private var unitSettings = UnitSystemSettings()
|
||||
|
||||
var sharedModelContainer: ModelContainer = {
|
||||
let schema = Schema([
|
||||
Item.self,
|
||||
])
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
||||
|
||||
do {
|
||||
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||
// Try the simple approach first
|
||||
return try ModelContainer(for: ElectricalSystem.self, SavedLoad.self, Item.self)
|
||||
} catch {
|
||||
fatalError("Could not create ModelContainer: \(error)")
|
||||
print("Failed to create ModelContainer with simple approach: \(error)")
|
||||
|
||||
// Try in-memory as fallback
|
||||
do {
|
||||
let schema = Schema([ElectricalSystem.self, SavedLoad.self, Item.self])
|
||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true)
|
||||
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
||||
} catch {
|
||||
fatalError("Could not create even in-memory ModelContainer: \(error)")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(unitSettings)
|
||||
}
|
||||
.modelContainer(sharedModelContainer)
|
||||
}
|
||||
|
||||
150
Cable/CableCalculator.swift
Normal file
150
Cable/CableCalculator.swift
Normal file
@@ -0,0 +1,150 @@
|
||||
//
|
||||
// CableCalculator.swift
|
||||
// Cable
|
||||
//
|
||||
// Created by Stefan Lange-Hegermann on 11.09.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
class CableCalculator: ObservableObject {
|
||||
@Published var voltage: Double = 12.0
|
||||
@Published var current: Double = 5.0
|
||||
@Published var power: Double = 60.0
|
||||
@Published var length: Double = 10.0
|
||||
@Published var loadName: String = "My Load"
|
||||
|
||||
var calculatedPower: Double {
|
||||
voltage * current
|
||||
}
|
||||
|
||||
var calculatedCurrent: Double {
|
||||
voltage > 0 ? power / voltage : 0
|
||||
}
|
||||
|
||||
func updateFromCurrent() {
|
||||
power = voltage * current
|
||||
}
|
||||
|
||||
func updateFromPower() {
|
||||
current = voltage > 0 ? power / voltage : 0
|
||||
}
|
||||
|
||||
func recommendedCrossSection(for unitSystem: UnitSystem) -> Double {
|
||||
let lengthInMeters = unitSystem == .metric ? length : length * 0.3048 // ft to m
|
||||
// Simplified calculation: minimum cross-section based on current and voltage drop
|
||||
let maxVoltageDrop = voltage * 0.05 // 5% voltage drop limit
|
||||
let resistivity = 0.017 // Copper resistivity at 20°C (Ω⋅mm²/m)
|
||||
let calculatedMinCrossSection = (2 * current * lengthInMeters * resistivity) / maxVoltageDrop
|
||||
|
||||
if unitSystem == .imperial {
|
||||
// Standard AWG wire sizes
|
||||
let standardAWG = [20, 18, 16, 14, 12, 10, 8, 6, 4, 2, 1, 0, 00, 000, 0000]
|
||||
let awgCrossSections = [0.519, 0.823, 1.31, 2.08, 3.31, 5.26, 8.37, 13.3, 21.2, 33.6, 42.4, 53.5, 67.4, 85.0, 107.0]
|
||||
|
||||
// Find the smallest AWG that meets the requirement
|
||||
for (index, crossSection) in awgCrossSections.enumerated() {
|
||||
if crossSection >= calculatedMinCrossSection {
|
||||
return Double(standardAWG[index])
|
||||
}
|
||||
}
|
||||
return Double(standardAWG.last!) // Largest available
|
||||
} else {
|
||||
// Standard metric cable cross-sections in mm²
|
||||
let standardSizes = [0.75, 1.0, 1.5, 2.5, 4.0, 6.0, 10.0, 16.0, 25.0, 35.0, 50.0, 70.0, 95.0, 120.0, 150.0, 185.0, 240.0, 300.0, 400.0, 500.0, 630.0]
|
||||
|
||||
// Find the smallest standard size that meets the requirement
|
||||
return standardSizes.first { $0 >= max(0.75, calculatedMinCrossSection) } ?? standardSizes.last!
|
||||
}
|
||||
}
|
||||
|
||||
func crossSection(for unitSystem: UnitSystem) -> Double {
|
||||
recommendedCrossSection(for: unitSystem)
|
||||
}
|
||||
|
||||
func voltageDrop(for unitSystem: UnitSystem) -> Double {
|
||||
let lengthInMeters = unitSystem == .metric ? length : length * 0.3048
|
||||
let crossSectionMM2 = unitSystem == .metric ? crossSection(for: unitSystem) : crossSectionFromAWG(crossSection(for: unitSystem))
|
||||
let resistivity = 0.017
|
||||
let effectiveCurrent = current // Always use the current property which gets updated
|
||||
return (2 * effectiveCurrent * lengthInMeters * resistivity) / crossSectionMM2
|
||||
}
|
||||
|
||||
func voltageDropPercentage(for unitSystem: UnitSystem) -> Double {
|
||||
(voltageDrop(for: unitSystem) / voltage) * 100
|
||||
}
|
||||
|
||||
func powerLoss(for unitSystem: UnitSystem) -> Double {
|
||||
let effectiveCurrent = current
|
||||
return effectiveCurrent * voltageDrop(for: unitSystem)
|
||||
}
|
||||
|
||||
var recommendedFuse: Int {
|
||||
let targetFuse = current * 1.25 // 125% of load current for safety
|
||||
|
||||
// Common fuse values in amperes
|
||||
let standardFuses = [1, 2, 3, 5, 7, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500, 600, 700, 800]
|
||||
|
||||
// Find the smallest standard fuse that's >= target
|
||||
return standardFuses.first { $0 >= Int(targetFuse.rounded(.up)) } ?? standardFuses.last!
|
||||
}
|
||||
|
||||
// AWG conversion helper for voltage drop calculations
|
||||
private func crossSectionFromAWG(_ awg: Double) -> Double {
|
||||
let awgSizes = [20: 0.519, 18: 0.823, 16: 1.31, 14: 2.08, 12: 3.31, 10: 5.26, 8: 8.37, 6: 13.3, 4: 21.2, 2: 33.6, 1: 42.4, 0: 53.5]
|
||||
|
||||
// Handle 00, 000, 0000 AWG (represented as negative values)
|
||||
if awg == 00 { return 67.4 }
|
||||
if awg == 000 { return 85.0 }
|
||||
if awg == 0000 { return 107.0 }
|
||||
|
||||
return awgSizes[Int(awg)] ?? 0.75
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
class ElectricalSystem {
|
||||
var name: String = ""
|
||||
var location: String = ""
|
||||
var timestamp: Date = Date()
|
||||
var iconName: String = "building.2"
|
||||
var colorName: String = "blue"
|
||||
|
||||
init(name: String, location: String = "", iconName: String = "building.2", colorName: String = "blue") {
|
||||
self.name = name
|
||||
self.location = location
|
||||
self.timestamp = Date()
|
||||
self.iconName = iconName
|
||||
self.colorName = colorName
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
class SavedLoad {
|
||||
var name: String = ""
|
||||
var voltage: Double = 0.0
|
||||
var current: Double = 0.0
|
||||
var power: Double = 0.0
|
||||
var length: Double = 0.0
|
||||
var crossSection: Double = 0.0
|
||||
var timestamp: Date = Date()
|
||||
var iconName: String = "lightbulb"
|
||||
var colorName: String = "blue"
|
||||
var isWattMode: Bool = false
|
||||
var system: ElectricalSystem?
|
||||
|
||||
init(name: String, voltage: Double, current: Double, power: Double, length: Double, crossSection: Double, iconName: String = "lightbulb", colorName: String = "blue", isWattMode: Bool = false, system: ElectricalSystem? = nil) {
|
||||
self.name = name
|
||||
self.voltage = voltage
|
||||
self.current = current
|
||||
self.power = power
|
||||
self.length = length
|
||||
self.crossSection = crossSection
|
||||
self.timestamp = Date()
|
||||
self.iconName = iconName
|
||||
self.colorName = colorName
|
||||
self.isWattMode = isWattMode
|
||||
self.system = system
|
||||
}
|
||||
}
|
||||
693
Cable/CalculatorView.swift
Normal file
693
Cable/CalculatorView.swift
Normal file
@@ -0,0 +1,693 @@
|
||||
//
|
||||
// CalculatorView.swift
|
||||
// Cable
|
||||
//
|
||||
// Created by Stefan Lange-Hegermann on 11.09.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct CalculatorView: View {
|
||||
@EnvironmentObject var unitSettings: UnitSystemSettings
|
||||
@StateObject private var calculator = CableCalculator()
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Query private var savedLoads: [SavedLoad]
|
||||
@State private var showingLibrary = false
|
||||
@State private var isWattMode = false
|
||||
@State private var editingValue: EditingValue? = nil
|
||||
@State private var showingLoadEditor = false
|
||||
|
||||
let savedLoad: SavedLoad?
|
||||
|
||||
init(savedLoad: SavedLoad? = nil) {
|
||||
self.savedLoad = savedLoad
|
||||
}
|
||||
|
||||
enum EditingValue {
|
||||
case voltage, current, power, length
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
badgesSection
|
||||
circuitDiagram
|
||||
resultsSection
|
||||
mainContent
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle("")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
navigationTitle
|
||||
}
|
||||
|
||||
if savedLoad == nil {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
saveCurrentLoad()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingLibrary) {
|
||||
LoadLibraryView(calculator: calculator)
|
||||
}
|
||||
.sheet(isPresented: $showingLoadEditor) {
|
||||
LoadEditorView(
|
||||
loadName: Binding(
|
||||
get: { calculator.loadName },
|
||||
set: {
|
||||
calculator.loadName = $0
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
),
|
||||
iconName: Binding(
|
||||
get: { savedLoad?.iconName ?? "lightbulb" },
|
||||
set: {
|
||||
savedLoad?.iconName = $0
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
),
|
||||
colorName: Binding(
|
||||
get: { savedLoad?.colorName ?? "blue" },
|
||||
set: {
|
||||
savedLoad?.colorName = $0
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
.alert("Edit Length", isPresented: Binding(
|
||||
get: { editingValue == .length },
|
||||
set: { if !$0 { editingValue = nil } }
|
||||
)) {
|
||||
TextField("Length", value: $calculator.length, format: .number)
|
||||
.keyboardType(.decimalPad)
|
||||
Button("Cancel", role: .cancel) { editingValue = nil }
|
||||
Button("Save") {
|
||||
editingValue = nil
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
} message: {
|
||||
Text("Enter length in \(unitSettings.unitSystem.lengthUnit)")
|
||||
}
|
||||
.alert("Edit Voltage", isPresented: Binding(
|
||||
get: { editingValue == .voltage },
|
||||
set: { if !$0 { editingValue = nil } }
|
||||
)) {
|
||||
TextField("Voltage", value: $calculator.voltage, format: .number)
|
||||
.keyboardType(.decimalPad)
|
||||
Button("Cancel", role: .cancel) { editingValue = nil }
|
||||
Button("Save") {
|
||||
editingValue = nil
|
||||
if isWattMode {
|
||||
calculator.updateFromPower()
|
||||
} else {
|
||||
calculator.updateFromCurrent()
|
||||
}
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
} message: {
|
||||
Text("Enter voltage in volts (V)")
|
||||
}
|
||||
.alert("Edit Current", isPresented: Binding(
|
||||
get: { editingValue == .current },
|
||||
set: { if !$0 { editingValue = nil } }
|
||||
)) {
|
||||
TextField("Current", value: $calculator.current, format: .number)
|
||||
.keyboardType(.decimalPad)
|
||||
Button("Cancel", role: .cancel) { editingValue = nil }
|
||||
Button("Save") {
|
||||
editingValue = nil
|
||||
calculator.updateFromCurrent()
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
} message: {
|
||||
Text("Enter current in amperes (A)")
|
||||
}
|
||||
.alert("Edit Power", isPresented: Binding(
|
||||
get: { editingValue == .power },
|
||||
set: { if !$0 { editingValue = nil } }
|
||||
)) {
|
||||
TextField("Power", value: $calculator.power, format: .number)
|
||||
.keyboardType(.decimalPad)
|
||||
Button("Cancel", role: .cancel) { editingValue = nil }
|
||||
Button("Save") {
|
||||
editingValue = nil
|
||||
calculator.updateFromPower()
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
} message: {
|
||||
Text("Enter power in watts (W)")
|
||||
}
|
||||
.onAppear {
|
||||
if let savedLoad = savedLoad {
|
||||
loadConfiguration(from: savedLoad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadIcon: String {
|
||||
savedLoad?.iconName ?? "lightbulb"
|
||||
}
|
||||
|
||||
private var loadColor: Color {
|
||||
let colorName = savedLoad?.colorName ?? "blue"
|
||||
switch colorName {
|
||||
case "blue": return .blue
|
||||
case "green": return .green
|
||||
case "orange": return .orange
|
||||
case "red": return .red
|
||||
case "purple": return .purple
|
||||
case "yellow": return .yellow
|
||||
case "pink": return .pink
|
||||
case "teal": return .teal
|
||||
case "indigo": return .indigo
|
||||
case "mint": return .mint
|
||||
case "cyan": return .cyan
|
||||
case "brown": return .brown
|
||||
case "gray": return .gray
|
||||
default: return .blue
|
||||
}
|
||||
}
|
||||
|
||||
private var navigationTitle: some View {
|
||||
Button(action: {
|
||||
showingLoadEditor = true
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(loadColor)
|
||||
.frame(width: 24, height: 24)
|
||||
|
||||
Image(systemName: loadIcon)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
Text(calculator.loadName)
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private var badgesSection: some View {
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 4) {
|
||||
Text("FUSE")
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(calculator.recommendedFuse)A")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.orange.opacity(0.1))
|
||||
.cornerRadius(6)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text("WIRE")
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
Text(String(format: unitSettings.unitSystem == .imperial ?
|
||||
"%.1fft @ %.0f AWG" :
|
||||
"%.1fm @ %.1fmm²",
|
||||
unitSettings.unitSystem == .imperial ? calculator.length * 3.28084 : calculator.length,
|
||||
calculator.crossSection(for: unitSettings.unitSystem)))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(6)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
private var circuitDiagram: some View {
|
||||
HStack(spacing: 0) {
|
||||
voltageSection
|
||||
wiringSection
|
||||
loadBox
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
private var voltageSection: some View {
|
||||
VStack(spacing: 0) {
|
||||
VStack() {
|
||||
HStack {
|
||||
Button(action: {
|
||||
editingValue = .voltage
|
||||
}) {
|
||||
Text(String(format: "%.1fV", calculator.voltage))
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.frame(width: 60)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var wiringSection: some View {
|
||||
VStack {
|
||||
HStack(alignment: .center, spacing: 2) {
|
||||
Circle()
|
||||
.fill(.red)
|
||||
.frame(width: 8, height: 8)
|
||||
Rectangle()
|
||||
.fill(.red)
|
||||
.frame(height: 4)
|
||||
.frame(width: 10)
|
||||
Rectangle()
|
||||
.fill(.green)
|
||||
.frame(height: 30)
|
||||
.frame(width:80)
|
||||
.overlay(
|
||||
Text("\(calculator.recommendedFuse)A")
|
||||
.foregroundColor(.white)
|
||||
.fontWeight(.bold)
|
||||
)
|
||||
.cornerRadius(6)
|
||||
|
||||
Rectangle()
|
||||
.fill(.red)
|
||||
.frame(height: 4)
|
||||
.frame(maxWidth: .infinity)
|
||||
}.offset(x: 0, y: -11)
|
||||
|
||||
HStack(alignment: .center, spacing: 2) {
|
||||
Circle()
|
||||
.fill(Color.primary)
|
||||
.frame(width: 8, height: 8)
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.primary)
|
||||
.frame(height: 4)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadBox: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color.gray.opacity(0.2))
|
||||
.frame(width: 120, height: 90)
|
||||
.overlay(loadContent)
|
||||
.overlay(connectionPoints)
|
||||
}
|
||||
}
|
||||
|
||||
private var loadContent: some View {
|
||||
VStack {
|
||||
Text(calculator.loadName.uppercased())
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Button(action: {
|
||||
editingValue = isWattMode ? .power : .current
|
||||
}) {
|
||||
Text(String(format: "%.1fA", calculator.current))
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button(action: {
|
||||
editingValue = isWattMode ? .current : .power
|
||||
}) {
|
||||
Text(String(format: "%.0fW", calculator.calculatedPower))
|
||||
.font(.caption)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
private var connectionPoints: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(.red)
|
||||
.frame(width: 8, height: 8)
|
||||
.offset(x: -4, y: 17)
|
||||
Spacer()
|
||||
}
|
||||
Spacer()
|
||||
HStack {
|
||||
Circle()
|
||||
.fill(Color.primary)
|
||||
.frame(width: 8, height: 8)
|
||||
.offset(x: -4, y: -17)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.frame(width: 120, height: 80)
|
||||
}
|
||||
|
||||
private var resultsSection: some View {
|
||||
HStack {
|
||||
Group {
|
||||
Button(action: {}) {
|
||||
Text(unitSettings.unitSystem == .imperial ?
|
||||
String(format: "%.0f AWG", calculator.crossSection(for: unitSettings.unitSystem)) :
|
||||
String(format: "%.1f mm²", calculator.crossSection(for: unitSettings.unitSystem)))
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Text("•").foregroundColor(.secondary)
|
||||
|
||||
Button(action: { editingValue = .length }) {
|
||||
Text(String(format: "%.1f %@", calculator.length, unitSettings.unitSystem.lengthUnit))
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Text("•").foregroundColor(.secondary)
|
||||
|
||||
Button(action: {}) {
|
||||
Text(String(format: "%.1fV (%.1f%%)", calculator.voltageDrop(for: unitSettings.unitSystem), calculator.voltageDropPercentage(for: unitSettings.unitSystem)))
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(calculator.voltageDropPercentage(for: unitSettings.unitSystem) > 5 ? .orange : .primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Text("•").foregroundColor(.secondary)
|
||||
|
||||
Button(action: {}) {
|
||||
Text(String(format: "%.1fW", calculator.powerLoss(for: unitSettings.unitSystem)))
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private var mainContent: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
Divider().padding(.horizontal)
|
||||
slidersSection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var slidersSection: some View {
|
||||
VStack(spacing: 30) {
|
||||
voltageSlider
|
||||
currentPowerSlider
|
||||
lengthSlider
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
private var voltageSlider: some View {
|
||||
SliderSection(title: "Voltage",
|
||||
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])
|
||||
.onChange(of: calculator.voltage) {
|
||||
if isWattMode {
|
||||
calculator.updateFromPower()
|
||||
} else {
|
||||
calculator.updateFromCurrent()
|
||||
}
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var currentPowerSlider: some View {
|
||||
if isWattMode {
|
||||
SliderSection(title: "Power",
|
||||
value: $calculator.power,
|
||||
range: 0...2000,
|
||||
unit: "W",
|
||||
buttonText: "Watt",
|
||||
buttonAction: {
|
||||
isWattMode = false
|
||||
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])
|
||||
.onChange(of: calculator.power) {
|
||||
calculator.updateFromPower()
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
} else {
|
||||
SliderSection(title: "Current",
|
||||
value: $calculator.current,
|
||||
range: 0...100,
|
||||
unit: "A",
|
||||
buttonText: "Ampere",
|
||||
buttonAction: {
|
||||
isWattMode = true
|
||||
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])
|
||||
.onChange(of: calculator.current) {
|
||||
calculator.updateFromCurrent()
|
||||
calculator.objectWillChange.send()
|
||||
autoUpdateSavedLoad()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var lengthSlider: some View {
|
||||
SliderSection(title: "Cable Length (\(unitSettings.unitSystem.lengthUnit))",
|
||||
value: $calculator.length,
|
||||
range: 0...20,
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func saveCurrentLoad() {
|
||||
let savedLoad = SavedLoad(
|
||||
name: calculator.loadName.isEmpty ? "Unnamed Load" : calculator.loadName,
|
||||
voltage: calculator.voltage,
|
||||
current: calculator.current,
|
||||
power: calculator.power,
|
||||
length: calculator.length,
|
||||
crossSection: calculator.crossSection(for: .metric), // Always save in metric
|
||||
iconName: "lightbulb",
|
||||
colorName: "blue",
|
||||
isWattMode: isWattMode,
|
||||
system: nil // For now, new loads aren't associated with a system
|
||||
)
|
||||
modelContext.insert(savedLoad)
|
||||
}
|
||||
|
||||
private func loadConfiguration(from savedLoad: SavedLoad) {
|
||||
calculator.loadName = savedLoad.name
|
||||
calculator.voltage = savedLoad.voltage
|
||||
calculator.current = savedLoad.current
|
||||
calculator.power = savedLoad.power
|
||||
calculator.length = savedLoad.length
|
||||
isWattMode = savedLoad.isWattMode
|
||||
}
|
||||
|
||||
private func autoUpdateSavedLoad() {
|
||||
guard let savedLoad = savedLoad else { return }
|
||||
|
||||
savedLoad.name = calculator.loadName.isEmpty ? "Unnamed Load" : calculator.loadName
|
||||
savedLoad.voltage = calculator.voltage
|
||||
savedLoad.current = calculator.current
|
||||
savedLoad.power = calculator.power
|
||||
savedLoad.length = calculator.length
|
||||
savedLoad.crossSection = calculator.crossSection(for: .metric)
|
||||
savedLoad.timestamp = Date()
|
||||
savedLoad.isWattMode = isWattMode
|
||||
// Icon and color are updated directly through bindings in the editor
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Supporting Views
|
||||
|
||||
struct SliderSection: View {
|
||||
let title: String
|
||||
@Binding var value: Double
|
||||
let range: ClosedRange<Double>
|
||||
let unit: String
|
||||
var buttonText: String?
|
||||
var buttonAction: (() -> Void)?
|
||||
var tapAction: (() -> Void)?
|
||||
var snapValues: [Double]?
|
||||
|
||||
init(title: String, value: Binding<Double>, range: ClosedRange<Double>, unit: String, buttonText: String? = nil, buttonAction: (() -> Void)? = nil, tapAction: (() -> Void)? = nil, snapValues: [Double]? = nil) {
|
||||
self.title = title
|
||||
self._value = value
|
||||
self.range = range
|
||||
self.unit = unit
|
||||
self.buttonText = buttonText
|
||||
self.buttonAction = buttonAction
|
||||
self.tapAction = tapAction
|
||||
self.snapValues = snapValues
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
if let buttonText = buttonText {
|
||||
Button(buttonText) {
|
||||
buttonAction?()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
tapAction?()
|
||||
}) {
|
||||
Text(String(format: "%.1f %@", value, unit))
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(tapAction == nil)
|
||||
|
||||
HStack {
|
||||
Text(String(format: "%.0f%@", range.lowerBound, unit))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Slider(value: $value, in: range)
|
||||
.onChange(of: value) {
|
||||
// Always round to 1 decimal place first
|
||||
value = round(value * 10) / 10
|
||||
|
||||
if let snapValues = snapValues {
|
||||
// Find the closest snap value
|
||||
let closest = snapValues.min { abs($0 - value) < abs($1 - value) }
|
||||
if let closest = closest, abs(closest - value) < 0.5 {
|
||||
value = closest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(String(format: "%.0f%@", range.upperBound, unit))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadLibraryView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query private var savedLoads: [SavedLoad]
|
||||
let calculator: CableCalculator
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
ForEach(savedLoads) { load in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(load.name)
|
||||
.fontWeight(.medium)
|
||||
HStack {
|
||||
Text(String(format: "%.1fV", load.voltage))
|
||||
Text("•")
|
||||
Text(String(format: "%.1fA", load.current))
|
||||
Text("•")
|
||||
Text(String(format: "%.1fm", load.length))
|
||||
Spacer()
|
||||
Text(load.timestamp, format: .dateTime.month().day())
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
loadConfiguration(from: load)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteLoads)
|
||||
}
|
||||
.navigationTitle("Load Library")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadConfiguration(from savedLoad: SavedLoad) {
|
||||
calculator.loadName = savedLoad.name
|
||||
calculator.voltage = savedLoad.voltage
|
||||
calculator.current = savedLoad.current
|
||||
calculator.length = savedLoad.length
|
||||
}
|
||||
|
||||
private func deleteLoads(offsets: IndexSet) {
|
||||
for index in offsets {
|
||||
modelContext.delete(savedLoads[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CalculatorView()
|
||||
.modelContainer(for: SavedLoad.self, inMemory: true)
|
||||
.environmentObject(UnitSystemSettings())
|
||||
}
|
||||
@@ -1,61 +1,674 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Cable
|
||||
//
|
||||
// Created by Stefan Lange-Hegermann on 11.09.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct ContentView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@Query private var items: [Item]
|
||||
|
||||
@EnvironmentObject var unitSettings: UnitSystemSettings
|
||||
@State private var selection = 1
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List {
|
||||
ForEach(items) { item in
|
||||
NavigationLink {
|
||||
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
|
||||
} label: {
|
||||
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
|
||||
}
|
||||
TabView(selection: $selection) {
|
||||
SystemsView()
|
||||
.tabItem {
|
||||
Label("Systems", systemImage: "building.2")
|
||||
}
|
||||
.onDelete(perform: deleteItems)
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
.tag(1)
|
||||
SystemView()
|
||||
.tabItem {
|
||||
Label("System", systemImage: "square.grid.3x2")
|
||||
}
|
||||
ToolbarItem {
|
||||
Button(action: addItem) {
|
||||
Label("Add Item", systemImage: "plus")
|
||||
}
|
||||
.tag(2)
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gearshape")
|
||||
}
|
||||
}
|
||||
} detail: {
|
||||
Text("Select an item")
|
||||
}
|
||||
}
|
||||
|
||||
private func addItem() {
|
||||
withAnimation {
|
||||
let newItem = Item(timestamp: Date())
|
||||
modelContext.insert(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteItems(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(items[index])
|
||||
}
|
||||
.tag(3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SystemsView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@EnvironmentObject var unitSettings: UnitSystemSettings
|
||||
@Query(sort: \ElectricalSystem.timestamp, order: .reverse) private var systems: [ElectricalSystem]
|
||||
@State private var newSystemToEdit: ElectricalSystem?
|
||||
@State private var systemToEdit: ElectricalSystem?
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if systems.isEmpty {
|
||||
systemsEmptyState
|
||||
} else {
|
||||
List {
|
||||
ForEach(systems) { system in
|
||||
NavigationLink(destination: LoadsView(system: system)) {
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(colorForName(system.colorName))
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: system.iconName)
|
||||
.font(.title3)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
.onTapGesture {
|
||||
systemToEdit = system
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(system.name)
|
||||
.fontWeight(.medium)
|
||||
|
||||
if !system.location.isEmpty {
|
||||
Text(system.location)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text(system.timestamp, format: .dateTime.month().day().hour().minute())
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteSystems)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Systems")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
createNewSystem()
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $newSystemToEdit) { system in
|
||||
LoadsView(system: system)
|
||||
}
|
||||
.sheet(item: $systemToEdit) { system in
|
||||
SystemEditorView(
|
||||
systemName: Binding(
|
||||
get: { system.name },
|
||||
set: { system.name = $0 }
|
||||
),
|
||||
location: Binding(
|
||||
get: { system.location },
|
||||
set: { system.location = $0 }
|
||||
),
|
||||
iconName: Binding(
|
||||
get: { system.iconName },
|
||||
set: { system.iconName = $0 }
|
||||
),
|
||||
colorName: Binding(
|
||||
get: { system.colorName },
|
||||
set: { system.colorName = $0 }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var systemsEmptyState: some View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 24) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.blue.opacity(0.1))
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Image(systemName: "building.2")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("No Systems Yet")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("Create your first electrical system to start managing loads and calculations.")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
createNewSystem()
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 16))
|
||||
Text("Create System")
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
private func createNewSystem() {
|
||||
let existingNames = Set(systems.map { $0.name })
|
||||
var systemName = "New System"
|
||||
var counter = 1
|
||||
|
||||
while existingNames.contains(systemName) {
|
||||
counter += 1
|
||||
systemName = "New System \(counter)"
|
||||
}
|
||||
|
||||
let newSystem = ElectricalSystem(
|
||||
name: systemName,
|
||||
location: "",
|
||||
iconName: "building.2",
|
||||
colorName: "blue"
|
||||
)
|
||||
modelContext.insert(newSystem)
|
||||
|
||||
newSystemToEdit = newSystem
|
||||
}
|
||||
|
||||
private func deleteSystems(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(systems[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func colorForName(_ colorName: String) -> Color {
|
||||
switch colorName {
|
||||
case "blue": return .blue
|
||||
case "green": return .green
|
||||
case "orange": return .orange
|
||||
case "red": return .red
|
||||
case "purple": return .purple
|
||||
case "yellow": return .yellow
|
||||
case "pink": return .pink
|
||||
case "teal": return .teal
|
||||
case "indigo": return .indigo
|
||||
case "mint": return .mint
|
||||
case "cyan": return .cyan
|
||||
case "brown": return .brown
|
||||
case "gray": return .gray
|
||||
default: return .blue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadsView: View {
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
@EnvironmentObject var unitSettings: UnitSystemSettings
|
||||
@Query(sort: \SavedLoad.timestamp, order: .reverse) private var allLoads: [SavedLoad]
|
||||
@State private var newLoadToEdit: SavedLoad?
|
||||
|
||||
let system: ElectricalSystem
|
||||
|
||||
init(system: ElectricalSystem) {
|
||||
self.system = system
|
||||
}
|
||||
|
||||
private var savedLoads: [SavedLoad] {
|
||||
allLoads.filter { $0.system == system }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 0) {
|
||||
librarySection
|
||||
|
||||
if savedLoads.isEmpty {
|
||||
emptyStateView
|
||||
} else {
|
||||
List {
|
||||
ForEach(savedLoads) { load in
|
||||
NavigationLink(destination: CalculatorView(savedLoad: load)) {
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.fill(colorForName(load.colorName))
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
Image(systemName: load.iconName)
|
||||
.font(.title3)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Text(load.name)
|
||||
.fontWeight(.medium)
|
||||
Spacer()
|
||||
Text(load.timestamp, format: .dateTime.month().day().hour().minute())
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
|
||||
// Secondary info
|
||||
HStack {
|
||||
Group {
|
||||
Text(String(format: "%.1fV", load.voltage))
|
||||
Text("•")
|
||||
if load.isWattMode {
|
||||
Text(String(format: "%.0fW", load.power))
|
||||
} else {
|
||||
Text(String(format: "%.1fA", load.current))
|
||||
}
|
||||
Text("•")
|
||||
Text(String(format: "%.1f%@",
|
||||
unitSettings.unitSystem == .metric ? load.length : load.length * 3.28084,
|
||||
unitSettings.unitSystem.lengthUnit))
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Prominent fuse and wire gauge display
|
||||
HStack(spacing: 12) {
|
||||
HStack(spacing: 4) {
|
||||
Text("FUSE")
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(recommendedFuse(for: load))A")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.orange.opacity(0.1))
|
||||
.cornerRadius(6)
|
||||
|
||||
HStack(spacing: 4) {
|
||||
Text("WIRE")
|
||||
.font(.caption2)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.secondary)
|
||||
Text(String(format: unitSettings.unitSystem == .imperial ? "%.0f AWG" : "%.1fmm²",
|
||||
unitSettings.unitSystem == .imperial ? awgFromCrossSection(load.crossSection) : load.crossSection))
|
||||
.font(.subheadline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(6)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteLoads)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(system.name)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack {
|
||||
Button(action: {
|
||||
createNewLoad()
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $newLoadToEdit) { load in
|
||||
CalculatorView(savedLoad: load)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var librarySection: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Component Library")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
Text("Browse electrical components from VoltPlan")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
// TODO: Open VoltPlan component library
|
||||
print("Opening VoltPlan component library...")
|
||||
}) {
|
||||
HStack(spacing: 6) {
|
||||
Text("Browse")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
}
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyStateView: some View {
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 24) {
|
||||
// Icon
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.blue.opacity(0.1))
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Image(systemName: "bolt.circle")
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
// Title and subtitle
|
||||
VStack(spacing: 8) {
|
||||
Text("No Loads Yet")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text("Get started by creating a new electrical load calculation or browse components from the VoltPlan library.")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
|
||||
// Action buttons
|
||||
VStack(spacing: 12) {
|
||||
Button(action: {
|
||||
createNewLoad()
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
.font(.system(size: 16))
|
||||
Text("Create New Load")
|
||||
.fontWeight(.medium)
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button(action: {
|
||||
// TODO: Open VoltPlan component library
|
||||
print("Opening VoltPlan component library...")
|
||||
}) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "square.grid.3x3")
|
||||
.font(.system(size: 16))
|
||||
Text("Browse VoltPlan Library")
|
||||
.fontWeight(.medium)
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.system(size: 12))
|
||||
}
|
||||
.foregroundColor(.blue)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Safety disclaimer
|
||||
VStack(spacing: 8) {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.orange)
|
||||
.font(.system(size: 16))
|
||||
Text("Important Safety Notice")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
Text("This app provides estimates for educational purposes only. Always consult qualified electricians and follow local electrical codes for actual installations. Electrical work can be dangerous and should only be performed by licensed professionals.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
.padding(.bottom, 32)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteLoads(offsets: IndexSet) {
|
||||
withAnimation {
|
||||
for index in offsets {
|
||||
modelContext.delete(savedLoads[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createNewLoad() {
|
||||
let existingNames = Set(savedLoads.map { $0.name })
|
||||
var loadName = "New Load"
|
||||
var counter = 1
|
||||
|
||||
while existingNames.contains(loadName) {
|
||||
counter += 1
|
||||
loadName = "New Load \(counter)"
|
||||
}
|
||||
|
||||
let newLoad = SavedLoad(
|
||||
name: loadName,
|
||||
voltage: 12.0,
|
||||
current: 5.0,
|
||||
power: 60.0, // 12V * 5A = 60W
|
||||
length: 10.0,
|
||||
crossSection: 1.0,
|
||||
iconName: "lightbulb",
|
||||
colorName: "blue",
|
||||
isWattMode: false,
|
||||
system: system
|
||||
)
|
||||
modelContext.insert(newLoad)
|
||||
|
||||
// Navigate to the new load
|
||||
newLoadToEdit = newLoad
|
||||
}
|
||||
|
||||
private func colorForName(_ colorName: String) -> Color {
|
||||
switch colorName {
|
||||
case "blue": return .blue
|
||||
case "green": return .green
|
||||
case "orange": return .orange
|
||||
case "red": return .red
|
||||
case "purple": return .purple
|
||||
case "yellow": return .yellow
|
||||
case "pink": return .pink
|
||||
case "teal": return .teal
|
||||
case "indigo": return .indigo
|
||||
case "mint": return .mint
|
||||
case "cyan": return .cyan
|
||||
case "brown": return .brown
|
||||
case "gray": return .gray
|
||||
default: return .blue
|
||||
}
|
||||
}
|
||||
|
||||
private func awgFromCrossSection(_ crossSectionMM2: Double) -> Double {
|
||||
let awgSizes = [(20, 0.519), (18, 0.823), (16, 1.31), (14, 2.08), (12, 3.31),
|
||||
(10, 5.26), (8, 8.37), (6, 13.3), (4, 21.2), (2, 33.6),
|
||||
(1, 42.4), (0, 53.5), (00, 67.4), (000, 85.0), (0000, 107.0)]
|
||||
|
||||
// Find the closest AWG size
|
||||
let closest = awgSizes.min { abs($0.1 - crossSectionMM2) < abs($1.1 - crossSectionMM2) }
|
||||
return Double(closest?.0 ?? 20)
|
||||
}
|
||||
|
||||
private func recommendedFuse(for load: SavedLoad) -> Int {
|
||||
let targetFuse = load.current * 1.25 // 125% of load current for safety
|
||||
|
||||
// Common fuse values in amperes
|
||||
let standardFuses = [1, 2, 3, 5, 7, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 100, 125, 150, 175, 200, 225, 250, 300, 350, 400, 450, 500, 600, 700, 800]
|
||||
|
||||
// Find the smallest standard fuse that's >= target
|
||||
return standardFuses.first { $0 >= Int(targetFuse.rounded(.up)) } ?? standardFuses.last!
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct SystemView: View {
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 24) {
|
||||
Spacer()
|
||||
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "square.grid.3x2")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Text("System View")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("Coming soon - manage your electrical systems and panels here.")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, 48)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("System")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var unitSettings: UnitSystemSettings
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("Units") {
|
||||
Picker("Unit System", selection: $unitSettings.unitSystem) {
|
||||
ForEach(UnitSystem.allCases, id: \.self) { system in
|
||||
Text(system.displayName).tag(system)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
Section {
|
||||
HStack {
|
||||
Text("Wire Cross-Section:")
|
||||
Spacer()
|
||||
Text(unitSettings.unitSystem.wireAreaUnit)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Length:")
|
||||
Spacer()
|
||||
Text(unitSettings.unitSystem.lengthUnit)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Current Units")
|
||||
} footer: {
|
||||
Text("Changing the unit system will apply to all calculations in the app.")
|
||||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.foregroundColor(.orange)
|
||||
.font(.system(size: 18))
|
||||
Text("Safety Disclaimer")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("This application provides electrical calculations for educational and estimation purposes only.")
|
||||
.font(.body)
|
||||
|
||||
Text("Important:")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("• Always consult qualified electricians for actual installations")
|
||||
Text("• Follow all local electrical codes and regulations")
|
||||
Text("• Electrical work should only be performed by licensed professionals")
|
||||
Text("• These calculations may not account for all environmental factors")
|
||||
Text("• The app developers assume no liability for electrical installations")
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
.modelContainer(for: Item.self, inMemory: true)
|
||||
.environmentObject(UnitSystemSettings())
|
||||
}
|
||||
|
||||
177
Cable/ItemEditorView.swift
Normal file
177
Cable/ItemEditorView.swift
Normal file
@@ -0,0 +1,177 @@
|
||||
//
|
||||
// ItemEditorView.swift
|
||||
// Cable
|
||||
//
|
||||
// Created by Stefan Lange-Hegermann on 16.09.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ItemEditorView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
let title: String
|
||||
let nameFieldLabel: String
|
||||
let previewSubtitle: String
|
||||
let icons: [String]
|
||||
let additionalFields: () -> AnyView
|
||||
|
||||
@Binding var name: String
|
||||
@Binding var iconName: String
|
||||
@Binding var colorName: String
|
||||
|
||||
@State private var tempName: String
|
||||
@State private var tempIconName: String
|
||||
@State private var tempColorName: String
|
||||
|
||||
private let curatedColors: [(String, Color)] = [
|
||||
("blue", .blue),
|
||||
("green", .green),
|
||||
("orange", .orange),
|
||||
("red", .red),
|
||||
("purple", .purple),
|
||||
("yellow", .yellow),
|
||||
("pink", .pink),
|
||||
("teal", .teal),
|
||||
("indigo", .indigo),
|
||||
("mint", .mint),
|
||||
("cyan", .cyan),
|
||||
("brown", .brown),
|
||||
("gray", .gray)
|
||||
]
|
||||
|
||||
init(
|
||||
title: String,
|
||||
nameFieldLabel: String,
|
||||
previewSubtitle: String,
|
||||
icons: [String],
|
||||
name: Binding<String>,
|
||||
iconName: Binding<String>,
|
||||
colorName: Binding<String>,
|
||||
@ViewBuilder additionalFields: @escaping () -> AnyView = { AnyView(EmptyView()) }
|
||||
) {
|
||||
self.title = title
|
||||
self.nameFieldLabel = nameFieldLabel
|
||||
self.previewSubtitle = previewSubtitle
|
||||
self.icons = icons
|
||||
self.additionalFields = additionalFields
|
||||
self._name = name
|
||||
self._iconName = iconName
|
||||
self._colorName = colorName
|
||||
self._tempName = State(initialValue: name.wrappedValue)
|
||||
self._tempIconName = State(initialValue: iconName.wrappedValue)
|
||||
self._tempColorName = State(initialValue: colorName.wrappedValue)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("Preview") {
|
||||
HStack {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(selectedColor)
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Image(systemName: tempIconName)
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(tempName.isEmpty ? nameFieldLabel : tempName)
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Text(previewSubtitle)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
Section("Details") {
|
||||
TextField(nameFieldLabel, text: $tempName)
|
||||
.autocapitalization(.words)
|
||||
|
||||
additionalFields()
|
||||
}
|
||||
|
||||
Section("Icon") {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 5), spacing: 16) {
|
||||
ForEach(icons, id: \.self) { icon in
|
||||
Button(action: {
|
||||
tempIconName = icon
|
||||
}) {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(tempIconName == icon ? selectedColor : Color(.systemGray5))
|
||||
.frame(width: 50, height: 50)
|
||||
|
||||
Image(systemName: icon)
|
||||
.font(.title3)
|
||||
.foregroundColor(tempIconName == icon ? .white : .primary)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
Section("Color") {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: 16) {
|
||||
ForEach(curatedColors, id: \.0) { colorName, color in
|
||||
Button(action: {
|
||||
tempColorName = colorName
|
||||
}) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 40, height: 40)
|
||||
|
||||
if tempColorName == colorName {
|
||||
Image(systemName: "checkmark")
|
||||
.font(.headline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
}
|
||||
.navigationTitle(title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
saveChanges()
|
||||
dismiss()
|
||||
}
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var selectedColor: Color {
|
||||
curatedColors.first { $0.0 == tempColorName }?.1 ?? .blue
|
||||
}
|
||||
|
||||
private func saveChanges() {
|
||||
name = tempName
|
||||
iconName = tempIconName
|
||||
colorName = tempColorName
|
||||
}
|
||||
}
|
||||
42
Cable/LoadEditorView.swift
Normal file
42
Cable/LoadEditorView.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// LoadEditorView.swift
|
||||
// Cable
|
||||
//
|
||||
// Created by Stefan Lange-Hegermann on 11.09.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LoadEditorView: View {
|
||||
@Binding var loadName: String
|
||||
@Binding var iconName: String
|
||||
@Binding var colorName: String
|
||||
|
||||
private let loadIcons = [
|
||||
"lightbulb", "lamp.desk", "fan", "tv", "poweroutlet.strip","poweroutlet.type.c", "bolt", "xbox.logo", "playstation.logo", "batteryblock", "speaker.wave.2", "refrigerator",
|
||||
"washer", "dishwasher", "stove", "microwave", "dryer", "cooktop",
|
||||
"car", "bolt.car", "engine.combustion", "wrench.adjustable",
|
||||
"cpu", "desktopcomputer", "laptopcomputer", "iphone", "camera",
|
||||
"gamecontroller", "headphones", "printer", "wifi", "antenna.radiowaves.left.and.right"
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
ItemEditorView(
|
||||
title: "Edit Load",
|
||||
nameFieldLabel: "Load name",
|
||||
previewSubtitle: "Preview",
|
||||
icons: loadIcons,
|
||||
name: $loadName,
|
||||
iconName: $iconName,
|
||||
colorName: $colorName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@State var name = "My Load"
|
||||
@State var icon = "lightbulb"
|
||||
@State var color = "blue"
|
||||
|
||||
return LoadEditorView(loadName: $name, iconName: $icon, colorName: $color)
|
||||
}
|
||||
66
Cable/SystemEditorView.swift
Normal file
66
Cable/SystemEditorView.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// SystemEditorView.swift
|
||||
// Cable
|
||||
//
|
||||
// Created by Stefan Lange-Hegermann on 16.09.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SystemEditorView: View {
|
||||
@Binding var systemName: String
|
||||
@Binding var location: String
|
||||
@Binding var iconName: String
|
||||
@Binding var colorName: String
|
||||
|
||||
@State private var tempLocation: String
|
||||
|
||||
private let systemIcons = [
|
||||
"building.2", "house", "building", "factory", "office.building", "tent", "car.garage", "sailboat",
|
||||
"airplane", "ferry", "bus", "truck.box", "van.side", "rv",
|
||||
"server.rack", "externaldrive", "cpu", "gear", "wrench.adjustable", "hammer",
|
||||
"lightbulb", "bolt", "powerplug", "battery.100", "solar.panel", "windmill",
|
||||
"engine.combustion", "fuelpump", "drop", "flame", "snowflake", "thermometer"
|
||||
]
|
||||
|
||||
init(systemName: Binding<String>, location: Binding<String>, iconName: Binding<String>, colorName: Binding<String>) {
|
||||
self._systemName = systemName
|
||||
self._location = location
|
||||
self._iconName = iconName
|
||||
self._colorName = colorName
|
||||
self._tempLocation = State(initialValue: location.wrappedValue)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ItemEditorView(
|
||||
title: "Edit System",
|
||||
nameFieldLabel: "System name",
|
||||
previewSubtitle: tempLocation.isEmpty ? "Location (optional)" : tempLocation,
|
||||
icons: systemIcons,
|
||||
name: $systemName,
|
||||
iconName: $iconName,
|
||||
colorName: $colorName,
|
||||
additionalFields: {
|
||||
AnyView(
|
||||
TextField("Location (optional)", text: $tempLocation)
|
||||
.autocapitalization(.words)
|
||||
.onChange(of: tempLocation) { _, newValue in
|
||||
location = newValue
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
tempLocation = location
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@State var name = "My System"
|
||||
@State var location = "Main Building"
|
||||
@State var icon = "building.2"
|
||||
@State var color = "blue"
|
||||
|
||||
return SystemEditorView(systemName: $name, location: $location, iconName: $icon, colorName: $color)
|
||||
}
|
||||
54
Cable/UnitSystem.swift
Normal file
54
Cable/UnitSystem.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// UnitSystem.swift
|
||||
// Cable
|
||||
//
|
||||
// Created by Stefan Lange-Hegermann on 11.09.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum UnitSystem: String, CaseIterable {
|
||||
case metric = "metric"
|
||||
case imperial = "imperial"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .metric:
|
||||
return "Metric (mm², m)"
|
||||
case .imperial:
|
||||
return "Imperial (AWG, ft)"
|
||||
}
|
||||
}
|
||||
|
||||
var wireAreaUnit: String {
|
||||
switch self {
|
||||
case .metric:
|
||||
return "mm²"
|
||||
case .imperial:
|
||||
return "AWG"
|
||||
}
|
||||
}
|
||||
|
||||
var lengthUnit: String {
|
||||
switch self {
|
||||
case .metric:
|
||||
return "m"
|
||||
case .imperial:
|
||||
return "ft"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class UnitSystemSettings: ObservableObject {
|
||||
@Published var unitSystem: UnitSystem {
|
||||
didSet {
|
||||
UserDefaults.standard.set(unitSystem.rawValue, forKey: "unitSystem")
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
let savedSystem = UserDefaults.standard.string(forKey: "unitSystem") ?? UnitSystem.metric.rawValue
|
||||
self.unitSystem = UnitSystem(rawValue: savedSystem) ?? .metric
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user