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)
- }
-}