From c97d37711c732f99773c70f1c09ecd19a4b61906 Mon Sep 17 00:00:00 2001 From: neon443 <69979447+neon443@users.noreply.github.com> Date: Sat, 3 May 2025 10:36:17 +0100 Subject: [PATCH] Fix import/Export events rewrote the daysTillEventt() --- NearFuture.xcodeproj/project.pbxproj | 10 ++ NearFuture/EventListView.swift | 4 +- NearFuture/ExportView.swift | 27 ++++ NearFuture/ImportView.swift | 61 +++++++++ NearFuture/Item.swift | 184 ++++++++------------------- NearFuture/SettingsView.swift | 48 +------ NearFuture/iCloudSettingsView.swift | 2 +- 7 files changed, 160 insertions(+), 176 deletions(-) create mode 100644 NearFuture/ExportView.swift create mode 100644 NearFuture/ImportView.swift diff --git a/NearFuture.xcodeproj/project.pbxproj b/NearFuture.xcodeproj/project.pbxproj index 460d5f5..28f3746 100644 --- a/NearFuture.xcodeproj/project.pbxproj +++ b/NearFuture.xcodeproj/project.pbxproj @@ -16,6 +16,9 @@ A920C2BE2D24021A00E4F9B1 /* SFSymbolsPicker in Frameworks */ = {isa = PBXBuildFile; productRef = A920C2BD2D24021A00E4F9B1 /* SFSymbolsPicker */; }; A920C2C12D2403CA00E4F9B1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C2C02D2403CA00E4F9B1 /* ContentView.swift */; }; A93BC0942D2B18A3002E8BBD /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93BC0932D2B18A3002E8BBD /* StatsView.swift */; }; + A973B26C2DC551310028F8A2 /* ImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A973B26B2DC551310028F8A2 /* ImportView.swift */; }; + A973B2702DC552EB0028F8A2 /* ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A973B26F2DC552EB0028F8A2 /* ExportView.swift */; }; + A973B2712DC553050028F8A2 /* ExportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A973B26F2DC552EB0028F8A2 /* ExportView.swift */; }; A977CC922DBBB48000DED8C0 /* ArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977CC912DBBB48000DED8C0 /* ArchiveView.swift */; }; A977CC9A2DBD74FE00DED8C0 /* HelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977CC992DBD74FE00DED8C0 /* HelpView.swift */; }; A979F57F2D26B1300094C0B3 /* EditEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A979F57E2D26B1300094C0B3 /* EditEventView.swift */; }; @@ -74,6 +77,8 @@ A920C2B72D2401A300E4F9B1 /* AddEventView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddEventView.swift; sourceTree = ""; }; A920C2C02D2403CA00E4F9B1 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; A93BC0932D2B18A3002E8BBD /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = ""; }; + A973B26B2DC551310028F8A2 /* ImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportView.swift; sourceTree = ""; }; + A973B26F2DC552EB0028F8A2 /* ExportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportView.swift; sourceTree = ""; }; A977CC912DBBB48000DED8C0 /* ArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveView.swift; sourceTree = ""; }; A977CC992DBD74FE00DED8C0 /* HelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelpView.swift; sourceTree = ""; }; A979F57E2D26B1300094C0B3 /* EditEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEventView.swift; sourceTree = ""; }; @@ -165,6 +170,8 @@ A93BC0932D2B18A3002E8BBD /* StatsView.swift */, A977CC992DBD74FE00DED8C0 /* HelpView.swift */, A920C2B42D2401A100E4F9B1 /* SettingsView.swift */, + A973B26B2DC551310028F8A2 /* ImportView.swift */, + A973B26F2DC552EB0028F8A2 /* ExportView.swift */, A985104D2DB256430013D5FF /* iCloudSettingsView.swift */, A980FC302D920097006A778F /* Info.plist */, A920C28D2D24011A00E4F9B1 /* Assets.xcassets */, @@ -361,8 +368,10 @@ buildActionMask = 2147483647; files = ( A920C2BB2D2401A400E4F9B1 /* AddEventView.swift in Sources */, + A973B26C2DC551310028F8A2 /* ImportView.swift in Sources */, A98510502DB263F00013D5FF /* EventListView.swift in Sources */, A920C2C12D2403CA00E4F9B1 /* ContentView.swift in Sources */, + A973B2712DC553050028F8A2 /* ExportView.swift in Sources */, A920C2B82D2401A300E4F9B1 /* SettingsView.swift in Sources */, A977CC9A2DBD74FE00DED8C0 /* HelpView.swift in Sources */, A920C28C2D24011400E4F9B1 /* Item.swift in Sources */, @@ -389,6 +398,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A973B2702DC552EB0028F8A2 /* ExportView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/NearFuture/EventListView.swift b/NearFuture/EventListView.swift index 3695f04..2083efa 100644 --- a/NearFuture/EventListView.swift +++ b/NearFuture/EventListView.swift @@ -76,7 +76,9 @@ struct EventListView: View { .foregroundStyle(.one) } Button() { - event.complete.toggle() + withAnimation { + event.complete.toggle() + } let eventToModify = viewModel.events.firstIndex() { currEvent in currEvent.id == event.id } diff --git a/NearFuture/ExportView.swift b/NearFuture/ExportView.swift new file mode 100644 index 0000000..ef30633 --- /dev/null +++ b/NearFuture/ExportView.swift @@ -0,0 +1,27 @@ +// +// ExportView.swift +// NearFuture +// +// Created by neon443 on 02/05/2025. +// + +import SwiftUI + +struct ExportView: View { + @ObservedObject var viewModel: EventViewModel + var body: some View { + List { + Button() { + UIPasteboard.general.string = viewModel.exportEvents() + } label: { + Label("Copy Events", systemImage: "document.on.clipboard") + } + Text(viewModel.exportEvents()) + .textSelection(.enabled) + } + } +} + +#Preview { + ExportView(viewModel: dummyEventViewModel()) +} diff --git a/NearFuture/ImportView.swift b/NearFuture/ImportView.swift new file mode 100644 index 0000000..03279ba --- /dev/null +++ b/NearFuture/ImportView.swift @@ -0,0 +1,61 @@ +// +// ImportView.swift +// NearFuture +// +// Created by neon443 on 02/05/2025. +// + +import SwiftUI + +struct ImportView: View { + @ObservedObject var viewModel: EventViewModel + @Binding var importStr: String + + @State private var image: String = "clock.fill" + @State private var text: String = "Ready..." + @State private var fgColor: Color = .yellow + + var body: some View { + List { + Section("Status") { + Label(text, systemImage: image) + .contentTransition(.numericText()) + .foregroundStyle(fgColor) + } + TextField("", text: $importStr) + Button() { + do throws { + try viewModel.importEvents(importStr) + withAnimation { + image = "checkmark.circle.fill" + text = "Complete" + fgColor = .green + } + } catch importError.invalidB64 { + withAnimation { + image = "xmark.app.fill" + text = "Invalid base64 input." + fgColor = .red + } + } catch { + withAnimation { + image = "xmark.app.fill" + text = error.localizedDescription + fgColor = .red + } + } + } label: { + Label("Import", systemImage: "tray.and.arrow.down.fill") + } + .disabled(importStr.isEmpty) + } + + } +} + +#Preview { + ImportView( + viewModel: dummyEventViewModel(), + importStr: .constant("kljadfskljafdlkj;==") + ) +} diff --git a/NearFuture/Item.swift b/NearFuture/Item.swift index 1d530fb..9412792 100644 --- a/NearFuture/Item.swift +++ b/NearFuture/Item.swift @@ -35,7 +35,22 @@ struct Event: Identifiable, Codable { } } -struct ColorCodable: Codable { +struct ColorCodable: Codable, Equatable { + init(_ color: Color) { + let uiColor = UIColor(color) + var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 1.0 + uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) + + self.red = Double(r) + self.green = Double(g) + self.blue = Double(b) + } + init(red: Double, green: Double, blue: Double) { + self.red = red + self.green = green + self.blue = blue + } + var red: Double var green: Double var blue: Double @@ -57,103 +72,34 @@ struct ColorCodable: Codable { self.blue = cc.blue } } - - init(_ color: Color) { - let uiColor = UIColor(color) - var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 1.0 - uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) - - self.red = Double(r) - self.green = Double(g) - self.blue = Double(b) - } - init(red: Double, green: Double, blue: Double) { - self.red = red - self.green = green - self.blue = blue - } } -func daysUntilEvent(_ eventDate: Date) -> (short: String, long: String) { +func daysUntilEvent(_ eventDate: Date) -> (long: String, short: String) { let calendar = Calendar.current - let currentDate = Date() - let components = calendar.dateComponents([.day, .hour, .minute], from: currentDate, to: eventDate) - guard let days = components.day else { - return ("N/A", "N/A") - } - guard let hours = components.hour else { - return ("N/A", "N/A") - } - guard let minutes = components.minute else { - return ("N/A", "N/A") - } + let now = Date() - enum RetUnit { - case days - case hours - case minutes - } - func ret(days: Int = 0, hours: Int = 0, minutes: Int = 0, unit: RetUnit) -> (String, String) { - var future: Bool = true - var days = days - var hours = hours - var minutes = minutes - if days < 0 || hours < 0 || minutes < 0 { - future = false - days.negate() - hours.negate() - minutes.negate() - } else { - future = true - } - switch unit { - case .days: - return ( - "\(future ? "" : "-")\(days)d", - "\(days) day\(plu(days)) \(future ? "" : "ago")" - ) - case .hours: - return ( - "\(future ? "" : "-")\(hours)h", - "\(hours) hour\(plu(hours)) \(future ? "" : "ago")" - ) - case .minutes: - return ( - "\(future ? "" : "-")\(minutes)m", - "\(minutes) min\(plu(minutes)) \(future ? "" : "ago")" - ) - } - } - switch eventDate > Date() { - case true: - //future - if days == 0 { - if hours == 0 { - //less than 1h - return ret(minutes: minutes, unit: .minutes) - } else { - //less than 24h - return ret(hours: hours, unit: .hours) - } - } else { - //grater than 24h - return ret(days: days, unit: .days) - } - case false: + let isToday = calendar.isDate(now, inSameDayAs: eventDate) + let components = calendar.dateComponents([.second, .day], from: now, to: eventDate) + + guard !isToday else { return ("Today", "Today") } + let secsComponents = eventDate.timeIntervalSinceNow + guard let daysCompontents = components.day else { return ("N/A", "N/A") } + let secs = Double(secsComponents) + var days = 0 + var long = "" + var short = "" + if secs < 0 { //past - if days == 0 { - if hours == 0 { - //less than 1h - return ret(minutes: minutes, unit: .minutes) - } else { - //less than 24h - return ret(hours: hours, unit: .hours) - } - } else { - //grater than 24h - return ret(days: days, unit: .days) - } + days = Int(floor(secs/86400)) + long = "\(-days) day\(plu(days)) ago" + short = "\(days)d" + } else { + //future + days = Int(ceil(secs/86400)) + long = "\(days) day\(plu(days))" + short = "\(days)d" } + return (long, short) } class EventViewModel: ObservableObject { @@ -297,57 +243,25 @@ class EventViewModel: ObservableObject { saveEvents() } - func exportEvents() -> String? { + func exportEvents() -> String { let encoder = JSONEncoder() - - // Custom date encoding strategy to handle date formatting - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" - encoder.dateEncodingStrategy = .formatted(dateFormatter) - - do { - // Encode the events array to JSON data - let encodedData = try encoder.encode(events) - - // Convert the JSON data to a string - if let jsonString = String(data: encodedData, encoding: .utf8) { - return jsonString - } else { - print("Failed to convert encoded data to string") - return nil - } - } catch { - print("Failed to encode events: \(error.localizedDescription)") - return nil + if let json = try? encoder.encode(self.events) { + return "\(json.base64EncodedString())" } + return "" } - func importEvents(_ imp: String) { - guard let impData = imp.data(using: .utf8) else { - print("Failed to convert string to data") - return + func importEvents(_ imported: String) throws { + guard let data = Data(base64Encoded: imported) else { + throw importError.invalidB64 } - - // Create a JSONDecoder let decoder = JSONDecoder() - - // Add a custom date formatter for decoding the date string - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // Adjust this to the date format you're using - decoder.dateDecodingStrategy = .formatted(dateFormatter) - do { - // Attempt to decode the events from the provided data - let decoded = try decoder.decode([Event].self, from: impData) - print("Successfully decoded events: \(decoded)") - - // Save and reload after importing events + let decoded = try decoder.decode([Event].self, from: data) self.events = decoded saveEvents() - loadEvents() } catch { - // Print error if decoding fails - print("Failed to decode events: \(error.localizedDescription)") + throw error } } @@ -444,3 +358,7 @@ func plu(_ inp: Int) -> String { if inp < 0 { input.negate() } return "\(input == 1 ? "" : "s")" } + +public enum importError: Error { + case invalidB64 +} diff --git a/NearFuture/SettingsView.swift b/NearFuture/SettingsView.swift index 884c608..348d647 100644 --- a/NearFuture/SettingsView.swift +++ b/NearFuture/SettingsView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SettingsView: View { - @State var viewModel: EventViewModel + @ObservedObject var viewModel: EventViewModel @State private var hasUbiquitous: Bool = false @State private var lastSyncWasSuccessful: Bool = false @@ -68,47 +68,17 @@ struct SettingsView: View { viewModel.sync() updateStatus() } - NavigationLink() { - NavigationStack() { - Button() { - UIPasteboard.general.string = "\(viewModel.exportEvents() ?? "")" - print(viewModel.exportEvents() as Any) - } label: { - Text("copy") - } - Text("\(viewModel.exportEvents() ?? "")") - } + ImportView(viewModel: viewModel, importStr: $importStr) } label: { - Image(systemName: "list.bullet.rectangle") - Text("Export events") + Label("Import Events", systemImage: "tray.and.arrow.down.fill") + .foregroundStyle(.one) } NavigationLink() { - NavigationStack() { - VStack { - TextEditor(text: $importStr) - .foregroundStyle(.foreground, .gray) - .background(.gray) - .frame(width: 200, height: 400) - .shadow(radius: 5) - Button() { - viewModel.importEvents(importStr) - } label: { - Text("import events") - } - .buttonStyle(BorderedProminentButtonStyle()) - Button() { - if let pb = UIPasteboard.general.string { - print(pb) - } - } label: { - Text("print pb") - } - } - } + ExportView(viewModel: viewModel) } label: { - Image(systemName: "square.and.arrow.down") - Text("Import events") + Label("Export Events", systemImage: "square.and.arrow.up") + .foregroundStyle(.one) } Section("Tip") { @@ -147,7 +117,3 @@ struct SettingsView: View { #Preview { SettingsView(viewModel: dummyEventViewModel()) } - -func test() -> Void { - -} diff --git a/NearFuture/iCloudSettingsView.swift b/NearFuture/iCloudSettingsView.swift index 03a0b72..4b924cd 100644 --- a/NearFuture/iCloudSettingsView.swift +++ b/NearFuture/iCloudSettingsView.swift @@ -191,6 +191,6 @@ struct iCloudSettingsView: View { lastSyncWasNormalAgo: .constant(true), localCountEqualToiCloud: .constant(true), icloudCountEqualToLocal: .constant(true), - updateStatus: test + updateStatus: {} ) }