Kaynağa Gözat

Redesign Calendar agenda widget as compact planner view.

Rework the agenda panel into a medium mobile-style layout with quick navigation chips, a dedicated today card, focused next-up event section, and top-right open-calendar action while removing the extra upcoming list.

Made-with: Cursor
huzaifahayat12 3 ay önce
ebeveyn
işleme
73fcc15626

+ 160 - 91
google_apps/Widgets/CalendarDesktopWidgetView.swift

@@ -152,62 +152,24 @@ struct CalendarDesktopWidgetView: View {
         ZStack(alignment: .topLeading) {
             calendarGradientBackground
                 .frame(maxWidth: .infinity, maxHeight: .infinity)
-            VStack(alignment: .leading, spacing: 0) {
-                HStack(spacing: 10) {
-                    AppIconView(app: app, size: 30, showAppBackground: false, iconPaddingFactor: 0.08)
-                    VStack(alignment: .leading, spacing: 2) {
-                        Text(app.name)
-                            .font(.system(size: 14, weight: .bold))
-                            .foregroundStyle(.white.opacity(0.95))
-                        Text("Agenda")
-                            .font(.system(size: 10, weight: .semibold))
-                            .foregroundStyle(.white.opacity(0.65))
-                    }
-                    Spacer(minLength: 0)
-                    if isPreview {
-                        Text("Sample")
-                            .font(.system(size: 10, weight: .bold))
-                            .foregroundStyle(.white.opacity(0.5))
-                            .padding(.horizontal, 8)
-                            .padding(.vertical, 4)
-                            .background(Capsule().fill(Color.white.opacity(0.12)))
-                    }
-                }
-                .padding(.bottom, 10)
-
-                quickAddRow
-                    .padding(.bottom, 12)
+            VStack(alignment: .leading, spacing: 10) {
+                agendaHeader
+                agendaQuickNavRow
+                mediumTodayDateCard
 
                 if model.accessDenied && !isPreview {
-                    Text("Allow Calendar access in System Settings to show your events.")
-                        .font(.system(size: 12, weight: .medium))
-                        .foregroundStyle(.white.opacity(0.75))
-                    Button(action: openPrivacyCalendars) {
-                        Text("Privacy…")
-                            .font(.system(size: 12, weight: .bold))
-                    }
-                    .buttonStyle(.plain)
-                    .foregroundStyle(Color(red: 0.75, green: 0.9, blue: 1))
-                    .padding(.top, 4)
-                    Spacer(minLength: 0)
+                    accessDeniedCard
                 } else {
-                    ScrollView {
-                        LazyVStack(alignment: .leading, spacing: 16) {
-                            ForEach(model.agendaDays) { day in
-                                agendaDaySection(day)
-                            }
-                            if model.agendaDays.isEmpty {
-                                Text(isPreview ? "Sample events appear here." : "No events in the next several days.")
-                                    .font(.system(size: 12, weight: .medium))
-                                    .foregroundStyle(.white.opacity(0.6))
-                                    .padding(.top, 8)
-                            }
-                        }
-                        .padding(.bottom, 8)
-                    }
+                    nextEventCard
+                }
+
+                Spacer(minLength: 0)
+
+                calendarPrimaryButton(title: "Open full agenda", systemImage: "calendar.badge.clock") {
+                    openCalendarPath("/calendar/u/0/r/agenda")
                 }
             }
-            .padding(14)
+            .padding(12)
             .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
         }
         .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
@@ -215,22 +177,54 @@ struct CalendarDesktopWidgetView: View {
         .opacity(isPreview ? 0.88 : 1)
     }
 
-    private var quickAddRow: some View {
+    private var agendaHeader: some View {
         HStack(spacing: 8) {
-            quickChip(title: "New event", systemImage: "plus.circle.fill") {
-                openCalendarPath("/calendar/u/0/r/eventedit")
+            AppIconView(app: app, size: 28, showAppBackground: false, iconPaddingFactor: 0.08)
+            VStack(alignment: .leading, spacing: 1) {
+                Text(app.name)
+                    .font(.system(size: 14, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.95))
+                Text("Planner")
+                    .font(.system(size: 10, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.65))
             }
-            quickChip(title: "Tasks", systemImage: "checkmark.square") {
-                openAbsoluteURL(URL(string: "https://tasks.google.com/")!)
+            Spacer(minLength: 0)
+            Button(action: {
+                NSApp.activate(ignoringOtherApps: true)
+                openCalendarPath("/calendar/u/0/r")
+            }) {
+                Image(systemName: "arrow.up.right.square")
+                    .font(.system(size: 12, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.92))
+                    .frame(width: 32, height: 32)
+                    .background(Circle().fill(Color.white.opacity(0.14)))
             }
-            quickChip(title: "Week", systemImage: "calendar.badge.clock") {
+            .buttonStyle(.plain)
+            .disabled(isPreview)
+            .allowsHitTesting(!isPreview)
+            .help("Open Calendar")
+        }
+    }
+
+    private var agendaQuickNavRow: some View {
+        HStack(spacing: 8) {
+            compactAgendaChip(title: "Today", systemImage: "calendar") {
+                openCalendarPath("/calendar/u/0/r/day")
+            }
+            compactAgendaChip(title: "Week", systemImage: "calendar.badge.clock") {
                 openCalendarPath("/calendar/u/0/r/week")
             }
+            compactAgendaChip(title: "Month", systemImage: "calendar.circle") {
+                openCalendarPath("/calendar/u/0/r/month")
+            }
+            compactAgendaChip(title: "New", systemImage: "plus.circle.fill", emphasized: true) {
+                openCalendarPath("/calendar/u/0/r/eventedit")
+            }
             Spacer(minLength: 0)
         }
     }
 
-    private func quickChip(title: String, systemImage: String, action: @escaping () -> Void) -> some View {
+    private func compactAgendaChip(title: String, systemImage: String, emphasized: Bool = false, action: @escaping () -> Void) -> some View {
         Button(action: {
             NSApp.activate(ignoringOtherApps: true)
             action()
@@ -239,19 +233,19 @@ struct CalendarDesktopWidgetView: View {
                 Image(systemName: systemImage)
                     .font(.system(size: 11, weight: .semibold))
                 Text(title)
-                    .font(.system(size: 11, weight: .bold))
+                    .font(.system(size: 10, weight: .bold))
                     .lineLimit(1)
             }
             .foregroundStyle(.white.opacity(0.92))
-            .padding(.horizontal, 10)
-            .padding(.vertical, 8)
+            .padding(.horizontal, 9)
+            .padding(.vertical, 7)
             .background(
                 RoundedRectangle(cornerRadius: 10, style: .continuous)
-                    .fill(Color.black.opacity(0.28))
+                    .fill(emphasized ? Color(red: 0.24, green: 0.58, blue: 0.95).opacity(0.56) : Color.black.opacity(0.28))
             )
             .overlay(
                 RoundedRectangle(cornerRadius: 10, style: .continuous)
-                    .stroke(Color.white.opacity(0.14), lineWidth: 1)
+                    .stroke(Color.white.opacity(emphasized ? 0.34 : 0.14), lineWidth: 1)
             )
         }
         .buttonStyle(.plain)
@@ -259,39 +253,97 @@ struct CalendarDesktopWidgetView: View {
         .allowsHitTesting(!isPreview)
     }
 
-    private func agendaDaySection(_ day: CalendarAgendaDay) -> some View {
+    private var accessDeniedCard: some View {
         VStack(alignment: .leading, spacing: 8) {
-            Text(day.dayLabel)
-                .font(.system(size: 11, weight: .bold))
-                .foregroundStyle(.white.opacity(0.45))
-
-            ForEach(day.events) { ev in
-                VStack(alignment: .leading, spacing: 4) {
-                    HStack(alignment: .firstTextBaseline) {
-                        Text(Self.eventTimeRange(ev))
-                            .font(.system(size: 11, weight: .bold))
-                            .foregroundStyle(Color(red: 0.75, green: 0.92, blue: 0.85))
-                            .frame(width: 108, alignment: .leading)
-                        Text(ev.title.isEmpty ? "(No title)" : ev.title)
-                            .font(.system(size: 12, weight: .semibold))
-                            .foregroundStyle(.white.opacity(0.95))
-                            .lineLimit(2)
-                    }
-                    if let loc = ev.location, !loc.isEmpty {
-                        HStack(spacing: 4) {
-                            Image(systemName: "mappin.and.ellipse")
-                                .font(.system(size: 9, weight: .semibold))
-                            Text(loc)
+            Text("Calendar access is off")
+                .font(.system(size: 12, weight: .bold))
+                .foregroundStyle(.white.opacity(0.92))
+            Text("Enable Calendar permission in System Settings to show your schedule.")
+                .font(.system(size: 11, weight: .medium))
+                .foregroundStyle(.white.opacity(0.72))
+                .fixedSize(horizontal: false, vertical: true)
+            Button(action: openPrivacyCalendars) {
+                Text("Open Privacy settings")
+                    .font(.system(size: 11, weight: .bold))
+                    .foregroundStyle(Color(red: 0.75, green: 0.9, blue: 1))
+            }
+            .buttonStyle(.plain)
+        }
+        .padding(10)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.22)))
+    }
+
+    private var mediumTodayDateCard: some View {
+        let now = Date()
+        let day = Calendar.current.component(.day, from: now)
+        return HStack(spacing: 12) {
+            Text("\(day)")
+                .font(.system(size: 30, weight: .bold, design: .rounded))
+                .foregroundStyle(.white)
+                .frame(width: 48, alignment: .leading)
+            VStack(alignment: .leading, spacing: 2) {
+                Text("Today")
+                    .font(.system(size: 10, weight: .bold))
+                    .foregroundStyle(.white.opacity(0.54))
+                Text(Self.fullDateFormatter.string(from: now))
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.92))
+                    .lineLimit(1)
+            }
+            Spacer(minLength: 0)
+        }
+        .padding(10)
+        .frame(maxWidth: .infinity, alignment: .leading)
+        .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.22)))
+        .overlay(
+            RoundedRectangle(cornerRadius: 12, style: .continuous)
+                .stroke(Color.white.opacity(0.12), lineWidth: 1)
+        )
+    }
+
+    private var nextEventCard: some View {
+        VStack(alignment: .leading, spacing: 6) {
+            Text("Next up")
+                .font(.system(size: 10, weight: .bold))
+                .foregroundStyle(.white.opacity(0.54))
+
+            if let ev = model.nextEvent {
+                Button(action: {
+                    NSApp.activate(ignoringOtherApps: true)
+                    openCalendarSearch(query: ev.title)
+                }) {
+                    HStack(spacing: 8) {
+                        Image(systemName: "clock.fill")
+                            .font(.system(size: 11, weight: .semibold))
+                            .foregroundStyle(Color(red: 0.78, green: 0.9, blue: 1))
+                        VStack(alignment: .leading, spacing: 2) {
+                            Text(ev.title.isEmpty ? "(No title)" : ev.title)
+                                .font(.system(size: 12, weight: .bold))
+                                .foregroundStyle(.white.opacity(0.95))
+                                .lineLimit(1)
+                            Text(Self.eventTimeRange(ev))
                                 .font(.system(size: 10, weight: .medium))
-                                .lineLimit(2)
+                                .foregroundStyle(.white.opacity(0.66))
                         }
-                        .foregroundStyle(.white.opacity(0.45))
-                        .padding(.leading, 108)
+                        Spacer(minLength: 0)
+                        Image(systemName: "arrow.up.right")
+                            .font(.system(size: 10, weight: .bold))
+                            .foregroundStyle(.white.opacity(0.45))
                     }
+                    .padding(10)
+                    .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.24)))
                 }
-                .padding(10)
-                .frame(maxWidth: .infinity, alignment: .leading)
-                .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.2)))
+                .buttonStyle(.plain)
+                .disabled(isPreview)
+                .allowsHitTesting(!isPreview)
+            } else {
+                Text(isPreview ? "Product review at 2:00 PM" : "No upcoming events")
+                    .font(.system(size: 12, weight: .semibold))
+                    .foregroundStyle(.white.opacity(0.75))
+                    .padding(10)
+                    .frame(maxWidth: .infinity, alignment: .leading)
+                    .background(RoundedRectangle(cornerRadius: 12, style: .continuous).fill(Color.black.opacity(0.2)))
             }
         }
     }
