
- Comprehensive README update documenting v2.0 architectural changes - Updated git remote to ssh://rockvilleav@git.rockvilletollandsda.church:10443/RTSDA/RTSDA-iOS.git - Documented unified ChurchService and 60% code reduction - Added new features: Home Feed, responsive reading, enhanced UI - Corrected license information (GPL v3 with church content copyright) - Updated build instructions and technical stack details
308 lines
12 KiB
Swift
308 lines
12 KiB
Swift
import SwiftUI
|
|
#if canImport(EventKit)
|
|
import EventKit
|
|
#endif
|
|
|
|
// MARK: - Shared Components
|
|
|
|
struct EventActionButtons: View {
|
|
let event: ChurchEvent
|
|
@Binding var showingDirections: Bool
|
|
@Binding var showingShareSheet: Bool
|
|
@Binding var showingCalendarAlert: Bool
|
|
@Binding var calendarMessage: String
|
|
let style: ActionButtonStyle
|
|
|
|
enum ActionButtonStyle {
|
|
case iphone
|
|
case ipad
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 12) {
|
|
if !event.location.isEmpty {
|
|
Button(action: { EventDetailActions.handleDirections(for: event, showingDirections: $showingDirections) }) {
|
|
HStack {
|
|
Image(systemName: EventDetailActions.isOnlineEvent(event) ? "link" : "location.fill")
|
|
Text(EventDetailActions.isOnlineEvent(event) ? "Join Event" : "Get Directions")
|
|
}
|
|
.applyButtonStyle(style)
|
|
.foregroundColor(.white)
|
|
.if(style == .ipad) { view in
|
|
view.background(Color.blue, in: Capsule())
|
|
}
|
|
.if(style == .iphone) { view in
|
|
view.background(Color.blue, in: RoundedRectangle(cornerRadius: 10))
|
|
}
|
|
}
|
|
}
|
|
|
|
#if canImport(EventKit)
|
|
Button(action: {
|
|
EventDetailActions.addToCalendar(
|
|
event: event,
|
|
showingCalendarAlert: $showingCalendarAlert,
|
|
calendarMessage: $calendarMessage
|
|
)
|
|
}) {
|
|
HStack {
|
|
Image(systemName: "calendar.badge.plus")
|
|
Text("Add to Calendar")
|
|
}
|
|
.applyButtonStyle(style)
|
|
.foregroundColor(.white)
|
|
.if(style == .ipad) { view in
|
|
view.background(Color.orange, in: Capsule())
|
|
}
|
|
.if(style == .iphone) { view in
|
|
view.background(Color.orange, in: RoundedRectangle(cornerRadius: 10))
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if os(iOS)
|
|
Button(action: {
|
|
DispatchQueue.main.async {
|
|
showingShareSheet = true
|
|
}
|
|
}) {
|
|
HStack {
|
|
Image(systemName: "square.and.arrow.up")
|
|
Text("Share Event")
|
|
}
|
|
.applyButtonStyle(style)
|
|
.foregroundColor(.primary)
|
|
.if(style == .ipad) { view in
|
|
view.background(.thickMaterial, in: Capsule())
|
|
}
|
|
.if(style == .iphone) { view in
|
|
view.background(Color.gray.opacity(0.2), in: RoundedRectangle(cornerRadius: 10))
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Shared Actions
|
|
|
|
struct EventDetailActions {
|
|
static func isOnlineEvent(_ event: ChurchEvent) -> Bool {
|
|
let onlineKeywords = ["zoom", "whatsapp", "online", "virtual", "webinar", "meeting", "call"]
|
|
return onlineKeywords.contains { keyword in
|
|
event.location.lowercased().contains(keyword)
|
|
}
|
|
}
|
|
|
|
static func handleDirections(for event: ChurchEvent, showingDirections: Binding<Bool>) {
|
|
if isOnlineEvent(event) {
|
|
if let locationUrl = event.locationUrl, let url = URL(string: locationUrl) {
|
|
UIApplication.shared.open(url)
|
|
} else {
|
|
showingDirections.wrappedValue = true
|
|
}
|
|
} else {
|
|
let encodedLocation = event.location.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
|
if let url = URL(string: "http://maps.apple.com/?q=\(encodedLocation)") {
|
|
UIApplication.shared.open(url)
|
|
}
|
|
}
|
|
}
|
|
|
|
#if canImport(EventKit)
|
|
static func addToCalendar(event: ChurchEvent, showingCalendarAlert: Binding<Bool>, calendarMessage: Binding<String>) {
|
|
let eventStore = EKEventStore()
|
|
|
|
if #available(iOS 17.0, *) {
|
|
eventStore.requestWriteOnlyAccessToEvents { granted, error in
|
|
handleCalendarAccess(granted: granted, error: error, eventStore: eventStore, event: event, showingCalendarAlert: showingCalendarAlert, calendarMessage: calendarMessage)
|
|
}
|
|
} else {
|
|
eventStore.requestAccess(to: .event) { granted, error in
|
|
handleCalendarAccess(granted: granted, error: error, eventStore: eventStore, event: event, showingCalendarAlert: showingCalendarAlert, calendarMessage: calendarMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func handleCalendarAccess(granted: Bool, error: Error?, eventStore: EKEventStore, event: ChurchEvent, showingCalendarAlert: Binding<Bool>, calendarMessage: Binding<String>) {
|
|
DispatchQueue.main.async {
|
|
if granted && error == nil {
|
|
// Use Rust function to parse event data (RTSDA Architecture Rules compliance)
|
|
guard let eventJson = try? JSONEncoder().encode(event),
|
|
let jsonString = String(data: eventJson, encoding: .utf8) else {
|
|
calendarMessage.wrappedValue = "Failed to process event data."
|
|
showingCalendarAlert.wrappedValue = true
|
|
return
|
|
}
|
|
|
|
let calendarDataJson = createCalendarEventData(eventJson: jsonString)
|
|
let parsedDataJson = parseCalendarEventData(calendarJson: calendarDataJson)
|
|
|
|
guard let data = parsedDataJson.data(using: .utf8),
|
|
let calendarData = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
calendarMessage.wrappedValue = "Failed to parse calendar response"
|
|
showingCalendarAlert.wrappedValue = true
|
|
return
|
|
}
|
|
|
|
guard let success = calendarData["success"] as? Bool, success else {
|
|
let errorMsg = calendarData["error"] as? String ?? "Failed to parse event dates"
|
|
calendarMessage.wrappedValue = errorMsg
|
|
showingCalendarAlert.wrappedValue = true
|
|
return
|
|
}
|
|
|
|
let calendarEvent = EKEvent(eventStore: eventStore)
|
|
calendarEvent.title = calendarData["title"] as? String ?? event.title
|
|
calendarEvent.notes = calendarData["description"] as? String ?? event.description
|
|
calendarEvent.location = calendarData["location"] as? String ?? event.location
|
|
|
|
// Use parsed timestamps from Rust
|
|
if let startTimestamp = calendarData["start_timestamp"] as? TimeInterval,
|
|
let endTimestamp = calendarData["end_timestamp"] as? TimeInterval {
|
|
calendarEvent.startDate = Date(timeIntervalSince1970: startTimestamp)
|
|
calendarEvent.endDate = Date(timeIntervalSince1970: endTimestamp)
|
|
} else {
|
|
calendarMessage.wrappedValue = "Failed to parse event timestamps."
|
|
showingCalendarAlert.wrappedValue = true
|
|
return
|
|
}
|
|
|
|
// Add recurrence rule if event is recurring
|
|
if let hasRecurrence = calendarData["has_recurrence"] as? Bool, hasRecurrence,
|
|
let recurringType = calendarData["recurring_type"] as? String {
|
|
if let recurrenceRule = createRecurrenceRule(from: recurringType) {
|
|
calendarEvent.recurrenceRules = [recurrenceRule]
|
|
}
|
|
}
|
|
|
|
calendarEvent.calendar = eventStore.defaultCalendarForNewEvents
|
|
|
|
do {
|
|
// Use .futureEvents for recurring events, .thisEvent for single events
|
|
let span: EKSpan = (calendarData["has_recurrence"] as? Bool == true) ? .futureEvents : .thisEvent
|
|
try eventStore.save(calendarEvent, span: span)
|
|
|
|
let message = (calendarData["has_recurrence"] as? Bool == true) ?
|
|
"Recurring event series successfully added to your calendar!" :
|
|
"Event successfully added to your calendar!"
|
|
calendarMessage.wrappedValue = message
|
|
showingCalendarAlert.wrappedValue = true
|
|
} catch {
|
|
calendarMessage.wrappedValue = "Failed to add event to calendar. Please try again."
|
|
showingCalendarAlert.wrappedValue = true
|
|
}
|
|
} else {
|
|
calendarMessage.wrappedValue = "Calendar access is required to add events. Please enable it in Settings."
|
|
showingCalendarAlert.wrappedValue = true
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func createRecurrenceRule(from recurringType: String) -> EKRecurrenceRule? {
|
|
switch recurringType.uppercased() {
|
|
case "DAILY":
|
|
// Daily Prayer Meeting: Sunday-Friday (exclude Saturday)
|
|
let weekdays = [
|
|
EKRecurrenceDayOfWeek(.sunday),
|
|
EKRecurrenceDayOfWeek(.monday),
|
|
EKRecurrenceDayOfWeek(.tuesday),
|
|
EKRecurrenceDayOfWeek(.wednesday),
|
|
EKRecurrenceDayOfWeek(.thursday),
|
|
EKRecurrenceDayOfWeek(.friday)
|
|
]
|
|
return EKRecurrenceRule(
|
|
recurrenceWith: .weekly,
|
|
interval: 1,
|
|
daysOfTheWeek: weekdays,
|
|
daysOfTheMonth: nil,
|
|
monthsOfTheYear: nil,
|
|
weeksOfTheYear: nil,
|
|
daysOfTheYear: nil,
|
|
setPositions: nil,
|
|
end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date())
|
|
)
|
|
case "WEEKLY":
|
|
return EKRecurrenceRule(
|
|
recurrenceWith: .weekly,
|
|
interval: 1,
|
|
end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date())
|
|
)
|
|
case "BIWEEKLY":
|
|
return EKRecurrenceRule(
|
|
recurrenceWith: .weekly,
|
|
interval: 2,
|
|
end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date())
|
|
)
|
|
case "MONTHLY":
|
|
return EKRecurrenceRule(
|
|
recurrenceWith: .monthly,
|
|
interval: 1,
|
|
end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date())
|
|
)
|
|
case "FIRST_TUESDAY":
|
|
let dayOfWeek = EKRecurrenceDayOfWeek(.tuesday)
|
|
return EKRecurrenceRule(
|
|
recurrenceWith: .monthly,
|
|
interval: 1,
|
|
daysOfTheWeek: [dayOfWeek],
|
|
daysOfTheMonth: nil,
|
|
monthsOfTheYear: nil,
|
|
weeksOfTheYear: nil,
|
|
daysOfTheYear: nil,
|
|
setPositions: [1],
|
|
end: EKRecurrenceEnd(end: Calendar.current.date(byAdding: .year, value: 1, to: Date()) ?? Date())
|
|
)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static func createShareText(for event: ChurchEvent) -> String {
|
|
return """
|
|
\(event.title)
|
|
|
|
\(event.description)
|
|
|
|
📅 \(event.isMultiDay ? event.formattedDateTime : event.formattedDateRange)
|
|
🕐 \(event.formattedTime)
|
|
📍 \(event.location)
|
|
|
|
Join us at Rockville Tolland SDA Church!
|
|
"""
|
|
}
|
|
}
|
|
|
|
// MARK: - Helper Extensions
|
|
|
|
extension View {
|
|
func applyButtonStyle(_ style: EventActionButtons.ActionButtonStyle) -> AnyView {
|
|
switch style {
|
|
case .iphone:
|
|
return AnyView(
|
|
self
|
|
.frame(maxWidth: .infinity)
|
|
.padding()
|
|
)
|
|
case .ipad:
|
|
return AnyView(
|
|
self
|
|
.font(.headline)
|
|
.padding(.horizontal, 24)
|
|
.padding(.vertical, 12)
|
|
)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
|
|
if condition {
|
|
transform(self)
|
|
} else {
|
|
self
|
|
}
|
|
}
|
|
}
|
|
|