Load incomplete content, show errors
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
|
@@ -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()
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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 }
|
||||
}
|
||||
}
|
23
CHDataManagement/Views/Generation/IssueStatus.swift
Normal file
23
CHDataManagement/Views/Generation/IssueStatus.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user