Generate tag overview, add file action
This commit is contained in:
@ -11,15 +11,11 @@ struct AddFileView: View {
|
||||
@Binding
|
||||
var selectedFile: FileResource?
|
||||
|
||||
@Binding
|
||||
var selectedImage: ImageResource?
|
||||
|
||||
@State
|
||||
private var filesToAdd: [FileToAdd] = []
|
||||
|
||||
init(selectedImage: Binding<ImageResource?>, selectedFile: Binding<FileResource?>) {
|
||||
init(selectedFile: Binding<FileResource?>) {
|
||||
_selectedFile = selectedFile
|
||||
_selectedImage = selectedImage
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@ -111,6 +107,5 @@ struct AddFileView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AddFileView(selectedImage: .constant(nil),
|
||||
selectedFile: .constant(nil))
|
||||
AddFileView(selectedFile: .constant(nil))
|
||||
}
|
||||
|
@ -2,15 +2,36 @@ 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",
|
||||
@ -28,15 +49,108 @@ struct FileDetailView: View {
|
||||
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 file.type.isImage {
|
||||
Text("Image size")
|
||||
.font(.headline)
|
||||
Text("\(Int(file.size.width)) x \(Int(file.size.height)) (\(file.aspectRatio))")
|
||||
.foregroundStyle(.secondary)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,68 +47,71 @@ struct MultiFileSelectionView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
VStack {
|
||||
Text("Selected files")
|
||||
.font(.title)
|
||||
List {
|
||||
ForEach(newSelection) { file in
|
||||
HStack {
|
||||
Image(systemSymbol: .minusCircleFill)
|
||||
.foregroundStyle(.red)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { deselect(file: file) }
|
||||
Text(file.id)
|
||||
Spacer()
|
||||
GeometryReader { geo in
|
||||
HStack {
|
||||
VStack {
|
||||
Text("Selected files")
|
||||
.font(.title)
|
||||
List {
|
||||
ForEach(newSelection) { file in
|
||||
HStack {
|
||||
Image(systemSymbol: .minusCircleFill)
|
||||
.foregroundStyle(.red)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { deselect(file: file) }
|
||||
Text(file.id)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.onMove(perform: moveSelectedFile)
|
||||
}
|
||||
.onMove(perform: moveSelectedFile)
|
||||
}
|
||||
HStack {
|
||||
Button("Cancel") {
|
||||
DispatchQueue.main.async {
|
||||
HStack {
|
||||
Button("Cancel") {
|
||||
DispatchQueue.main.async {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Button("Save") {
|
||||
selectedFiles = newSelection
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Button("Save") {
|
||||
selectedFiles = newSelection
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
Picker("", selection: $selectedFileType) {
|
||||
let all: FileTypeCategory? = nil
|
||||
Text("All").tag(all)
|
||||
ForEach(FileTypeCategory.allCases) { type in
|
||||
Text(type.text).tag(type)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.trailing, 7)
|
||||
.disabled(allowedType != nil)
|
||||
TextField("", text: $searchString, prompt: Text("Search"))
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.horizontal, 8)
|
||||
List(filteredFiles) { file in
|
||||
HStack {
|
||||
if newSelection.contains(file) {
|
||||
Image(systemSymbol: .checkmarkCircleFill)
|
||||
.foregroundStyle(.gray)
|
||||
} else {
|
||||
Image(systemSymbol: .plusCircleFill)
|
||||
.foregroundStyle(.green)
|
||||
}.frame(width: geo.size.width / 2)
|
||||
VStack {
|
||||
Picker("", selection: $selectedFileType) {
|
||||
let all: FileTypeCategory? = nil
|
||||
Text("All").tag(all)
|
||||
ForEach(FileTypeCategory.allCases) { type in
|
||||
Text(type.text).tag(type)
|
||||
}
|
||||
Text(file.id)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { select(file: file) }
|
||||
}
|
||||
.padding(.trailing, 7)
|
||||
.disabled(allowedType != nil)
|
||||
TextField("", text: $searchString, prompt: Text("Search"))
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.horizontal, 8)
|
||||
List(filteredFiles) { file in
|
||||
HStack {
|
||||
if newSelection.contains(file) {
|
||||
Image(systemSymbol: .checkmarkCircleFill)
|
||||
.foregroundStyle(.gray)
|
||||
} else {
|
||||
Image(systemSymbol: .plusCircleFill)
|
||||
.foregroundStyle(.green)
|
||||
}
|
||||
Text(file.id)
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { select(file: file) }
|
||||
}
|
||||
}.frame(width: geo.size.width / 2)
|
||||
}
|
||||
.frame(minHeight: 500, idealHeight: 600)
|
||||
}
|
||||
.frame(minHeight: 500, idealHeight: 600)
|
||||
.padding()
|
||||
|
||||
}
|
||||
|
||||
private func deselect(file: FileResource) {
|
||||
|
@ -4,11 +4,11 @@ struct GenericPropertyView<Content>: View where Content: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
let footer: LocalizedStringKey?
|
||||
|
||||
let content: Content
|
||||
|
||||
public init(title: LocalizedStringKey, footer: LocalizedStringKey, @ViewBuilder content: () -> Content) {
|
||||
public init(title: LocalizedStringKey, footer: LocalizedStringKey? = nil, @ViewBuilder content: () -> Content) {
|
||||
self.title = title
|
||||
self.footer = footer
|
||||
self.content = content()
|
||||
@ -18,10 +18,16 @@ struct GenericPropertyView<Content>: View where Content: View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
content
|
||||
Text(footer)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
|
||||
if let footer {
|
||||
content
|
||||
Text(footer)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
} else {
|
||||
content
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,12 @@ struct LocalizedPageDetailView: View {
|
||||
footer: "Prevent the date and title from being printed on the page")
|
||||
.disabled(isExternalPage)
|
||||
|
||||
if let url = page.originalUrl {
|
||||
GenericPropertyView(title: "Original URL") {
|
||||
Text(url)
|
||||
}
|
||||
}
|
||||
|
||||
OptionalStringPropertyView(
|
||||
title: "Preview Title",
|
||||
text: $page.linkPreviewTitle,
|
||||
|
@ -28,8 +28,8 @@ struct AddTagView: View {
|
||||
content: content,
|
||||
id: "tag",
|
||||
isVisible: true,
|
||||
german: .init(content: .mock, urlComponent: "tag", name: "Neuer Tag"),
|
||||
english: .init(content: .mock, urlComponent: "tag-en", name: "New Tag"))
|
||||
german: .init(content: content, urlComponent: "tag", name: "Neuer Tag"),
|
||||
english: .init(content: content, urlComponent: "tag-en", name: "New Tag"))
|
||||
// Add to top of the list, and resort when changing the name
|
||||
content.tags.insert(newTag, at: 0)
|
||||
dismiss()
|
||||
|
@ -24,19 +24,24 @@ struct LocalizedTagDetailView: View {
|
||||
footer: "The url component to use in the url for this tag",
|
||||
validation: tag.isValid,
|
||||
update: { tag.urlComponent = $0 })
|
||||
|
||||
Text("Original url")
|
||||
.font(.headline)
|
||||
Text(tag.originalUrl ?? "-")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 1)
|
||||
.padding(.bottom)
|
||||
|
||||
|
||||
if let url = tag.originalUrl {
|
||||
GenericPropertyView(title: "Original URL") {
|
||||
Text(url)
|
||||
}
|
||||
}
|
||||
|
||||
OptionalStringPropertyView(
|
||||
title: "Subtitle",
|
||||
text: $tag.subtitle,
|
||||
footer: "The subtitle/tagline to use")
|
||||
|
||||
OptionalStringPropertyView(
|
||||
title: "Preview Title",
|
||||
text: $tag.linkPreviewTitle,
|
||||
prompt: tag.name,
|
||||
footer: "The title to use for the tag in previews and on tag pages")
|
||||
|
||||
OptionalImagePropertyView(
|
||||
title: "Preview Image",
|
||||
selectedImage: $tag.linkPreviewImage,
|
||||
|
@ -21,8 +21,17 @@ struct TagDetailView: View {
|
||||
value: $tag.isVisible,
|
||||
footer: "Indicate if the tag should appear in the tag list of posts and pages. If the tag is not visible, then it can still be used as a filter.")
|
||||
|
||||
IdPropertyView(
|
||||
id: $tag.id,
|
||||
title: "Tag id",
|
||||
footer: "The unique id of the tag for references",
|
||||
validation: tag.isValid) {
|
||||
tag.update(id: $0)
|
||||
}
|
||||
|
||||
LocalizedTagDetailView(
|
||||
tag: tag.localized(in: language))
|
||||
.id(tag.id + language.rawValue)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
Reference in New Issue
Block a user