Show errors during loading

This commit is contained in:
Christoph Hagen 2025-01-09 09:31:05 +01:00
parent a7197b9628
commit 93c8d06f23
12 changed files with 97 additions and 39 deletions

View File

@ -183,6 +183,7 @@
E2FD1D212D2EB22900B48627 /* ModelLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D202D2EB22700B48627 /* ModelLoader.swift */; }; E2FD1D212D2EB22900B48627 /* ModelLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D202D2EB22700B48627 /* ModelLoader.swift */; };
E2FD1D232D2EB27000B48627 /* LoadingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D222D2EB26C00B48627 /* LoadingResult.swift */; }; E2FD1D232D2EB27000B48627 /* LoadingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D222D2EB26C00B48627 /* LoadingResult.swift */; };
E2FD1D252D2EBA8000B48627 /* TagOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D242D2EBA7C00B48627 /* TagOverview.swift */; }; E2FD1D252D2EBA8000B48627 /* TagOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D242D2EBA7C00B48627 /* TagOverview.swift */; };
E2FD1D282D2F2DAD00B48627 /* ErrorPrinter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D272D2F2D9100B48627 /* ErrorPrinter.swift */; };
E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */; }; E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */; };
E2FE0EE82D16D4A3002963B7 /* ConvertThrowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */; }; E2FE0EE82D16D4A3002963B7 /* ConvertThrowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */; };
E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */; }; E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */; };
@ -412,6 +413,7 @@
E2FD1D202D2EB22700B48627 /* ModelLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelLoader.swift; sourceTree = "<group>"; }; E2FD1D202D2EB22700B48627 /* ModelLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelLoader.swift; sourceTree = "<group>"; };
E2FD1D222D2EB26C00B48627 /* LoadingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingResult.swift; sourceTree = "<group>"; }; E2FD1D222D2EB26C00B48627 /* LoadingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingResult.swift; sourceTree = "<group>"; };
E2FD1D242D2EBA7C00B48627 /* TagOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverview.swift; sourceTree = "<group>"; }; E2FD1D242D2EBA7C00B48627 /* TagOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverview.swift; sourceTree = "<group>"; };
E2FD1D272D2F2D9100B48627 /* ErrorPrinter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorPrinter.swift; sourceTree = "<group>"; };
E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = "<group>"; }; E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = "<group>"; };
E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = "<group>"; }; E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = "<group>"; };
E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = "<group>"; }; E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = "<group>"; };
@ -699,6 +701,7 @@
E2A37D0F2CE5375E0000979F /* Storage */ = { E2A37D0F2CE5375E0000979F /* Storage */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2FD1D272D2F2D9100B48627 /* ErrorPrinter.swift */,
E229904D2D135349009F8D77 /* SecurityBookmark.swift */, E229904D2D135349009F8D77 /* SecurityBookmark.swift */,
E22990492D10BB90009F8D77 /* SecurityScopeBookmark.swift */, E22990492D10BB90009F8D77 /* SecurityScopeBookmark.swift */,
E22990472D10B7B7009F8D77 /* StorageAccessError.swift */, E22990472D10B7B7009F8D77 /* StorageAccessError.swift */,
@ -1216,6 +1219,7 @@
E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */, E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */,
E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */, E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */,
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
E2FD1D282D2F2DAD00B48627 /* ErrorPrinter.swift in Sources */,
E2FD1D1F2D2E9CC200B48627 /* ItemId.swift in Sources */, E2FD1D1F2D2E9CC200B48627 /* ItemId.swift in Sources */,
E2FE0EFA2D25AFBA002963B7 /* PageHeader.swift in Sources */, E2FE0EFA2D25AFBA002963B7 /* PageHeader.swift in Sources */,
E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */, E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */,

View File

@ -35,7 +35,10 @@ final class PageGenerator {
rawPageContent = existing rawPageContent = existing
} else { } else {
rawPageContent = makeEmptyPageContent(in: language) rawPageContent = makeEmptyPageContent(in: language)
results.markPageAsEmpty() // Only mark non-draft pages as empty
if !page.isDraft {
results.markPageAsEmpty()
}
} }
let pageContent = contentGenerator.generatePage(from: rawPageContent) let pageContent = contentGenerator.generatePage(from: rawPageContent)

