237 lines
7.6 KiB
Swift
237 lines
7.6 KiB
Swift
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")
|
|
|
|
BoolPropertyView(
|
|
title: "Asset",
|
|
value: $file.isAsset,
|
|
footer: "Indicate that this file should be treated as an asset")
|
|
|
|
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)
|
|
}
|