From c77a68954c63217c3978b69ed8b25e048427160a Mon Sep 17 00:00:00 2001 From: Rishabh Bansal <122753012+risban933@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:46:06 -0700 Subject: [PATCH 1/4] docs: add README --- Epoch/Epoch/AIAssistant.swift | 32 +++++++ Epoch/Epoch/ContentView.swift | 61 ------------- Epoch/Epoch/EpochApp.swift | 28 ++---- Epoch/Epoch/EventKitManager.swift | 64 +++++++++++++ Epoch/Epoch/Info.plist | 8 +- .../Epoch/Intents/AddQuickCaptureIntent.swift | 14 +++ Epoch/Epoch/Intents/PlanTonightIntent.swift | 29 ++++++ .../Epoch/Intents/WhatShouldIPrepIntent.swift | 25 ++++++ Epoch/Epoch/Item.swift | 18 ---- Epoch/Epoch/KeychainService.swift | 32 +++++++ Epoch/Epoch/Models.swift | 38 ++++++++ Epoch/Epoch/NLRouter.swift | 26 ++++++ Epoch/Epoch/Notifications.swift | 45 ++++++++++ Epoch/Epoch/Persistence.swift | 90 +++++++++++++++++++ Epoch/Epoch/PlannerEngine.swift | 24 +++++ Epoch/Epoch/Utilities.swift | 29 ++++++ Epoch/Epoch/Views/ClassCalendarsView.swift | 36 ++++++++ Epoch/Epoch/Views/DashboardView.swift | 54 +++++++++++ Epoch/Epoch/Views/QuickAddView.swift | 22 +++++ Epoch/Epoch/Views/SchedulesView.swift | 23 +++++ Epoch/Epoch/Views/SettingsView.swift | 33 +++++++ README.md | 4 + 22 files changed, 632 insertions(+), 103 deletions(-) create mode 100644 Epoch/Epoch/AIAssistant.swift delete mode 100644 Epoch/Epoch/ContentView.swift create mode 100644 Epoch/Epoch/EventKitManager.swift create mode 100644 Epoch/Epoch/Intents/AddQuickCaptureIntent.swift create mode 100644 Epoch/Epoch/Intents/PlanTonightIntent.swift create mode 100644 Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift delete mode 100644 Epoch/Epoch/Item.swift create mode 100644 Epoch/Epoch/KeychainService.swift create mode 100644 Epoch/Epoch/Models.swift create mode 100644 Epoch/Epoch/NLRouter.swift create mode 100644 Epoch/Epoch/Notifications.swift create mode 100644 Epoch/Epoch/Persistence.swift create mode 100644 Epoch/Epoch/PlannerEngine.swift create mode 100644 Epoch/Epoch/Utilities.swift create mode 100644 Epoch/Epoch/Views/ClassCalendarsView.swift create mode 100644 Epoch/Epoch/Views/DashboardView.swift create mode 100644 Epoch/Epoch/Views/QuickAddView.swift create mode 100644 Epoch/Epoch/Views/SchedulesView.swift create mode 100644 Epoch/Epoch/Views/SettingsView.swift create mode 100644 README.md diff --git a/Epoch/Epoch/AIAssistant.swift b/Epoch/Epoch/AIAssistant.swift new file mode 100644 index 0000000..0cc9dd0 --- /dev/null +++ b/Epoch/Epoch/AIAssistant.swift @@ -0,0 +1,32 @@ +import Foundation + +/// Minimal URLSession based AI planner client. +struct AIAssistant { + struct Request: Codable { + var classes: [String] + var tasks: [String] + var start: Date + var end: Date + } + struct Response: Codable { + struct Block: Codable { let title: String; let start: Date; let end: Date } + var blocks: [Block] + } + + var baseURL: URL + var apiKey: String + + func generatePlan(request: Request) async throws -> Response { + var req = URLRequest(url: baseURL) + req.httpMethod = "POST" + req.addValue("application/json", forHTTPHeaderField: "Content-Type") + req.addValue(apiKey, forHTTPHeaderField: "X-API-Key") + req.httpBody = try JSONEncoder().encode(request) + + let (data, response) = try await URLSession.shared.data(for: req) + guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { + throw URLError(.badServerResponse) + } + return try JSONDecoder().decode(Response.self, from: data) + } +} diff --git a/Epoch/Epoch/ContentView.swift b/Epoch/Epoch/ContentView.swift deleted file mode 100644 index 9d07408..0000000 --- a/Epoch/Epoch/ContentView.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// ContentView.swift -// Epoch -// -// Created by Rishabh Bansal on 8/10/25. -// - -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) - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - EditButton() - } - 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) -} diff --git a/Epoch/Epoch/EpochApp.swift b/Epoch/Epoch/EpochApp.swift index b85023e..8b3ae6e 100644 --- a/Epoch/Epoch/EpochApp.swift +++ b/Epoch/Epoch/EpochApp.swift @@ -1,32 +1,16 @@ -// -// EpochApp.swift -// Epoch -// -// Created by Rishabh Bansal on 8/10/25. -// - import SwiftUI -import SwiftData @main struct EpochApp: App { - var sharedModelContainer: ModelContainer = { - let schema = Schema([ - Item.self, - ]) - let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false) - - do { - return try ModelContainer(for: schema, configurations: [modelConfiguration]) - } catch { - fatalError("Could not create ModelContainer: \(error)") - } - }() + @StateObject private var settingsModel = SettingsModel() + @StateObject private var eventKit = EventKitManager() var body: some Scene { WindowGroup { - ContentView() + DashboardView() + .environmentObject(eventKit) + .environmentObject(settingsModel) + .task { await NotificationManager.shared.requestAuthorization() } } - .modelContainer(sharedModelContainer) } } diff --git a/Epoch/Epoch/EventKitManager.swift b/Epoch/Epoch/EventKitManager.swift new file mode 100644 index 0000000..9362b06 --- /dev/null +++ b/Epoch/Epoch/EventKitManager.swift @@ -0,0 +1,64 @@ +import Foundation +import EventKit + +/// Handles EventKit operations for calendars and reminders. +@MainActor +final class EventKitManager: ObservableObject { + private let store = EKEventStore() + var eventStore: EKEventStore { store } + @Published var studyCalendar: EKCalendar? + + init() { + Task { await loadStudyCalendar() } + } + + func requestAccess() async throws { + try await store.requestFullAccessToEvents() + try await store.requestFullAccessToReminders() + await loadStudyCalendar() + } + + func loadStudyCalendar() async { + let name = "Study Plan" + if let existing = store.calendars(for: .event).first(where: { $0.title == name }) { + studyCalendar = existing + return + } + let cal = EKCalendar(for: .event, eventStore: store) + cal.title = name + cal.source = store.defaultCalendarForNewEvents?.source + try? store.saveCalendar(cal, commit: true) + studyCalendar = cal + } + + func tomorrowClasses(from calendars: [EKCalendar]) -> [EKEvent] { + let start = DateUtils.startOfTomorrow() + let end = DateUtils.endOfTomorrow() + let predicate = store.predicateForEvents(withStart: start, end: end, calendars: calendars) + return store.events(matching: predicate) + } + + func addStudyEvent(title: String, start: Date, end: Date, note: String, reminderID: String?) throws { + guard let calendar = studyCalendar else { return } + let event = EKEvent(eventStore: store) + event.calendar = calendar + event.title = "Study: \(title)" + event.startDate = start + event.endDate = end + event.notes = "epoch://study/\(reminderID ?? UUID().uuidString)\n\(note)" + try store.save(event, span: .thisEvent) + } + + func addReminder(text: String, due: Date?) throws -> EKReminder { + let reminder = EKReminder(eventStore: store) + reminder.title = text + reminder.calendar = store.defaultCalendarForNewReminders() + if let due { + reminder.dueDateComponents = Calendar.current.dateComponents([.year,.month,.day,.hour,.minute], from: due) + let alarm = EKAlarm(absoluteDate: due) + reminder.addAlarm(alarm) + } + try store.save(reminder, commit: true) + return reminder + } +} diff --git a/Epoch/Epoch/Info.plist b/Epoch/Epoch/Info.plist index 666cc76..b7aea5a 100644 --- a/Epoch/Epoch/Info.plist +++ b/Epoch/Epoch/Info.plist @@ -2,7 +2,11 @@ - UIBackgroundModes - + UIBackgroundModes + + NSCalendarsFullAccessUsageDescription + Access to calendars is needed to schedule study sessions. + NSRemindersFullAccessUsageDescription + Access to reminders lets Epoch plan tasks and deadlines. diff --git a/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift b/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift new file mode 100644 index 0000000..3314479 --- /dev/null +++ b/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift @@ -0,0 +1,14 @@ +import AppIntents +import EventKit + +struct AddQuickCaptureIntent: AppIntent { + static var title: LocalizedStringResource = "Add Quick Capture" + @Parameter(title: "Text") var text: String + + func perform() async throws -> some IntentResult & ReturnsValue { + let manager = EventKitManager() + try await manager.requestAccess() + let reminder = try manager.addReminder(text: text, due: nil) + return .result(value: "Added reminder \(reminder.title)") + } +} diff --git a/Epoch/Epoch/Intents/PlanTonightIntent.swift b/Epoch/Epoch/Intents/PlanTonightIntent.swift new file mode 100644 index 0000000..bd3be2c --- /dev/null +++ b/Epoch/Epoch/Intents/PlanTonightIntent.swift @@ -0,0 +1,29 @@ +import AppIntents +import EventKit + +struct PlanTonightIntent: AppIntent { + static var title: LocalizedStringResource = "Plan Tonight" + + func perform() async throws -> some IntentResult & ReturnsValue { + let manager = EventKitManager() + try await manager.requestAccess() + let settings = Settings() + let reminders = try await fetchReminders(manager: manager) + let busy: [EKEvent] = [] + let blocks = PlannerEngine().plan(reminders: reminders, busy: busy, settings: settings) + for block in blocks { + try manager.addStudyEvent(title: block.title, start: block.start, end: block.end, note: "", reminderID: block.reminderID) + } + return .result(value: "Planned \(blocks.count) blocks") + } + + private func fetchReminders(manager: EventKitManager) async throws -> [EKReminder] { + let predicate = EKEventStore().predicateForIncompleteReminders(withDueDateStarting: nil, ending: DateUtils.endOfTomorrow(), calendars: nil) + let reminders = try await withCheckedThrowingContinuation { cont in + EKEventStore().fetchReminders(matching: predicate) { rems in + cont.resume(returning: rems ?? []) + } + } + return reminders.filter { ($0.dueDateComponents?.date ?? Date()) <= DateUtils.endOfTomorrow() } + } +} diff --git a/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift b/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift new file mode 100644 index 0000000..937cc76 --- /dev/null +++ b/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift @@ -0,0 +1,25 @@ +import AppIntents +import EventKit + +struct WhatShouldIPrepIntent: AppIntent { + static var title: LocalizedStringResource = "What Should I Prep" + + func perform() async throws -> some IntentResult & ReturnsValue { + let manager = EventKitManager() + try await manager.requestAccess() + let reminders = try await fetchReminders() + let count = reminders.count + let firstClass = manager.tomorrowClasses(from: []) .first?.title ?? "none" + return .result(value: "You have \(count) items. First class: \(firstClass)") + } + + private func fetchReminders() async throws -> [EKReminder] { + let predicate = EKEventStore().predicateForIncompleteReminders(withDueDateStarting: nil, ending: DateUtils.endOfTomorrow(), calendars: nil) + let reminders = try await withCheckedThrowingContinuation { cont in + EKEventStore().fetchReminders(matching: predicate) { rems in + cont.resume(returning: rems ?? []) + } + } + return reminders + } +} diff --git a/Epoch/Epoch/Item.swift b/Epoch/Epoch/Item.swift deleted file mode 100644 index bb6a6b9..0000000 --- a/Epoch/Epoch/Item.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Item.swift -// Epoch -// -// Created by Rishabh Bansal on 8/10/25. -// - -import Foundation -import SwiftData - -@Model -final class Item { - var timestamp: Date - - init(timestamp: Date) { - self.timestamp = timestamp - } -} diff --git a/Epoch/Epoch/KeychainService.swift b/Epoch/Epoch/KeychainService.swift new file mode 100644 index 0000000..9a84071 --- /dev/null +++ b/Epoch/Epoch/KeychainService.swift @@ -0,0 +1,32 @@ +import Foundation +import Security + +struct KeychainService { + static let service = "com.rishabh.Epoch.ai" + static let account = "apiKey" + + static func save(key: String) throws { + guard let data = key.data(using: .utf8) else { return } + let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account] + SecItemDelete(query as CFDictionary) + var addQuery = query + addQuery[kSecValueData as String] = data + let status = SecItemAdd(addQuery as CFDictionary, nil) + guard status == errSecSuccess else { throw NSError(domain: NSOSStatusErrorDomain, code: Int(status)) } + } + + static func load() throws -> String? { + let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, + kSecAttrService as String: service, + kSecAttrAccount as String: account, + kSecReturnData as String: true] + var result: AnyObject? + let status = SecItemCopyMatching(query as CFDictionary, &result) + if status == errSecSuccess, let data = result as? Data { + return String(data: data, encoding: .utf8) + } + return nil + } +} diff --git a/Epoch/Epoch/Models.swift b/Epoch/Epoch/Models.swift new file mode 100644 index 0000000..b3a13b0 --- /dev/null +++ b/Epoch/Epoch/Models.swift @@ -0,0 +1,38 @@ +import Foundation +import EventKit + +/// Represents a study block scheduled by the planner. +struct PlanBlock: Identifiable, Codable { + var id = UUID() + var start: Date + var end: Date + var title: String + var subject: Subject + var reminderID: String? +} + +/// Simple subject enumeration with a `generic` fallback. +enum Subject: String, Codable, CaseIterable, Identifiable { + case generic + case calc + case stats + case physics + case chemistry + case history + case english + var id: String { rawValue } +} + +/// User editable settings persisted via `SettingsStore`. +struct Settings: Codable { + var classCalendarIDs: [String] = [] + var prepWindowStart: DateComponents = DateComponents(hour: 18, minute: 0) + var prepWindowEnd: DateComponents = DateComponents(hour: 22, minute: 0) + var bedtime: DateComponents = DateComponents(hour: 22, minute: 30) + var blockDuration: TimeInterval = 50 * 60 + var breakDuration: TimeInterval = 10 * 60 + var useQuickCaptureListOnly: Bool = false + var aliasTable: [String: Subject] = [:] + var lastPlanSummary: String? + var aiAssistEnabled: Bool = false +} diff --git a/Epoch/Epoch/NLRouter.swift b/Epoch/Epoch/NLRouter.swift new file mode 100644 index 0000000..6924ab4 --- /dev/null +++ b/Epoch/Epoch/NLRouter.swift @@ -0,0 +1,26 @@ +import Foundation +import NaturalLanguage + +/// Provides subject detection and basic heuristics using `NaturalLanguage`. +struct NLRouter { + var aliases: [String: Subject] + + func subject(for text: String) -> Subject { + let lower = text.lowercased() + for (key, value) in aliases { + if lower.contains(key.lowercased()) { return value } + } + let tagger = NLTagger(tagSchemes: [.lexicalClass]) + tagger.string = lower + if lower.contains("calc") { return .calc } + if lower.contains("phys") { return .physics } + if lower.contains("stat") { return .stats } + return .generic + } + + func hasUrgency(in text: String) -> Bool { + let urgencyWords = ["due", "exam", "final", "quiz", "project"] + let lower = text.lowercased() + return urgencyWords.contains { lower.contains($0) } + } +} diff --git a/Epoch/Epoch/Notifications.swift b/Epoch/Epoch/Notifications.swift new file mode 100644 index 0000000..447c461 --- /dev/null +++ b/Epoch/Epoch/Notifications.swift @@ -0,0 +1,45 @@ +import Foundation +import UserNotifications +import SwiftUI + +/// Handles local notifications and actions. +final class NotificationManager: NSObject, ObservableObject { + static let shared = NotificationManager() + private let center = UNUserNotificationCenter.current() + + func requestAuthorization() async { + _ = try? await center.requestAuthorization(options: [.alert, .badge, .sound]) + center.delegate = self + registerCategories() + } + + func registerCategories() { + let addAction = UNTextInputNotificationAction(identifier: "ADD_REMINDER", title: "Add", options: []) + let quick = UNNotificationCategory(identifier: "QUICK_CAPTURE", actions: [addAction], intentIdentifiers: []) + center.setNotificationCategories([quick]) + } + + func scheduleNudge(at components: DateComponents) { + let content = UNMutableNotificationContent() + content.title = "Anything to prep?" + content.categoryIdentifier = "QUICK_CAPTURE" + let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true) + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + center.add(request) + } +} + +extension NotificationManager: UNUserNotificationCenterDelegate { + nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { + if response.actionIdentifier == "ADD_REMINDER", let text = (response as? UNTextInputNotificationResponse)?.userText { + await MainActor.run { + // Handle quick add via notification + NotificationCenter.default.post(name: .quickCaptureText, object: text) + } + } + } +} + +extension Notification.Name { + static let quickCaptureText = Notification.Name("quickCaptureText") +} diff --git a/Epoch/Epoch/Persistence.swift b/Epoch/Epoch/Persistence.swift new file mode 100644 index 0000000..8a19eda --- /dev/null +++ b/Epoch/Epoch/Persistence.swift @@ -0,0 +1,90 @@ +import Foundation +#if canImport(CloudKit) +import CloudKit +#endif + +/// Abstract persistence layer for settings. +protocol SettingsStore: AnyObject { + func load() async throws -> Settings + func save(_ settings: Settings) async throws + var description: String { get } +} + +/// Observable container for settings that chooses appropriate backend. +@MainActor +final class SettingsModel: ObservableObject { + @Published var settings: Settings + private var store: SettingsStore + var summary: String { settings.lastPlanSummary ?? "No plan" } + var description: String { store.description } + + init(store: SettingsStore = UserDefaultsStore()) { + self.store = store + self.settings = Settings() + Task { try? await load() } + } + + func load() async throws { + settings = try await store.load() + } + + func save() { + Task { try? await store.save(settings) } + } + + func switchStore(_ newStore: SettingsStore) { + store = newStore + save() + } +} + +/// Local `UserDefaults` backed store. +final class UserDefaultsStore: SettingsStore { + private let key = "settings" + private let defaults = UserDefaults.standard + var description: String { "Local" } + + func load() async throws -> Settings { + if let data = defaults.data(forKey: key), + let s = try? JSONDecoder().decode(Settings.self, from: data) { + return s + } + return Settings() + } + + func save(_ settings: Settings) async throws { + let data = try JSONEncoder().encode(settings) + defaults.set(data, forKey: key) + } +} + +#if canImport(CloudKit) +/// Simple CloudKit backed store. Falls back to `UserDefaults` on error. +final class CloudKitStore: SettingsStore { + private let container: CKContainer + private let recordID = CKRecord.ID(recordName: "settings") + var description: String { "iCloud" } + + init?(containerID: String) { + self.container = CKContainer(identifier: containerID) + } + + func load() async throws -> Settings { + do { + let db = container.privateCloudDatabase + let record = try await db.record(for: recordID) + if let data = record["data"] as? Data { + return try JSONDecoder().decode(Settings.self, from: data) + } + } catch { } + return Settings() + } + + func save(_ settings: Settings) async throws { + let db = container.privateCloudDatabase + let record = CKRecord(recordType: "Settings", recordID: recordID) + record["data"] = try JSONEncoder().encode(settings) as CKRecordValue + _ = try await db.modifyRecords(saving: [record], deleting: []) + } +} +#endif diff --git a/Epoch/Epoch/PlannerEngine.swift b/Epoch/Epoch/PlannerEngine.swift new file mode 100644 index 0000000..7b50050 --- /dev/null +++ b/Epoch/Epoch/PlannerEngine.swift @@ -0,0 +1,24 @@ +import Foundation +import EventKit + +/// Pure functions for scoring and allocation of study blocks. +struct PlannerEngine { + /// Produce plan blocks for tonight based on reminders and busy events. + func plan(reminders: [EKReminder], busy: [EKEvent], settings: Settings) -> [PlanBlock] { + let now = Date().addingTimeInterval(15*60) + var cursor = now + let bedtime = DateUtils.components(settings.bedtime) + var blocks: [PlanBlock] = [] + let sorted = reminders.sorted { ($0.dueDateComponents?.date ?? now) < ($1.dueDateComponents?.date ?? now) } + for reminder in sorted { + guard cursor < bedtime else { break } + let duration = settings.blockDuration + let end = cursor.addingTimeInterval(duration) + if end > bedtime { break } + let block = PlanBlock(start: cursor, end: end, title: reminder.title, subject: .generic, reminderID: reminder.calendarItemIdentifier) + blocks.append(block) + cursor = end.addingTimeInterval(settings.breakDuration) + } + return blocks + } +} diff --git a/Epoch/Epoch/Utilities.swift b/Epoch/Epoch/Utilities.swift new file mode 100644 index 0000000..3927278 --- /dev/null +++ b/Epoch/Epoch/Utilities.swift @@ -0,0 +1,29 @@ +import Foundation +import EventKit +import SwiftUI + +enum DateUtils { + static func startOfToday() -> Date { + Calendar.current.startOfDay(for: Date()) + } + static func startOfTomorrow() -> Date { + Calendar.current.date(byAdding: .day, value: 1, to: startOfToday())! + } + static func endOfTomorrow() -> Date { + Calendar.current.date(byAdding: DateComponents(day: 2, second: -1), to: startOfToday())! + } + static func components(_ time: DateComponents) -> Date { + Calendar.current.nextDate(after: startOfToday(), matching: time, matchingPolicy: .nextTimePreservingSmallerComponents) ?? Date() + } +} + +extension EKCalendar { + /// Safe UIColor from optional `cgColor` provided by EventKit. + var uiColor: UIColor { UIColor(cgColor: cgColor ?? UIColor.systemBlue.cgColor) } +} + +extension Color { + init(ekColor: EKCalendar) { + self.init(ekColor.uiColor) + } +} diff --git a/Epoch/Epoch/Views/ClassCalendarsView.swift b/Epoch/Epoch/Views/ClassCalendarsView.swift new file mode 100644 index 0000000..3a7894f --- /dev/null +++ b/Epoch/Epoch/Views/ClassCalendarsView.swift @@ -0,0 +1,36 @@ +import SwiftUI +import EventKit + +struct ClassCalendarsView: View { + @EnvironmentObject var manager: EventKitManager + @EnvironmentObject var settingsModel: SettingsModel + + var body: some View { + List { + ForEach(manager.eventStore.calendars(for: .event), id: \.self) { cal in + let binding = Binding( + get: { settingsModel.settings.classCalendarIDs.contains(cal.calendarIdentifier) }, + set: { isOn in + if isOn { + settingsModel.settings.classCalendarIDs.append(cal.calendarIdentifier) + } else { + settingsModel.settings.classCalendarIDs.removeAll { $0 == cal.calendarIdentifier } + } + settingsModel.save() + } + ) + HStack { + Circle().fill(Color(ekColor: cal)).frame(width: 8, height: 8) + Toggle(cal.title, isOn: binding) + } + } + } + .navigationTitle("Class Calendars") + } +} + +#Preview { + ClassCalendarsView() + .environmentObject(EventKitManager()) + .environmentObject(SettingsModel()) +} diff --git a/Epoch/Epoch/Views/DashboardView.swift b/Epoch/Epoch/Views/DashboardView.swift new file mode 100644 index 0000000..c6e86f4 --- /dev/null +++ b/Epoch/Epoch/Views/DashboardView.swift @@ -0,0 +1,54 @@ +import SwiftUI +import EventKit + +struct DashboardView: View { + @EnvironmentObject var manager: EventKitManager + @EnvironmentObject var settingsStore: SettingsModel + @State private var blocks: [PlanBlock] = [] + @State private var showPlanner = false + + var body: some View { + NavigationStack { + List { + Section("Insights") { + Text(settingsStore.summary) + } + Section("Plan") { + ForEach(blocks) { block in + VStack(alignment: .leading) { + Text(block.title) + Text("\(block.start.formatted(date: .omitted, time: .shortened)) – \(block.end.formatted(date: .omitted, time: .shortened))") + .font(.footnote) + } + } + } + } + .navigationTitle("Epoch") + .toolbar { + Button("Plan Tonight") { runPlanner() } + } + } + .onReceive(NotificationCenter.default.publisher(for: .quickCaptureText)) { note in + if let text = note.object as? String { + try? manager.addReminder(text: text, due: nil) + } + } + } + + func runPlanner() { + Task { + let predicate = EKEventStore().predicateForIncompleteReminders(withDueDateStarting: nil, ending: DateUtils.endOfTomorrow(), calendars: nil) + let rems = try await withCheckedThrowingContinuation { cont in + EKEventStore().fetchReminders(matching: predicate) { r in cont.resume(returning: r ?? []) } + } + let blocks = PlannerEngine().plan(reminders: rems, busy: [], settings: settingsStore.settings) + await MainActor.run { self.blocks = blocks } + } + } +} + +#Preview { + DashboardView() + .environmentObject(EventKitManager()) + .environmentObject(SettingsModel()) +} diff --git a/Epoch/Epoch/Views/QuickAddView.swift b/Epoch/Epoch/Views/QuickAddView.swift new file mode 100644 index 0000000..8f80e48 --- /dev/null +++ b/Epoch/Epoch/Views/QuickAddView.swift @@ -0,0 +1,22 @@ +import SwiftUI + +struct QuickAddView: View { + @EnvironmentObject var manager: EventKitManager + @State private var text: String = "" + + var body: some View { + VStack { + TextField("Reminder", text: $text) + .textFieldStyle(.roundedBorder) + Button("Add") { + try? manager.addReminder(text: text, due: nil) + text = "" + } + } + .padding() + } +} + +#Preview { + QuickAddView().environmentObject(EventKitManager()) +} diff --git a/Epoch/Epoch/Views/SchedulesView.swift b/Epoch/Epoch/Views/SchedulesView.swift new file mode 100644 index 0000000..497b347 --- /dev/null +++ b/Epoch/Epoch/Views/SchedulesView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct SchedulesView: View { + @EnvironmentObject var settingsModel: SettingsModel + @State private var nudgeTime = DateComponents(hour: 20, minute: 0) + + var body: some View { + Form { + DatePicker("Nudge Time", selection: Binding( + get: { DateUtils.components(nudgeTime) }, + set: { nudgeTime = Calendar.current.dateComponents([.hour, .minute], from: $0) } + ), displayedComponents: .hourAndMinute) + Button("Reschedule Nudges") { + NotificationManager.shared.scheduleNudge(at: nudgeTime) + } + } + .navigationTitle("Schedules") + } +} + +#Preview { + SchedulesView().environmentObject(SettingsModel()) +} diff --git a/Epoch/Epoch/Views/SettingsView.swift b/Epoch/Epoch/Views/SettingsView.swift new file mode 100644 index 0000000..e680b5f --- /dev/null +++ b/Epoch/Epoch/Views/SettingsView.swift @@ -0,0 +1,33 @@ +import SwiftUI + +struct SettingsView: View { + @EnvironmentObject var settingsModel: SettingsModel + @State private var aiKey: String = (try? KeychainService.load()) ?? "" + + var body: some View { + Form { + Section("Durations") { + Stepper(value: $settingsModel.settings.blockDuration, in: 1500...3600, step: 300) { + Text("Block: \(Int(settingsModel.settings.blockDuration/60)) min") + } + Stepper(value: $settingsModel.settings.breakDuration, in: 300...900, step: 300) { + Text("Break: \(Int(settingsModel.settings.breakDuration/60)) min") + } + } + Section("AI") { + SecureField("API Key", text: $aiKey) + Toggle("Enable", isOn: $settingsModel.settings.aiAssistEnabled) + Button("Save Key") { try? KeychainService.save(key: aiKey) } + } + Section("Storage") { + Text("Backend: \(settingsModel.description)") + } + } + .navigationTitle("Settings") + .onDisappear { settingsModel.save() } + } +} + +#Preview { + SettingsView().environmentObject(SettingsModel()) +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..80f1a6a --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Epoch + +Prototype SwiftUI study planner demonstrating EventKit, notifications, NLP, and App Intents. + From 3e06691f7e672887e3bbe918adf6fe2b74519333 Mon Sep 17 00:00:00 2001 From: Rishabh Bansal <122753012+risban933@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:52:33 -0700 Subject: [PATCH 2/4] Fix AppIntent actor isolation --- Epoch/Epoch/Intents/PlanTonightIntent.swift | 1 + Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Epoch/Epoch/Intents/PlanTonightIntent.swift b/Epoch/Epoch/Intents/PlanTonightIntent.swift index bd3be2c..6ea2f5a 100644 --- a/Epoch/Epoch/Intents/PlanTonightIntent.swift +++ b/Epoch/Epoch/Intents/PlanTonightIntent.swift @@ -4,6 +4,7 @@ import EventKit struct PlanTonightIntent: AppIntent { static var title: LocalizedStringResource = "Plan Tonight" + @MainActor func perform() async throws -> some IntentResult & ReturnsValue { let manager = EventKitManager() try await manager.requestAccess() diff --git a/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift b/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift index 937cc76..4d7f01b 100644 --- a/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift +++ b/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift @@ -4,12 +4,13 @@ import EventKit struct WhatShouldIPrepIntent: AppIntent { static var title: LocalizedStringResource = "What Should I Prep" + @MainActor func perform() async throws -> some IntentResult & ReturnsValue { let manager = EventKitManager() try await manager.requestAccess() let reminders = try await fetchReminders() let count = reminders.count - let firstClass = manager.tomorrowClasses(from: []) .first?.title ?? "none" + let firstClass = manager.tomorrowClasses(from: []).first?.title ?? "none" return .result(value: "You have \(count) items. First class: \(firstClass)") } From 1dfd63ece89c11d329d4bd82af6a75f0db2ad4c7 Mon Sep 17 00:00:00 2001 From: Rishabh Bansal <122753012+risban933@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:59:03 -0700 Subject: [PATCH 3/4] Fix async actor warnings --- Epoch/Epoch/Intents/AddQuickCaptureIntent.swift | 3 ++- Epoch/Epoch/Views/QuickAddView.swift | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift b/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift index 3314479..e5e48dc 100644 --- a/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift +++ b/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift @@ -5,10 +5,11 @@ struct AddQuickCaptureIntent: AppIntent { static var title: LocalizedStringResource = "Add Quick Capture" @Parameter(title: "Text") var text: String + @MainActor func perform() async throws -> some IntentResult & ReturnsValue { let manager = EventKitManager() try await manager.requestAccess() let reminder = try manager.addReminder(text: text, due: nil) - return .result(value: "Added reminder \(reminder.title)") + return .result(value: "Added reminder \(reminder.title ?? text)") } } diff --git a/Epoch/Epoch/Views/QuickAddView.swift b/Epoch/Epoch/Views/QuickAddView.swift index 8f80e48..1eddb59 100644 --- a/Epoch/Epoch/Views/QuickAddView.swift +++ b/Epoch/Epoch/Views/QuickAddView.swift @@ -9,7 +9,7 @@ struct QuickAddView: View { TextField("Reminder", text: $text) .textFieldStyle(.roundedBorder) Button("Add") { - try? manager.addReminder(text: text, due: nil) + _ = try? manager.addReminder(text: text, due: nil) text = "" } } From bba2ac5c83814409635fb2f2385b18a75bb8fb5d Mon Sep 17 00:00:00 2001 From: Rishabh Bansal Date: Sun, 24 Aug 2025 11:47:16 -0700 Subject: [PATCH 4/4] a --- Epoch/Epoch.xcodeproj/project.pbxproj | 550 ------------------ .../contents.xcworkspacedata | 7 - .../UserInterfaceState.xcuserstate | Bin 16581 -> 0 bytes .../xcschemes/xcschememanagement.plist | 14 - Epoch/Epoch/AIAssistant.swift | 32 - .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 35 -- Epoch/Epoch/Assets.xcassets/Contents.json | 6 - Epoch/Epoch/Epoch.entitlements | 10 - Epoch/Epoch/EpochApp.swift | 16 - Epoch/Epoch/EventKitManager.swift | 64 -- Epoch/Epoch/Info.plist | 12 - .../Epoch/Intents/AddQuickCaptureIntent.swift | 15 - Epoch/Epoch/Intents/PlanTonightIntent.swift | 30 - .../Epoch/Intents/WhatShouldIPrepIntent.swift | 26 - Epoch/Epoch/KeychainService.swift | 32 - Epoch/Epoch/Models.swift | 38 -- Epoch/Epoch/NLRouter.swift | 26 - Epoch/Epoch/Notifications.swift | 45 -- Epoch/Epoch/Persistence.swift | 90 --- Epoch/Epoch/PlannerEngine.swift | 24 - Epoch/Epoch/Utilities.swift | 29 - Epoch/Epoch/Views/ClassCalendarsView.swift | 36 -- Epoch/Epoch/Views/DashboardView.swift | 54 -- Epoch/Epoch/Views/QuickAddView.swift | 22 - Epoch/Epoch/Views/SchedulesView.swift | 23 - Epoch/Epoch/Views/SettingsView.swift | 33 -- Epoch/EpochTests/EpochTests.swift | 17 - Epoch/EpochUITests/EpochUITests.swift | 41 -- .../EpochUITestsLaunchTests.swift | 33 -- 30 files changed, 1371 deletions(-) delete mode 100644 Epoch/Epoch.xcodeproj/project.pbxproj delete mode 100644 Epoch/Epoch.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 Epoch/Epoch.xcodeproj/project.xcworkspace/xcuserdata/rishabhbansal.xcuserdatad/UserInterfaceState.xcuserstate delete mode 100644 Epoch/Epoch.xcodeproj/xcuserdata/rishabhbansal.xcuserdatad/xcschemes/xcschememanagement.plist delete mode 100644 Epoch/Epoch/AIAssistant.swift delete mode 100644 Epoch/Epoch/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 Epoch/Epoch/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Epoch/Epoch/Assets.xcassets/Contents.json delete mode 100644 Epoch/Epoch/Epoch.entitlements delete mode 100644 Epoch/Epoch/EpochApp.swift delete mode 100644 Epoch/Epoch/EventKitManager.swift delete mode 100644 Epoch/Epoch/Info.plist delete mode 100644 Epoch/Epoch/Intents/AddQuickCaptureIntent.swift delete mode 100644 Epoch/Epoch/Intents/PlanTonightIntent.swift delete mode 100644 Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift delete mode 100644 Epoch/Epoch/KeychainService.swift delete mode 100644 Epoch/Epoch/Models.swift delete mode 100644 Epoch/Epoch/NLRouter.swift delete mode 100644 Epoch/Epoch/Notifications.swift delete mode 100644 Epoch/Epoch/Persistence.swift delete mode 100644 Epoch/Epoch/PlannerEngine.swift delete mode 100644 Epoch/Epoch/Utilities.swift delete mode 100644 Epoch/Epoch/Views/ClassCalendarsView.swift delete mode 100644 Epoch/Epoch/Views/DashboardView.swift delete mode 100644 Epoch/Epoch/Views/QuickAddView.swift delete mode 100644 Epoch/Epoch/Views/SchedulesView.swift delete mode 100644 Epoch/Epoch/Views/SettingsView.swift delete mode 100644 Epoch/EpochTests/EpochTests.swift delete mode 100644 Epoch/EpochUITests/EpochUITests.swift delete mode 100644 Epoch/EpochUITests/EpochUITestsLaunchTests.swift diff --git a/Epoch/Epoch.xcodeproj/project.pbxproj b/Epoch/Epoch.xcodeproj/project.pbxproj deleted file mode 100644 index 3ea3057..0000000 --- a/Epoch/Epoch.xcodeproj/project.pbxproj +++ /dev/null @@ -1,550 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 90; - objects = { - -/* Begin PBXContainerItemProxy section */ - 496F511D2E4977400085091C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 496F51032E49773F0085091C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 496F510A2E49773F0085091C; - remoteInfo = Epoch; - }; - 496F51272E4977400085091C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 496F51032E49773F0085091C /* Project object */; - proxyType = 1; - remoteGlobalIDString = 496F510A2E49773F0085091C; - remoteInfo = Epoch; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 496F510B2E49773F0085091C /* Epoch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Epoch.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 496F511C2E4977400085091C /* EpochTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EpochTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 496F51262E4977400085091C /* EpochUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EpochUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 496F512E2E4977400085091C /* Exceptions for "Epoch" folder in "Epoch" target */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Info.plist, - ); - target = 496F510A2E49773F0085091C /* Epoch */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - -/* Begin PBXFileSystemSynchronizedRootGroup section */ - 496F510D2E49773F0085091C /* Epoch */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - 496F512E2E4977400085091C /* Exceptions for "Epoch" folder in "Epoch" target */, - ); - path = Epoch; - sourceTree = ""; - }; - 496F511F2E4977400085091C /* EpochTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = EpochTests; - sourceTree = ""; - }; - 496F51292E4977400085091C /* EpochUITests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - path = EpochUITests; - sourceTree = ""; - }; -/* End PBXFileSystemSynchronizedRootGroup section */ - -/* Begin PBXFrameworksBuildPhase section */ - 496F51082E49773F0085091C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - files = ( - ); - }; - 496F51192E4977400085091C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - files = ( - ); - }; - 496F51232E4977400085091C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - files = ( - ); - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 496F51022E49773F0085091C = { - isa = PBXGroup; - children = ( - 496F510D2E49773F0085091C /* Epoch */, - 496F511F2E4977400085091C /* EpochTests */, - 496F51292E4977400085091C /* EpochUITests */, - 496F510C2E49773F0085091C /* Products */, - ); - sourceTree = ""; - }; - 496F510C2E49773F0085091C /* Products */ = { - isa = PBXGroup; - children = ( - 496F510B2E49773F0085091C /* Epoch.app */, - 496F511C2E4977400085091C /* EpochTests.xctest */, - 496F51262E4977400085091C /* EpochUITests.xctest */, - ); - name = Products; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 496F510A2E49773F0085091C /* Epoch */ = { - isa = PBXNativeTarget; - buildConfigurationList = 496F512F2E4977400085091C /* Build configuration list for PBXNativeTarget "Epoch" */; - buildPhases = ( - 496F51072E49773F0085091C /* Sources */, - 496F51082E49773F0085091C /* Frameworks */, - 496F51092E49773F0085091C /* Resources */, - ); - buildRules = ( - ); - fileSystemSynchronizedGroups = ( - 496F510D2E49773F0085091C /* Epoch */, - ); - name = Epoch; - productName = Epoch; - productReference = 496F510B2E49773F0085091C /* Epoch.app */; - productType = "com.apple.product-type.application"; - }; - 496F511B2E4977400085091C /* EpochTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 496F51342E4977400085091C /* Build configuration list for PBXNativeTarget "EpochTests" */; - buildPhases = ( - 496F51182E4977400085091C /* Sources */, - 496F51192E4977400085091C /* Frameworks */, - 496F511A2E4977400085091C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 496F511E2E4977400085091C /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 496F511F2E4977400085091C /* EpochTests */, - ); - name = EpochTests; - productName = EpochTests; - productReference = 496F511C2E4977400085091C /* EpochTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 496F51252E4977400085091C /* EpochUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 496F51372E4977400085091C /* Build configuration list for PBXNativeTarget "EpochUITests" */; - buildPhases = ( - 496F51222E4977400085091C /* Sources */, - 496F51232E4977400085091C /* Frameworks */, - 496F51242E4977400085091C /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 496F51282E4977400085091C /* PBXTargetDependency */, - ); - fileSystemSynchronizedGroups = ( - 496F51292E4977400085091C /* EpochUITests */, - ); - name = EpochUITests; - productName = EpochUITests; - productReference = 496F51262E4977400085091C /* EpochUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 496F51032E49773F0085091C /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1640; - LastUpgradeCheck = 1640; - TargetAttributes = { - 496F510A2E49773F0085091C = { - CreatedOnToolsVersion = 16.4; - }; - 496F511B2E4977400085091C = { - CreatedOnToolsVersion = 16.4; - TestTargetID = 496F510A2E49773F0085091C; - }; - 496F51252E4977400085091C = { - CreatedOnToolsVersion = 16.4; - TestTargetID = 496F510A2E49773F0085091C; - }; - }; - }; - buildConfigurationList = 496F51062E49773F0085091C /* Build configuration list for PBXProject "Epoch" */; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 496F51022E49773F0085091C; - minimizedProjectReferenceProxies = 1; - preferredProjectObjectVersion = 90; - productRefGroup = 496F510C2E49773F0085091C /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 496F510A2E49773F0085091C /* Epoch */, - 496F511B2E4977400085091C /* EpochTests */, - 496F51252E4977400085091C /* EpochUITests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 496F51092E49773F0085091C /* Resources */ = { - isa = PBXResourcesBuildPhase; - files = ( - ); - }; - 496F511A2E4977400085091C /* Resources */ = { - isa = PBXResourcesBuildPhase; - files = ( - ); - }; - 496F51242E4977400085091C /* Resources */ = { - isa = PBXResourcesBuildPhase; - files = ( - ); - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 496F51072E49773F0085091C /* Sources */ = { - isa = PBXSourcesBuildPhase; - files = ( - ); - }; - 496F51182E4977400085091C /* Sources */ = { - isa = PBXSourcesBuildPhase; - files = ( - ); - }; - 496F51222E4977400085091C /* Sources */ = { - isa = PBXSourcesBuildPhase; - files = ( - ); - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 496F511E2E4977400085091C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 496F510A2E49773F0085091C /* Epoch */; - targetProxy = 496F511D2E4977400085091C /* PBXContainerItemProxy */; - }; - 496F51282E4977400085091C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 496F510A2E49773F0085091C /* Epoch */; - targetProxy = 496F51272E4977400085091C /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - 496F51302E4977400085091C /* Debug configuration for PBXNativeTarget "Epoch" */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Epoch/Epoch.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = UZD4BS94DT; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Epoch/Info.plist; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.rishabh.Epoch; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 496F51312E4977400085091C /* Release configuration for PBXNativeTarget "Epoch" */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = Epoch/Epoch.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = UZD4BS94DT; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = Epoch/Info.plist; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.rishabh.Epoch; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - 496F51322E4977400085091C /* Debug configuration for PBXProject "Epoch" */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - DEVELOPMENT_TEAM = UZD4BS94DT; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.5; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 496F51332E4977400085091C /* Release configuration for PBXProject "Epoch" */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEVELOPMENT_TEAM = UZD4BS94DT; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.5; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 496F51352E4977400085091C /* Debug configuration for PBXNativeTarget "EpochTests" */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = UZD4BS94DT; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.5; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.rishabh.EpochTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Epoch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Epoch"; - }; - name = Debug; - }; - 496F51362E4977400085091C /* Release configuration for PBXNativeTarget "EpochTests" */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = UZD4BS94DT; - GENERATE_INFOPLIST_FILE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 18.5; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.rishabh.EpochTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Epoch.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Epoch"; - }; - name = Release; - }; - 496F51382E4977400085091C /* Debug configuration for PBXNativeTarget "EpochUITests" */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = UZD4BS94DT; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.rishabh.EpochUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Epoch; - }; - name = Debug; - }; - 496F51392E4977400085091C /* Release configuration for PBXNativeTarget "EpochUITests" */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = UZD4BS94DT; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.rishabh.EpochUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Epoch; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 496F51062E49773F0085091C /* Build configuration list for PBXProject "Epoch" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 496F51322E4977400085091C /* Debug configuration for PBXProject "Epoch" */, - 496F51332E4977400085091C /* Release configuration for PBXProject "Epoch" */, - ); - defaultConfigurationName = Release; - }; - 496F512F2E4977400085091C /* Build configuration list for PBXNativeTarget "Epoch" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 496F51302E4977400085091C /* Debug configuration for PBXNativeTarget "Epoch" */, - 496F51312E4977400085091C /* Release configuration for PBXNativeTarget "Epoch" */, - ); - defaultConfigurationName = Release; - }; - 496F51342E4977400085091C /* Build configuration list for PBXNativeTarget "EpochTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 496F51352E4977400085091C /* Debug configuration for PBXNativeTarget "EpochTests" */, - 496F51362E4977400085091C /* Release configuration for PBXNativeTarget "EpochTests" */, - ); - defaultConfigurationName = Release; - }; - 496F51372E4977400085091C /* Build configuration list for PBXNativeTarget "EpochUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 496F51382E4977400085091C /* Debug configuration for PBXNativeTarget "EpochUITests" */, - 496F51392E4977400085091C /* Release configuration for PBXNativeTarget "EpochUITests" */, - ); - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 496F51032E49773F0085091C /* Project object */; -} diff --git a/Epoch/Epoch.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Epoch/Epoch.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/Epoch/Epoch.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Epoch/Epoch.xcodeproj/project.xcworkspace/xcuserdata/rishabhbansal.xcuserdatad/UserInterfaceState.xcuserstate b/Epoch/Epoch.xcodeproj/project.xcworkspace/xcuserdata/rishabhbansal.xcuserdatad/UserInterfaceState.xcuserstate deleted file mode 100644 index 181eac3653019ef9d6931d867ac28edb72e9eb1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16581 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 - - - - SchemeUserState - - Epoch.xcscheme_^#shared#^_ - - orderHint - 0 - - - - diff --git a/Epoch/Epoch/AIAssistant.swift b/Epoch/Epoch/AIAssistant.swift deleted file mode 100644 index 0cc9dd0..0000000 --- a/Epoch/Epoch/AIAssistant.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation - -/// Minimal URLSession based AI planner client. -struct AIAssistant { - struct Request: Codable { - var classes: [String] - var tasks: [String] - var start: Date - var end: Date - } - struct Response: Codable { - struct Block: Codable { let title: String; let start: Date; let end: Date } - var blocks: [Block] - } - - var baseURL: URL - var apiKey: String - - func generatePlan(request: Request) async throws -> Response { - var req = URLRequest(url: baseURL) - req.httpMethod = "POST" - req.addValue("application/json", forHTTPHeaderField: "Content-Type") - req.addValue(apiKey, forHTTPHeaderField: "X-API-Key") - req.httpBody = try JSONEncoder().encode(request) - - let (data, response) = try await URLSession.shared.data(for: req) - guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { - throw URLError(.badServerResponse) - } - return try JSONDecoder().decode(Response.self, from: data) - } -} diff --git a/Epoch/Epoch/Assets.xcassets/AccentColor.colorset/Contents.json b/Epoch/Epoch/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/Epoch/Epoch/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Epoch/Epoch/Assets.xcassets/AppIcon.appiconset/Contents.json b/Epoch/Epoch/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 2305880..0000000 --- a/Epoch/Epoch/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "tinted" - } - ], - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Epoch/Epoch/Assets.xcassets/Contents.json b/Epoch/Epoch/Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/Epoch/Epoch/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Epoch/Epoch/Epoch.entitlements b/Epoch/Epoch/Epoch.entitlements deleted file mode 100644 index 18aff0c..0000000 --- a/Epoch/Epoch/Epoch.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - - diff --git a/Epoch/Epoch/EpochApp.swift b/Epoch/Epoch/EpochApp.swift deleted file mode 100644 index 8b3ae6e..0000000 --- a/Epoch/Epoch/EpochApp.swift +++ /dev/null @@ -1,16 +0,0 @@ -import SwiftUI - -@main -struct EpochApp: App { - @StateObject private var settingsModel = SettingsModel() - @StateObject private var eventKit = EventKitManager() - - var body: some Scene { - WindowGroup { - DashboardView() - .environmentObject(eventKit) - .environmentObject(settingsModel) - .task { await NotificationManager.shared.requestAuthorization() } - } - } -} diff --git a/Epoch/Epoch/EventKitManager.swift b/Epoch/Epoch/EventKitManager.swift deleted file mode 100644 index 9362b06..0000000 --- a/Epoch/Epoch/EventKitManager.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation -import EventKit - -/// Handles EventKit operations for calendars and reminders. -@MainActor -final class EventKitManager: ObservableObject { - private let store = EKEventStore() - var eventStore: EKEventStore { store } - @Published var studyCalendar: EKCalendar? - - init() { - Task { await loadStudyCalendar() } - } - - func requestAccess() async throws { - try await store.requestFullAccessToEvents() - try await store.requestFullAccessToReminders() - await loadStudyCalendar() - } - - func loadStudyCalendar() async { - let name = "Study Plan" - if let existing = store.calendars(for: .event).first(where: { $0.title == name }) { - studyCalendar = existing - return - } - let cal = EKCalendar(for: .event, eventStore: store) - cal.title = name - cal.source = store.defaultCalendarForNewEvents?.source - try? store.saveCalendar(cal, commit: true) - studyCalendar = cal - } - - func tomorrowClasses(from calendars: [EKCalendar]) -> [EKEvent] { - let start = DateUtils.startOfTomorrow() - let end = DateUtils.endOfTomorrow() - let predicate = store.predicateForEvents(withStart: start, end: end, calendars: calendars) - return store.events(matching: predicate) - } - - func addStudyEvent(title: String, start: Date, end: Date, note: String, reminderID: String?) throws { - guard let calendar = studyCalendar else { return } - let event = EKEvent(eventStore: store) - event.calendar = calendar - event.title = "Study: \(title)" - event.startDate = start - event.endDate = end - event.notes = "epoch://study/\(reminderID ?? UUID().uuidString)\n\(note)" - try store.save(event, span: .thisEvent) - } - - func addReminder(text: String, due: Date?) throws -> EKReminder { - let reminder = EKReminder(eventStore: store) - reminder.title = text - reminder.calendar = store.defaultCalendarForNewReminders() - if let due { - reminder.dueDateComponents = Calendar.current.dateComponents([.year,.month,.day,.hour,.minute], from: due) - let alarm = EKAlarm(absoluteDate: due) - reminder.addAlarm(alarm) - } - try store.save(reminder, commit: true) - return reminder - } -} diff --git a/Epoch/Epoch/Info.plist b/Epoch/Epoch/Info.plist deleted file mode 100644 index b7aea5a..0000000 --- a/Epoch/Epoch/Info.plist +++ /dev/null @@ -1,12 +0,0 @@ - - - - - UIBackgroundModes - - NSCalendarsFullAccessUsageDescription - Access to calendars is needed to schedule study sessions. - NSRemindersFullAccessUsageDescription - Access to reminders lets Epoch plan tasks and deadlines. - - diff --git a/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift b/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift deleted file mode 100644 index e5e48dc..0000000 --- a/Epoch/Epoch/Intents/AddQuickCaptureIntent.swift +++ /dev/null @@ -1,15 +0,0 @@ -import AppIntents -import EventKit - -struct AddQuickCaptureIntent: AppIntent { - static var title: LocalizedStringResource = "Add Quick Capture" - @Parameter(title: "Text") var text: String - - @MainActor - func perform() async throws -> some IntentResult & ReturnsValue { - let manager = EventKitManager() - try await manager.requestAccess() - let reminder = try manager.addReminder(text: text, due: nil) - return .result(value: "Added reminder \(reminder.title ?? text)") - } -} diff --git a/Epoch/Epoch/Intents/PlanTonightIntent.swift b/Epoch/Epoch/Intents/PlanTonightIntent.swift deleted file mode 100644 index 6ea2f5a..0000000 --- a/Epoch/Epoch/Intents/PlanTonightIntent.swift +++ /dev/null @@ -1,30 +0,0 @@ -import AppIntents -import EventKit - -struct PlanTonightIntent: AppIntent { - static var title: LocalizedStringResource = "Plan Tonight" - - @MainActor - func perform() async throws -> some IntentResult & ReturnsValue { - let manager = EventKitManager() - try await manager.requestAccess() - let settings = Settings() - let reminders = try await fetchReminders(manager: manager) - let busy: [EKEvent] = [] - let blocks = PlannerEngine().plan(reminders: reminders, busy: busy, settings: settings) - for block in blocks { - try manager.addStudyEvent(title: block.title, start: block.start, end: block.end, note: "", reminderID: block.reminderID) - } - return .result(value: "Planned \(blocks.count) blocks") - } - - private func fetchReminders(manager: EventKitManager) async throws -> [EKReminder] { - let predicate = EKEventStore().predicateForIncompleteReminders(withDueDateStarting: nil, ending: DateUtils.endOfTomorrow(), calendars: nil) - let reminders = try await withCheckedThrowingContinuation { cont in - EKEventStore().fetchReminders(matching: predicate) { rems in - cont.resume(returning: rems ?? []) - } - } - return reminders.filter { ($0.dueDateComponents?.date ?? Date()) <= DateUtils.endOfTomorrow() } - } -} diff --git a/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift b/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift deleted file mode 100644 index 4d7f01b..0000000 --- a/Epoch/Epoch/Intents/WhatShouldIPrepIntent.swift +++ /dev/null @@ -1,26 +0,0 @@ -import AppIntents -import EventKit - -struct WhatShouldIPrepIntent: AppIntent { - static var title: LocalizedStringResource = "What Should I Prep" - - @MainActor - func perform() async throws -> some IntentResult & ReturnsValue { - let manager = EventKitManager() - try await manager.requestAccess() - let reminders = try await fetchReminders() - let count = reminders.count - let firstClass = manager.tomorrowClasses(from: []).first?.title ?? "none" - return .result(value: "You have \(count) items. First class: \(firstClass)") - } - - private func fetchReminders() async throws -> [EKReminder] { - let predicate = EKEventStore().predicateForIncompleteReminders(withDueDateStarting: nil, ending: DateUtils.endOfTomorrow(), calendars: nil) - let reminders = try await withCheckedThrowingContinuation { cont in - EKEventStore().fetchReminders(matching: predicate) { rems in - cont.resume(returning: rems ?? []) - } - } - return reminders - } -} diff --git a/Epoch/Epoch/KeychainService.swift b/Epoch/Epoch/KeychainService.swift deleted file mode 100644 index 9a84071..0000000 --- a/Epoch/Epoch/KeychainService.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import Security - -struct KeychainService { - static let service = "com.rishabh.Epoch.ai" - static let account = "apiKey" - - static func save(key: String) throws { - guard let data = key.data(using: .utf8) else { return } - let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: account] - SecItemDelete(query as CFDictionary) - var addQuery = query - addQuery[kSecValueData as String] = data - let status = SecItemAdd(addQuery as CFDictionary, nil) - guard status == errSecSuccess else { throw NSError(domain: NSOSStatusErrorDomain, code: Int(status)) } - } - - static func load() throws -> String? { - let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrAccount as String: account, - kSecReturnData as String: true] - var result: AnyObject? - let status = SecItemCopyMatching(query as CFDictionary, &result) - if status == errSecSuccess, let data = result as? Data { - return String(data: data, encoding: .utf8) - } - return nil - } -} diff --git a/Epoch/Epoch/Models.swift b/Epoch/Epoch/Models.swift deleted file mode 100644 index b3a13b0..0000000 --- a/Epoch/Epoch/Models.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import EventKit - -/// Represents a study block scheduled by the planner. -struct PlanBlock: Identifiable, Codable { - var id = UUID() - var start: Date - var end: Date - var title: String - var subject: Subject - var reminderID: String? -} - -/// Simple subject enumeration with a `generic` fallback. -enum Subject: String, Codable, CaseIterable, Identifiable { - case generic - case calc - case stats - case physics - case chemistry - case history - case english - var id: String { rawValue } -} - -/// User editable settings persisted via `SettingsStore`. -struct Settings: Codable { - var classCalendarIDs: [String] = [] - var prepWindowStart: DateComponents = DateComponents(hour: 18, minute: 0) - var prepWindowEnd: DateComponents = DateComponents(hour: 22, minute: 0) - var bedtime: DateComponents = DateComponents(hour: 22, minute: 30) - var blockDuration: TimeInterval = 50 * 60 - var breakDuration: TimeInterval = 10 * 60 - var useQuickCaptureListOnly: Bool = false - var aliasTable: [String: Subject] = [:] - var lastPlanSummary: String? - var aiAssistEnabled: Bool = false -} diff --git a/Epoch/Epoch/NLRouter.swift b/Epoch/Epoch/NLRouter.swift deleted file mode 100644 index 6924ab4..0000000 --- a/Epoch/Epoch/NLRouter.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation -import NaturalLanguage - -/// Provides subject detection and basic heuristics using `NaturalLanguage`. -struct NLRouter { - var aliases: [String: Subject] - - func subject(for text: String) -> Subject { - let lower = text.lowercased() - for (key, value) in aliases { - if lower.contains(key.lowercased()) { return value } - } - let tagger = NLTagger(tagSchemes: [.lexicalClass]) - tagger.string = lower - if lower.contains("calc") { return .calc } - if lower.contains("phys") { return .physics } - if lower.contains("stat") { return .stats } - return .generic - } - - func hasUrgency(in text: String) -> Bool { - let urgencyWords = ["due", "exam", "final", "quiz", "project"] - let lower = text.lowercased() - return urgencyWords.contains { lower.contains($0) } - } -} diff --git a/Epoch/Epoch/Notifications.swift b/Epoch/Epoch/Notifications.swift deleted file mode 100644 index 447c461..0000000 --- a/Epoch/Epoch/Notifications.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation -import UserNotifications -import SwiftUI - -/// Handles local notifications and actions. -final class NotificationManager: NSObject, ObservableObject { - static let shared = NotificationManager() - private let center = UNUserNotificationCenter.current() - - func requestAuthorization() async { - _ = try? await center.requestAuthorization(options: [.alert, .badge, .sound]) - center.delegate = self - registerCategories() - } - - func registerCategories() { - let addAction = UNTextInputNotificationAction(identifier: "ADD_REMINDER", title: "Add", options: []) - let quick = UNNotificationCategory(identifier: "QUICK_CAPTURE", actions: [addAction], intentIdentifiers: []) - center.setNotificationCategories([quick]) - } - - func scheduleNudge(at components: DateComponents) { - let content = UNMutableNotificationContent() - content.title = "Anything to prep?" - content.categoryIdentifier = "QUICK_CAPTURE" - let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true) - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) - center.add(request) - } -} - -extension NotificationManager: UNUserNotificationCenterDelegate { - nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { - if response.actionIdentifier == "ADD_REMINDER", let text = (response as? UNTextInputNotificationResponse)?.userText { - await MainActor.run { - // Handle quick add via notification - NotificationCenter.default.post(name: .quickCaptureText, object: text) - } - } - } -} - -extension Notification.Name { - static let quickCaptureText = Notification.Name("quickCaptureText") -} diff --git a/Epoch/Epoch/Persistence.swift b/Epoch/Epoch/Persistence.swift deleted file mode 100644 index 8a19eda..0000000 --- a/Epoch/Epoch/Persistence.swift +++ /dev/null @@ -1,90 +0,0 @@ -import Foundation -#if canImport(CloudKit) -import CloudKit -#endif - -/// Abstract persistence layer for settings. -protocol SettingsStore: AnyObject { - func load() async throws -> Settings - func save(_ settings: Settings) async throws - var description: String { get } -} - -/// Observable container for settings that chooses appropriate backend. -@MainActor -final class SettingsModel: ObservableObject { - @Published var settings: Settings - private var store: SettingsStore - var summary: String { settings.lastPlanSummary ?? "No plan" } - var description: String { store.description } - - init(store: SettingsStore = UserDefaultsStore()) { - self.store = store - self.settings = Settings() - Task { try? await load() } - } - - func load() async throws { - settings = try await store.load() - } - - func save() { - Task { try? await store.save(settings) } - } - - func switchStore(_ newStore: SettingsStore) { - store = newStore - save() - } -} - -/// Local `UserDefaults` backed store. -final class UserDefaultsStore: SettingsStore { - private let key = "settings" - private let defaults = UserDefaults.standard - var description: String { "Local" } - - func load() async throws -> Settings { - if let data = defaults.data(forKey: key), - let s = try? JSONDecoder().decode(Settings.self, from: data) { - return s - } - return Settings() - } - - func save(_ settings: Settings) async throws { - let data = try JSONEncoder().encode(settings) - defaults.set(data, forKey: key) - } -} - -#if canImport(CloudKit) -/// Simple CloudKit backed store. Falls back to `UserDefaults` on error. -final class CloudKitStore: SettingsStore { - private let container: CKContainer - private let recordID = CKRecord.ID(recordName: "settings") - var description: String { "iCloud" } - - init?(containerID: String) { - self.container = CKContainer(identifier: containerID) - } - - func load() async throws -> Settings { - do { - let db = container.privateCloudDatabase - let record = try await db.record(for: recordID) - if let data = record["data"] as? Data { - return try JSONDecoder().decode(Settings.self, from: data) - } - } catch { } - return Settings() - } - - func save(_ settings: Settings) async throws { - let db = container.privateCloudDatabase - let record = CKRecord(recordType: "Settings", recordID: recordID) - record["data"] = try JSONEncoder().encode(settings) as CKRecordValue - _ = try await db.modifyRecords(saving: [record], deleting: []) - } -} -#endif diff --git a/Epoch/Epoch/PlannerEngine.swift b/Epoch/Epoch/PlannerEngine.swift deleted file mode 100644 index 7b50050..0000000 --- a/Epoch/Epoch/PlannerEngine.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation -import EventKit - -/// Pure functions for scoring and allocation of study blocks. -struct PlannerEngine { - /// Produce plan blocks for tonight based on reminders and busy events. - func plan(reminders: [EKReminder], busy: [EKEvent], settings: Settings) -> [PlanBlock] { - let now = Date().addingTimeInterval(15*60) - var cursor = now - let bedtime = DateUtils.components(settings.bedtime) - var blocks: [PlanBlock] = [] - let sorted = reminders.sorted { ($0.dueDateComponents?.date ?? now) < ($1.dueDateComponents?.date ?? now) } - for reminder in sorted { - guard cursor < bedtime else { break } - let duration = settings.blockDuration - let end = cursor.addingTimeInterval(duration) - if end > bedtime { break } - let block = PlanBlock(start: cursor, end: end, title: reminder.title, subject: .generic, reminderID: reminder.calendarItemIdentifier) - blocks.append(block) - cursor = end.addingTimeInterval(settings.breakDuration) - } - return blocks - } -} diff --git a/Epoch/Epoch/Utilities.swift b/Epoch/Epoch/Utilities.swift deleted file mode 100644 index 3927278..0000000 --- a/Epoch/Epoch/Utilities.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation -import EventKit -import SwiftUI - -enum DateUtils { - static func startOfToday() -> Date { - Calendar.current.startOfDay(for: Date()) - } - static func startOfTomorrow() -> Date { - Calendar.current.date(byAdding: .day, value: 1, to: startOfToday())! - } - static func endOfTomorrow() -> Date { - Calendar.current.date(byAdding: DateComponents(day: 2, second: -1), to: startOfToday())! - } - static func components(_ time: DateComponents) -> Date { - Calendar.current.nextDate(after: startOfToday(), matching: time, matchingPolicy: .nextTimePreservingSmallerComponents) ?? Date() - } -} - -extension EKCalendar { - /// Safe UIColor from optional `cgColor` provided by EventKit. - var uiColor: UIColor { UIColor(cgColor: cgColor ?? UIColor.systemBlue.cgColor) } -} - -extension Color { - init(ekColor: EKCalendar) { - self.init(ekColor.uiColor) - } -} diff --git a/Epoch/Epoch/Views/ClassCalendarsView.swift b/Epoch/Epoch/Views/ClassCalendarsView.swift deleted file mode 100644 index 3a7894f..0000000 --- a/Epoch/Epoch/Views/ClassCalendarsView.swift +++ /dev/null @@ -1,36 +0,0 @@ -import SwiftUI -import EventKit - -struct ClassCalendarsView: View { - @EnvironmentObject var manager: EventKitManager - @EnvironmentObject var settingsModel: SettingsModel - - var body: some View { - List { - ForEach(manager.eventStore.calendars(for: .event), id: \.self) { cal in - let binding = Binding( - get: { settingsModel.settings.classCalendarIDs.contains(cal.calendarIdentifier) }, - set: { isOn in - if isOn { - settingsModel.settings.classCalendarIDs.append(cal.calendarIdentifier) - } else { - settingsModel.settings.classCalendarIDs.removeAll { $0 == cal.calendarIdentifier } - } - settingsModel.save() - } - ) - HStack { - Circle().fill(Color(ekColor: cal)).frame(width: 8, height: 8) - Toggle(cal.title, isOn: binding) - } - } - } - .navigationTitle("Class Calendars") - } -} - -#Preview { - ClassCalendarsView() - .environmentObject(EventKitManager()) - .environmentObject(SettingsModel()) -} diff --git a/Epoch/Epoch/Views/DashboardView.swift b/Epoch/Epoch/Views/DashboardView.swift deleted file mode 100644 index c6e86f4..0000000 --- a/Epoch/Epoch/Views/DashboardView.swift +++ /dev/null @@ -1,54 +0,0 @@ -import SwiftUI -import EventKit - -struct DashboardView: View { - @EnvironmentObject var manager: EventKitManager - @EnvironmentObject var settingsStore: SettingsModel - @State private var blocks: [PlanBlock] = [] - @State private var showPlanner = false - - var body: some View { - NavigationStack { - List { - Section("Insights") { - Text(settingsStore.summary) - } - Section("Plan") { - ForEach(blocks) { block in - VStack(alignment: .leading) { - Text(block.title) - Text("\(block.start.formatted(date: .omitted, time: .shortened)) – \(block.end.formatted(date: .omitted, time: .shortened))") - .font(.footnote) - } - } - } - } - .navigationTitle("Epoch") - .toolbar { - Button("Plan Tonight") { runPlanner() } - } - } - .onReceive(NotificationCenter.default.publisher(for: .quickCaptureText)) { note in - if let text = note.object as? String { - try? manager.addReminder(text: text, due: nil) - } - } - } - - func runPlanner() { - Task { - let predicate = EKEventStore().predicateForIncompleteReminders(withDueDateStarting: nil, ending: DateUtils.endOfTomorrow(), calendars: nil) - let rems = try await withCheckedThrowingContinuation { cont in - EKEventStore().fetchReminders(matching: predicate) { r in cont.resume(returning: r ?? []) } - } - let blocks = PlannerEngine().plan(reminders: rems, busy: [], settings: settingsStore.settings) - await MainActor.run { self.blocks = blocks } - } - } -} - -#Preview { - DashboardView() - .environmentObject(EventKitManager()) - .environmentObject(SettingsModel()) -} diff --git a/Epoch/Epoch/Views/QuickAddView.swift b/Epoch/Epoch/Views/QuickAddView.swift deleted file mode 100644 index 1eddb59..0000000 --- a/Epoch/Epoch/Views/QuickAddView.swift +++ /dev/null @@ -1,22 +0,0 @@ -import SwiftUI - -struct QuickAddView: View { - @EnvironmentObject var manager: EventKitManager - @State private var text: String = "" - - var body: some View { - VStack { - TextField("Reminder", text: $text) - .textFieldStyle(.roundedBorder) - Button("Add") { - _ = try? manager.addReminder(text: text, due: nil) - text = "" - } - } - .padding() - } -} - -#Preview { - QuickAddView().environmentObject(EventKitManager()) -} diff --git a/Epoch/Epoch/Views/SchedulesView.swift b/Epoch/Epoch/Views/SchedulesView.swift deleted file mode 100644 index 497b347..0000000 --- a/Epoch/Epoch/Views/SchedulesView.swift +++ /dev/null @@ -1,23 +0,0 @@ -import SwiftUI - -struct SchedulesView: View { - @EnvironmentObject var settingsModel: SettingsModel - @State private var nudgeTime = DateComponents(hour: 20, minute: 0) - - var body: some View { - Form { - DatePicker("Nudge Time", selection: Binding( - get: { DateUtils.components(nudgeTime) }, - set: { nudgeTime = Calendar.current.dateComponents([.hour, .minute], from: $0) } - ), displayedComponents: .hourAndMinute) - Button("Reschedule Nudges") { - NotificationManager.shared.scheduleNudge(at: nudgeTime) - } - } - .navigationTitle("Schedules") - } -} - -#Preview { - SchedulesView().environmentObject(SettingsModel()) -} diff --git a/Epoch/Epoch/Views/SettingsView.swift b/Epoch/Epoch/Views/SettingsView.swift deleted file mode 100644 index e680b5f..0000000 --- a/Epoch/Epoch/Views/SettingsView.swift +++ /dev/null @@ -1,33 +0,0 @@ -import SwiftUI - -struct SettingsView: View { - @EnvironmentObject var settingsModel: SettingsModel - @State private var aiKey: String = (try? KeychainService.load()) ?? "" - - var body: some View { - Form { - Section("Durations") { - Stepper(value: $settingsModel.settings.blockDuration, in: 1500...3600, step: 300) { - Text("Block: \(Int(settingsModel.settings.blockDuration/60)) min") - } - Stepper(value: $settingsModel.settings.breakDuration, in: 300...900, step: 300) { - Text("Break: \(Int(settingsModel.settings.breakDuration/60)) min") - } - } - Section("AI") { - SecureField("API Key", text: $aiKey) - Toggle("Enable", isOn: $settingsModel.settings.aiAssistEnabled) - Button("Save Key") { try? KeychainService.save(key: aiKey) } - } - Section("Storage") { - Text("Backend: \(settingsModel.description)") - } - } - .navigationTitle("Settings") - .onDisappear { settingsModel.save() } - } -} - -#Preview { - SettingsView().environmentObject(SettingsModel()) -} diff --git a/Epoch/EpochTests/EpochTests.swift b/Epoch/EpochTests/EpochTests.swift deleted file mode 100644 index 13b340c..0000000 --- a/Epoch/EpochTests/EpochTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// EpochTests.swift -// EpochTests -// -// Created by Rishabh Bansal on 8/10/25. -// - -import Testing -@testable import Epoch - -struct EpochTests { - - @Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. - } - -} diff --git a/Epoch/EpochUITests/EpochUITests.swift b/Epoch/EpochUITests/EpochUITests.swift deleted file mode 100644 index 2d21201..0000000 --- a/Epoch/EpochUITests/EpochUITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// EpochUITests.swift -// EpochUITests -// -// Created by Rishabh Bansal on 8/10/25. -// - -import XCTest - -final class EpochUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - @MainActor - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - @MainActor - func testLaunchPerformance() throws { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } -} diff --git a/Epoch/EpochUITests/EpochUITestsLaunchTests.swift b/Epoch/EpochUITests/EpochUITestsLaunchTests.swift deleted file mode 100644 index 5c066d8..0000000 --- a/Epoch/EpochUITests/EpochUITestsLaunchTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// EpochUITestsLaunchTests.swift -// EpochUITests -// -// Created by Rishabh Bansal on 8/10/25. -// - -import XCTest - -final class EpochUITestsLaunchTests: XCTestCase { - - override class var runsForEachTargetApplicationUIConfiguration: Bool { - true - } - - override func setUpWithError() throws { - continueAfterFailure = false - } - - @MainActor - func testLaunch() throws { - let app = XCUIApplication() - app.launch() - - // Insert steps here to perform after app launch but before taking a screenshot, - // such as logging into a test account or navigating somewhere in the app - - let attachment = XCTAttachment(screenshot: app.screenshot()) - attachment.name = "Launch Screen" - attachment.lifetime = .keepAlways - add(attachment) - } -}