diff --git a/Sources/CalendarService.swift b/Sources/CalendarService.swift index 0f5cbf4..4763f4c 100644 --- a/Sources/CalendarService.swift +++ b/Sources/CalendarService.swift @@ -27,6 +27,14 @@ class CalendarService: ObservableObject { ) { [weak self] _ in self?.handleEventStoreChanged() } + + NotificationCenter.default.addObserver( + forName: .NSCalendarDayChanged, + object: nil, + queue: .main + ) { [weak self] _ in + self?.handleDayChanged() + } } @MainActor @@ -35,6 +43,11 @@ class CalendarService: ObservableObject { loadEvents() } + @MainActor + private func handleDayChanged() { + loadEvents() + } + @MainActor func requestAccess() async { do { diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index b1d3a3c..650f2f1 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -126,34 +126,45 @@ struct DayGroup { struct EventRow: View { let event: EKEvent + @State private var showingDetail = false var body: some View { - VStack(alignment: .leading, spacing: 4) { - HStack { - if event.isAllDay { - Text("終日") - .font(.caption) - .foregroundColor(.secondary) - } else { - Text(timeString(from: event.startDate, to: event.endDate)) + Button(action: { + showingDetail = true + }) { + VStack(alignment: .leading, spacing: 4) { + HStack { + if event.isAllDay { + Text("終日") + .font(.caption) + .foregroundColor(.secondary) + } else { + Text(timeString(from: event.startDate, to: event.endDate)) + .font(.caption) + .foregroundColor(.secondary) + } + if let calendar = event.calendar { + Circle() + .fill(Color(calendar.color)) + .frame(width: 8, height: 8) + } + } + Text(event.title ?? "無題のイベント") + .font(.body) + if let location = event.location, !location.isEmpty { + Text(location) .font(.caption) .foregroundColor(.secondary) } - if let calendar = event.calendar { - Circle() - .fill(Color(calendar.color)) - .frame(width: 8, height: 8) - } - } - Text(event.title ?? "無題のイベント") - .font(.body) - if let location = event.location, !location.isEmpty { - Text(location) - .font(.caption) - .foregroundColor(.secondary) } + .padding(.vertical, 4) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .popover(isPresented: $showingDetail) { + EventDetailView(event: event) } - .padding(.vertical, 4) } private func timeString(from start: Date, to end: Date) -> String { @@ -162,3 +173,88 @@ struct EventRow: View { return "\(formatter.string(from: start)) - \(formatter.string(from: end))" } } + +struct EventDetailView: View { + let event: EKEvent + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + if let calendar = event.calendar { + Circle() + .fill(Color(calendar.color)) + .frame(width: 12, height: 12) + Text(calendar.title) + .font(.caption) + .foregroundColor(.secondary) + } + } + + Text(event.title ?? "無題のイベント") + .font(.headline) + + Divider() + + VStack(alignment: .leading, spacing: 8) { + if event.isAllDay { + HStack { + Image(systemName: "clock") + .foregroundColor(.secondary) + .frame(width: 20) + Text("終日") + } + } else { + HStack(alignment: .top) { + Image(systemName: "clock") + .foregroundColor(.secondary) + .frame(width: 20) + VStack(alignment: .leading, spacing: 2) { + Text(formatDateTime(event.startDate)) + Text("〜") + .foregroundColor(.secondary) + .font(.caption) + Text(formatDateTime(event.endDate)) + } + } + } + + if let location = event.location, !location.isEmpty { + HStack(alignment: .top) { + Image(systemName: "location") + .foregroundColor(.secondary) + .frame(width: 20) + Text(location) + } + } + + if let notes = event.notes, !notes.isEmpty { + HStack(alignment: .top) { + Image(systemName: "note.text") + .foregroundColor(.secondary) + .frame(width: 20) + Text(notes) + .textSelection(.enabled) + } + } + + if let url = event.url { + HStack(alignment: .top) { + Image(systemName: "link") + .foregroundColor(.secondary) + .frame(width: 20) + Link(url.absoluteString, destination: url) + } + } + } + } + .padding() + .frame(minWidth: 300, maxWidth: 400) + } + + private func formatDateTime(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "M月d日(E) HH:mm" + formatter.locale = Locale(identifier: "ja_JP") + return formatter.string(from: date) + } +}