|
|
@@ -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"
|