View File

@ -60,6 +60,9 @@ struct MainView: App {
@State @State
private var selectedTag: Tag? private var selectedTag: Tag?
@State
private var selectedFile: FileResource?
@State @State
private var selectedSection: SettingsSection = .folders private var selectedSection: SettingsSection = .folders
@ -85,7 +88,7 @@ struct MainView: App {
case .tags: case .tags:
TagListView(selectedTag: $selectedTag) TagListView(selectedTag: $selectedTag)
case .files: case .files:
FileListView(selectedFile: $content.selectedFile) FileListView(selectedFile: $selectedFile)
case .generation: case .generation:
SettingsListView(selectedSection: $selectedSection) SettingsListView(selectedSection: $selectedSection)
} }
@ -101,7 +104,7 @@ struct MainView: App {
case .tags: case .tags:
SelectedContentView<TagContentView>(selected: $selectedTag) SelectedContentView<TagContentView>(selected: $selectedTag)
case .files: case .files:
SelectedContentView<FileContentView>(selected: $content.selectedFile) SelectedContentView<FileContentView>(selected: $selectedFile)
case .generation: case .generation:
GenerationContentView(selected: $selectedSection) GenerationContentView(selected: $selectedSection)
} }
@ -117,7 +120,7 @@ struct MainView: App {
case .tags: case .tags:
SelectedDetailView<TagDetailView>(selected: $selectedTag) SelectedDetailView<TagDetailView>(selected: $selectedTag)
case .files: case .files:
SelectedDetailView<FileDetailView>(selected: $content.selectedFile) SelectedDetailView<FileDetailView>(selected: $selectedFile)
case .generation: case .generation:
GenerationDetailView(section: selectedSection) GenerationDetailView(section: selectedSection)
} }
@ -133,7 +136,7 @@ struct MainView: App {
case .tags: case .tags:
AddTagView(selected: $selectedTag) AddTagView(selected: $selectedTag)
case .files: case .files:
AddFileView(selectedFile: $content.selectedFile) AddFileView(selectedFile: $selectedFile)
case .generation: case .generation:
Text("Not implemented") Text("Not implemented")
} }

View File

@ -40,9 +40,6 @@ final class Content: ObservableObject {
@Published @Published
private(set) var shouldGenerateWebsite = false private(set) var shouldGenerateWebsite = false
@State
var selectedFile: FileResource?
let imageGenerator: ImageGenerator let imageGenerator: ImageGenerator
init(settings: Settings, init(settings: Settings,
@ -136,16 +133,18 @@ final class Content: ObservableObject {
tag.remove(file) tag.remove(file)
} }
settings.remove(file) settings.remove(file)
if selectedFile == file {
selectedFile = nil
}
} }
func file(withOutputPath: String) -> FileResource? { func file(withOutputPath: String) -> FileResource? {
files.first { $0.absoluteUrl == withOutputPath } files.first { $0.absoluteUrl == withOutputPath }
} }
private let errorPrinter = ErrorPrinter()
func loadFromDisk(callback: @escaping (_ errors: [String]) -> ()) { func loadFromDisk(callback: @escaping (_ errors: [String]) -> ()) {
defer {
storage.contentScope?.delegate = errorPrinter
}
DispatchQueue.global().async { DispatchQueue.global().async {
let loader = ModelLoader(content: self, storage: self.storage) let loader = ModelLoader(content: self, storage: self.storage)
let result = loader.load() let result = loader.load()

View File

@ -3,6 +3,8 @@ import SwiftUI
final class FileResource: Item, LocalizedItem { final class FileResource: Item, LocalizedItem {
override var itemType: ItemType { .file }
let type: FileType let type: FileType
/// Indicate if the file content is stored by the app /// Indicate if the file content is stored by the app

View File

@ -7,6 +7,8 @@ enum ItemType: String, Equatable, Hashable {
case tag = "tag" case tag = "tag"
case file = "file"
case tagOverview = "tag-overview" case tagOverview = "tag-overview"
} }

View File

@ -105,6 +105,12 @@ final class LoadingContext {
return nil return nil
} }
return tagOverview return tagOverview
case .file:
guard let id = itemId.id else {
error("Missing file id in itemId")
return nil
}
return file(id)
} }
} }
} }

