Added support to bulletin view to display links for hymns to open in Adventist Hymnarium
This commit is contained in:
parent
a3862061a8
commit
01aecc4bb3
79
Info.plist
79
Info.plist
|
@ -2,50 +2,13 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>AVInitialRouteSharingPolicy</key>
|
||||
<string>LongFormVideo</string>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>org.rockvilletollandsda.rtsda.refresh</string>
|
||||
<string>org.rockvilletollandsda.rtsda.processing</string>
|
||||
</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>
|
||||
<array>
|
||||
<string>sschool</string>
|
||||
|
@ -58,6 +21,19 @@
|
|||
<string>tiktok</string>
|
||||
<string>spotify</string>
|
||||
</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>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
@ -65,12 +41,23 @@
|
|||
<key>UISceneConfigurations</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>AVInitialRouteSharingPolicy</key>
|
||||
<string>LongFormVideo</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<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>
|
||||
</plist>
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue