Add CSV export for days
This commit is contained in:
parent
00e4da3f21
commit
e9f6bafe33
@ -49,6 +49,8 @@
|
|||||||
E2E69B682A529FA800C6035E /* TransferHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B672A529FA800C6035E /* TransferHandler.swift */; };
|
E2E69B682A529FA800C6035E /* TransferHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E69B672A529FA800C6035E /* TransferHandler.swift */; };
|
||||||
E2FD1D6B2D4CE64300B48627 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = E2FD1D6A2D4CE64300B48627 /* ZIPFoundation */; };
|
E2FD1D6B2D4CE64300B48627 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = E2FD1D6A2D4CE64300B48627 /* ZIPFoundation */; };
|
||||||
E2FD1D6F2D4CE83700B48627 /* ExportSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D6E2D4CE83700B48627 /* ExportSheet.swift */; };
|
E2FD1D6F2D4CE83700B48627 /* ExportSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D6E2D4CE83700B48627 /* ExportSheet.swift */; };
|
||||||
|
E2FD1D712D4CF5EB00B48627 /* CsvConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D702D4CF5E700B48627 /* CsvConverter.swift */; };
|
||||||
|
E2FD1D732D4CF82100B48627 /* ShareCsvSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D722D4CF81900B48627 /* ShareCsvSheet.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -93,6 +95,8 @@
|
|||||||
E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDevice.swift; sourceTree = "<group>"; };
|
E2E69B652A4DA48B00C6035E /* BluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDevice.swift; sourceTree = "<group>"; };
|
||||||
E2E69B672A529FA800C6035E /* TransferHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferHandler.swift; sourceTree = "<group>"; };
|
E2E69B672A529FA800C6035E /* TransferHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferHandler.swift; sourceTree = "<group>"; };
|
||||||
E2FD1D6E2D4CE83700B48627 /* ExportSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSheet.swift; sourceTree = "<group>"; };
|
E2FD1D6E2D4CE83700B48627 /* ExportSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportSheet.swift; sourceTree = "<group>"; };
|
||||||
|
E2FD1D702D4CF5E700B48627 /* CsvConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CsvConverter.swift; sourceTree = "<group>"; };
|
||||||
|
E2FD1D722D4CF81900B48627 /* ShareCsvSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareCsvSheet.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -112,6 +116,7 @@
|
|||||||
88404DD92A2F4DB100D30244 /* Storage */ = {
|
88404DD92A2F4DB100D30244 /* Storage */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2FD1D702D4CF5E700B48627 /* CsvConverter.swift */,
|
||||||
88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */,
|
88404DDA2A2F4DCA00D30244 /* MeasurementDailyCount.swift */,
|
||||||
88CDE0672A2698B400114294 /* PersistentStorage.swift */,
|
88CDE0672A2698B400114294 /* PersistentStorage.swift */,
|
||||||
E2A553F82A399F58005204C3 /* Log.swift */,
|
E2A553F82A399F58005204C3 /* Log.swift */,
|
||||||
@ -196,6 +201,7 @@
|
|||||||
E2A554042A4ADA93005204C3 /* TransferView.swift */,
|
E2A554042A4ADA93005204C3 /* TransferView.swift */,
|
||||||
E2E69B5F2A4CD48F00C6035E /* IconAndTextView.swift */,
|
E2E69B5F2A4CD48F00C6035E /* IconAndTextView.swift */,
|
||||||
E2FD1D6E2D4CE83700B48627 /* ExportSheet.swift */,
|
E2FD1D6E2D4CE83700B48627 /* ExportSheet.swift */,
|
||||||
|
E2FD1D722D4CF81900B48627 /* ShareCsvSheet.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -312,6 +318,7 @@
|
|||||||
E2A554142A4C9C96005204C3 /* DeviceDataRequest.swift in Sources */,
|
E2A554142A4C9C96005204C3 /* DeviceDataRequest.swift in Sources */,
|
||||||
88CDE0512A2508E900114294 /* ContentView.swift in Sources */,
|
88CDE0512A2508E900114294 /* ContentView.swift in Sources */,
|
||||||
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */,
|
88CDE0702A28AEA300114294 /* TemperatureMeasurement.swift in Sources */,
|
||||||
|
E2FD1D732D4CF82100B48627 /* ShareCsvSheet.swift in Sources */,
|
||||||
E2A554162A4C9D2E005204C3 /* DeviceDataResetRequest.swift in Sources */,
|
E2A554162A4C9D2E005204C3 /* DeviceDataResetRequest.swift in Sources */,
|
||||||
88404DE12A31CA6B00D30244 /* BluetoothResponseType.swift in Sources */,
|
88404DE12A31CA6B00D30244 /* BluetoothResponseType.swift in Sources */,
|
||||||
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */,
|
88404DDF2A2F68E100D30244 /* TemperatureDayOverview.swift in Sources */,
|
||||||
@ -337,6 +344,7 @@
|
|||||||
88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */,
|
88404DD22A2F0D8F00D30244 /* Double+Extensions.swift in Sources */,
|
||||||
E2A554102A4C9C68005204C3 /* DeviceRequest.swift in Sources */,
|
E2A554102A4C9C68005204C3 /* DeviceRequest.swift in Sources */,
|
||||||
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
E253A9222A2B39B700EC6B28 /* Color+Extensions.swift in Sources */,
|
||||||
|
E2FD1D712D4CF5EB00B48627 /* CsvConverter.swift in Sources */,
|
||||||
E2E69B682A529FA800C6035E /* TransferHandler.swift in Sources */,
|
E2E69B682A529FA800C6035E /* TransferHandler.swift in Sources */,
|
||||||
88404DEB2A37BE3000D30244 /* DeviceWakeCause.swift in Sources */,
|
88404DEB2A37BE3000D30244 /* DeviceWakeCause.swift in Sources */,
|
||||||
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
88CDE06D2A28A92000114294 /* DeviceInfo.swift in Sources */,
|
||||||
|
Binary file not shown.
40
TempTrack/Storage/CsvConverter.swift
Normal file
40
TempTrack/Storage/CsvConverter.swift
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct CsvConverter {
|
||||||
|
|
||||||
|
private let dateFormatter: DateFormatter
|
||||||
|
|
||||||
|
init() {
|
||||||
|
dateFormatter = .init()
|
||||||
|
dateFormatter.dateStyle = .short
|
||||||
|
dateFormatter.timeStyle = .medium
|
||||||
|
}
|
||||||
|
|
||||||
|
private var header: String {
|
||||||
|
"Index;Date;Sensor 0;Sensor 1\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert(measurements: [TemperatureMeasurement]) -> String {
|
||||||
|
header + measurements.enumerated().map { (index, measurement) -> String in
|
||||||
|
let parts: [String] = [
|
||||||
|
"\(index)",
|
||||||
|
dateFormatter.string(from: measurement.date),
|
||||||
|
convert(measurement.sensor0),
|
||||||
|
convert(measurement.sensor1)
|
||||||
|
]
|
||||||
|
|
||||||
|
return parts.joined(separator: ";")
|
||||||
|
}.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convert(_ value: TemperatureValue) -> String {
|
||||||
|
switch value {
|
||||||
|
case .notFound:
|
||||||
|
return ""
|
||||||
|
case .invalidMeasurement:
|
||||||
|
return "?"
|
||||||
|
case .value(let value):
|
||||||
|
return String(format:"%.1f", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
private let df: DateFormatter = {
|
private let df: DateFormatter = {
|
||||||
let df = DateFormatter()
|
let df = DateFormatter()
|
||||||
@ -21,20 +22,46 @@ struct DayView: View {
|
|||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
var storage: PersistentStorage
|
var storage: PersistentStorage
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showShareSheet = false
|
||||||
|
|
||||||
|
private let title: String
|
||||||
|
|
||||||
|
init(dateIndex: Int) {
|
||||||
|
self.dateIndex = dateIndex
|
||||||
|
self.title = Date(dateIndex: dateIndex)
|
||||||
|
.formatted(date: .abbreviated, time: .omitted)
|
||||||
|
}
|
||||||
|
|
||||||
var entries: [TemperatureMeasurement] {
|
var entries: [TemperatureMeasurement] {
|
||||||
storage.loadMeasurements(for: dateIndex)
|
storage.loadMeasurements(for: dateIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
VStack {
|
||||||
TemperatureDayOverview(points: entries)
|
TemperatureDayOverview(points: entries)
|
||||||
List(entries) { entry in
|
List(entries) { entry in
|
||||||
HStack {
|
HStack {
|
||||||
Text(df.string(from: entry.date))
|
Text(entry.date.formatted(date: .omitted, time: .standard))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(entry.displayText)
|
Text(entry.displayText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.navigationTitle(title)
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem {
|
||||||
|
Button(action: { showShareSheet = true }) {
|
||||||
|
Label("Export", systemSymbol: .arrowUpDoc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showShareSheet) {
|
||||||
|
ShareCsvSheet(isPresented: $showShareSheet,
|
||||||
|
measurements: entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DayView_Previews: PreviewProvider {
|
struct DayView_Previews: PreviewProvider {
|
||||||
|
@ -39,6 +39,7 @@ struct ExportSheet: View {
|
|||||||
Button("Delete archive", action: deleteArchive)
|
Button("Delete archive", action: deleteArchive)
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
Button("Dismiss", action: { isPresented = false })
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
91
TempTrack/Views/ShareCsvSheet.swift
Normal file
91
TempTrack/Views/ShareCsvSheet.swift
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ShareCsvSheet: View {
|
||||||
|
|
||||||
|
@Binding var isPresented: Bool
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isCreatingFile = false
|
||||||
|
|
||||||
|
@State var url: URL?
|
||||||
|
|
||||||
|
@State var error: String?
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
private var storage: PersistentStorage
|
||||||
|
|
||||||
|
let measurements: [TemperatureMeasurement]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text("Export")
|
||||||
|
.font(.title)
|
||||||
|
Text("Create a CSV file of all measurements for the day")
|
||||||
|
if isCreatingFile {
|
||||||
|
ProgressView()
|
||||||
|
Text("Creating file...")
|
||||||
|
}
|
||||||
|
if let error {
|
||||||
|
Text(error)
|
||||||
|
.foregroundStyle(.red)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Button("Create file", action: createFile)
|
||||||
|
.disabled(isCreatingFile)
|
||||||
|
.padding()
|
||||||
|
if let url {
|
||||||
|
ShareLink(item: url) {
|
||||||
|
Label("Share file", systemImage: "square.and.arrow.up")
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
Button("Delete file", action: deleteFile)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
Button("Dismiss", action: { isPresented = false })
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createFile() {
|
||||||
|
guard !isCreatingFile else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isCreatingFile = true
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
do {
|
||||||
|
let url = FileManager.default.temporaryDirectory.appendingPathComponent("export.csv")
|
||||||
|
let converter = CsvConverter()
|
||||||
|
let content = converter.convert(measurements: measurements)
|
||||||
|
try content.write(to: url, atomically: true, encoding: .utf8)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.url = url
|
||||||
|
self.error = nil
|
||||||
|
self.isCreatingFile = false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.error = "Failed to save file: \(error.localizedDescription)"
|
||||||
|
self.isCreatingFile = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteFile() {
|
||||||
|
guard let url else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
self.error = nil
|
||||||
|
self.url = nil
|
||||||
|
} catch {
|
||||||
|
self.error = "Failed to delete archive: \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ShareCsvSheet(isPresented: .constant(true),
|
||||||
|
measurements: [])
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user