import SwiftUI private struct ButtonAction { let name: String let action: () -> Void } private struct PopupSheet: View { @Binding var isPresented: Bool @Binding var title: String @Binding var message: String var body: some View { VStack { Text(title) .font(.headline) Text(message) Button("Dismiss") { message = "" isPresented = false } }.padding() } } private struct PageIssueGenericView: View { let issue: PageIssue let buttons: [ButtonAction] var body: some View { HStack { VStack(alignment: .leading) { Text(issue.message.description) Text("\(issue.title) (\(issue.language.rawValue.uppercased()))") .font(.caption) } Spacer() ForEach(buttons, id: \.name) { button in Button(button.name, action: button.action) } } } } struct PageIssueView: View { let issue: PageIssue @EnvironmentObject private var checker: PageIssueChecker @EnvironmentObject private var content: Content @State private var showPopupMessage = false @State private var popupTitle = "Error" @State private var popupMessage = "" @State private var showPagePicker = false @State private var selectedPage: Page? @State private var showFilePicker = false @State private var selectedFile: FileResource? private var buttons: [ButtonAction] { switch issue.message { case .warning: return [.init(name: "Retry", action: retryPageCheck)] case .failedToLoadContent: return [.init(name: "Retry", action: retryPageCheck)] case .failedToParseContent: return [.init(name: "Retry", action: retryPageCheck)] case .missingFile(let missing, _): return [ .init(name: "Select file", action: { selectFile(missingFile: missing) }), .init(name: "Create external file", action: { createExternalFile(fileId: missing) }) ] case .missingPage(let missing, _): return [ .init(name: "Select page", action: selectPage), .init(name: "Create page", action: { createPage(pageId: missing) }) ] case .missingTag(let missing, _): return [ .init(name: "Select tag", action: { selectTag(missingPage: missing) }), .init(name: "Create tag", action: { createTag(tagId: missing) }) ] case .invalidCommand(_, let markdown): return [.init(name: "Replace text", action: { replaceCommand(originalText: markdown) })] } } var body: some View { PageIssueGenericView(issue: issue, buttons: buttons) .sheet(isPresented: $showPopupMessage) { PopupSheet(isPresented: $showPopupMessage, title: $popupTitle, message: $popupMessage) } .sheet(isPresented: $showPagePicker) { if let page = selectedPage { didSelect(page: page) } } content: { PagePickerView(selectedPage: $selectedPage) } .sheet(isPresented: $showFilePicker) { if let file = selectedFile { didSelect(file: file) } } content: { FileSelectionView(selectedFile: $selectedFile) } } private func show(error: String) { DispatchQueue.main.async { self.popupTitle = "Error" self.popupMessage = error self.showPopupMessage = true } } private func show(info: String) { DispatchQueue.main.async { self.popupTitle = "Info" self.popupMessage = info self.showPopupMessage = true } } private func retryPageCheck() { DispatchQueue.main.async { checker.check(pages: content.pages, clearListBeforeStart: false) } } private func selectFile(missingFile: String) { selectedFile = nil showFilePicker = true } private func didSelect(file newFile: FileResource) { guard case .missingFile(let missingFile, let markdown) = issue.message else { show(error: "Inconsistency: Selected file, but issue is not a missing file") return } replace(missing: missingFile, with: newFile.id, in: markdown) retryPageCheck() DispatchQueue.main.async { selectedFile = nil } } private func createExternalFile(fileId: String) { guard content.isValidIdForFile(fileId) else { show(error: "Invalid file id, can't create external file") return } let file = FileResource( content: content, id: fileId, isExternallyStored: true, english: "", german: "") content.add(file) retryPageCheck() } private func selectPage() { selectedPage = nil showPagePicker = true } private func didSelect(page newPage: Page) { guard case .missingPage(let missingPage, let markdown) = issue.message else { show(error: "Inconsistency: Selected page, but issue is not a missing page") return } replace(missing: missingPage, with: newPage.id, in: markdown) retryPageCheck() DispatchQueue.main.async { selectedPage = nil } } private func createPage(pageId: String) { guard content.isValidIdForTagOrPageOrPost(pageId) else { show(error: "Invalid page id, can't create page") return } let deString = pageId + "-" + ContentLanguage.german.rawValue let page = Page( content: content, id: pageId, externalLink: nil, isDraft: true, createdDate: .now, hideDate: false, startDate: .now, endDate: nil, german: .init(content: content, urlString: deString, title: pageId), english: .init(content: content, urlString: pageId, title: pageId), tags: [], requiredFiles: []) content.pages.insert(page, at: 0) retryPageCheck() } private func selectTag(missingPage: String) { // TODO: Show sheet to select a tag // TODO: Replace tag id in page content with new tag id retryPageCheck() } private func createTag(tagId: String) { guard content.isValidIdForTagOrPageOrPost(tagId) else { show(error: "Invalid tag id, can't create tag") return } let tag = Tag(content: content, id: tagId) content.tags.append(tag) retryPageCheck() } private func replaceCommand(originalText: String) { // TODO: Show sheet with text input // TODO: Replace original text in page content with new text retryPageCheck() } // MARK: Page Content manipulation private func replace(missing: String, with newText: String, in markdown: String) { let newString = markdown.replacingOccurrences(of: missing, with: newText) guard newString != markdown else { show(error: "No change in content detected trying to perform replacement") return } let occurrences = findOccurrences(of: markdown, in: issue.page, language: issue.language) guard !occurrences.isEmpty else { show(error: "No occurrences of '\(markdown)' found in the page") return } replace(markdown, with: newString, in: issue.page, language: issue.language) show(info: "Replaced \(occurrences.count) occurrences of '\(missing)' with '\(newText)'") retryPageCheck() } private func replace(_ oldString: String, with newString: String, in page: Page, language: ContentLanguage) { guard let pageContent = content.storage.pageContent(for: page.id, language: language) else { print("Failed to replace in page \(page.id) (\(language)), no content") return } let modified = pageContent.replacingOccurrences(of: oldString, with: newString) guard content.storage.save(pageContent: modified, for: page.id, in: language) else { print("Replaced \(oldString) with \(newString) in page \(page.id) (\(language))") return } } private func findOccurrences(of searchString: String, in page: Page, language: ContentLanguage) -> [String] { guard let parts = content.storage.pageContent(for: page.id, language: language)? .components(separatedBy: searchString) else { print("Failed to get page content to find occurrences, no content") return [] } var occurrences: [String] = [] for index in parts.indices.dropLast() { let start = parts[index].suffix(10) let end = parts[index+1].prefix(10) let full = "...\(start)\(searchString)\(end)...".replacingOccurrences(of: "\n", with: "\\n") occurrences.append(full) } return occurrences } }