@@ -344,6 +396,17 @@ struct CalendarDesktopWidgetView: View {
         InAppBrowserWindowManager.shared.open(url: url, title: app.name)
     }
 
+    private func openCalendarSearch(query: String) {
+        guard !isPreview else { return }
+        let q = query.trimmingCharacters(in: .whitespacesAndNewlines)
+        guard !q.isEmpty else {
+            openCalendarPath("/calendar/u/0/r")
+            return
+        }
+        let encoded = q.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) ?? q
+        openCalendarPath("/calendar/u/0/r/search?q=\(encoded)")
+    }
+
     private func openPrivacyCalendars() {
         guard !isPreview else { return }
         let s = "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars"
@@ -364,6 +427,12 @@ struct CalendarDesktopWidgetView: View {
         return f
     }()
 
+    private static let fullDateFormatter: DateFormatter = {
+        let f = DateFormatter()
+        f.dateFormat = "EEEE, MMM d"
+        return f
+    }()
+
     private static func eventTimeRange(_ ev: CalendarEventSnapshot) -> String {
         if ev.isAllDay {
             return "All day"

+ 2 - 2
google_apps/Widgets/WidgetTemplates.swift

@@ -611,7 +611,7 @@ extension WidgetLayoutMode {
         case .calendar(let mode):
             switch mode {
             case .compact: return CGSize(width: 230, height: 260)
-            case .agenda: return CGSize(width: 420, height: 480)
+            case .agenda: return CGSize(width: 420, height: 320)
             }
         case .interactiveMap:
             return CGSize(width: 420, height: 300)
@@ -683,7 +683,7 @@ extension WidgetLayoutMode {
         case .calendar(let mode):
             switch mode {
             case .compact: return CGSize(width: 210, height: 240)
-            case .agenda: return CGSize(width: 380, height: 440)
+            case .agenda: return CGSize(width: 380, height: 290)
             }
         case .interactiveMap:
             return CGSize(width: 380, height: 270)