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 } var imageToDisplay: Image { guard let imageData = content.storage.fileData(for: id) else { print("Failed to load data for image \(id)") return failureImage } if fileSize == nil { DispatchQueue.main.async { self.fileSize = imageData.count } } guard let loadedImage = NSImage(data: imageData) else { print("Failed to create image \(id)") return failureImage } if loadedImage.size != imageDimensions { DispatchQueue.main.async { self.imageDimensions = loadedImage.size } } return .init(nsImage: loadedImage) } func determineImageDimensions() { let size = getImageDimensions() DispatchQueue.main.async { self.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 determineFileSize() { DispatchQueue.global(qos: .userInitiated).async { let size = self.content.storage.size(of: self.id) DispatchQueue.main.async { self.fileSize = size } } } func removeGeneratedImages() { 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) -> ImageSet { let description = self.localized(in: language) return .init( image: self, maxWidth: width, maxHeight: height, description: description, quality: quality) } 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 } }