import SwiftUI struct FileDetailView: View { @EnvironmentObject private var content: Content @ObservedObject var file: FileResource @State private var showFileSelection = false @State private var selectedFile: FileResource? var body: some View { VStack(alignment: .leading) { DetailTitle( title: "File", text: "A file that can be used in a post or page") VStack(alignment: .leading) { Button("Show in Finder", action: showFileInFinder) Button("Mark as changed", action: markFileAsChanged) Button("Delete resource", action: deleteFile) if file.isExternallyStored { Button("Import file", action: replaceFile) } else { Button("Replace file", action: replaceFile) Button("Make external", action: convertToExternal) } } IdPropertyView( id: $file.id, title: "Name", footer: "The unique name of the file, which is also used to reference it in posts and pages.", validation: file.isValid, update: { file.update(id: $0) }) StringPropertyView( title: "German Description", text: $file.german, footer: "The description for the file in German. Descriptions are used for images and to explain the content of a file.") StringPropertyView( title: "English Description", text: $file.english, footer: "The description for the file in English. Descriptions are used for images and to explain the content of a file.") if let imageDimensions = file.imageDimensions { GenericPropertyView(title: "Image dimensions") { Text("\(Int(imageDimensions.width)) x \(Int(imageDimensions.height)) (\(file.aspectRatio))") } #warning("Add button to show image versions") } if let fileSize = file.fileSize { GenericPropertyView(title: "File size") { Text(formatBytes(fileSize)) } } Spacer() }.padding() .onAppear { if file.fileSize == nil { file.determineFileSize() } } } private func formatBytes(_ bytes: Int) -> String { let formatter = ByteCountFormatter() formatter.allowedUnits = [.useMB, .useKB, .useBytes] // Customize units if needed formatter.countStyle = .file return formatter.string(fromByteCount: Int64(bytes)) } private func showFileInFinder() { content.storage.openFinderWindow(withSelectedFile: file.id) } private func markFileAsChanged() { DispatchQueue.main.async { file.determineImageDimensions() file.determineFileSize() // Force regeneration of images and/or file copying file.removeFileFromOutputFolder() // Trigger content view update to reload image file.didChange() } } private func replaceFile() { guard let url = openFilePanel() else { print("File '\(file.id)': No file selected as replacement") return } guard content.storage.importExternalFile(at: url, fileId: file.id) else { print("File '\(file.id)': Failed to replace file") return } markFileAsChanged() if file.isExternallyStored { DispatchQueue.main.async { file.isExternallyStored = false } } } private func openFilePanel() -> URL? { let panel = NSOpenPanel() panel.canChooseFiles = true panel.canChooseDirectories = false panel.allowsMultipleSelection = false panel.showsHiddenFiles = false panel.title = "Select file to replace" panel.prompt = "" let response = panel.runModal() guard response == .OK else { print("File '\(file.id)': Failed to select file to replace") return nil } return panel.url } private func convertToExternal() { guard !file.isExternallyStored else { return } guard content.storage.delete(file: file.id) else { print("File '\(file.id)': Failed to delete file to make it external") return } DispatchQueue.main.async { file.fileSize = nil file.isExternallyStored = true } } private func deleteFile() { if !file.isExternallyStored { guard content.storage.delete(file: file.id) else { print("File '\(file.id)': Failed to delete file in content folder") return } } content.remove(file) } } extension FileDetailView: MainContentView { init(item: FileResource) { self.init(file: item) } static let itemDescription = "a file" } #Preview { FileDetailView(file: .mock) }