import SwiftUI import SFSafeSymbols import HighlightedTextEditor enum PageContentSaveStatus { case isSaved case failedToSave case needsSave case notLoaded case isSaving var symbol: SFSymbol { switch self { case .notLoaded: return .questionmarkSquareDashed case .isSaved: return .checkmarkCircleFill case .needsSave, .isSaving: return .hourglassCircleFill case .failedToSave: return .exclamationmarkTriangleFill } } var color: Color { switch self { case .isSaved, .notLoaded: return .green case .needsSave, .isSaving: return .yellow case .failedToSave: return .red } } } struct LocalizedPageContentView: View { @EnvironmentObject var content: Content @ObservedObject var page: Page let language: ContentLanguage @State private var pageContent: String = "" @State private var generationResults: PageGenerationResults? @State private var saveState: PageContentSaveStatus = .notLoaded @State private var lastSave: Date = .now @State private var lastModification: Date = .now var body: some View { VStack(alignment: .leading) { HStack { Image(systemSymbol: saveState.symbol) .foregroundStyle(saveState.color) if let generationResults { PageContentResultsView(results: generationResults) } let linkingPosts = content.posts.filter { $0.linkedPage == page } TextWithPopup( symbol: .ipadAndArrowForward, title: "Post linking to page", text: "\(linkingPosts.count) linking posts", items: linkingPosts.map { $0.title(in: language) }) }.foregroundStyle(.secondary) InsertableItemsView() HighlightedTextEditor( text: $pageContent, highlightRules: .markdown) .onChange(of: pageContent) { didChangeContent() } } .onAppear(perform: loadContent) .onDisappear { saveContentIfNeeded(isFinalSave: true) } } private func didChangeContent() { if saveState == .notLoaded { // Content was changed due to loading saveState = .isSaved return } saveState = .needsSave lastModification = .now // Wait a few seconds for a save, to allow additional changes // Reduces the number of saves DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10)) { self.saveContentIfNeeded() } } private func loadContent() { let language = language guard page.localized(in: language).hasContent else { pageContent = "New file" return } guard let content = page.pageContent(in: language) else { print("Failed to load page content") pageContent = "Failed to load" return } guard content != "" else { pageContent = "New file" return } pageContent = content checkContent() } private func saveContentIfNeeded(isFinalSave: Bool = false) { switch saveState { case .isSaved, .notLoaded, .isSaving: return default: break } if !isFinalSave, Date.now.timeIntervalSince(lastModification) < 10 { // Additional modification made // Wait for next scheduled invocation of saveIfNeeded() // if the overall unsaved time is not too long if Date.now.timeIntervalSince(lastSave) < 60 { //print("Waiting while modifying") return } print("Saving content after 30 seconds of modifications") } saveState = .isSaving saveUnconditionally() } private func saveUnconditionally() { guard pageContent != "New file", pageContent != "" else { guard page.removeContent(in: language) else { print("Failed to remove empty content from disk") saveState = .failedToSave return } saveState = .notLoaded generationResults = nil return } guard page.save(pageContent: pageContent, in: language) else { print("Failed to save content") saveState = .failedToSave return } saveState = .isSaved lastSave = .now checkContent() } private func checkContent() { guard !self.content.isGeneratingWebsite else { return } self.content.check(content: pageContent, of: page, for: language) { self.generationResults = $0 } } }