import Foundation import SwiftUI final class FileResource: Item { let type: FileType @Published var isExternallyStored: Bool @Published var german: String @Published var english: String /// The dimensions of the image @Published var imageDimensions: CGSize? = nil /// The size of the file in bytes @Published var fileSize: Int? = nil init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) { self.type = FileType(fileExtension: id.fileExtension) self.english = en self.german = de self.isExternallyStored = isExternallyStored super.init(content: content, id: id) } /** Only for bundle images */ init(resourceImage: String, type: FileType) { self.type = type self.english = "A test image included in the bundle" self.german = "Ein Testbild aus dem Bundle" self.isExternallyStored = true super.init(content: .mock, id: resourceImage) // TODO: Add images to mock } // MARK: Text func textContent() -> String { content.storage.fileContent(for: id) ?? "" } func dataContent() -> Data? { content.storage.fileData(for: id) } // MARK: Images var aspectRatio: CGFloat { guard let imageDimensions else { return 0 } guard imageDimensions.height > 0 else { return 0 } return imageDimensions.width / imageDimensions.height } private func update(imageDimensions size: CGSize?) { guard let imageDimensions, let size else { // First computation DispatchQueue.main.async { self.imageDimensions = size } return } guard imageDimensions != size else { return } // Image must have changed, so force regeneration DispatchQueue.main.async { self.imageDimensions = size self.didChange() self.removeGeneratedImages() } } var imageToDisplay: Image { guard let imageData = content.storage.fileData(for: id) else { print("Failed to load data for image \(id)") return failureImage } update(fileSize: imageData.count) guard let loadedImage = NSImage(data: imageData) else { print("Failed to create image \(id)") return failureImage } update(imageDimensions: loadedImage.size) return .init(nsImage: loadedImage) } func determineImageDimensions() { let size = getImageDimensions() self.update(imageDimensions: size) } private func getImageDimensions() -> CGSize? { guard type.isImage else { return nil } guard let imageData = content.storage.fileData(for: id) else { return nil } guard let loadedImage = NSImage(data: imageData) else { return nil } return loadedImage.size } func update(fileSize size: Int?) { guard let fileSize, let size else { // First computation DispatchQueue.main.async { self.fileSize = size } return } guard fileSize != size else { return } // File must have changed DispatchQueue.main.async { self.fileSize = size self.didChange() self.removeGeneratedImages() } } func determineFileSize() { DispatchQueue.global(qos: .userInitiated).async { let size = self.content.storage.size(of: self.id) self.update(fileSize: size) } } func removeGeneratedImages() { guard type.isImage else { return } content.imageGenerator.removeVersions(of: id) content.storage.deleteInOutputFolder(path: outputImageFolder) } private var failureImage: Image { Image(systemSymbol: .exclamationmarkTriangle) } /// The path to the output folder where image versions are stored (no leading slash) var outputImageFolder: String { "\(content.settings.paths.imagesOutputFolderPath)/\(id.fileNameWithoutExtension)" } func outputPath(width: Int, height: Int, type: FileType?) -> String { let prefix = "/\(outputImageFolder)/\(width)x\(height)" guard let ext = type?.fileExtension else { return prefix } return prefix + "." + ext } func imageSet(width: Int, height: Int, language: ContentLanguage, quality: CGFloat = 0.7, extraAttributes: String? = nil) -> ImageSet { let description = self.localized(in: language) return .init( image: self, maxWidth: width, maxHeight: height, description: description, quality: quality, extraAttributes: extraAttributes) } func imageVersion(width: Int, height: Int, type: FileType) -> ImageVersion { .init(image: self, type: type, maximumWidth: width, maximumHeight: height) } // MARK: Paths func removeFileFromOutputFolder() { content.storage.deleteInOutputFolder(path: absoluteUrl) if type.isImage { removeGeneratedImages() } } /** Get the url path to a file in the output folder. The result is an absolute path from the output folder for use in HTML. */ var absoluteUrl: String { let path = pathPrefix + "/" + id return makeCleanAbsolutePath(path) } private var pathPrefix: String { if type.isImage { return content.settings.paths.imagesOutputFolderPath } if type.isVideo { return content.settings.paths.videosOutputFolderPath } if type.isAudio { return content.settings.paths.audioOutputFolderPath } if type.isAsset { return content.settings.paths.assetsOutputFolderPath } return content.settings.paths.filesOutputFolderPath } // MARK: File func isValid(id: String) -> Bool { !id.isEmpty && content.isValidIdForFile(id) && content.isNewIdForFile(id) } @discardableResult func update(id newId: String) -> Bool { guard !isExternallyStored else { id = newId return true } guard content.storage.move(file: id, to: newId) else { print("Failed to move file \(id) to \(newId)") return false } id = newId return true } } extension FileResource: LocalizedItem { } extension FileResource: CustomStringConvertible { var description: String { id } }