diff --git a/Gas Man.xcodeproj/project.pbxproj b/Gas Man.xcodeproj/project.pbxproj
index 6df2ad5..71c59f3 100644
--- a/Gas Man.xcodeproj/project.pbxproj
+++ b/Gas Man.xcodeproj/project.pbxproj
@@ -296,12 +296,14 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Gas Man/Info.plist";
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.travel";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
+ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
@@ -309,15 +311,16 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
- MACOSX_DEPLOYMENT_TARGET = 14.7;
+ MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "pro.thelinux.Gas-Man";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
+ SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2,7";
+ TARGETED_DEVICE_FAMILY = "1,2";
XROS_DEPLOYMENT_TARGET = 2.1;
};
name = Debug;
@@ -336,12 +339,14 @@
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Gas Man/Info.plist";
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.travel";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
+ INFOPLIST_KEY_UIStatusBarStyle = UIStatusBarStyleDarkContent;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
@@ -349,15 +354,16 @@
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
- MACOSX_DEPLOYMENT_TARGET = 14.7;
+ MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = "pro.thelinux.Gas-Man";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto;
- SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
+ SUPPORTS_MACCATALYST = NO;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2,7";
+ TARGETED_DEVICE_FAMILY = "1,2";
XROS_DEPLOYMENT_TARGET = 2.1;
};
name = Release;
diff --git a/Gas Man.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Gas Man.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/Gas Man.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Gas Man.xcodeproj/project.xcworkspace/xcuserdata/kkenny.xcuserdatad/WorkspaceSettings.xcsettings b/Gas Man.xcodeproj/project.xcworkspace/xcuserdata/kkenny.xcuserdatad/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..bbfef02
--- /dev/null
+++ b/Gas Man.xcodeproj/project.xcworkspace/xcuserdata/kkenny.xcuserdatad/WorkspaceSettings.xcsettings
@@ -0,0 +1,14 @@
+
+
+
+
+ BuildLocationStyle
+ UseAppPreferences
+ CustomBuildLocationType
+ RelativeToDerivedData
+ DerivedDataLocationStyle
+ Default
+ ShowSharedSchemesAutomaticallyEnabled
+
+
+
diff --git a/Gas Man.xcodeproj/xcshareddata/xcschemes/Gas Man.xcscheme b/Gas Man.xcodeproj/xcshareddata/xcschemes/Gas Man.xcscheme
new file mode 100644
index 0000000..26fe07a
--- /dev/null
+++ b/Gas Man.xcodeproj/xcshareddata/xcschemes/Gas Man.xcscheme
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Gas Man.xcodeproj/xcuserdata/kkenny.xcuserdatad/xcschemes/xcschememanagement.plist b/Gas Man.xcodeproj/xcuserdata/kkenny.xcuserdatad/xcschemes/xcschememanagement.plist
index 20c817c..503f867 100644
--- a/Gas Man.xcodeproj/xcuserdata/kkenny.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Gas Man.xcodeproj/xcuserdata/kkenny.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -10,5 +10,23 @@
0
+ SuppressBuildableAutocreation
+
+ 3D34444F2D889F7D00AA3172
+
+ primary
+
+
+ 3D3444662D889F7F00AA3172
+
+ primary
+
+
+ 3D3444702D889F8000AA3172
+
+ primary
+
+
+
diff --git a/Gas Man/AddFuelLogView.swift b/Gas Man/AddFuelLogView.swift
new file mode 100644
index 0000000..e5bbcf6
--- /dev/null
+++ b/Gas Man/AddFuelLogView.swift
@@ -0,0 +1,249 @@
+//
+// AddFuelLogView.swift
+// Gas Man
+//
+// Created by Kameron Kenny on 3/17/25.
+//
+
+import SwiftUI
+import CoreData
+
+struct AddFuelLogView: View {
+ @Environment(\.managedObjectContext) private var viewContext
+ @Environment(\.dismiss) var dismiss
+
+ // Form fields
+ @State private var date = Date()
+ @State private var odometer = ""
+ @State private var fuelVolume = ""
+ @State private var cost = ""
+ @State private var locationCoordinates = ""
+ @State private var locationName = ""
+ @State private var selectedOctane: Int = 87 // Default or from previous record
+ @State private var pricePerGalon = ""
+
+ // Allowed octane options
+ let octaneOptions = [87, 89, 91, 92, 93, 95]
+
+ // Location manager for automatic location updates
+ @StateObject private var locationManager = LocationManager()
+
+ // For tracking the previous odometer reading
+ @State private var previousOdometer: Double? = nil
+ @State private var showOdometerAlert = false
+
+ // Flag to avoid update loops in our calculation logic
+ @State private var isUpdatingCalculation = false
+
+ // Computed property for formatted previous odometer value
+ private var previousOdometerString: String {
+ previousOdometer.map { String(format: "%.0f", $0) } ?? "N/A"
+ }
+
+ // Computed property for the odometer alert
+ var odometerAlert: Alert {
+ let messageString = "Odometer reading must be greater than the previous record (\(previousOdometerString))."
+ return Alert(
+ title: Text("Odometer Reading Error"),
+ message: Text(messageString),
+ dismissButton: .default(Text("OK"))
+ )
+ }
+
+ var body: some View {
+ NavigationView {
+ Form {
+ fuelLogDetailsSection
+ }
+ .navigationTitle("Add Fuel Log")
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("Save") { saveFuelLog() }
+ }
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("Cancel") { dismiss() }
+ }
+ }
+ .onChange(of: locationManager.location) { newLocation in
+ if let loc = newLocation {
+ locationCoordinates = "\(loc.coordinate.latitude), \(loc.coordinate.longitude)"
+ }
+ }
+ .onChange(of: locationManager.placemark) { newPlacemark in
+ if let placemark = newPlacemark {
+ locationName = placemark.name ?? ""
+ }
+ }
+ .onAppear(perform: loadPreviousData)
+ .alert(isPresented: $showOdometerAlert) { odometerAlert }
+ }
+ }
+
+ // MARK: - Subviews
+
+ private var fuelLogDetailsSection: some View {
+ Section(header: Text("Fuel Log Details")) {
+ DatePicker("Date", selection: $date, displayedComponents: [.date, .hourAndMinute])
+
+ // Odometer field
+ HStack(spacing: 4) {
+ Text("Odometer: ")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ TextField("", text: $odometer)
+ .keyboardType(.decimalPad)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ }
+
+ // Fuel Volume field
+ HStack(spacing: 4) {
+ Text("Gallons: ")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ TextField("", text: $fuelVolume)
+ .keyboardType(.decimalPad)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .onChange(of: fuelVolume) { _ in updateCalculatedValues() }
+ }
+
+ // Price per Gallon field
+ HStack(spacing: 4) {
+ Text("Price/Gal: $")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ TextField("", text: $pricePerGalon)
+ .keyboardType(.decimalPad)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .onChange(of: pricePerGalon) { _ in updateCalculatedValues() }
+ }
+
+ // Cost field
+ HStack(spacing: 4) {
+ Text("Cost: $")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ TextField("", text: $cost)
+ .keyboardType(.decimalPad)
+ .textFieldStyle(RoundedBorderTextFieldStyle())
+ .onChange(of: cost) { _ in updateCalculatedValues() }
+ }
+
+ Button("Get Current Location") {
+ locationManager.requestLocation()
+ }
+
+ if !locationCoordinates.isEmpty {
+ Text("Coordinates: \(locationCoordinates)")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ TextField("Location Coordinates", text: $locationCoordinates)
+
+ HStack(spacing: 4) {
+ Text("Location Name:")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ TextField("", text: $locationName)
+ }
+
+ Picker("Octane", selection: $selectedOctane) {
+ ForEach(octaneOptions, id: \.self) { option in
+ Text("\(option)").tag(option)
+ }
+ }
+ .pickerStyle(MenuPickerStyle())
+ }
+ }
+
+ // MARK: - Helper Methods
+
+ private func loadPreviousData() {
+ let fetchRequest: NSFetchRequest = FuelLog.fetchRequest()
+ fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
+ fetchRequest.fetchLimit = 1
+ if let lastFuelLog = try? viewContext.fetch(fetchRequest).first {
+ selectedOctane = Int(lastFuelLog.octane)
+ previousOdometer = lastFuelLog.odometer
+ odometer = String(format: "%.0f", lastFuelLog.odometer)
+ } else {
+ selectedOctane = 87
+ odometer = ""
+ }
+ }
+
+ private func roundToThree(_ value: Double) -> Double {
+ return (value * 1000).rounded() / 1000
+ }
+
+ private func roundToTwo(_ value: Double) -> Double {
+ return (value * 100).rounded() / 100
+ }
+
+ private func updateCalculatedValues() {
+ // Prevent recursive updates.
+ guard !isUpdatingCalculation else { return }
+ isUpdatingCalculation = true
+
+ let fuel = Double(fuelVolume)
+ let costVal = Double(cost)
+ let price = Double(pricePerGalon)
+
+ // 1. If fuelVolume and pricePerGalon are provided, calculate cost.
+ if let f = fuel, let p = price, f > 0 {
+ let computedCost = roundToTwo(f * p)
+ let computedCostStr = String(format: "%.2f", computedCost)
+ if cost != computedCostStr {
+ cost = computedCostStr
+ }
+ }
+ // 2. If fuelVolume and cost are provided, and pricePerGalon is empty, calculate pricePerGalon.
+ else if let f = fuel, let c = costVal, f > 0, pricePerGalon.trimmingCharacters(in: .whitespaces).isEmpty {
+ let computedPrice = roundToThree(c / f)
+ let computedPriceStr = String(format: "%.3f", computedPrice)
+ if pricePerGalon != computedPriceStr {
+ pricePerGalon = computedPriceStr
+ }
+ }
+ // 3. If pricePerGalon and cost are provided, and fuelVolume is empty, calculate fuelVolume.
+ else if let p = price, let c = costVal, p > 0, fuelVolume.trimmingCharacters(in: .whitespaces).isEmpty {
+ let computedFuel = roundToThree(c / p)
+ let computedFuelStr = String(format: "%.3f", computedFuel)
+ if fuelVolume != computedFuelStr {
+ fuelVolume = computedFuelStr
+ }
+ }
+
+ isUpdatingCalculation = false
+ }
+
+ private func saveFuelLog() {
+ guard let newOdometer = Double(odometer) else {
+ return
+ }
+ // Validate that the new odometer reading is greater than the previous record
+ if let previous = previousOdometer, newOdometer <= previous {
+ showOdometerAlert = true
+ return
+ }
+
+ let newLog = FuelLog(context: viewContext)
+ newLog.id = UUID()
+ newLog.date = date
+ newLog.odometer = newOdometer
+ newLog.fuelVolume = Double(fuelVolume) ?? 0
+ newLog.cost = Double(cost) ?? 0
+ newLog.locationCoordinates = locationCoordinates
+ newLog.locationName = locationName
+ newLog.octane = Int16(selectedOctane)
+ newLog.pricePerGalon = Double(pricePerGalon) ?? 0
+
+ do {
+ try viewContext.save()
+ dismiss()
+ } catch {
+ let nsError = error as NSError
+ fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+ }
+ }
+}
diff --git a/Gas Man/AddMaintenanceView.swift b/Gas Man/AddMaintenanceView.swift
new file mode 100644
index 0000000..090ff26
--- /dev/null
+++ b/Gas Man/AddMaintenanceView.swift
@@ -0,0 +1,73 @@
+//
+// AddMaintenanceView.swift
+// Gas Man
+//
+// Created by Kameron Kenny on 3/17/25.
+//
+
+
+import SwiftUI
+
+struct AddMaintenanceView: View {
+ @Environment(\.managedObjectContext) private var viewContext
+ @Environment(\.dismiss) var dismiss
+
+ @State private var date = Date()
+ @State private var odometer = ""
+ @State private var eventType = ""
+ @State private var cost = ""
+ @State private var notes = ""
+ @State private var locationCoordinates = ""
+ @State private var locationName = ""
+
+ var body: some View {
+ NavigationView {
+ Form {
+ Section(header: Text("Maintenance Details")) {
+ DatePicker("Date", selection: $date, displayedComponents: [.date, .hourAndMinute])
+ TextField("Odometer Reading", text: $odometer)
+ .keyboardType(.decimalPad)
+ TextField("Service Type", text: $eventType)
+ TextField("Cost ($)", text: $cost)
+ .keyboardType(.decimalPad)
+ TextField("Notes", text: $notes)
+ TextField("Location Coordinates", text: $locationCoordinates)
+ TextField("Location Name", text: $locationName)
+ }
+ }
+ .navigationTitle("Add Maintenance")
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button("Save") {
+ saveMaintenance()
+ }
+ }
+ ToolbarItem(placement: .navigationBarLeading) {
+ Button("Cancel") {
+ dismiss()
+ }
+ }
+ }
+ }
+ }
+
+ private func saveMaintenance() {
+ let newEvent = MaintenanceEvent(context: viewContext)
+ newEvent.id = UUID()
+ newEvent.date = date
+ newEvent.odometer = Double(odometer) ?? 0
+ newEvent.eventType = eventType
+ newEvent.cost = Double(cost) ?? 0
+ newEvent.notes = notes
+ newEvent.locationCoordinates = locationCoordinates
+ newEvent.locationName = locationName
+
+ do {
+ try viewContext.save()
+ dismiss()
+ } catch {
+ let nsError = error as NSError
+ fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+ }
+ }
+}
diff --git a/Gas Man/ContentView.swift b/Gas Man/ContentView.swift
index 9201124..e9f3237 100644
--- a/Gas Man/ContentView.swift
+++ b/Gas Man/ContentView.swift
@@ -6,83 +6,83 @@
//
import SwiftUI
-import CoreData
+//import CoreData
+//
+//struct ContentView: View {
+// @Environment(\.managedObjectContext) private var viewContext
+//
+// @FetchRequest(
+// sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
+// animation: .default)
+// private var items: FetchedResults-
+//
+// var body: some View {
+// NavigationView {
+// List {
+// ForEach(items) { item in
+// NavigationLink {
+// Text("Item at \(item.timestamp!, formatter: itemFormatter)")
+// } label: {
+// Text(item.timestamp!, formatter: itemFormatter)
+// }
+// }
+// .onDelete(perform: deleteItems)
+// }
+// .toolbar {
+//#if os(iOS)
+// ToolbarItem(placement: .navigationBarTrailing) {
+// EditButton()
+// }
+//#endif
+// ToolbarItem {
+// Button(action: addItem) {
+// Label("Add Item", systemImage: "plus")
+// }
+// }
+// }
+// Text("Select an item")
+// }
+// }
+//
+// private func addItem() {
+// withAnimation {
+// let newItem = Item(context: viewContext)
+// newItem.timestamp = Date()
+//
+// do {
+// try viewContext.save()
+// } catch {
+// // Replace this implementation with code to handle the error appropriately.
+// // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
+// let nsError = error as NSError
+// fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+// }
+// }
+// }
+//
+// private func deleteItems(offsets: IndexSet) {
+// withAnimation {
+// offsets.map { items[$0] }.forEach(viewContext.delete)
+//
+// do {
+// try viewContext.save()
+// } catch {
+// // Replace this implementation with code to handle the error appropriately.
+// // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
+// let nsError = error as NSError
+// fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+// }
+// }
+// }
+//}
+//
+//private let itemFormatter: DateFormatter = {
+// let formatter = DateFormatter()
+// formatter.dateStyle = .short
+// formatter.timeStyle = .medium
+// return formatter
+//}()
-struct ContentView: View {
- @Environment(\.managedObjectContext) private var viewContext
-
- @FetchRequest(
- sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
- animation: .default)
- private var items: FetchedResults
-
-
- var body: some View {
- NavigationView {
- List {
- ForEach(items) { item in
- NavigationLink {
- Text("Item at \(item.timestamp!, formatter: itemFormatter)")
- } label: {
- Text(item.timestamp!, formatter: itemFormatter)
- }
- }
- .onDelete(perform: deleteItems)
- }
- .toolbar {
-#if os(iOS)
- ToolbarItem(placement: .navigationBarTrailing) {
- EditButton()
- }
-#endif
- ToolbarItem {
- Button(action: addItem) {
- Label("Add Item", systemImage: "plus")
- }
- }
- }
- Text("Select an item")
- }
- }
-
- private func addItem() {
- withAnimation {
- let newItem = Item(context: viewContext)
- newItem.timestamp = Date()
-
- do {
- try viewContext.save()
- } catch {
- // Replace this implementation with code to handle the error appropriately.
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
- let nsError = error as NSError
- fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
- }
- }
- }
-
- private func deleteItems(offsets: IndexSet) {
- withAnimation {
- offsets.map { items[$0] }.forEach(viewContext.delete)
-
- do {
- try viewContext.save()
- } catch {
- // Replace this implementation with code to handle the error appropriately.
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
- let nsError = error as NSError
- fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
- }
- }
- }
-}
-
-private let itemFormatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .short
- formatter.timeStyle = .medium
- return formatter
-}()
-
-#Preview {
- ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
-}
+//#Preview {
+// ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
+//}
diff --git a/Gas Man/FuelLogDetailView.swift b/Gas Man/FuelLogDetailView.swift
new file mode 100644
index 0000000..6857a66
--- /dev/null
+++ b/Gas Man/FuelLogDetailView.swift
@@ -0,0 +1,74 @@
+//
+// FuelLogDetailView.swift
+// Gas Man
+//
+// Created by Kameron Kenny on 3/17/25.
+//
+
+import SwiftUI
+
+private let dateFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+ return formatter
+}()
+
+
+struct FuelLogDetailView: View {
+ var fuelLog: FuelLog
+
+ var body: some View {
+ Form {
+ Section(header: Text("Basic Information")) {
+ HStack {
+ Text("Date:")
+ Spacer()
+ Text(fuelLog.date ?? Date(), formatter: dateFormatter)
+ }
+ HStack {
+ Text("Odometer:")
+ Spacer()
+ Text("\(fuelLog.odometer, specifier: "%.0f") miles")
+ }
+ }
+ Section(header: Text("Fuel Information")) {
+ HStack {
+ Text("Fuel Volume:")
+ Spacer()
+ Text("\(fuelLog.fuelVolume, specifier: "%.3f") gallons")
+ }
+ HStack {
+ Text("Cost:")
+ Spacer()
+ Text("$\(fuelLog.cost, specifier: "%.2f")")
+ }
+ HStack {
+ Text("Price per Gallon:")
+ Spacer()
+ Text("$\(fuelLog.pricePerGalon, specifier: "%.3f")")
+ }
+ }
+ Section(header: Text("Location")) {
+ HStack {
+ Text("Coordinates:")
+ Spacer()
+ Text(fuelLog.locationCoordinates ?? "N/A")
+ }
+ HStack {
+ Text("Location Name:")
+ Spacer()
+ Text(fuelLog.locationName ?? "N/A")
+ }
+ }
+ Section(header: Text("Fuel Details")) {
+ HStack {
+ Text("Octane:")
+ Spacer()
+ Text("\(fuelLog.octane)")
+ }
+ }
+ }
+ .navigationTitle("Fuel Log Detail")
+ }
+}
diff --git a/Gas Man/FuelLogListView.swift b/Gas Man/FuelLogListView.swift
new file mode 100644
index 0000000..f184b57
--- /dev/null
+++ b/Gas Man/FuelLogListView.swift
@@ -0,0 +1,85 @@
+//
+// FuelLogListView.swift
+// Gas Man
+//
+// Created by Kameron Kenny on 3/17/25.
+//
+
+
+import SwiftUI
+import CoreData
+
+struct FuelLogListView: View {
+ @Environment(\.managedObjectContext) private var viewContext
+ @FetchRequest(
+ sortDescriptors: [NSSortDescriptor(keyPath: \FuelLog.date, ascending: false)],
+ animation: .default)
+ private var fuelLogs: FetchedResults
+
+ @State private var showingAddFuelLog = false // Controls sheet presentation
+
+ var body: some View {
+ NavigationView {
+ List {
+ ForEach(Array(fuelLogs.enumerated()), id: \.element) { index, log in
+ // Compute distance since the previous record:
+ let distance: Double? = {
+ if index < fuelLogs.count - 1 {
+ let previousLog = fuelLogs[index + 1]
+ let milesDriven = log.odometer - previousLog.odometer
+ return milesDriven > 0 ? milesDriven : nil
+ }
+ return nil
+ }()
+
+ // Compute MPG (if possible)
+ let mpg: Double? = {
+ if index < fuelLogs.count - 1 {
+ let previousLog = fuelLogs[index + 1]
+ let milesDriven = log.odometer - previousLog.odometer
+ if log.fuelVolume > 0 && milesDriven > 0 {
+ return milesDriven / log.fuelVolume
+ }
+ }
+ return nil
+ }()
+
+ NavigationLink(destination: FuelLogDetailView(fuelLog: log)) {
+ FuelLogSummaryView(log: log, mpg: mpg, distanceSincePrevious: distance)
+ }
+ }
+ .onDelete(perform: deleteFuelLogs)
+ }
+ .listStyle(InsetGroupedListStyle())
+ .navigationTitle("Fuel Logs")
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button(action: {
+ showingAddFuelLog = true // Present AddFuelLogView
+ }) {
+ Label("Add Fuel Log", systemImage: "plus")
+ }
+ }
+ ToolbarItem(placement: .navigationBarLeading) {
+ EditButton()
+ }
+ }
+ // Sheet presentation for AddFuelLogView
+ .sheet(isPresented: $showingAddFuelLog) {
+ AddFuelLogView().environment(\.managedObjectContext, viewContext)
+ }
+ }
+ }
+
+ private func deleteFuelLogs(offsets: IndexSet) {
+ withAnimation {
+ offsets.map { fuelLogs[$0] }.forEach(viewContext.delete)
+ do {
+ try viewContext.save()
+ } catch {
+ let nsError = error as NSError
+ fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+ }
+ }
+ }
+}
diff --git a/Gas Man/FuelLogSummaryView.swift b/Gas Man/FuelLogSummaryView.swift
new file mode 100644
index 0000000..c1d2acc
--- /dev/null
+++ b/Gas Man/FuelLogSummaryView.swift
@@ -0,0 +1,84 @@
+//
+// FuelLogSummaryView.swift
+// Gas Man
+//
+// Created by Kameron Kenny on 3/17/25.
+//
+
+
+import SwiftUI
+
+private let dateFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+ return formatter
+}()
+
+struct FuelLogSummaryView: View {
+ var log: FuelLog
+ var mpg: Double?
+ var distanceSincePrevious: Double?
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ // Row 1: Date and MPG (if available)
+ HStack {
+ Text(log.date ?? Date(), formatter: dateFormatter)
+ .font(.subheadline)
+ .foregroundColor(.primary)
+ Spacer()
+ if let mpg = mpg {
+ Text("MPG: \(mpg, specifier: "%.3f")")
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ }
+ }
+ Divider()
+ // Row 2: Distance (instead of odometer) and Fuel Volume
+ HStack {
+ VStack(alignment: .leading) {
+ Text("Distance")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ if let distance = distanceSincePrevious {
+ Text("\(distance, specifier: "%.0f") miles")
+ .bold()
+ } else {
+ Text("N/A")
+ .bold()
+ }
+ }
+ Spacer()
+ VStack(alignment: .leading) {
+ Text("Fuel")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ Text("\(log.fuelVolume, specifier: "%.3f") gal")
+ .bold()
+ }
+ }
+ // Row 3: Cost and Price per Gallon
+ HStack {
+ VStack(alignment: .leading) {
+ Text("Cost")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ Text("$\(log.cost, specifier: "%.2f")")
+ .bold()
+ }
+ Spacer()
+ VStack(alignment: .leading) {
+ Text("Price/Gal")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ Text("$\(log.pricePerGalon, specifier: "%.3f")")
+ .bold()
+ }
+ }
+ }
+ .padding(8)
+ .background(Color(.secondarySystemBackground))
+ .cornerRadius(8)
+ }
+}
diff --git a/Gas Man/Gas_Man.entitlements b/Gas Man/Gas_Man.entitlements
index 53000f3..ba12f8a 100644
--- a/Gas Man/Gas_Man.entitlements
+++ b/Gas Man/Gas_Man.entitlements
@@ -14,7 +14,9 @@
com.apple.security.app-sandbox
- com.apple.security.files.user-selected.read-only
+ com.apple.security.files.user-selected.read-write
+
+ com.apple.security.personal-information.location
diff --git a/Gas Man/Gas_Man.xcdatamodeld/Gas_Man.xcdatamodel/contents b/Gas Man/Gas_Man.xcdatamodeld/Gas_Man.xcdatamodel/contents
index e8d6ec8..066b8a8 100644
--- a/Gas Man/Gas_Man.xcdatamodeld/Gas_Man.xcdatamodel/contents
+++ b/Gas Man/Gas_Man.xcdatamodeld/Gas_Man.xcdatamodel/contents
@@ -1,9 +1,25 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
\ No newline at end of file
diff --git a/Gas Man/Gas_ManApp.swift b/Gas Man/Gas_ManApp.swift
index 5dc3538..c989573 100644
--- a/Gas Man/Gas_ManApp.swift
+++ b/Gas Man/Gas_ManApp.swift
@@ -13,8 +13,9 @@ struct Gas_ManApp: App {
var body: some Scene {
WindowGroup {
- ContentView()
+ MainTabView() // Replace with your actual view name.
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}
+
diff --git a/Gas Man/Info.plist b/Gas Man/Info.plist
index ca9a074..5aee0c5 100644
--- a/Gas Man/Info.plist
+++ b/Gas Man/Info.plist
@@ -2,6 +2,8 @@
+ NSLocationWhenInUseUsageDescription
+ This app needs your location to automatically fill in location details for fuel and maintenance logs
UIBackgroundModes
remote-notification
diff --git a/Gas Man/LocationManager.swift b/Gas Man/LocationManager.swift
new file mode 100644
index 0000000..0cf34fb
--- /dev/null
+++ b/Gas Man/LocationManager.swift
@@ -0,0 +1,55 @@
+//
+// LocationManager.swift
+// Gas Man
+//
+// Created by Kameron Kenny on 3/17/25.
+//
+
+
+import Foundation
+import CoreLocation
+import Combine
+
+class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
+ private let manager = CLLocationManager()
+
+ @Published var location: CLLocation?
+ @Published var placemark: CLPlacemark?
+
+ override init() {
+ super.init()
+ manager.delegate = self
+ manager.desiredAccuracy = kCLLocationAccuracyBest
+ }
+
+ func requestLocation() {
+ manager.requestWhenInUseAuthorization()
+ manager.requestLocation()
+ }
+
+ // CLLocationManagerDelegate
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+ if let loc = locations.first {
+ DispatchQueue.main.async {
+ self.location = loc
+ }
+ // Reverse geocode
+ let geocoder = CLGeocoder()
+ geocoder.reverseGeocodeLocation(loc) { [weak self] placemarks, error in
+ if let error = error {
+ print("Reverse geocode error: \(error)")
+ return
+ }
+ if let firstPlacemark = placemarks?.first {
+ DispatchQueue.main.async {
+ self?.placemark = firstPlacemark
+ }
+ }
+ }
+ }
+ }
+
+ func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
+ print("Failed to get user location: \(error)")
+ }
+}
diff --git a/Gas Man/MainTabView.swift b/Gas Man/MainTabView.swift
new file mode 100644
index 0000000..aa38f94
--- /dev/null
+++ b/Gas Man/MainTabView.swift
@@ -0,0 +1,23 @@
+//
+// MainTabView.swift
+// Gas Man
+//
+// Created by Kameron Kenny on 3/17/25.
+//
+
+import SwiftUI
+
+struct MainTabView: View {
+ var body: some View {
+ TabView {
+ FuelLogListView()
+ .tabItem {
+ Label("Fuel", systemImage: "fuelpump.fill")
+ }
+ MaintenanceListView()
+ .tabItem {
+ Label("Maintenance", systemImage: "wrench.fill")
+ }
+ }
+ }
+}
diff --git a/Gas Man/MaintenanceListView.swift b/Gas Man/MaintenanceListView.swift
new file mode 100644
index 0000000..0eae57a
--- /dev/null
+++ b/Gas Man/MaintenanceListView.swift
@@ -0,0 +1,78 @@
+//
+// MaintenanceListView.swift
+// Gas Man
+//
+// Created by Kameron Kenny on 3/17/25.
+//
+
+
+import SwiftUI
+import CoreData
+
+struct MaintenanceListView: View {
+ @Environment(\.managedObjectContext) private var viewContext
+ @FetchRequest(
+ sortDescriptors: [NSSortDescriptor(keyPath: \MaintenanceEvent.date, ascending: false)],
+ animation: .default)
+ private var maintenanceEvents: FetchedResults
+
+ @State private var showingAddMaintenance = false
+
+ var body: some View {
+ NavigationView {
+ List {
+ ForEach(maintenanceEvents) { event in
+ VStack(alignment: .leading, spacing: 4) {
+ Text("Date: \(event.date ?? Date(), formatter: dateFormatter)")
+ Text("Odometer: \(event.odometer, specifier: "%.0f") miles")
+ Text("Type: \(event.eventType)")
+ Text("Cost: $\(event.cost, specifier: "%.2f")")
+ if let notes = event.notes, !notes.isEmpty {
+ Text("Notes: \(notes)")
+ }
+ if let locationName = event.locationName, !locationName.isEmpty {
+ Text("Location: \(locationName)")
+ }
+ if let locationCoordinates = event.locationCoordinates, !locationCoordinates.isEmpty {
+ Text("Coordinates: \(locationCoordinates)")
+ }
+ }
+ }
+ .onDelete(perform: deleteMaintenance)
+ }
+ .navigationTitle("Maintenance")
+ .toolbar {
+ ToolbarItem(placement: .navigationBarTrailing) {
+ Button(action: { showingAddMaintenance.toggle() }) {
+ Label("Add Maintenance", systemImage: "plus")
+ }
+ }
+ ToolbarItem(placement: .navigationBarLeading) {
+ EditButton()
+ }
+ }
+ .sheet(isPresented: $showingAddMaintenance) {
+ AddMaintenanceView().environment(\.managedObjectContext, viewContext)
+ }
+ }
+ }
+
+ private func deleteMaintenance(offsets: IndexSet) {
+ withAnimation {
+ offsets.map { maintenanceEvents[$0] }.forEach(viewContext.delete)
+ do {
+ try viewContext.save()
+ } catch {
+ let nsError = error as NSError
+ fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
+ }
+ }
+ }
+}
+
+private let dateFormatter: DateFormatter = {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .short
+ formatter.timeStyle = .short
+ return formatter
+}()
diff --git a/Gas Man/Persistence.swift b/Gas Man/Persistence.swift
index e9118a4..535289a 100644
--- a/Gas Man/Persistence.swift
+++ b/Gas Man/Persistence.swift
@@ -9,49 +9,31 @@ import CoreData
struct PersistenceController {
static let shared = PersistenceController()
-
- @MainActor
- static let preview: PersistenceController = {
- let result = PersistenceController(inMemory: true)
- let viewContext = result.container.viewContext
- for _ in 0..<10 {
- let newItem = Item(context: viewContext)
- newItem.timestamp = Date()
- }
- do {
- try viewContext.save()
- } catch {
- // Replace this implementation with code to handle the error appropriately.
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
- let nsError = error as NSError
- fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
- }
- return result
- }()
-
+
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
+ // "GasMan" must match the name of your .xcdatamodeld file.
container = NSPersistentCloudKitContainer(name: "Gas_Man")
+
if inMemory {
- container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
+ container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
}
- container.loadPersistentStores(completionHandler: { (storeDescription, error) in
+
+ // Enable CloudKit options if necessary
+ guard let description = container.persistentStoreDescriptions.first else {
+ fatalError("No persistent store description found.")
+ }
+ description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
+ description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.pro.thelinux.GasMan") // Update with your container ID
+
+ container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
- // Replace this implementation with code to handle the error appropriately.
- // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
-
- /*
- Typical reasons for an error here include:
- * The parent directory does not exist, cannot be created, or disallows writing.
- * The persistent store is not accessible, due to permissions or data protection when the device is locked.
- * The device is out of space.
- * The store could not be migrated to the current model version.
- Check the error message to determine what the actual problem was.
- */
fatalError("Unresolved error \(error), \(error.userInfo)")
}
- })
+ }
+
+ // Automatically merge changes from iCloud
container.viewContext.automaticallyMergesChangesFromParent = true
}
}