From 7ebc9d8404d234c935da639e4bed072164b832e6 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Wed, 5 Feb 2025 21:46:46 +0100 Subject: [PATCH] Save and check page content automatically --- CHDataManagement/Model/Page.swift | 8 + CHDataManagement/Storage/Storage.swift | 6 + .../Pages/LocalizedPageContentView.swift | 139 ++++++++++++------ 3 files changed, 105 insertions(+), 48 deletions(-) diff --git a/CHDataManagement/Model/Page.swift b/CHDataManagement/Model/Page.swift index b120247..b0158b8 100644 --- a/CHDataManagement/Model/Page.swift +++ b/CHDataManagement/Model/Page.swift @@ -155,6 +155,14 @@ final class Page: Item, DateItem, LocalizedItem { content.storage.pageContent(for: id, language: language) } + func removeContent(in language: ContentLanguage) -> Bool { + guard content.storage.remove(pageContent: id, in: language) else { + return false + } + localized(in: language).hasContent = false + return true + } + func save(pageContent: String, in language: ContentLanguage) -> Bool { guard content.storage.save(pageContent: pageContent, for: id, in: language) else { return false diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index cd1c7e9..ac1a45d 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -77,6 +77,12 @@ final class Storage: ObservableObject { return contentScope.write(pageContent, to: path) } + func remove(pageContent pageId: String, in language: ContentLanguage) -> Bool { + guard let contentScope else { return false } + let path = pageContentPath(page: pageId, language: language) + return contentScope.deleteFile(at: path) + } + func save(page: Page.Data, for pageId: String) -> Bool { guard let contentScope else { return false } let path = pageMetadataPath(page: pageId) diff --git a/CHDataManagement/Views/Pages/LocalizedPageContentView.swift b/CHDataManagement/Views/Pages/LocalizedPageContentView.swift index 9e30b1a..9d8964d 100644 --- a/CHDataManagement/Views/Pages/LocalizedPageContentView.swift +++ b/CHDataManagement/Views/Pages/LocalizedPageContentView.swift @@ -2,6 +2,37 @@ import SwiftUI import SFSafeSymbols import HighlightedTextEditor +enum PageContentSaveStatus { + case isSaved + case failedToSave + case needsSave + case notLoaded + + var symbol: SFSymbol { + switch self { + case .notLoaded: + return .questionmarkSquareDashed + case .isSaved: + return .checkmarkCircleFill + case .needsSave: + return .hourglassCircleFill + case .failedToSave: + return .exclamationmarkTriangleFill + } + } + + var color: Color { + switch self { + case .isSaved, .notLoaded: + return .green + case .needsSave: + return .yellow + case .failedToSave: + return .red + } + } +} + struct LocalizedPageContentView: View { @EnvironmentObject @@ -15,36 +46,23 @@ struct LocalizedPageContentView: View { @State private var pageContent: String = "" - @State - private var pageContentUsedForGeneration: String = "" - @State private var generationResults: PageGenerationResults? @State - private var didChangeContent = false + 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(alignment: .firstTextBaseline) { - Button(action: loadContent) { - Text("Load") - } - Button(action: saveContent) { - Text("Save") - } - Button(action: checkContent) { - Text("Check") - }.disabled(content.isGeneratingWebsite) - if content.isGeneratingWebsite { - ProgressView() - .scaleEffect(0.6) - .frame(height: 15) - } - Spacer() - } - HStack { + Image(systemSymbol: saveState.symbol) + .foregroundStyle(saveState.color) if let generationResults { PageContentResultsView(results: generationResults) } @@ -59,69 +77,94 @@ struct LocalizedPageContentView: View { text: $pageContent, highlightRules: .markdown) .onChange(of: pageContent) { - didChangeContent = true + didChangeContent() } } .onAppear(perform: loadContent) - .onDisappear(perform: saveContent) + .onDisappear { saveContentIfNeeded(isFinalSave: true) } + } + + private func didChangeContent() { + if saveState == .notLoaded { + // Content was changed due to loading + saveState = .isSaved + return + } + saveState = .needsSave + + // 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" - DispatchQueue.main.async { - didChangeContent = false - } return } guard let content = page.pageContent(in: language) else { print("Failed to load page content") pageContent = "Failed to load" - DispatchQueue.main.async { - didChangeContent = false - } return } guard content != "" else { pageContent = "New file" - DispatchQueue.main.async { - didChangeContent = false - } return } pageContent = content checkContent() - - DispatchQueue.main.async { - didChangeContent = false - } } - private func saveContent() { + private func saveContentIfNeeded(isFinalSave: Bool = false) { + switch saveState { + case .isSaved, .notLoaded: + 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") + } + saveUnconditionally() + } + + private func saveUnconditionally() { guard pageContent != "New file", pageContent != "" else { - // TODO: Delete file? - return - } - guard didChangeContent 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 } - didChangeContent = false + saveState = .isSaved + checkContent() } private func checkContent() { - let content = self.pageContent - guard content != pageContentUsedForGeneration else { - return - } guard !self.content.isGeneratingWebsite else { return } - self.content.check(content: content, of: page, for: language) { + self.content.check(content: pageContent, of: page, for: language) { self.generationResults = $0 } }