Save and check page content automatically

This commit is contained in:
Christoph Hagen 2025-02-05 21:46:46 +01:00
parent 41171c31db
commit 7ebc9d8404
3 changed files with 105 additions and 48 deletions

View File

@ -155,6 +155,14 @@ final class Page: Item, DateItem, LocalizedItem {
content.storage.pageContent(for: id, language: language) 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 { func save(pageContent: String, in language: ContentLanguage) -> Bool {
guard content.storage.save(pageContent: pageContent, for: id, in: language) else { guard content.storage.save(pageContent: pageContent, for: id, in: language) else {
return false return false

View File

@ -77,6 +77,12 @@ final class Storage: ObservableObject {
return contentScope.write(pageContent, to: path) 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 { func save(page: Page.Data, for pageId: String) -> Bool {
guard let contentScope else { return false } guard let contentScope else { return false }
let path = pageMetadataPath(page: pageId) let path = pageMetadataPath(page: pageId)

View File

@ -2,6 +2,37 @@ import SwiftUI
import SFSafeSymbols import SFSafeSymbols
import HighlightedTextEditor 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 { struct LocalizedPageContentView: View {
@EnvironmentObject @EnvironmentObject
@ -15,36 +46,23 @@ struct LocalizedPageContentView: View {
@State @State
private var pageContent: String = "" private var pageContent: String = ""
@State
private var pageContentUsedForGeneration: String = ""
@State @State
private var generationResults: PageGenerationResults? private var generationResults: PageGenerationResults?
@State @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 { var body: some View {
VStack(alignment: .leading) { 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 { HStack {
Image(systemSymbol: saveState.symbol)
.foregroundStyle(saveState.color)
if let generationResults { if let generationResults {
PageContentResultsView(results: generationResults) PageContentResultsView(results: generationResults)
} }
@ -59,69 +77,94 @@ struct LocalizedPageContentView: View {
text: $pageContent, text: $pageContent,
highlightRules: .markdown) highlightRules: .markdown)
.onChange(of: pageContent) { .onChange(of: pageContent) {
didChangeContent = true didChangeContent()
} }
} }
.onAppear(perform: loadContent) .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() { private func loadContent() {
let language = language let language = language
guard page.localized(in: language).hasContent else { guard page.localized(in: language).hasContent else {
pageContent = "New file" pageContent = "New file"
DispatchQueue.main.async {
didChangeContent = false
}
return return
} }
guard let content = page.pageContent(in: language) else { guard let content = page.pageContent(in: language) else {
print("Failed to load page content") print("Failed to load page content")
pageContent = "Failed to load" pageContent = "Failed to load"
DispatchQueue.main.async {
didChangeContent = false
}
return return
} }
guard content != "" else { guard content != "" else {
pageContent = "New file" pageContent = "New file"
DispatchQueue.main.async {
didChangeContent = false
}
return return
} }
pageContent = content pageContent = content
checkContent() 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 { guard pageContent != "New file", pageContent != "" else {
// TODO: Delete file? guard page.removeContent(in: language) else {
return print("Failed to remove empty content from disk")
} saveState = .failedToSave
guard didChangeContent else { return
}
saveState = .notLoaded
generationResults = nil
return return
} }
guard page.save(pageContent: pageContent, in: language) else { guard page.save(pageContent: pageContent, in: language) else {
print("Failed to save content") print("Failed to save content")
saveState = .failedToSave
return return
} }
didChangeContent = false saveState = .isSaved
checkContent()
} }
private func checkContent() { private func checkContent() {
let content = self.pageContent
guard content != pageContentUsedForGeneration else {
return
}
guard !self.content.isGeneratingWebsite else { guard !self.content.isGeneratingWebsite else {
return return
} }
self.content.check(content: content, of: page, for: language) { self.content.check(content: pageContent, of: page, for: language) {
self.generationResults = $0 self.generationResults = $0
} }
} }