RTSDA-iOS/Views/BulletinView.swift

337 lines
15 KiB
Swift

@preconcurrency import SwiftUI
@preconcurrency import WebKit
struct BulletinView: View {
@State private var isLoading = true
@Environment(\.dismiss) private var dismiss
var body: some View {
WebViewWithRefresh(url: URL(string: "https://rtsda.updates.church")!, isLoading: $isLoading)
.navigationTitle("Bulletin")
.navigationBarTitleDisplayMode(.inline)
.background(Color(uiColor: .systemBackground))
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
if isLoading {
ProgressView()
.progressViewStyle(.circular)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
dismiss()
}) {
Image(systemName: "xmark")
.foregroundColor(.primary)
}
}
}
}
}
struct WebViewWithRefresh: UIViewRepresentable {
let url: URL
@Binding var isLoading: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
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
// Add swipe gesture recognizer
let swipeGesture = UISwipeGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleSwipe(_:)))
swipeGesture.direction = .right
webView.addGestureRecognizer(swipeGesture)
// Configure refresh control with custom appearance
let refreshControl = UIRefreshControl()
refreshControl.addTarget(context.coordinator,
action: #selector(Coordinator.handleRefresh),
for: .valueChanged)
// Set the refresh control's background color
refreshControl.backgroundColor = .clear
webView.scrollView.refreshControl = refreshControl
// Set background colors
webView.isOpaque = false
webView.backgroundColor = .clear
webView.scrollView.backgroundColor = .clear
let request = URLRequest(url: url)
webView.load(request)
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
// No updates needed
}
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 {
if let webView = gesture.view as? WKWebView {
print("📱 Attempting to trigger back action")
// JavaScript to click the Chakra UI back button
let script = """
function findBackButton() {
// Common back button selectors
var selectors = [
'button[aria-label="Go back"]',
'button.chakra-button[aria-label*="back"]',
'button.chakra-button svg[aria-label*="back"]',
'button.chakra-button span svg[aria-hidden="true"]',
'button svg[data-icon="arrow-left"]',
'button.chakra-button svg',
'button.chakra-button'
];
for (var i = 0; i < selectors.length; i++) {
var buttons = document.querySelectorAll(selectors[i]);
for (var j = 0; j < buttons.length; j++) {
var button = buttons[j];
// Check if it looks like a back button
if (button.textContent.toLowerCase().includes('back') ||
button.getAttribute('aria-label')?.toLowerCase().includes('back') ||
button.innerHTML.toLowerCase().includes('back')) {
console.log('Found back button:', button.outerHTML);
return button;
}
}
}
console.log('No back button found');
return null;
}
var backButton = findBackButton();
if (backButton) {
backButton.click();
true;
} else {
false;
}
"""
webView.evaluateJavaScript(script) { result, error in
if let error = error {
print(" JavaScript error: \(error.localizedDescription)")
} else if let success = result as? Bool {
print(success ? " Back button clicked" : " No back button found")
}
}
}
}
}
@objc func handleRefresh(sender: UIRefreshControl) {
parent.isLoading = true
if let webView = sender.superview?.superview as? WKWebView {
// Clear all website data
WKWebsiteDataStore.default().removeData(
ofTypes: [WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache],
modifiedSince: Date(timeIntervalSince1970: 0)
) {
DispatchQueue.main.async {
// Create a fresh request
if let url = webView.url {
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData)
webView.load(request)
}
}
}
}
}
// Navigation delegate methods
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
parent.isLoading = true
}
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) {
parent.isLoading = false
webView.scrollView.refreshControl?.endRefreshing()
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
parent.isLoading = false
webView.scrollView.refreshControl?.endRefreshing()
}
// Handle back navigation
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .backForward {
decisionHandler(.cancel)
parent.isLoading = false
return
}
decisionHandler(.allow)
}
}
}
#Preview {
NavigationStack {
BulletinView()
}
}