import WidgetKit import SwiftUI // MARK: - Data struct HabitsEntry: TimelineEntry { let date: Date let completed: Int let total: Int let tasksCount: Int let streakDays: Int var progress: Double { total > 0 ? Double(completed) / Double(total) : 0 } static let placeholder = HabitsEntry(date: Date(), completed: 3, total: 5, tasksCount: 2, streakDays: 7) } // MARK: - Provider struct HabitsProvider: TimelineProvider { func placeholder(in context: Context) -> HabitsEntry { .placeholder } func getSnapshot(in context: Context, completion: @escaping (HabitsEntry) -> Void) { completion(.placeholder) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { Task { let entry = await fetchData() let nextUpdate = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date() completion(Timeline(entries: [entry], policy: .after(nextUpdate))) } } private func fetchData() async -> HabitsEntry { guard let token = KeychainService.load(key: KeychainService.tokenKey), !token.isEmpty else { return .placeholder } let baseURL = "https://api.digital-home.site" // Fetch habits var habits: [WidgetHabit] = [] if let url = URL(string: "\(baseURL)/habits") { var req = URLRequest(url: url) req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") req.timeoutInterval = 10 if let (data, resp) = try? await URLSession.shared.data(for: req), (resp as? HTTPURLResponse)?.statusCode == 200 { habits = (try? JSONDecoder().decode([WidgetHabit].self, from: data)) ?? [] } } // Fetch today's tasks var tasks: [WidgetTask] = [] if let url = URL(string: "\(baseURL)/tasks/today") { var req = URLRequest(url: url) req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") req.timeoutInterval = 10 if let (data, resp) = try? await URLSession.shared.data(for: req), (resp as? HTTPURLResponse)?.statusCode == 200 { tasks = (try? JSONDecoder().decode([WidgetTask].self, from: data)) ?? [] } } let activeHabits = habits.filter { !($0.isArchived ?? false) } let todayWeekday = Calendar.current.component(.weekday, from: Date()) - 1 let todayHabits = activeHabits.filter { habit in guard habit.frequency == "weekly", let days = habit.targetDays, !days.isEmpty else { return true } return days.contains(todayWeekday) } // Check completed today (simplified — check completedToday from API or logs) let completed = todayHabits.filter { $0.completedToday ?? false }.count let activeTasks = tasks.filter { !$0.completed }.count return HabitsEntry( date: Date(), completed: completed, total: todayHabits.count, tasksCount: activeTasks, streakDays: 0 ) } } // Lightweight models for widget struct WidgetHabit: Codable { let id: Int let frequency: String let targetDays: [Int]? let isArchived: Bool? let completedToday: Bool? enum CodingKeys: String, CodingKey { case id, frequency case targetDays = "target_days" case isArchived = "is_archived" case completedToday = "completed_today" } } struct WidgetTask: Codable { let id: Int let completed: Bool } // MARK: - Views struct HabitsProgressWidget: Widget { let kind = "HabitsProgress" var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: HabitsProvider()) { entry in HabitsWidgetView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Прогресс дня") .description("Привычки и задачи на сегодня") .supportedFamilies([.systemSmall, .systemMedium]) } } struct HabitsWidgetView: View { let entry: HabitsEntry @Environment(\.widgetFamily) var family var body: some View { switch family { case .systemSmall: smallView case .systemMedium: mediumView default: smallView } } var smallView: some View { VStack(spacing: 10) { // Ring ZStack { Circle() .stroke(Color.white.opacity(0.1), lineWidth: 8) .frame(width: 64, height: 64) Circle() .trim(from: 0, to: entry.progress) .stroke(Color(hex: "0D9488"), style: StrokeStyle(lineWidth: 8, lineCap: .round)) .frame(width: 64, height: 64) .rotationEffect(.degrees(-90)) VStack(spacing: 0) { Text("\(entry.completed)") .font(.system(size: 20, weight: .bold, design: .rounded)) .foregroundColor(.white) Text("/\(entry.total)") .font(.system(size: 11)) .foregroundColor(.gray) } } Text("Привычки") .font(.caption2) .foregroundColor(.gray) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(hex: "06060f")) } var mediumView: some View { HStack(spacing: 16) { // Left: ring ZStack { Circle() .stroke(Color.white.opacity(0.1), lineWidth: 8) .frame(width: 72, height: 72) Circle() .trim(from: 0, to: entry.progress) .stroke(Color(hex: "0D9488"), style: StrokeStyle(lineWidth: 8, lineCap: .round)) .frame(width: 72, height: 72) .rotationEffect(.degrees(-90)) VStack(spacing: 0) { Text("\(entry.completed)") .font(.system(size: 22, weight: .bold, design: .rounded)) .foregroundColor(.white) Text("/\(entry.total)") .font(.system(size: 12)) .foregroundColor(.gray) } } // Right: stats VStack(alignment: .leading, spacing: 8) { Text("Прогресс дня") .font(.subheadline.bold()) .foregroundColor(.white) HStack(spacing: 12) { StatBadge(icon: "checkmark.circle.fill", value: "\(entry.completed)", label: "Готово", color: Color(hex: "0D9488")) StatBadge(icon: "calendar", value: "\(entry.tasksCount)", label: "Задач", color: Color(hex: "6366f1")) } // Progress bar GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 3).fill(Color.white.opacity(0.1)) RoundedRectangle(cornerRadius: 3) .fill(Color(hex: "0D9488")) .frame(width: geo.size.width * entry.progress) } } .frame(height: 6) } } .padding(16) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(hex: "06060f")) } } struct StatBadge: View { let icon: String let value: String let label: String let color: Color var body: some View { HStack(spacing: 4) { Image(systemName: icon).font(.caption2).foregroundColor(color) Text(value).font(.caption.bold()).foregroundColor(.white) Text(label).font(.caption2).foregroundColor(.gray) } } } // Color extension for widget extension Color { init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int: UInt64 = 0 Scanner(string: hex).scanHexInt64(&int) let r, g, b: UInt64 switch hex.count { case 6: (r, g, b) = (int >> 16, int >> 8 & 0xFF, int & 0xFF) default: (r, g, b) = (0, 0, 0) } self.init(.sRGB, red: Double(r)/255, green: Double(g)/255, blue: Double(b)/255) } }