From 01aecc4bb37ee38a39ab05358ac553b7418335fb Mon Sep 17 00:00:00 2001 From: RTSDA Date: Sat, 22 Mar 2025 17:30:26 -0400 Subject: [PATCH] Added support to bulletin view to display links for hymns to open in Adventist Hymnarium --- Info.plist | 79 ++++++-------- Models/Event.swift | 5 +- RTSDA.xcodeproj/project.pbxproj | 38 +++++-- RTSDAApp.swift | 9 ++ Services/AppAvailabilityService.swift | 76 ++++++++++---- ViewModels/EventsViewModel.swift | 43 +++++--- Views/BulletinView.swift | 146 +++++++++++++++++++++++++- Views/ContentView.swift | 28 ++++- 8 files changed, 328 insertions(+), 96 deletions(-) diff --git a/Info.plist b/Info.plist index 6ebb6cd..7f0a538 100644 --- a/Info.plist +++ b/Info.plist @@ -2,50 +2,13 @@ - UILaunchStoryboardName - LaunchScreen - UIBackgroundModes - - audio - remote-notification - + AVInitialRouteSharingPolicy + LongFormVideo BGTaskSchedulerPermittedIdentifiers org.rockvilletollandsda.rtsda.refresh org.rockvilletollandsda.rtsda.processing - UIAppFonts - - Montserrat-Regular.ttf - Montserrat-SemiBold.ttf - Lora-Italic.ttf - - NSCalendarsUsageDescription - We need access to your calendar to add church events that you're interested in attending. - NSCalendarsWriteOnlyAccessUsageDescription - We can add church events to your calendar without viewing your existing events. - NSLocationWhenInUseUsageDescription - Your location is used to provide directions to church events and activities. - NSLocationAlwaysAndWhenInUseUsageDescription - Your location is used to provide directions to church events and activities. - NSCameraUsageDescription - The camera can be used to scan QR codes for event registration or take photos during church events. - NSPhotoLibraryUsageDescription - Access to your photo library allows you to share photos from church events and save event QR codes. - NSMicrophoneUsageDescription - The microphone is used for live stream audio and voice interactions during online events. - NSContactsUsageDescription - Access to contacts allows you to easily share church events with friends and family. - NSRemindersUsageDescription - Reminders can be set for upcoming church events and activities you're interested in. - NSAppleMusicUsageDescription - Access to media library is used for hymns and worship music during services. - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - LSApplicationQueriesSchemes sschool @@ -58,6 +21,19 @@ tiktok spotify + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + NSCalendarsWriteOnlyAccessUsageDescription + We can add church events to your calendar without viewing your existing events. + UIAppFonts + + Montserrat-Regular.ttf + Montserrat-SemiBold.ttf + Lora-Italic.ttf + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -65,12 +41,23 @@ UISceneConfigurations - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - AVInitialRouteSharingPolicy - LongFormVideo + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIBackgroundModes + + audio + remote-notification + diff --git a/Models/Event.swift b/Models/Event.swift index aab72ba..d8807f3 100644 --- a/Models/Event.swift +++ b/Models/Event.swift @@ -298,17 +298,20 @@ struct Event: Identifiable, Codable { let formatters = [ { () -> DateFormatter in let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSZ" // PocketBase format + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSZ" + formatter.timeZone = TimeZone(identifier: "America/New_York")! // Eastern Time return formatter }(), { () -> ISO8601DateFormatter in let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + formatter.timeZone = TimeZone(identifier: "America/New_York")! // Eastern Time return formatter }(), { () -> ISO8601DateFormatter in let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime] + formatter.timeZone = TimeZone(identifier: "America/New_York")! // Eastern Time return formatter }() ] diff --git a/RTSDA.xcodeproj/project.pbxproj b/RTSDA.xcodeproj/project.pbxproj index 5ed6f48..7a4cb40 100644 --- a/RTSDA.xcodeproj/project.pbxproj +++ b/RTSDA.xcodeproj/project.pbxproj @@ -501,24 +501,33 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = RTSDA.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; DEVELOPMENT_TEAM = TQMND62F2W; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference"; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_NSAppleMusicUsageDescription = "Access to media library is used for hymns and worship music during services."; + INFOPLIST_KEY_NSCalendarsUsageDescription = "We need access to your calendar to add church events that you're interested in attending."; + INFOPLIST_KEY_NSCameraUsageDescription = "The camera can be used to scan QR codes for event registration or take photos during church events."; + INFOPLIST_KEY_NSContactsUsageDescription = "Access to contacts allows you to easily share church events with friends and family."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Your location is used to provide directions to church events and activities."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Your location is used to provide directions to church events and activities."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "The microphone is used for live stream audio and voice interactions during online events."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Access to your photo library allows you to share photos from church events and save event QR codes."; + INFOPLIST_KEY_NSRemindersUsageDescription = "Reminders can be set for upcoming church events and activities you're interested in."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.rtsda.appr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -535,24 +544,33 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = RTSDA.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; DEVELOPMENT_TEAM = TQMND62F2W; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference"; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_NSAppleMusicUsageDescription = "Access to media library is used for hymns and worship music during services."; + INFOPLIST_KEY_NSCalendarsUsageDescription = "We need access to your calendar to add church events that you're interested in attending."; + INFOPLIST_KEY_NSCameraUsageDescription = "The camera can be used to scan QR codes for event registration or take photos during church events."; + INFOPLIST_KEY_NSContactsUsageDescription = "Access to contacts allows you to easily share church events with friends and family."; + INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "Your location is used to provide directions to church events and activities."; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Your location is used to provide directions to church events and activities."; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "The microphone is used for live stream audio and voice interactions during online events."; + INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Access to your photo library allows you to share photos from church events and save event QR codes."; + INFOPLIST_KEY_NSRemindersUsageDescription = "Reminders can be set for upcoming church events and activities you're interested in."; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = com.rtsda.appr; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/RTSDAApp.swift b/RTSDAApp.swift index 3ce5478..9ed4cc8 100644 --- a/RTSDAApp.swift +++ b/RTSDAApp.swift @@ -11,6 +11,15 @@ import SwiftUI struct RTSDAApp: App { @StateObject private var configService = ConfigService.shared + init() { + // Enable standard orientations (portrait and landscape) + if #available(iOS 16.0, *) { + UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.forEach { windowScene in + windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: [.portrait, .landscapeLeft, .landscapeRight])) + } + } + } + var body: some Scene { WindowGroup { SplashScreenView() diff --git a/Services/AppAvailabilityService.swift b/Services/AppAvailabilityService.swift index b7b6a94..20da01c 100644 --- a/Services/AppAvailabilityService.swift +++ b/Services/AppAvailabilityService.swift @@ -38,6 +38,7 @@ class AppAvailabilityService { checkAvailability(urlScheme: Schemes.sabbathSchool) checkAvailability(urlScheme: Schemes.egw) checkAvailability(urlScheme: Schemes.bible) + checkAvailability(urlScheme: Schemes.hymnal) checkAvailability(urlScheme: Schemes.facebook) checkAvailability(urlScheme: Schemes.tiktok) checkAvailability(urlScheme: Schemes.spotify) @@ -87,34 +88,67 @@ class AppAvailabilityService { } } - private func handleFallback(urlScheme: String, fallbackURL: String) { - // Special handling for Sabbath School app - if urlScheme == Schemes.sabbathSchool { - if let altUrl = URL(string: Schemes.sabbathSchoolAlt) { - print("✅ Opening Sabbath School web app: \(altUrl)") - UIApplication.shared.open(altUrl) - return - } - } - // Special handling for EGW Writings app - else if urlScheme == Schemes.egw { - if let webUrl = URL(string: Schemes.egwWritingsWeb) { - print("✅ Opening EGW mobile web URL: \(webUrl)") - UIApplication.shared.open(webUrl) - return - } - } + // Open a specific hymn by number + func openHymnByNumber(_ hymnNumber: Int) { + // Format: adventisthymnarium://hymn?number=123 + let hymnalScheme = "\(Schemes.hymnal)hymn?number=\(hymnNumber)" + print("🎵 Attempting to open hymn #\(hymnNumber): \(hymnalScheme)") - // Try the fallback URL + if let hymnUrl = URL(string: hymnalScheme) { + if UIApplication.shared.canOpenURL(hymnUrl) { + UIApplication.shared.open(hymnUrl) { success in + if !success { + print("❌ Failed to open hymn #\(hymnNumber)") + self.openApp(urlScheme: Schemes.hymnal, fallbackURL: AppStoreURLs.hymnal) + } + } + } else { + print("❌ Cannot open hymn #\(hymnNumber)") + openApp(urlScheme: Schemes.hymnal, fallbackURL: AppStoreURLs.hymnal) + } + } else { + print("⚠️ Failed to create URL for hymn #\(hymnNumber)") + openApp(urlScheme: Schemes.hymnal, fallbackURL: AppStoreURLs.hymnal) + } + } + + private func handleFallback(urlScheme: String, fallbackURL: String) { + // Try the App Store URL first if let fallback = URL(string: fallbackURL) { - print("⬇️ Falling back to: \(fallback)") + print("⬇️ Opening App Store: \(fallback)") UIApplication.shared.open(fallback) { success in if !success { - print("❌ Failed to open fallback URL: \(fallback)") + print("❌ Failed to open App Store URL: \(fallback)") + + // Handle web fallbacks if App Store fails + if urlScheme == Schemes.egw { + if let webUrl = URL(string: Schemes.egwWritingsWeb) { + print("✅ Opening EGW mobile web URL: \(webUrl)") + UIApplication.shared.open(webUrl) + } + } else if urlScheme == Schemes.sabbathSchool { + if let altUrl = URL(string: Schemes.sabbathSchoolAlt) { + print("✅ Opening Sabbath School web app: \(altUrl)") + UIApplication.shared.open(altUrl) + } + } } } } else { - print("❌ Failed to create fallback URL: \(fallbackURL)") + print("❌ Failed to create App Store URL: \(fallbackURL)") + + // Handle web fallbacks if App Store URL is invalid + if urlScheme == Schemes.egw { + if let webUrl = URL(string: Schemes.egwWritingsWeb) { + print("✅ Opening EGW mobile web URL: \(webUrl)") + UIApplication.shared.open(webUrl) + } + } else if urlScheme == Schemes.sabbathSchool { + if let altUrl = URL(string: Schemes.sabbathSchoolAlt) { + print("✅ Opening Sabbath School web app: \(altUrl)") + UIApplication.shared.open(altUrl) + } + } } } } diff --git a/ViewModels/EventsViewModel.swift b/ViewModels/EventsViewModel.swift index 96f5a3b..809f738 100644 --- a/ViewModels/EventsViewModel.swift +++ b/ViewModels/EventsViewModel.swift @@ -13,25 +13,42 @@ class EventsViewModel: ObservableObject { error = nil do { + // Get current time let now = Date() - let calendar = Calendar.current - let todayStart = calendar.startOfDay(for: now) + print("🕒 Current time: \(now)") - // Keep events that either: - // 1. Start in the future (after today), or - // 2. Are today and haven't ended yet - events = try await pocketBaseService.fetchEvents() + // Show all events that haven't ended yet + let allEvents = try await pocketBaseService.fetchEvents() + print("📋 Total events from PocketBase: \(allEvents.count)") + + for event in allEvents { + print("🗓️ Event: \(event.title)") + print(" Start: \(event.startDate)") + print(" End: \(event.endDate)") + print(" Category: \(event.category.rawValue)") + print(" Recurring: \(event.reoccuring.rawValue)") + print(" Published: \(event.isPublished)") + } + + events = allEvents .filter { event in - let eventStart = calendar.startOfDay(for: event.startDate) - if eventStart > todayStart { - return true // Future event - } else if eventStart == todayStart { - return event.endDate > now // Today's event that hasn't ended - } - return false + // Subtract 5 hours from the current time to match the UTC offset + // Because PocketBase stores "5 AM Eastern" as "5 AM UTC" + // So when it's actually "10 AM UTC", PocketBase shows "5 AM UTC" + let utcOffset = -5 * 60 * 60 // -5 hours in seconds + let adjustedNow = now.addingTimeInterval(TimeInterval(utcOffset)) + let willShow = event.endDate > adjustedNow + print(" Compare - Event: \(event.title)") + print(" End time: \(event.endDate)") + print(" Current time (adjusted to UTC): \(adjustedNow)") + print(" Will show: \(willShow)") + return willShow } .sorted { $0.startDate < $1.startDate } + + print("✅ Filtered events count: \(events.count)") } catch { + print("❌ Error loading events: \(error)") self.error = error } diff --git a/Views/BulletinView.swift b/Views/BulletinView.swift index 3aeb4c8..1d6195b 100644 --- a/Views/BulletinView.swift +++ b/Views/BulletinView.swift @@ -38,7 +38,126 @@ struct WebViewWithRefresh: UIViewRepresentable { } func makeUIView(context: Context) -> WKWebView { + // Create configuration with script message handler let configuration = WKWebViewConfiguration() + let contentController = WKUserContentController() + + // Add hymn detection script + let hymnDetectionScript = """ + function detectAndModifyHymns() { + // Regular expression to match patterns like: + // - "Hymn XXX" or "Hymnal XXX" with optional quotes and title + // - "#XXX" with optional quotes and title + // - But NOT match when the number is followed by a colon (e.g., "10:45") + // - And NOT match when the number is actually part of a larger number + const hymnRegex = /(?:(hymn(?:al)?\\s+#?)|#)(\\d+)(?![\\d:\\.]|\\d*[apm])(?:\\s+["']([^"']+)["'])?/gi; + + // Extra check before creating links + function isValidHymnNumber(text, matchIndex, number) { + // Make sure this is not part of a time (e.g., "Hymn 10:45am") + const afterMatch = text.substring(matchIndex + number.length); + if (afterMatch.match(/^\\s*[:.]\\d|\\d*[apm]/)) { + return false; + } + return true; + } + + // Function to replace text with a styled link + function replaceWithLink(node) { + if (node.nodeType === 3) { + // Text node + const content = node.textContent; + if (hymnRegex.test(content)) { + // Reset regex lastIndex + hymnRegex.lastIndex = 0; + + // Create a temporary element + const span = document.createElement('span'); + let lastIndex = 0; + let match; + + // Find all matches and replace them with links + while ((match = hymnRegex.exec(content)) !== null) { + // Add text before the match + if (match.index > lastIndex) { + span.appendChild(document.createTextNode(content.substring(lastIndex, match.index))); + } + + // Get the hymn number + const hymnNumber = match[2]; + + // Extra validation to ensure this isn't part of a time + const prefixLength = match[0].length - hymnNumber.length; + const numberStartIndex = match.index + prefixLength; + + if (!isValidHymnNumber(content, numberStartIndex, hymnNumber)) { + // Just add the original text if it's not a valid hymn reference + span.appendChild(document.createTextNode(match[0])); + } else { + // Create link element for valid hymn numbers + const hymnTitle = match[3] ? ': ' + match[3] : ''; + const link = document.createElement('a'); + link.textContent = match[0]; + link.href = 'javascript:void(0)'; + link.className = 'hymn-link'; + link.setAttribute('data-hymn-number', hymnNumber); + link.style.color = '#0070c9'; + link.style.textDecoration = 'underline'; + link.style.fontWeight = 'bold'; + link.onclick = function(e) { + e.preventDefault(); + window.webkit.messageHandlers.hymnHandler.postMessage({ number: hymnNumber }); + }; + + span.appendChild(link); + } + + lastIndex = match.index + match[0].length; + } + + // Add any remaining text + if (lastIndex < content.length) { + span.appendChild(document.createTextNode(content.substring(lastIndex))); + } + + // Replace the original node with our span containing links + if (span.childNodes.length > 0) { + node.parentNode.replaceChild(span, node); + } + } + } else if (node.nodeType === 1 && node.nodeName !== 'SCRIPT' && node.nodeName !== 'STYLE' && node.nodeName !== 'A') { + // Element node, not a script or style tag or already a link + Array.from(node.childNodes).forEach(child => replaceWithLink(child)); + } + } + + // Process the document body + replaceWithLink(document.body); + + console.log('Hymn detection script executed'); + } + + // Call the function after page has loaded and whenever content changes + detectAndModifyHymns(); + + // Use a MutationObserver to detect DOM changes and reapply the links + const observer = new MutationObserver(mutations => { + detectAndModifyHymns(); + }); + observer.observe(document.body, { childList: true, subtree: true }); + """ + + let userScript = WKUserScript( + source: hymnDetectionScript, + injectionTime: .atDocumentEnd, + forMainFrameOnly: false + ) + + contentController.addUserScript(userScript) + contentController.add(context.coordinator, name: "hymnHandler") + configuration.userContentController = contentController + + // Create the web view with our configuration let webView = WKWebView(frame: .zero, configuration: configuration) webView.navigationDelegate = context.coordinator @@ -70,13 +189,28 @@ struct WebViewWithRefresh: UIViewRepresentable { // No updates needed } - class Coordinator: NSObject, WKNavigationDelegate { + class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { var parent: WebViewWithRefresh init(_ parent: WebViewWithRefresh) { self.parent = parent } + // Handle messages from JavaScript + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + if message.name == "hymnHandler" { + guard let body = message.body as? [String: Any], + let hymnNumberString = body["number"] as? String, + let hymnNumber = Int(hymnNumberString) else { + print("❌ Invalid hymn number received") + return + } + + print("🎵 Opening hymn #\(hymnNumber)") + AppAvailabilityService.shared.openHymnByNumber(hymnNumber) + } + } + @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) { print("🔄 Swipe detected") if gesture.state == .ended { @@ -161,6 +295,16 @@ struct WebViewWithRefresh: UIViewRepresentable { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { parent.isLoading = false webView.scrollView.refreshControl?.endRefreshing() + + // Execute the hymn detection script again after the page loads + let rerunScript = "detectAndModifyHymns();" + webView.evaluateJavaScript(rerunScript) { _, error in + if let error = error { + print("❌ Error running hymn detection script: \(error.localizedDescription)") + } else { + print("✅ Hymn detection script executed after page load") + } + } } func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { diff --git a/Views/ContentView.swift b/Views/ContentView.swift index ae0a71f..a483939 100644 --- a/Views/ContentView.swift +++ b/Views/ContentView.swift @@ -462,19 +462,39 @@ struct MoreView: View { NavigationStack { List { Section("Resources") { - Link(destination: URL(string: AppAvailabilityService.Schemes.bible)!) { + Button { + AppAvailabilityService.shared.openApp( + urlScheme: AppAvailabilityService.Schemes.bible, + fallbackURL: AppAvailabilityService.AppStoreURLs.bible + ) + } label: { Label("Bible", systemImage: "book.fill") } - Link(destination: URL(string: AppAvailabilityService.Schemes.sabbathSchool)!) { + Button { + AppAvailabilityService.shared.openApp( + urlScheme: AppAvailabilityService.Schemes.sabbathSchool, + fallbackURL: AppAvailabilityService.AppStoreURLs.sabbathSchool + ) + } label: { Label("Sabbath School", systemImage: "book.fill") } - Link(destination: URL(string: AppAvailabilityService.Schemes.egw)!) { + Button { + AppAvailabilityService.shared.openApp( + urlScheme: AppAvailabilityService.Schemes.egw, + fallbackURL: AppAvailabilityService.AppStoreURLs.egwWritings + ) + } label: { Label("EGW Writings", systemImage: "book.closed.fill") } - Link(destination: URL(string: AppAvailabilityService.Schemes.hymnal)!) { + Button { + AppAvailabilityService.shared.openApp( + urlScheme: AppAvailabilityService.Schemes.hymnal, + fallbackURL: AppAvailabilityService.AppStoreURLs.hymnal + ) + } label: { Label("SDA Hymnal", systemImage: "music.note") } }