diff --git a/NearFuture.xcodeproj/project.pbxproj b/NearFuture.xcodeproj/project.pbxproj index 8d55a3b..7508ead 100644 --- a/NearFuture.xcodeproj/project.pbxproj +++ b/NearFuture.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ A920C2BB2D2401A400E4F9B1 /* AddEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A920C2B72D2401A300E4F9B1 /* AddEventView.swift */; }; 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 */; }; A979F57F2D26B1300094C0B3 /* EditEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A979F57E2D26B1300094C0B3 /* EditEventView.swift */; }; A979F6052D270AF00094C0B3 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A979F6042D270AF00094C0B3 /* WidgetKit.framework */; }; A979F6072D270AF00094C0B3 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A979F6062D270AF00094C0B3 /* SwiftUI.framework */; }; @@ -60,6 +61,7 @@ A920C2B42D2401A100E4F9B1 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 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 = ""; }; A979F57E2D26B1300094C0B3 /* EditEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditEventView.swift; sourceTree = ""; }; A979F58B2D2700680094C0B3 /* NearFutureWidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgetsBundle.swift; sourceTree = ""; }; A979F58D2D2700680094C0B3 /* NearFutureWidgetsLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NearFutureWidgetsLiveActivity.swift; sourceTree = ""; }; @@ -124,6 +126,7 @@ A979F57E2D26B1300094C0B3 /* EditEventView.swift */, A920C2B72D2401A300E4F9B1 /* AddEventView.swift */, A920C2C02D2403CA00E4F9B1 /* ContentView.swift */, + A93BC0932D2B18A3002E8BBD /* StatsView.swift */, A920C2B42D2401A100E4F9B1 /* SettingsView.swift */, A920C2872D24011400E4F9B1 /* NearFutureApp.swift */, A920C28B2D24011400E4F9B1 /* Item.swift */, @@ -290,6 +293,7 @@ A920C2B82D2401A300E4F9B1 /* SettingsView.swift in Sources */, A920C28C2D24011400E4F9B1 /* Item.swift in Sources */, A920C2882D24011400E4F9B1 /* NearFutureApp.swift in Sources */, + A93BC0942D2B18A3002E8BBD /* StatsView.swift in Sources */, A979F57F2D26B1300094C0B3 /* EditEventView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -438,7 +442,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = NearFuture/NearFuture.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_ASSET_PATHS = "\"NearFuture/Preview Content\""; DEVELOPMENT_TEAM = P6PV2R9443; ENABLE_HARDENED_RUNTIME = NO; @@ -463,7 +467,9 @@ PRODUCT_BUNDLE_IDENTIFIER = dev.neon443.NearFuture; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -478,7 +484,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = NearFuture/NearFuture.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_ASSET_PATHS = "\"NearFuture/Preview Content\""; DEVELOPMENT_TEAM = P6PV2R9443; ENABLE_HARDENED_RUNTIME = NO; @@ -503,7 +509,9 @@ PRODUCT_BUNDLE_IDENTIFIER = dev.neon443.NearFuture; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -517,7 +525,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = NearFutureWidgets/NearFutureWidgetsExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = P6PV2R9443; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NearFutureWidgets/Info.plist; @@ -547,7 +555,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = NearFutureWidgets/NearFutureWidgetsExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 16; DEVELOPMENT_TEAM = P6PV2R9443; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = NearFutureWidgets/Info.plist; diff --git a/NearFuture.xcodeproj/project.xcworkspace/xcuserdata/neon443.xcuserdatad/UserInterfaceState.xcuserstate b/NearFuture.xcodeproj/project.xcworkspace/xcuserdata/neon443.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..c03860e Binary files /dev/null and b/NearFuture.xcodeproj/project.xcworkspace/xcuserdata/neon443.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/NearFuture.xcodeproj/xcshareddata/xcschemes/NearFuture.xcscheme b/NearFuture.xcodeproj/xcshareddata/xcschemes/NearFuture.xcscheme new file mode 100644 index 0000000..c7e1c25 --- /dev/null +++ b/NearFuture.xcodeproj/xcshareddata/xcschemes/NearFuture.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NearFuture.xcodeproj/xcshareddata/xcschemes/NearFutureWidgetsExtension.xcscheme b/NearFuture.xcodeproj/xcshareddata/xcschemes/NearFutureWidgetsExtension.xcscheme new file mode 100644 index 0000000..91a14d3 --- /dev/null +++ b/NearFuture.xcodeproj/xcshareddata/xcschemes/NearFutureWidgetsExtension.xcscheme @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NearFuture.xcodeproj/xcuserdata/neon443.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/NearFuture.xcodeproj/xcuserdata/neon443.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..84c4b29 --- /dev/null +++ b/NearFuture.xcodeproj/xcuserdata/neon443.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/NearFuture.xcodeproj/xcuserdata/neon443.xcuserdatad/xcschemes/xcschememanagement.plist b/NearFuture.xcodeproj/xcuserdata/neon443.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..3d4f3de --- /dev/null +++ b/NearFuture.xcodeproj/xcuserdata/neon443.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,32 @@ + + + + + SchemeUserState + + NearFuture.xcscheme_^#shared#^_ + + orderHint + 1 + + NearFutureWidgetsExtension.xcscheme_^#shared#^_ + + orderHint + 0 + + + SuppressBuildableAutocreation + + A920C2832D24011300E4F9B1 + + primary + + + A979F6012D270AF00094C0B3 + + primary + + + + + diff --git a/NearFuture/.DS_Store b/NearFuture/.DS_Store new file mode 100644 index 0000000..c4df035 Binary files /dev/null and b/NearFuture/.DS_Store differ diff --git a/NearFuture/AddEventView.swift b/NearFuture/AddEventView.swift index f095596..8bae115 100644 --- a/NearFuture/AddEventView.swift +++ b/NearFuture/AddEventView.swift @@ -45,11 +45,11 @@ struct AddEventView: View { Image(systemName: eventSymbol) .resizable() .scaledToFit() - .frame(width: 25, height: 25) + .frame(width: 20, height: 20) .foregroundStyle(eventColor) } - // .frame(width: 30) - .buttonStyle(.bordered) + .frame(width: 20) + .buttonStyle(.borderless) .sheet(isPresented: $isSymbolPickerPresented) { SymbolsPicker( selection: $eventSymbol, @@ -88,8 +88,12 @@ struct AddEventView: View { // date picker - DatePicker("Event Date", selection: $eventDate, displayedComponents: .date) - .datePickerStyle(WheelDatePickerStyle()) + HStack { + Spacer() + DatePicker("", selection: $eventDate, displayedComponents: .date) + .datePickerStyle(WheelDatePickerStyle()) + Spacer() + } // re-ocurrence Picker Picker("Recurrence", selection: $eventRecurrence) { @@ -122,7 +126,6 @@ struct AddEventView: View { Text("Save Event") .font(.headline) .cornerRadius(10) - .shadow(radius: 10) .buttonStyle(BorderedProminentButtonStyle()) } .disabled(eventName.isEmpty) diff --git a/NearFuture/ContentView.swift b/NearFuture/ContentView.swift index 684df33..3060e01 100644 --- a/NearFuture/ContentView.swift +++ b/NearFuture/ContentView.swift @@ -8,64 +8,6 @@ import SwiftUI import SwiftData -//struct ContentView: View { -// @Environment(\.modelContext) private var modelContext -// @Query private var items: [Item] -// -// 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)) -// } -// } -// .onDelete(perform: deleteItems) -// } -//#if os(macOS) -// .navigationSplitViewColumnWidth(min: 180, ideal: 200) -//#endif -// .toolbar { -//#if os(iOS) -// ToolbarItem(placement: .navigationBarTrailing) { -// EditButton() -// } -//#endif -// ToolbarItem { -// Button(action: addItem) { -// Label("Add Item", systemImage: "plus") -// } -// } -// } -// } 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]) -// } -// } -// } -//} -// -//#Preview { -// ContentView() -// .modelContainer(for: Item.self, inMemory: true) -//} - - struct ContentView: View { @StateObject private var viewModel = EventViewModel() @State private var eventName = "" @@ -124,94 +66,104 @@ struct ContentView: View { private enum Field { case Search } + var body: some View { - NavigationView { - ZStack { - backgroundGradient - .ignoresSafeArea(.all) - VStack { - ZStack { - TextField( - "\(Image(systemName: "magnifyingglass")) Search", - text: $searchInput - ) - .padding(.trailing, searchInput.isEmpty ? 0 : 30) - .animation(.spring, value: searchInput) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .focused($focusedField, equals: Field.Search) - .onSubmit { - focusedField = nil + TabView { + NavigationView { + ZStack { + backgroundGradient + .ignoresSafeArea(.all) + VStack { + ZStack { + TextField( + "\(Image(systemName: "magnifyingglass")) Search", + text: $searchInput + ) + .padding(.trailing, searchInput.isEmpty ? 0 : 30) + .animation(.spring, value: searchInput) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .focused($focusedField, equals: Field.Search) + .onSubmit { + focusedField = nil + } + MagicClearButton(text: $searchInput) + } + .padding(.horizontal) + List { + ForEach(filteredEvents) { event in + EventListView(viewModel: viewModel, event: event) + } + .onDelete(perform: viewModel.removeEvent) + if !searchInput.isEmpty { + HStack { + Image(systemName: "questionmark.square.dashed") + .resizable() + .scaledToFit() + .frame(width: 30, height: 30) + .padding(.trailing) + Text("Can't find what you're looking for?") + } + Text("Tip: The Search bar searches event names and descriptions") + Button() { + searchInput = "" + focusedField = nil + } label: { + HStack { + Image(systemName: "xmark") + Text("Clear Filters") + } + .foregroundStyle(Color.accentColor) + } + } } - MagicClearButton(text: $searchInput) } - .padding(.horizontal) - List { - ForEach(filteredEvents) { event in - EventListView(viewModel: viewModel, event: event) - } - .onDelete(perform: viewModel.removeEvent) - if !searchInput.isEmpty { - HStack { - Image(systemName: "questionmark.square.dashed") + .navigationTitle("Near Future") + .navigationBarTitleDisplayMode(.inline) + .sheet(isPresented: $showingAddEventView) { + AddEventView( + viewModel: viewModel, + eventName: $eventName, + eventSymbol: $eventSymbol, + eventColor: $eventColor, + eventDescription: $eventDescription, + eventDate: $eventDate, + eventRecurrence: $eventRecurrence, + adding: true //adding event + ) + } + .sheet( + isPresented: $showSettings) { + SettingsView( + viewModel: viewModel + ) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button() { + showingAddEventView.toggle() + } label: { + Image(systemName: "plus.circle") .resizable() .scaledToFit() - .frame(width: 30, height: 30) - .padding(.trailing) - Text("Can't find what you're looking for?") } - Text("Tip: The Search bar searches event names and descriptions") + } + ToolbarItem(placement: .topBarLeading) { Button() { - searchInput = "" - focusedField = nil + showSettings.toggle() } label: { - HStack { - Image(systemName: "xmark") - Text("Clear Filters") - } - .foregroundStyle(Color.accentColor) + Image(systemName: "gear") } } } } - .navigationTitle("Near Future") - .navigationBarTitleDisplayMode(.inline) - .sheet(isPresented: $showingAddEventView) { - AddEventView( - viewModel: viewModel, - eventName: $eventName, - eventSymbol: $eventSymbol, - eventColor: $eventColor, - eventDescription: $eventDescription, - eventDate: $eventDate, - eventRecurrence: $eventRecurrence, - adding: true //adding event - ) - } - .sheet( - isPresented: $showSettings) { - SettingsView( - viewModel: viewModel - ) - } - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button() { - showingAddEventView.toggle() - } label: { - Image(systemName: "plus.circle") - .resizable() - .scaledToFit() - } - } - ToolbarItem(placement: .topBarLeading) { - Button() { - showSettings.toggle() - } label: { - Image(systemName: "gear") - } - } - } } + .tabItem { + Label("Home", systemImage: "house") + } + StatsView(viewModel: viewModel) + .tabItem { + Label("Statistics", systemImage: "chart.pie") + } } } } diff --git a/NearFuture/Item.swift b/NearFuture/Item.swift index 95fe0fd..b22c631 100644 --- a/NearFuture/Item.swift +++ b/NearFuture/Item.swift @@ -38,7 +38,7 @@ struct ColorCodable: Codable { var green: Double var blue: Double var alpha: Double - //for the brainrotted: alpha is the opacity/transparency of the color, + //for the brainrot kids: alpha is the opacity/transparency of the color, //alpha == 0 completely transparent //alpha == 1 completely opaque @@ -217,6 +217,60 @@ class EventViewModel: ObservableObject { saveEvents() } + 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 + } + } + + func importEvents(_ imp: String) { + guard let impData = imp.data(using: .utf8) else { + print("Failed to convert string to data") + return + } + + // 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 + self.events = decoded + saveEvents() + loadEvents() + } catch { + // Print error if decoding fails + print("Failed to decode events: \(error.localizedDescription)") + } + } + //MARK: Danger Zone func dangerClearLocalData() { UserDefaults.standard.removeObject(forKey: "events") diff --git a/NearFuture/SettingsView.swift b/NearFuture/SettingsView.swift index 7e0b3ed..5d424a8 100644 --- a/NearFuture/SettingsView.swift +++ b/NearFuture/SettingsView.swift @@ -16,6 +16,7 @@ struct SettingsView: View { @State private var lastSyncWasNormalAgo: Bool = false @State private var localCountEqualToiCloud: Bool = false @State private var icloudCountEqualToLocal: Bool = false + @State private var importStr: String = "" func updateStatus() { let vm = viewModel @@ -67,6 +68,48 @@ struct SettingsView: View { updateStatus() } + NavigationLink() { + NavigationView() { + Button() { + UIPasteboard.general.string = "\(viewModel.exportEvents())" + print(viewModel.exportEvents()) + } label: { + Text("copy") + } + Text("\(viewModel.exportEvents())") + } + } label: { + Image(systemName: "list.bullet.rectangle") + Text("Export events") + } + NavigationLink() { + NavigationView() { + 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") + } + } + } + } label: { + Image(systemName: "square.and.arrow.down") + Text("Import events") + } + Section("Danger Zone") { Button("Delete local data", role: .destructive) { viewModel.dangerClearLocalData() diff --git a/NearFuture/StatsView.swift b/NearFuture/StatsView.swift new file mode 100644 index 0000000..c511dac --- /dev/null +++ b/NearFuture/StatsView.swift @@ -0,0 +1,67 @@ +// +// StatsView.swift +// NearFuture +// +// Created by Nihaal Sharma on 05/01/2025. +// + +import SwiftUI +import Charts + +struct StatsView: View { + @ObservedObject var viewModel: EventViewModel + + var body: some View { + NavigationView { + List { + Section(header: Text("Upcoming Events")) { + let upcomingEvents = viewModel.events.filter { $0.date > Date() } + Text("\(upcomingEvents.count) upcoming event\(upcomingEvents.count == 1 ? "" : "s")") + .font(.headline) + .foregroundStyle(Color.accentColor) + let pastEvents = viewModel.events.filter { $0.date < Date() } + Text("\(pastEvents.count) past event\(pastEvents.count == 1 ? "" : "s")") + .foregroundStyle(.gray) + } + + Section("Events by Month") { + let eventsByMonth = Dictionary(grouping: viewModel.events, by: { $0.date }) + ForEach(eventsByMonth.keys.sorted(), id: \.self) { month in + let count = eventsByMonth[month]?.count ?? 0 + Text("\(count) - \(month.formatted(date: .long, time: .omitted))") + } + } + + Section("Event Count") { + let eventCount = viewModel.events.count + Text("\(eventCount) event\(eventCount == 1 ? "" : "s")") + .font(.headline) + .foregroundStyle(Color.accentColor) + + ForEach(Event.RecurrenceType.allCases, id: \.self) { recurrence in + let count = viewModel.events.filter { $0.recurrence == recurrence }.count + let recurrenceStr = recurrence.rawValue.capitalized + var description: String { + if recurrenceStr == "None" { + return "One-Time event\(count == 1 ? "" : "s")" + } else { + return "\(recurrenceStr) event\(count == 1 ? "" : "s")" + } + } + Text("\(count) \(description)") + .font(.subheadline) + .foregroundStyle(Color.secondary) + } + } + } + .navigationTitle("Statistics") + .navigationBarTitleDisplayMode(.inline) + } + } +} + +#Preview { + StatsView( + viewModel: EventViewModel() + ) +}