// // MyCrib.swift // MyCrib // // Created by Trey Tartt on 11/5/25. // import WidgetKit import SwiftUI class CacheManager { struct CustomTask: Codable { let id: Int let title: String let description: String? let priority: String? let status: String? let dueDate: String? let category: String? enum CodingKeys: String, CodingKey { case id, title, description, priority, status, category case dueDate = "due_date" } } private let userDefaults = UserDefaults.standard private static let cacheKey = "cached_tasks" static func getData() -> [CustomTask] { do { let string = UserDefaults.standard.string(forKey: cacheKey) ?? "[]" let jsonData = string.data(using: .utf8)! let customTask = try JSONDecoder().decode([CustomTask].self, from: jsonData) return customTask } catch { print("Error decoding tasks: \(error)") return [] } } static func getUpcomingTasks() -> [CustomTask] { let allTasks = getData() // Filter for pending/in-progress tasks, sorted by due date let upcoming = allTasks.filter { task in let status = task.status?.lowercased() ?? "" return status == "pending" || status == "in_progress" || status == "in progress" } // Sort by due date (earliest first) return upcoming.sorted { task1, task2 in guard let date1 = task1.dueDate, let date2 = task2.dueDate else { return task1.dueDate != nil } return date1 < date2 } } } struct Provider: AppIntentTimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry( date: Date(), configuration: ConfigurationAppIntent(), upcomingTasks: [] ) } func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry { let tasks = CacheManager.getUpcomingTasks() return SimpleEntry( date: Date(), configuration: configuration, upcomingTasks: tasks ) } func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline { let tasks = CacheManager.getUpcomingTasks() // Update every hour let currentDate = Date() let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)! let entry = SimpleEntry( date: currentDate, configuration: configuration, upcomingTasks: tasks ) return Timeline(entries: [entry], policy: .after(nextUpdate)) } } struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationAppIntent let upcomingTasks: [CacheManager.CustomTask] var taskCount: Int { upcomingTasks.count } var nextTask: CacheManager.CustomTask? { upcomingTasks.first } } struct MyCribEntryView : View { var entry: Provider.Entry @Environment(\.widgetFamily) var family var body: some View { switch family { case .systemSmall: SmallWidgetView(entry: entry) case .systemMedium: MediumWidgetView(entry: entry) default: SmallWidgetView(entry: entry) } } } // MARK: - Small Widget View struct SmallWidgetView: View { let entry: SimpleEntry var body: some View { VStack(alignment: .leading, spacing: 0) { // Header HStack(spacing: 6) { Image(systemName: "house.fill") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(.blue) Text("MyCrib") .font(.system(size: 14, weight: .bold)) .foregroundStyle(.primary) Spacer() } // Task Count VStack(alignment: .leading, spacing: 4) { Text("\(entry.taskCount)") .font(.system(size: 36, weight: .bold)) .foregroundStyle(.blue) if entry.taskCount > 0 { Text(entry.taskCount == 1 ? "upcoming task" : "upcoming tasks") .font(.system(size: 12, weight: .medium)) .foregroundStyle(.secondary) } } // Next Task if let nextTask = entry.nextTask { VStack(alignment: .leading, spacing: 4) { Text("NEXT UP") .font(.system(size: 9, weight: .semibold)) .foregroundStyle(.secondary) .tracking(0.5) Text(nextTask.title) .font(.system(size: 12, weight: .semibold)) .lineLimit(2) .foregroundStyle(.primary) if let dueDate = nextTask.dueDate { HStack(spacing: 4) { Image(systemName: "calendar") .font(.system(size: 9)) Text(formatDate(dueDate)) .font(.system(size: 10, weight: .medium)) } .foregroundStyle(.orange) } } .padding(.top, 8) .padding(.horizontal, 10) .padding(.vertical, 8) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.blue.opacity(0.1)) ) } else { HStack { Image(systemName: "checkmark.circle.fill") .font(.system(size: 16)) .foregroundStyle(.green) // Text("All caught up!") // .font(.system(size: 11, weight: .medium)) // .foregroundStyle(.secondary) } .frame(maxWidth: .infinity) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.green.opacity(0.1)) ) } } .padding(16) } private func formatDate(_ dateString: String) -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" if let date = formatter.date(from: dateString) { let now = Date() let calendar = Calendar.current if calendar.isDateInToday(date) { return "Today" } else if calendar.isDateInTomorrow(date) { return "Tomorrow" } else { formatter.dateFormat = "MMM d" return formatter.string(from: date) } } return dateString } } // MARK: - Medium Widget View struct MediumWidgetView: View { let entry: SimpleEntry var body: some View { HStack(spacing: 16) { // Left side - Task count VStack(alignment: .leading, spacing: 4) { Spacer() Text("\(entry.taskCount)") .font(.system(size: 42, weight: .bold)) .foregroundStyle(.blue) Text(entry.taskCount == 1 ? "upcoming\n task" : "upcoming\ntasks") .font(.system(size: 11, weight: .medium)) .foregroundStyle(.secondary) .lineLimit(2) Spacer() } .frame(maxWidth: 75) Divider() // Right side - Next tasks VStack(alignment: .leading, spacing: 8) { if let nextTask = entry.nextTask { Text("NEXT UP") .font(.system(size: 9, weight: .semibold)) .foregroundStyle(.secondary) .tracking(0.5) } if entry.nextTask != nil { ForEach(Array(entry.upcomingTasks.prefix(3).enumerated()), id: \.element.id) { index, task in TaskRowView(task: task, index: index) } Spacer() } else { Spacer() VStack(spacing: 8) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 24)) .foregroundStyle(.green) Text("All caught up!") .font(.system(size: 12, weight: .medium)) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity) Spacer() } } .frame(maxWidth: .infinity) } .padding(16) } } // MARK: - Task Row struct TaskRowView: View { let task: CacheManager.CustomTask let index: Int var body: some View { HStack(spacing: 8) { Circle() .fill(priorityColor) .frame(width: 6, height: 6) VStack(alignment: .leading, spacing: 2) { Text(task.title) .font(.system(size: 11, weight: .semibold)) // .lineLimit(1) .foregroundStyle(.primary) if let dueDate = task.dueDate { HStack(spacing: 3) { Image(systemName: "calendar") .font(.system(size: 8)) Text(formatDate(dueDate)) .font(.system(size: 9, weight: .medium)) } .foregroundStyle(.secondary) } } Spacer() if let priority = task.priority { Text(priority.prefix(1).uppercased()) .font(.system(size: 9, weight: .bold)) .foregroundStyle(.white) .frame(width: 16, height: 16) .background( Circle() .fill(priorityColor) ) } } .padding(.vertical, 4) } private var priorityColor: Color { switch task.priority?.lowercased() { case "urgent": return .red case "high": return .orange case "medium": return .blue default: return .gray } } private func formatDate(_ dateString: String) -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" if let date = formatter.date(from: dateString) { let now = Date() let calendar = Calendar.current if calendar.isDateInToday(date) { return "Today" } else if calendar.isDateInTomorrow(date) { return "Tomorrow" } else { formatter.dateFormat = "MMM d" return formatter.string(from: date) } } return dateString } } struct MyCrib: Widget { let kind: String = "MyCrib" var body: some WidgetConfiguration { AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in MyCribEntryView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } } } // MARK: - Previews #Preview(as: .systemSmall) { MyCrib() } timeline: { SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [ CacheManager.CustomTask( id: 1, title: "Fix leaky faucet", description: "Kitchen sink needs repair", priority: "high", status: "pending", dueDate: "2024-12-15", category: "plumbing" ), CacheManager.CustomTask( id: 2, title: "Paint living room", description: nil, priority: "medium", status: "pending", dueDate: "2024-12-20", category: "painting" ) ] ) SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [] ) } #Preview(as: .systemMedium) { MyCrib() } timeline: { SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [ CacheManager.CustomTask( id: 1, title: "Fix leaky faucet", description: "Kitchen sink needs repair", priority: "high", status: "pending", dueDate: "2024-12-15", category: "plumbing" ), CacheManager.CustomTask( id: 2, title: "Paint living room", description: nil, priority: "medium", status: "pending", dueDate: "2024-12-20", category: "painting" ), CacheManager.CustomTask( id: 3, title: "Clean gutters", description: "Remove debris", priority: "low", status: "pending", dueDate: "2024-12-25", category: "maintenance" ) ] ) SimpleEntry( date: .now, configuration: ConfigurationAppIntent(), upcomingTasks: [] ) }