Added support to bulletin view to display links for hymns to open in Adventist Hymnarium

This commit is contained in:
RTSDA 2025-03-22 17:30:26 -04:00
parent a3862061a8
commit 01aecc4bb3
8 changed files with 328 additions and 96 deletions

View file

@ -2,50 +2,13 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>UILaunchStoryboardName</key> <key>AVInitialRouteSharingPolicy</key>
<string>LaunchScreen</string> <string>LongFormVideo</string>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>remote-notification</string>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>org.rockvilletollandsda.rtsda.refresh</string> <string>org.rockvilletollandsda.rtsda.refresh</string>
<string>org.rockvilletollandsda.rtsda.processing</string> <string>org.rockvilletollandsda.rtsda.processing</string>
</array> </array>
<key>UIAppFonts</key>
<array>
<string>Montserrat-Regular.ttf</string>
<string>Montserrat-SemiBold.ttf</string>
<string>Lora-Italic.ttf</string>
</array>
<key>NSCalendarsUsageDescription</key>
<string>We need access to your calendar to add church events that you're interested in attending.</string>
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
<string>We can add church events to your calendar without viewing your existing events.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is used to provide directions to church events and activities.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Your location is used to provide directions to church events and activities.</string>
<key>NSCameraUsageDescription</key>
<string>The camera can be used to scan QR codes for event registration or take photos during church events.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Access to your photo library allows you to share photos from church events and save event QR codes.</string>
<key>NSMicrophoneUsageDescription</key>
<string>The microphone is used for live stream audio and voice interactions during online events.</string>
<key>NSContactsUsageDescription</key>
<string>Access to contacts allows you to easily share church events with friends and family.</string>
<key>NSRemindersUsageDescription</key>
<string>Reminders can be set for upcoming church events and activities you're interested in.</string>
<key>NSAppleMusicUsageDescription</key>
<string>Access to media library is used for hymns and worship music during services.</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>sschool</string> <string>sschool</string>
@ -58,6 +21,19 @@
<string>tiktok</string> <string>tiktok</string>
<string>spotify</string> <string>spotify</string>
</array> </array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSCalendarsWriteOnlyAccessUsageDescription</key>
<string>We can add church events to your calendar without viewing your existing events.</string>
<key>UIAppFonts</key>
<array>
<string>Montserrat-Regular.ttf</string>
<string>Montserrat-SemiBold.ttf</string>
<string>Lora-Italic.ttf</string>
</array>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>
@ -65,12 +41,23 @@
<key>UISceneConfigurations</key> <key>UISceneConfigurations</key>
<dict/> <dict/>
</dict> </dict>
<key>NSAppTransportSecurity</key> <key>UISupportedInterfaceOrientations</key>
<dict> <array>
<key>NSAllowsArbitraryLoads</key> <string>UIInterfaceOrientationPortrait</string>
<true/> <string>UIInterfaceOrientationLandscapeLeft</string>
</dict> <string>UIInterfaceOrientationLandscapeRight</string>
<key>AVInitialRouteSharingPolicy</key> </array>
<string>LongFormVideo</string> <key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>remote-notification</string>
</array>
</dict> </dict>
</plist> </plist>

View file

@ -298,17 +298,20 @@ struct Event: Identifiable, Codable {
let formatters = [ let formatters = [
{ () -> DateFormatter in { () -> DateFormatter in
let formatter = DateFormatter() 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 return formatter
}(), }(),
{ () -> ISO8601DateFormatter in { () -> ISO8601DateFormatter in
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
formatter.timeZone = TimeZone(identifier: "America/New_York")! // Eastern Time
return formatter return formatter
}(), }(),
{ () -> ISO8601DateFormatter in { () -> ISO8601DateFormatter in
let formatter = ISO8601DateFormatter() let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime] formatter.formatOptions = [.withInternetDateTime]
formatter.timeZone = TimeZone(identifier: "America/New_York")! // Eastern Time
return formatter return formatter
}() }()
] ]

View file

@ -501,24 +501,33 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = RTSDA.entitlements; CODE_SIGN_ENTITLEMENTS = RTSDA.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
DEVELOPMENT_TEAM = TQMND62F2W; DEVELOPMENT_TEAM = TQMND62F2W;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Info.plist; INFOPLIST_FILE = Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference"; 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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.rtsda.appr; PRODUCT_BUNDLE_IDENTIFIER = com.rtsda.appr;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -535,24 +544,33 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = RTSDA.entitlements; CODE_SIGN_ENTITLEMENTS = RTSDA.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"Preview Content\"";
DEVELOPMENT_TEAM = TQMND62F2W; DEVELOPMENT_TEAM = TQMND62F2W;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Info.plist; INFOPLIST_FILE = Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.reference"; 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_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
IPHONEOS_DEPLOYMENT_TARGET = 17.0; IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.1;
PRODUCT_BUNDLE_IDENTIFIER = com.rtsda.appr; PRODUCT_BUNDLE_IDENTIFIER = com.rtsda.appr;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;

