import SwiftUI import SFSafeSymbols private extension Button { init(_ symbol: SFSymbol, action: @escaping @MainActor () -> Void) where Label == Image { self.init(action: action, label: { Image(systemSymbol: symbol) }) } } private struct ButtonIcon: View { let symbol: SFSymbol let action: @MainActor () -> Void init(_ symbol: SFSymbol, action: @escaping @MainActor () -> Void) { self.symbol = symbol self.action = action } var body: some View { Button(action: action) { Image(systemSymbol: symbol) .resizable() .aspectRatio(contentMode: .fit) .frame(height: 20) } } } struct FileDetailView: View { @EnvironmentObject private var content: Content @Environment(\.language) private var language @ObservedObject var file: FileResource var body: some View { ScrollView { VStack(alignment: .leading) { DetailTitle( title: "File", text: "A file that can be used in a post or page") GenericPropertyView(title: "Actions") { HStack(spacing: 10) { ButtonIcon(.folder, action: showFileInFinder) ButtonIcon(.arrowClockwise, action: markFileAsChanged) if file.isExternallyStored { ButtonIcon(.squareAndArrowDown, action: replaceFile) } else { ButtonIcon(.arrowLeftArrowRight, action: replaceFile) ButtonIcon(.squareDashed, action: convertToExternal) } ButtonIcon(.trash, action: deleteFile) .foregroundStyle(.red) } .buttonStyle(.plain) .foregroundStyle(.blue) } 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) }) switch language { case .english: OptionalStringPropertyView( title: "Description", text: $file.english, footer: "The description for the file. Descriptions are used for images and to explain the content of a file.") case .german: OptionalStringPropertyView( title: "Description", text: $file.german, footer: "The description for the file. Descriptions are used for images and to explain the content of a file.") } OptionalStringPropertyView( title: "Version", text: $file.version, footer: "The version of this file.") OptionalStringPropertyView( title: "Source URL", text: $file.sourceUrl, footer: "The url where this file can be downloaded from.") OptionalStringPropertyView( title: "Custom output path", text: $file.customOutputPath, footer: "A custom path where the file is stored in the output folder") if let imageDimensions = file.imageDimensions { GenericPropertyView(title: "Image dimensions") { Text("\(Int(imageDimensions.width)) x \(Int(imageDimensions.height)) (\(file.aspectRatio))") } } if let fileSize = file.fileSize { GenericPropertyView(title: "File size") { Text(formatBytes(fileSize)) } } GenericPropertyView(title: "Added") { Text(file.addedDate.formatted()) } GenericPropertyView(title: "Modified") { Text(file.modifiedDate.formatted()) } if !file.generatedImageVersions.isEmpty { GenericPropertyView(title: "Image versions") { VStack(alignment: .leading) { ForEach(file.generatedImageVersions.sorted(), id: \.self) { version in Text(version) } } } } }.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() file.modifiedDate = .now } } 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.removeFileContent(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() { 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) }