View File

@ -1,4 +1,17 @@
final class LoadingErrorHandler: SecurityBookmarkErrorDelegate {
let context: LoadingContext
init(context: LoadingContext) {
self.context = context
}
func securityBookmark(error: String) {
context.error("\(error)")
}
}
final class ModelLoader { final class ModelLoader {
let content: Content let content: Content
@ -7,10 +20,14 @@ final class ModelLoader {
let context: LoadingContext let context: LoadingContext
let errorHandler: LoadingErrorHandler
init(content: Content, storage: Storage) { init(content: Content, storage: Storage) {
self.content = content self.content = content
self.storage = storage self.storage = storage
self.context = .init(content: content) self.context = .init(content: content)
self.errorHandler = .init(context: context)
storage.contentScope?.delegate = errorHandler
} }
func load() -> LoadingResult { func load() -> LoadingResult {

View File

@ -0,0 +1,10 @@
final class ErrorPrinter {
}
extension ErrorPrinter: SecurityBookmarkErrorDelegate {
func securityBookmark(error: String) {
print(error)
}
}

View File

@ -1,6 +1,11 @@
import Foundation import Foundation
import AppKit import AppKit
protocol SecurityBookmarkErrorDelegate: AnyObject {
func securityBookmark(error: String)
}
struct SecurityBookmark { struct SecurityBookmark {
enum OverwriteBehaviour { enum OverwriteBehaviour {
@ -20,6 +25,8 @@ struct SecurityBookmark {
private let fm = FileManager.default private let fm = FileManager.default
weak var delegate: SecurityBookmarkErrorDelegate?
init(url: URL, isStale: Bool) { init(url: URL, isStale: Bool) {
self.url = url self.url = url
self.isStale = isStale self.isStale = isStale
@ -31,9 +38,7 @@ struct SecurityBookmark {
func openFinderWindow(relativePath: String) { func openFinderWindow(relativePath: String) {
with(relativePath: relativePath) { path in with(relativePath: relativePath) { path in
print("Opening file at \(path)")
NSWorkspace.shared.activateFileViewerSelecting([path]) NSWorkspace.shared.activateFileViewerSelecting([path])
return
} }
} }
@ -60,7 +65,7 @@ struct SecurityBookmark {
do { do {
data = try encoder.encode(value) data = try encoder.encode(value)
} catch { } catch {
print("Failed to encode \(value): \(error)") delegate?.securityBookmark(error: "Failed to encode \(value): \(error)")
return false return false
} }
return write(data, to: relativePath) return write(data, to: relativePath)
@ -71,6 +76,7 @@ struct SecurityBookmark {
createParentFolder: Bool = true, createParentFolder: Bool = true,
ifFileExists overwrite: OverwriteBehaviour = .writeIfChanged) -> Bool { ifFileExists overwrite: OverwriteBehaviour = .writeIfChanged) -> Bool {
guard let data = string.data(using: .utf8) else { guard let data = string.data(using: .utf8) else {
delegate?.securityBookmark(error: "Failed to encode string to write to \(relativePath)")
return false return false
} }
return write(data, to: relativePath, createParentFolder: createParentFolder, ifFileExists: overwrite) return write(data, to: relativePath, createParentFolder: createParentFolder, ifFileExists: overwrite)
@ -85,7 +91,9 @@ struct SecurityBookmark {
if exists(file) { if exists(file) {
switch overwrite { switch overwrite {
case .fail: return false case .fail:
delegate?.securityBookmark(error: "Failed to write \(relativePath): File exists")
return false
case .skip: return true case .skip: return true
case .write: break case .write: break
case .writeIfChanged: case .writeIfChanged:
@ -99,7 +107,7 @@ struct SecurityBookmark {
try createParentIfNeeded(of: file) try createParentIfNeeded(of: file)
try data.write(to: file) try data.write(to: file)
} catch { } catch {
print("Failed to write to file \(url.path()): \(error)") delegate?.securityBookmark(error: "Failed to write \(relativePath): \(error)")
return false return false
} }
return true return true
@ -124,7 +132,11 @@ struct SecurityBookmark {
guard let data = readData(at: relativePath) else { guard let data = readData(at: relativePath) else {
return nil return nil
} }
return String(data: data, encoding: .utf8) guard let result = String(data: data, encoding: .utf8) else {
delegate?.securityBookmark(error: "Failed to read \(relativePath): invalid UTF-8")
return nil
}
return result
} }
func readData(at relativePath: String) -> Data? { func readData(at relativePath: String) -> Data? {
@ -135,7 +147,7 @@ struct SecurityBookmark {
do { do {
return try Data(contentsOf: file) return try Data(contentsOf: file)
} catch { } catch {
print("Storage: Failed to read file \(relativePath): \(error)") delegate?.securityBookmark(error: "Failed to read \(relativePath) \(error)")
return nil return nil
} }
} }
@ -148,7 +160,7 @@ struct SecurityBookmark {
do { do {
return try decoder.decode(T.self, from: data) return try decoder.decode(T.self, from: data)
} catch { } catch {
print("Failed to decode file \(relativePath): \(error)") delegate?.securityBookmark(error: "Failed to decode \(relativePath): \(error)")
return nil return nil
} }
} }
@ -163,7 +175,7 @@ struct SecurityBookmark {
with(relativePath: relativeSource) { source in with(relativePath: relativeSource) { source in
if !exists(source) { if !exists(source) {
if !failIfMissing { return true } if !failIfMissing { return true }
print("ContentScope: Could not find file \(relativeSource) to move") delegate?.securityBookmark(error: "Failed to move \(relativeSource): File does not exist")
return false return false
} }
@ -171,7 +183,7 @@ struct SecurityBookmark {
if exists(destination) { if exists(destination) {
switch overwrite { switch overwrite {
case .fail: case .fail:
print("ContentScope: Could not move file \(relativeSource), file exists") delegate?.securityBookmark(error: "Failed to move to \(relativeDestination): File already exists")
return false return false
case .skip: return true case .skip: return true
case .write: break case .write: break
@ -190,7 +202,7 @@ struct SecurityBookmark {
try fm.moveItem(at: source, to: destination) try fm.moveItem(at: source, to: destination)
return true return true
} catch { } catch {
print("Failed to move \(source.path()) to \(destination.path())") delegate?.securityBookmark(error: "Failed to move \(source.path()) to \(destination.path()): \(error)")
return false return false
} }
} }
@ -204,7 +216,9 @@ struct SecurityBookmark {
do { do {
if destination.exists { if destination.exists {
switch overwrite { switch overwrite {
case .fail: return false case .fail:
delegate?.securityBookmark(error: "Failed to copy to \(relativePath): File already exists")
return false
case .skip: return true case .skip: return true
case .write: break case .write: break
case .writeIfChanged: case .writeIfChanged:
@ -220,7 +234,7 @@ struct SecurityBookmark {
try fm.copyItem(at: externalFile, to: destination) try fm.copyItem(at: externalFile, to: destination)
return true return true
} catch { } catch {
print("Failed to copy \(externalFile.path()) to \(destination.path())") delegate?.securityBookmark(error: "Failed to copy \(externalFile.path()) to \(relativePath): \(error)")
return false return false
} }
} }
@ -229,14 +243,13 @@ struct SecurityBookmark {
func deleteFile(at relativePath: String) -> Bool { func deleteFile(at relativePath: String) -> Bool {
with(relativePath: relativePath) { file in with(relativePath: relativePath) { file in
guard exists(file) else { guard exists(file) else {
print("Scope: No file to delete at \(file.path())")
return true return true
} }
do { do {
try fm.removeItem(at: file) try fm.removeItem(at: file)
return true return true
} catch { } catch {
print("Failed to delete file \(file.path()): \(error)") delegate?.securityBookmark(error: "Failed to delete \(relativePath): \(error)")
return false return false
} }
} }
@ -291,9 +304,7 @@ struct SecurityBookmark {
} }
func files(inRelativeFolder relativePath: String) -> [URL]? { func files(inRelativeFolder relativePath: String) -> [URL]? {
with(relativePath: relativePath) { folder in with(relativePath: relativePath, perform: files)
files(in: folder)
}
} }
/** /**
@ -312,14 +323,13 @@ struct SecurityBookmark {
do { do {
data = try Data(contentsOf: url) data = try Data(contentsOf: url)
} catch { } catch {
#warning("Get these errors") delegate?.securityBookmark(error: "Failed to read \(url.path()): \(error)")
print("Storage: Failed to read file \(url.path()): \(error)")
return return
} }
do { do {
items[id] = try decoder.decode(T.self, from: data) items[id] = try decoder.decode(T.self, from: data)
} catch { } catch {
print("Storage: Failed to decode file \(url.path()): \(error)") delegate?.securityBookmark(error: "Failed to decode \(url.path()): \(error)")
return return
} }
} }
@ -341,7 +351,7 @@ struct SecurityBookmark {
*/ */
func perform(_ operation: (URL) -> Bool) -> Bool { func perform(_ operation: (URL) -> Bool) -> Bool {
guard url.startAccessingSecurityScopedResource() else { guard url.startAccessingSecurityScopedResource() else {
print("Failed to start security scope") delegate?.securityBookmark(error: "Failed to start security scope")
return false return false
} }
defer { url.stopAccessingSecurityScopedResource() } defer { url.stopAccessingSecurityScopedResource() }
@ -353,7 +363,7 @@ struct SecurityBookmark {
*/ */
func perform<T>(_ operation: (URL) -> T?) -> T? { func perform<T>(_ operation: (URL) -> T?) -> T? {
guard url.startAccessingSecurityScopedResource() else { guard url.startAccessingSecurityScopedResource() else {
print("Failed to start security scope") delegate?.securityBookmark(error: "Failed to start security scope")
return nil return nil
} }
defer { url.stopAccessingSecurityScopedResource() } defer { url.stopAccessingSecurityScopedResource() }
@ -367,7 +377,7 @@ struct SecurityBookmark {
try createIfNeeded(folder) try createIfNeeded(folder)
return true return true
} catch { } catch {
print("Failed to create folder \(folder.path())") delegate?.securityBookmark(error: "Failed to create folder \(folder.path())")
return false return false
} }
} }
@ -383,7 +393,7 @@ struct SecurityBookmark {
do { do {
try fm.removeItem(at: file) try fm.removeItem(at: file)
} catch { } catch {
print("Failed to remove \(file.path()): \(error)") delegate?.securityBookmark(error: "Failed to delete \(file.path()): \(error)")
return false return false
} }
return true return true
@ -409,7 +419,7 @@ struct SecurityBookmark {
do { do {
return try files(in: folder).filter { !$0.lastPathComponent.hasPrefix(".") } return try files(in: folder).filter { !$0.lastPathComponent.hasPrefix(".") }
} catch { } catch {
print("Failed to read list of files in \(folder.path())") delegate?.securityBookmark(error: "Failed to read list of files in \(folder.path())")
return nil return nil
} }
} }

View File

@ -313,7 +313,9 @@ final class Storage: ObservableObject {
- Returns: A dictionary with the file ids as keys and the metadata file as a value. - Returns: A dictionary with the file ids as keys and the metadata file as a value.
*/ */
func loadAllFiles() -> [String : (data: FileResource.Data, isExternal: Bool)]? { func loadAllFiles() -> [String : (data: FileResource.Data, isExternal: Bool)]? {
guard let contentScope else { return nil } guard let contentScope else {
return nil
}
guard let list: [String : FileResource.Data] = contentScope.decodeJsonFiles(in: fileInfoFolderName) else { guard let list: [String : FileResource.Data] = contentScope.decodeJsonFiles(in: fileInfoFolderName) else {
return nil return nil
} }

View File

@ -67,7 +67,7 @@ struct PageContentResultsView: View {
@ObservedObject @ObservedObject
var results: PageGenerationResults var results: PageGenerationResults
#warning("Rework to only show a single popup will all files, and indicate missing ones") #warning("Rework to only show a single popup with all files, and indicate missing ones")
private var totalFileCount: Int { private var totalFileCount: Int {
results.usedFiles.count + results.missingFiles.count + results.missingLinkedFiles.count results.usedFiles.count + results.missingFiles.count + results.missingLinkedFiles.count
} }