View file

@ -11,6 +11,15 @@ import SwiftUI
struct RTSDAApp: App { struct RTSDAApp: App {
@StateObject private var configService = ConfigService.shared @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 { var body: some Scene {
WindowGroup { WindowGroup {
SplashScreenView() SplashScreenView()

View file

@ -38,6 +38,7 @@ class AppAvailabilityService {
checkAvailability(urlScheme: Schemes.sabbathSchool) checkAvailability(urlScheme: Schemes.sabbathSchool)
checkAvailability(urlScheme: Schemes.egw) checkAvailability(urlScheme: Schemes.egw)
checkAvailability(urlScheme: Schemes.bible) checkAvailability(urlScheme: Schemes.bible)
checkAvailability(urlScheme: Schemes.hymnal)
checkAvailability(urlScheme: Schemes.facebook) checkAvailability(urlScheme: Schemes.facebook)
checkAvailability(urlScheme: Schemes.tiktok) checkAvailability(urlScheme: Schemes.tiktok)
checkAvailability(urlScheme: Schemes.spotify) checkAvailability(urlScheme: Schemes.spotify)
@ -87,34 +88,67 @@ class AppAvailabilityService {
} }
} }
private func handleFallback(urlScheme: String, fallbackURL: String) { // Open a specific hymn by number
// Special handling for Sabbath School app func openHymnByNumber(_ hymnNumber: Int) {
if urlScheme == Schemes.sabbathSchool { // Format: adventisthymnarium://hymn?number=123
if let altUrl = URL(string: Schemes.sabbathSchoolAlt) { let hymnalScheme = "\(Schemes.hymnal)hymn?number=\(hymnNumber)"
print("✅ Opening Sabbath School web app: \(altUrl)") print("🎵 Attempting to open hymn #\(hymnNumber): \(hymnalScheme)")
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
}
}
// 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) { if let fallback = URL(string: fallbackURL) {
print("⬇️ Falling back to: \(fallback)") print("⬇️ Opening App Store: \(fallback)")
UIApplication.shared.open(fallback) { success in UIApplication.shared.open(fallback) { success in
if !success { 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 { } 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)
}
}
} }
} }
} }

View file

@ -13,25 +13,42 @@ class EventsViewModel: ObservableObject {
error = nil error = nil
do { do {
// Get current time
let now = Date() let now = Date()
let calendar = Calendar.current print("🕒 Current time: \(now)")
let todayStart = calendar.startOfDay(for: now)
// Keep events that either: // Show all events that haven't ended yet
// 1. Start in the future (after today), or let allEvents = try await pocketBaseService.fetchEvents()
// 2. Are today and haven't ended yet print("📋 Total events from PocketBase: \(allEvents.count)")
events = try await pocketBaseService.fetchEvents()
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 .filter { event in
let eventStart = calendar.startOfDay(for: event.startDate) // Subtract 5 hours from the current time to match the UTC offset
if eventStart > todayStart { // Because PocketBase stores "5 AM Eastern" as "5 AM UTC"
return true // Future event // So when it's actually "10 AM UTC", PocketBase shows "5 AM UTC"
} else if eventStart == todayStart { let utcOffset = -5 * 60 * 60 // -5 hours in seconds
return event.endDate > now // Today's event that hasn't ended let adjustedNow = now.addingTimeInterval(TimeInterval(utcOffset))
} let willShow = event.endDate > adjustedNow
return false 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 } .sorted { $0.startDate < $1.startDate }
print("✅ Filtered events count: \(events.count)")
} catch { } catch {
print("❌ Error loading events: \(error)")
self.error = error self.error = error
} }

View file

@ -38,7 +38,126 @@ struct WebViewWithRefresh: UIViewRepresentable {
} }
func makeUIView(context: Context) -> WKWebView { func makeUIView(context: Context) -> WKWebView {
// Create configuration with script message handler
let configuration = WKWebViewConfiguration() 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) let webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = context.coordinator webView.navigationDelegate = context.coordinator
@ -70,13 +189,28 @@ struct WebViewWithRefresh: UIViewRepresentable {
// No updates needed // No updates needed
} }
class Coordinator: NSObject, WKNavigationDelegate { class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
var parent: WebViewWithRefresh var parent: WebViewWithRefresh
init(_ parent: WebViewWithRefresh) { init(_ parent: WebViewWithRefresh) {
self.parent = parent 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) { @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) {
print("🔄 Swipe detected") print("🔄 Swipe detected")
if gesture.state == .ended { if gesture.state == .ended {
@ -161,6 +295,16 @@ struct WebViewWithRefresh: UIViewRepresentable {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
parent.isLoading = false parent.isLoading = false
webView.scrollView.refreshControl?.endRefreshing() 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) { func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {

View file

@ -462,19 +462,39 @@ struct MoreView: View {
NavigationStack { NavigationStack {
List { List {
Section("Resources") { 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") 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") 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") 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") Label("SDA Hymnal", systemImage: "music.note")
} }
} }