import Foundation import SwiftUI import Combine final class Content: ObservableObject { @ObservedObject var storage: Storage @Published var settings: Settings! @Published var posts: [Post] @Published var pages: [Page] @Published var tags: [Tag] @Published var files: [FileResource] @Published var tagOverview: Tag? @Published var results: GenerationResults @Published var storageErrors: [StorageError] = [] @Published var generationStatus: String = "Ready to generate" @Published private(set) var isGeneratingWebsite = false @Published private(set) var shouldGenerateWebsite = false @Published private(set) var saveState: SaveState = .isSaved let imageGenerator: ImageGenerator var errorCallback: ((StorageError) -> Void)? init() { let settings = Settings.default self.settings = settings self.posts = [] self.pages = [] self.tags = [] self.files = [] self.tagOverview = nil self.results = .init() let storage = Storage() self.storage = storage self.imageGenerator = ImageGenerator( storage: storage, settings: settings) storage.errorNotification = { [weak self] error in self?.storageErrors.append(error) } settings.content = self } private func clear() { self.settings = .default self.posts = [] self.pages = [] self.tags = [] self.files = [] self.tagOverview = nil self.results = .init() } var images: [FileResource] { files.filter { $0.type.isImage } } func set(isGenerating: Bool) { self.isGeneratingWebsite = isGenerating } func set(shouldGenerate: Bool) { self.shouldGenerateWebsite = shouldGenerate } func add(_ file: FileResource) { // TODO: Insert at correct index? files.insert(file, at: 0) } func add(_ page: Page) { // TODO: Insert at correct index? pages.insert(page, at: 0) } func update(contentPath: URL, callback: @escaping () -> ()) { guard storage.save(contentPath: contentPath) else { return } loadFromDisk(callback: callback) } func remove(_ file: FileResource) { files.remove(file) for post in posts { post.remove(file) } for page in pages { page.remove(file) } for tag in tags { tag.remove(file) } settings.remove(file) } func file(withOutputPath: String) -> FileResource? { files.first { $0.absoluteUrl == withOutputPath } } func loadFromDisk(callback: @escaping () -> ()) { DispatchQueue.global().async { 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 { guard file.type.isVideo else { continue } guard !file.isExternallyStored else { continue } guard !storage.hasVideoThumbnail(for: file.id) else { continue } if await imageGenerator.createVideoThumbnail(for: file.id) { print("Generated thumbnail for \(file.id)") file.didChange() } } } } // MARK: Saving private(set) var lastSave: Date = .now private(set) var lastModification: Date = .now func update(saveState: SaveState) { DispatchQueue.main.async { self.saveState = saveState } } func setModificationTimestamp() { self.lastModification = .now } func setLastSaveTimestamp() { self.lastSave = .now } }