Load incomplete content, show errors

This commit is contained in:
Christoph Hagen 2025-02-05 20:56:02 +01:00
parent 2b88584ba1
commit d556a51228
9 changed files with 142 additions and 118 deletions

View File

@ -14,6 +14,8 @@
E20BCC9F2D53851400B8DBEB /* SelectableListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCC9E2D53850A00B8DBEB /* SelectableListItem.swift */; };
E20BCCA32D5398AA00B8DBEB /* LocalizedAudioSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCA22D5398AA00B8DBEB /* LocalizedAudioSettingsDetailView.swift */; };
E20BCCAB2D53B86900B8DBEB /* GenerationResultsIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCAA2D53B85300B8DBEB /* GenerationResultsIssueView.swift */; };
E20BCCAD2D53F48100B8DBEB /* IssueStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCAC2D53F48100B8DBEB /* IssueStatus.swift */; };
E20BCCAF2D53F4A500B8DBEB /* GenerationStringIssuesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20BCCAE2D53F4A500B8DBEB /* GenerationStringIssuesView.swift */; };
E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850082CEE01BF0090B18B /* PagePickerView.swift */; };
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500A2CEE02FA0090B18B /* Content+Mock.swift */; };
E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; };
@ -274,6 +276,8 @@
E20BCC9E2D53850A00B8DBEB /* SelectableListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableListItem.swift; sourceTree = "<group>"; };
E20BCCA22D5398AA00B8DBEB /* LocalizedAudioSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedAudioSettingsDetailView.swift; sourceTree = "<group>"; };
E20BCCAA2D53B85300B8DBEB /* GenerationResultsIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResultsIssueView.swift; sourceTree = "<group>"; };
E20BCCAC2D53F48100B8DBEB /* IssueStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IssueStatus.swift; sourceTree = "<group>"; };
E20BCCAE2D53F4A500B8DBEB /* GenerationStringIssuesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationStringIssuesView.swift; sourceTree = "<group>"; };
E21850082CEE01BF0090B18B /* PagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePickerView.swift; sourceTree = "<group>"; };
E218500A2CEE02FA0090B18B /* Content+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Mock.swift"; sourceTree = "<group>"; };
E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
@ -542,6 +546,8 @@
E20BCCA02D53985500B8DBEB /* Generation */ = {
isa = PBXGroup;
children = (
E20BCCAE2D53F4A500B8DBEB /* GenerationStringIssuesView.swift */,
E20BCCAC2D53F48100B8DBEB /* IssueStatus.swift */,
E20BCCAA2D53B85300B8DBEB /* GenerationResultsIssueView.swift */,
E25DA5702D01015400AEF16D /* GenerationContentView.swift */,
);
@ -879,9 +885,9 @@
E2B85F462C42C7CA0047CD0C /* Views */ = {
isa = PBXGroup;
children = (
E20BCCA02D53985500B8DBEB /* Generation */,
E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */,
E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */,
E20BCCA02D53985500B8DBEB /* Generation */,
E2A21C372CB9A4F10060935B /* Generic */,
E2B85F4B2C4B8B7F0047CD0C /* Posts */,
E2A21C322CB5BCAC0060935B /* Pages */,
@ -1291,6 +1297,7 @@
E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */,
E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */,
E2FE0F662D2C3B3A002963B7 /* LabelsBlock.swift in Sources */,
E20BCCAF2D53F4A500B8DBEB /* GenerationStringIssuesView.swift in Sources */,
E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */,
E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */,
E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */,
@ -1375,6 +1382,7 @@
E2FD1D642D47EF4200B48627 /* DetailListItem.swift in Sources */,
E2FE0F0B2D2689FF002963B7 /* FeedGeneratorSource.swift in Sources */,
E2DD04742C276F31003BFF1F /* MainView.swift in Sources */,
E20BCCAD2D53F48100B8DBEB /* IssueStatus.swift in Sources */,
E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */,
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,

View File

@ -23,7 +23,6 @@ import SFSafeSymbols
**Fixes**
- Files: Id change: Check all page contents for links to the renamed file and replace occurences
- Database: Show errors during loading
- Investigate issue with spaces in content file names
*/

View File

@ -20,6 +20,11 @@ struct StorageErrorView: View {
}
}
.frame(minHeight: 300)
if content.saveState == .savingPausedDueToLoadErrors {
Button("Allow saving", action: { content.resumeSavingAfterLoadingErrors() })
.padding()
Text("Saving has been disabled to prevent data corruption due to loading errors. Enable saving to save the partially loaded data.")
}
Button("Dismiss", action: { isPresented = false })
.padding()
}

View File

@ -16,9 +16,13 @@ extension Content {
}
func saveIfNeeded() {
guard saveState != .isSaved else {
switch saveState {
case .isSaved, .savingPausedDueToLoadErrors, .storageNotInitialized:
return
default:
break
}
if Date.now.timeIntervalSince(lastModification) < 5 {
// Additional modification made
// Wait for next scheduled invocation of saveIfNeeded()
@ -43,7 +47,6 @@ extension Content {
}
private func saveToDisk() -> Bool {
guard didLoadContent else { return false }
guard storage.contentScope != nil else {
print("Storage not initialized, not saving content")
return false

View File

@ -4,9 +4,6 @@ import Combine
final class Content: ObservableObject {
@Published
var didLoadContent = false
@ObservedObject
var storage: Storage
@ -132,31 +129,37 @@ final class Content: ObservableObject {
func loadFromDisk(callback: @escaping () -> ()) {
DispatchQueue.global().async {
let loader = ModelLoader(content: self, storage: self.storage)
let result = loader.load()
guard result.errors.isEmpty else {
DispatchQueue.main.async {
self.didLoadContent = false
self.storageErrors.append(contentsOf: result.errors)
callback()
}
return
}
DispatchQueue.main.async {
self.files = result.files
self.posts = result.posts
self.pages = result.pages
self.tags = result.tags
self.settings = result.settings
self.tagOverview = result.tagOverview
self.didLoadContent = true
callback()
self.generateMissingVideoThumbnails()
}
self.loadInBackground(callback: callback)
}
}
private func loadInBackground(callback: @escaping () -> ()) {
let loader = ModelLoader(content: self, storage: self.storage)
let result = loader.load()
DispatchQueue.main.async {
self.files = result.files
self.posts = result.posts
self.pages = result.pages
self.tags = result.tags
self.settings = result.settings
self.tagOverview = result.tagOverview
self.storageErrors.append(contentsOf: result.errors)
if !result.errors.isEmpty {
self.saveState = .savingPausedDueToLoadErrors
} else {
self.saveState = .isSaved
}
callback()
self.generateMissingVideoThumbnails()
}
}
func resumeSavingAfterLoadingErrors() {
saveState = .needsSave
saveIfNeeded()
}
func generateMissingVideoThumbnails() {
Task {
for file in self.files {

View File

@ -3,6 +3,7 @@ import SwiftUICore
enum SaveState {
case storageNotInitialized
case savingPausedDueToLoadErrors
case isSaved
case needsSave
case failedToSave
@ -11,6 +12,8 @@ enum SaveState {
switch self {
case .storageNotInitialized:
return .folderCircleFill
case .savingPausedDueToLoadErrors:
return .exclamationmarkCircleFill
case .isSaved:
return .checkmarkCircleFill
case .needsSave:
@ -28,7 +31,7 @@ enum SaveState {
return .green
case .needsSave:
return .yellow
case .failedToSave:
case .failedToSave, .savingPausedDueToLoadErrors:
return .red
}
}

View File

@ -1,91 +1,6 @@
import SwiftUI
import SFSafeSymbols
enum IssueStatus {
case nominal
case warning
case error
var symbol: SFSymbol {
switch self {
case .nominal: .checkmarkCircleFill
case .warning, .error: .exclamationmarkTriangle
}
}
var color: Color {
switch self {
case .nominal: .green
case .warning: .yellow
case .error: .red
}
}
}
struct GenerationStringIssuesView<T>: View where T: Hashable {
let text: String
let statusWhenNonEmpty: IssueStatus
@Binding
var items: Set<T>
let map: (T) -> String
@State
private var showList = false
var status: IssueStatus {
items.isEmpty ? .nominal : statusWhenNonEmpty
}
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<T>>, map: @escaping (T) -> String) {
self.text = text
self.statusWhenNonEmpty = statusWhenNonEmpty
self._items = items
self.map = map
}
var body: some View {
HStack {
Button(action: showListIfNonEmpty) {
Image(systemSymbol: status.symbol)
.foregroundStyle(status.color)
}.buttonStyle(.plain)
Text("\(items.count) \(text)")
}
.sheet(isPresented: $showList) {
VStack {
Text("\(items.count) \(text)")
.font(.title)
List(items.map(map).sorted(), id: \.self) { item in
Text(item)
}
.frame(minHeight: 400)
Button("Close") { showList = false }
}.padding()
}
}
private func showListIfNonEmpty() {
guard !items.isEmpty else {
return
}
showList = true
}
}
extension GenerationStringIssuesView where T == String {
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<String>>) {
self.text = text
self.statusWhenNonEmpty = statusWhenNonEmpty
self._items = items
self.map = { $0 }
}
}
struct GenerationResultsIssueView: View {
@State
@ -117,9 +32,9 @@ struct GenerationResultsIssueView: View {
}
private func showListIfNonEmpty() {
// guard !items.isEmpty else {
// return
// }
guard !items().isEmpty else {
return
}
showList = true
}
}

View File

@ -0,0 +1,65 @@
import SwiftUI
struct GenerationStringIssuesView<T>: View where T: Hashable {
let text: String
let statusWhenNonEmpty: IssueStatus
@Binding
var items: Set<T>
let map: (T) -> String
@State
private var showList = false
var status: IssueStatus {
items.isEmpty ? .nominal : statusWhenNonEmpty
}
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<T>>, map: @escaping (T) -> String) {
self.text = text
self.statusWhenNonEmpty = statusWhenNonEmpty
self._items = items
self.map = map
}
var body: some View {
HStack {
Button(action: showListIfNonEmpty) {
Image(systemSymbol: status.symbol)
.foregroundStyle(status.color)
}.buttonStyle(.plain)
Text("\(items.count) \(text)")
}
.sheet(isPresented: $showList) {
VStack {
Text("\(items.count) \(text)")
.font(.title)
List(items.map(map).sorted(), id: \.self) { item in
Text(item)
}
.frame(minHeight: 400)
Button("Close") { showList = false }
}.padding()
}
}
private func showListIfNonEmpty() {
guard !items.isEmpty else {
return
}
showList = true
}
}
extension GenerationStringIssuesView where T == String {
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<String>>) {
self.text = text
self.statusWhenNonEmpty = statusWhenNonEmpty
self._items = items
self.map = { $0 }
}
}

View File

@ -0,0 +1,23 @@
import SFSafeSymbols
import SwiftUICore
enum IssueStatus {
case nominal
case warning
case error
var symbol: SFSymbol {
switch self {
case .nominal: .checkmarkCircleFill
case .warning, .error: .exclamationmarkTriangle
}
}
var color: Color {
switch self {
case .nominal: .green
case .warning: .yellow
case .error: .red
}
}
}