Compare commits
7 Commits
062e7d289a
...
a4710d525b
Author | SHA1 | Date | |
---|---|---|---|
|
a4710d525b | ||
|
a8920a4cd2 | ||
|
cb041eb6ed | ||
|
329519e15b | ||
|
d6502fb09c | ||
|
dd720d6646 | ||
|
e689903f3c |
@@ -207,6 +207,7 @@
|
||||
E2F3B3982DC54F9400CFA712 /* ChangeObservingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B3972DC54F8600CFA712 /* ChangeObservingItem.swift */; };
|
||||
E2F3B39C2DC5542E00CFA712 /* LabelEditingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B39B2DC5542E00CFA712 /* LabelEditingView.swift */; };
|
||||
E2F3B39E2DC55B1C00CFA712 /* LabelCreationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B39D2DC55B1C00CFA712 /* LabelCreationView.swift */; };
|
||||
E2F3B3A22DC769C300CFA712 /* DeleteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B3A12DC769BF00CFA712 /* DeleteButton.swift */; };
|
||||
E2FD1D0D2D2DBBA600B48627 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */; };
|
||||
E2FD1D192D2DC4F500B48627 /* LoadingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */; };
|
||||
E2FD1D1B2D2DC63800B48627 /* LinkPreviewDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */; };
|
||||
@@ -490,6 +491,7 @@
|
||||
E2F3B3972DC54F8600CFA712 /* ChangeObservingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeObservingItem.swift; sourceTree = "<group>"; };
|
||||
E2F3B39B2DC5542E00CFA712 /* LabelEditingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelEditingView.swift; sourceTree = "<group>"; };
|
||||
E2F3B39D2DC55B1C00CFA712 /* LabelCreationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelCreationView.swift; sourceTree = "<group>"; };
|
||||
E2F3B3A12DC769BF00CFA712 /* DeleteButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteButton.swift; sourceTree = "<group>"; };
|
||||
E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = "<group>"; };
|
||||
E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingContext.swift; sourceTree = "<group>"; };
|
||||
E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewDetailView.swift; sourceTree = "<group>"; };
|
||||
@@ -829,6 +831,7 @@
|
||||
E2A21C372CB9A4F10060935B /* Generic */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2F3B3A12DC769BF00CFA712 /* DeleteButton.swift */,
|
||||
E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */,
|
||||
E22990312D0F7678009F8D77 /* DatePropertyView.swift */,
|
||||
E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */,
|
||||
@@ -1449,6 +1452,7 @@
|
||||
E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */,
|
||||
E2521DFC2D5020BE00C56662 /* PostContentGenerator.swift in Sources */,
|
||||
E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */,
|
||||
E2F3B3A22DC769C300CFA712 /* DeleteButton.swift in Sources */,
|
||||
E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */,
|
||||
E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */,
|
||||
E2FE0F422D2B4821002963B7 /* OtherCodeBlock.swift in Sources */,
|
||||
|
@@ -260,6 +260,18 @@ final class GenerationResults: ObservableObject {
|
||||
let unused = existingFiles.subtracting(outputFiles)
|
||||
update { self.unusedFilesInOutput = unused }
|
||||
}
|
||||
|
||||
func sources(forMissingPage page: String) -> [(page: LocalizedItemId, source: String)] {
|
||||
var all = [(page: LocalizedItemId, source: String)]()
|
||||
for (id, results) in cache {
|
||||
guard let sources = results.missingLinkedPages[page]?.sorted() else {
|
||||
continue
|
||||
}
|
||||
let additions = sources.map { (page: id, source: $0) }
|
||||
all.append(contentsOf: additions)
|
||||
}
|
||||
return all
|
||||
}
|
||||
}
|
||||
|
||||
private extension Dictionary where Value == Set<LocalizedItemId> {
|
||||
|
@@ -16,4 +16,28 @@ final class SelectedContent: ObservableObject {
|
||||
|
||||
@Published
|
||||
var file: FileResource?
|
||||
|
||||
func remove(_ post: Post) {
|
||||
if self.post == post {
|
||||
self.post = nil
|
||||
}
|
||||
}
|
||||
|
||||
func remove(_ page: Page) {
|
||||
if self.page == page {
|
||||
self.page = nil
|
||||
}
|
||||
}
|
||||
|
||||
func remove(_ tag: Tag) {
|
||||
if self.tag == tag {
|
||||
self.tag = nil
|
||||
}
|
||||
}
|
||||
|
||||
func remove(_ file: FileResource) {
|
||||
if self.file == file {
|
||||
self.file = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -113,6 +113,8 @@ final class Content: ObservableObject {
|
||||
loadFromDisk(callback: callback)
|
||||
}
|
||||
|
||||
// MARK: Removing items
|
||||
|
||||
func remove(_ file: FileResource) {
|
||||
files.remove(file)
|
||||
for post in posts {
|
||||
@@ -129,6 +131,33 @@ final class Content: ObservableObject {
|
||||
settings.remove(file)
|
||||
}
|
||||
|
||||
func remove(_ post: Post) {
|
||||
posts.remove(post)
|
||||
}
|
||||
|
||||
func remove(_ page: Page) {
|
||||
pages.remove(page)
|
||||
for post in posts {
|
||||
if post.linkedPage == page {
|
||||
post.linkedPage = nil
|
||||
}
|
||||
}
|
||||
// TODO: Check for page links and other references in content
|
||||
}
|
||||
|
||||
func remove(_ tag: Tag) {
|
||||
tags.remove(tag)
|
||||
for post in posts {
|
||||
post.tags.remove(tag)
|
||||
}
|
||||
for page in pages {
|
||||
page.tags.remove(tag)
|
||||
}
|
||||
// TODO: Check for tag links and other references is content
|
||||
}
|
||||
|
||||
// MARK: Loading
|
||||
|
||||
func file(withOutputPath: String) -> FileResource? {
|
||||
files.first { $0.absoluteUrl == withOutputPath }
|
||||
}
|
||||
@@ -214,11 +243,17 @@ final class Content: ObservableObject {
|
||||
|
||||
private var imageDimensions: [String: CGSize] = [:]
|
||||
|
||||
private let imageDimensionsQueue = DispatchQueue(label: "imageDimensionsQueue")
|
||||
|
||||
func dimensions(of image: String) -> CGSize? {
|
||||
imageDimensions[image]
|
||||
imageDimensionsQueue.sync {
|
||||
imageDimensions[image]
|
||||
}
|
||||
}
|
||||
|
||||
func cache(dimensions: CGSize?, of image: String) {
|
||||
imageDimensions[image] = dimensions
|
||||
imageDimensionsQueue.sync {
|
||||
imageDimensions[image] = dimensions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,3 +31,10 @@ extension LocalizedItemId: Comparable {
|
||||
return lhs.language < rhs.language
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedItemId: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
"\(itemType) (\(language))"
|
||||
}
|
||||
}
|
||||
|
@@ -147,6 +147,27 @@ final class Storage: ObservableObject {
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
Completely delete a post file from the content folder
|
||||
*/
|
||||
func delete(page pageId: String) -> Bool {
|
||||
guard let contentScope else { return false }
|
||||
guard contentScope.deleteFile(at: pageMetadataPath(page: pageId)) else {
|
||||
return false
|
||||
}
|
||||
// Move the existing content files
|
||||
var result = true
|
||||
for language in ContentLanguage.allCases {
|
||||
// Copy as many files as possible, since metadata was already moved
|
||||
// Don't fail early
|
||||
if !contentScope.deleteFile(at: pageContentPath(page: pageId, language: language)) {
|
||||
print("Failed to delete content file \(language) of page \(pageId)")
|
||||
result = false
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: Posts
|
||||
|
||||
private func postFileName(_ postId: String) -> String {
|
||||
@@ -186,6 +207,14 @@ final class Storage: ObservableObject {
|
||||
return contentScope.move(postFilePath(post: postId), to: postFilePath(post: newId))
|
||||
}
|
||||
|
||||
/**
|
||||
Completely delete a post file from the content folder
|
||||
*/
|
||||
func delete(post postId: String) -> Bool {
|
||||
guard let contentScope else { return false }
|
||||
return contentScope.deleteFile(at: postFilePath(post: postId))
|
||||
}
|
||||
|
||||
// MARK: Tags
|
||||
|
||||
private func tagFileName(tagId: String) -> String {
|
||||
@@ -225,6 +254,11 @@ final class Storage: ObservableObject {
|
||||
return contentScope.move(tagFilePath(tag: tagId), to: tagFilePath(tag: newId))
|
||||
}
|
||||
|
||||
func delete(tag tagId: String) -> Bool {
|
||||
guard let contentScope else { return false }
|
||||
return contentScope.deleteFile(at: tagFilePath(tag: tagId))
|
||||
}
|
||||
|
||||
// MARK: Files
|
||||
|
||||
func size(of file: String) -> Int? {
|
||||
|
@@ -34,6 +34,9 @@ struct FileDetailView: View {
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@EnvironmentObject
|
||||
private var selection: SelectedContent
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@@ -218,6 +221,7 @@ struct FileDetailView: View {
|
||||
return
|
||||
}
|
||||
content.remove(file)
|
||||
selection.remove(file)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -12,6 +12,14 @@ struct GenerationContentView: View {
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
var draftPages: Set<Page> {
|
||||
Set(content.pages.filter { $0.isDraft })
|
||||
}
|
||||
|
||||
var draftPosts: Set<Post> {
|
||||
Set(content.posts.filter { $0.isDraft })
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Website Generation")
|
||||
@@ -45,7 +53,7 @@ struct GenerationContentView: View {
|
||||
GenerationStringIssuesView(
|
||||
text: "output files",
|
||||
statusWhenNonEmpty: .nominal,
|
||||
items: $content.results.outputFiles)
|
||||
items: content.results.outputFiles)
|
||||
GenerationResultsIssueView(
|
||||
text: "\(content.results.imagesToGenerate.count) images",
|
||||
status: .nominal,
|
||||
@@ -57,54 +65,68 @@ struct GenerationContentView: View {
|
||||
GenerationStringIssuesView(
|
||||
text: "external links",
|
||||
statusWhenNonEmpty: .nominal,
|
||||
items: $content.results.externalLinks)
|
||||
items: content.results.externalLinks)
|
||||
GenerationStringIssuesView(
|
||||
text: "required files",
|
||||
statusWhenNonEmpty: .nominal,
|
||||
items: $content.results.requiredFiles) { $0.id }
|
||||
items: content.results.requiredFiles) { $0.id }
|
||||
GenerationStringIssuesView(
|
||||
text: "external files",
|
||||
statusWhenNonEmpty: .nominal,
|
||||
items: $content.results.externalFiles) { $0.id }
|
||||
items: content.results.externalFiles) { $0.id }
|
||||
GenerationStringIssuesView(
|
||||
text: "empty pages",
|
||||
statusWhenNonEmpty: .warning,
|
||||
items: $content.results.emptyPages) { "\($0.pageId) (\($0.language))" }
|
||||
items: content.results.emptyPages) { "\($0.pageId) (\($0.language))" }
|
||||
GenerationStringIssuesView(
|
||||
text: "draft pages",
|
||||
statusWhenNonEmpty: .warning,
|
||||
items: draftPages) { $0.id }
|
||||
GenerationStringIssuesView(
|
||||
text: "draft posts",
|
||||
statusWhenNonEmpty: .warning,
|
||||
items: draftPosts) { $0.id }
|
||||
GenerationStringIssuesView(
|
||||
text: "additional output files",
|
||||
statusWhenNonEmpty: .warning,
|
||||
items: $content.results.unusedFilesInOutput)
|
||||
items: content.results.unusedFilesInOutput)
|
||||
GenerationStringIssuesView(
|
||||
text: "inaccessible files",
|
||||
items: $content.results.inaccessibleFiles) { $0.id }
|
||||
items: content.results.inaccessibleFiles) { $0.id }
|
||||
GenerationStringIssuesView(
|
||||
text: "unparsable files",
|
||||
items: $content.results.unparsableFiles) { $0.id }
|
||||
items: content.results.unparsableFiles) { $0.id }
|
||||
GenerationStringIssuesView(
|
||||
text: "unsaved output files",
|
||||
items: $content.results.unsavedOutputFiles)
|
||||
items: content.results.unsavedOutputFiles)
|
||||
GenerationStringIssuesView(
|
||||
text: "failed image generations",
|
||||
items: $content.results.failedImages) { $0.outputPath }
|
||||
items: content.results.failedImages) { $0.outputPath }
|
||||
GenerationStringIssuesView(
|
||||
text: "missing files",
|
||||
items: $content.results.missingFiles)
|
||||
items: content.results.missingFiles)
|
||||
GenerationStringIssuesView(
|
||||
text: "missing tags",
|
||||
items: $content.results.missingTags)
|
||||
items: content.results.missingTags)
|
||||
GenerationStringIssuesView(
|
||||
text: "missing pages",
|
||||
items: $content.results.missingPages)
|
||||
items: content.results.missingPages) { pageId in
|
||||
let sources = content.results.sources(forMissingPage: pageId)
|
||||
.map { "\($0.page): \($0.source)"}
|
||||
.joined(separator: ", ")
|
||||
|
||||
return "\(pageId) (\(sources))"
|
||||
}
|
||||
GenerationStringIssuesView(
|
||||
text: "invalid commands",
|
||||
items: $content.results.invalidCommands)
|
||||
items: content.results.invalidCommands)
|
||||
GenerationStringIssuesView(
|
||||
text: "invalid blocks",
|
||||
items: $content.results.invalidBlocks)
|
||||
items: content.results.invalidBlocks)
|
||||
GenerationStringIssuesView(
|
||||
text: "warnings",
|
||||
statusWhenNonEmpty: .warning,
|
||||
items: $content.results.warnings)
|
||||
items: content.results.warnings)
|
||||
HorizontalCenter {
|
||||
Button(action: { dismiss() }) {
|
||||
Text("Close")
|
||||
|
@@ -6,8 +6,7 @@ struct GenerationStringIssuesView<T>: View where T: Hashable {
|
||||
|
||||
let statusWhenNonEmpty: IssueStatus
|
||||
|
||||
@Binding
|
||||
var items: Set<T>
|
||||
let items: Set<T>
|
||||
|
||||
let map: (T) -> String
|
||||
|
||||
@@ -18,10 +17,10 @@ struct GenerationStringIssuesView<T>: View where T: Hashable {
|
||||
items.isEmpty ? .nominal : statusWhenNonEmpty
|
||||
}
|
||||
|
||||
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<T>>, map: @escaping (T) -> String) {
|
||||
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Set<T>, map: @escaping (T) -> String) {
|
||||
self.text = text
|
||||
self.statusWhenNonEmpty = statusWhenNonEmpty
|
||||
self._items = items
|
||||
self.items = items
|
||||
self.map = map
|
||||
}
|
||||
|
||||
@@ -56,10 +55,10 @@ struct GenerationStringIssuesView<T>: View where T: Hashable {
|
||||
|
||||
extension GenerationStringIssuesView where T == String {
|
||||
|
||||
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Binding<Set<String>>) {
|
||||
init(text: String, statusWhenNonEmpty: IssueStatus = .error, items: Set<String>) {
|
||||
self.text = text
|
||||
self.statusWhenNonEmpty = statusWhenNonEmpty
|
||||
self._items = items
|
||||
self.items = items
|
||||
self.map = { $0 }
|
||||
}
|
||||
}
|
||||
|
20
CHDataManagement/Views/Generic/DeleteButton.swift
Normal file
20
CHDataManagement/Views/Generic/DeleteButton.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import SwiftUI
|
||||
|
||||
struct DeleteButton: View {
|
||||
|
||||
let action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemSymbol: .trash)
|
||||
Text("Delete")
|
||||
.padding(.vertical, 8)
|
||||
Spacer()
|
||||
}
|
||||
.foregroundStyle(Color.white)
|
||||
.background(RoundedRectangle(cornerRadius: 8).fill(Color.red))
|
||||
}.buttonStyle(.plain)
|
||||
}
|
||||
}
|
@@ -9,6 +9,9 @@ struct PageDetailView: View {
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@EnvironmentObject
|
||||
private var selection: SelectedContent
|
||||
|
||||
@ObservedObject
|
||||
private var page: Page
|
||||
|
||||
@@ -73,10 +76,20 @@ struct PageDetailView: View {
|
||||
page: page.localized(in: language),
|
||||
transferImage: transferImage)
|
||||
.id(page.id + language.rawValue)
|
||||
DeleteButton(action: deletePage)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
private func deletePage() {
|
||||
guard content.storage.delete(page: page.id) else {
|
||||
print("Page '\(page.id)': Failed to delete file in content folder")
|
||||
return
|
||||
}
|
||||
content.remove(page)
|
||||
selection.remove(page)
|
||||
}
|
||||
}
|
||||
|
||||
extension PageDetailView: MainContentView {
|
||||
|
@@ -76,6 +76,7 @@ struct PostDetailView: View {
|
||||
LocalizedPostDetailView(
|
||||
post: post.localized(in: language),
|
||||
transferImage: transferImage)
|
||||
DeleteButton(action: deletePost)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -90,6 +91,15 @@ struct PostDetailView: View {
|
||||
selection.tab = .pages
|
||||
}
|
||||
}
|
||||
|
||||
private func deletePost() {
|
||||
guard content.storage.delete(post: post.id) else {
|
||||
print("Post '\(post.id)': Failed to delete file in content folder")
|
||||
return
|
||||
}
|
||||
content.remove(post)
|
||||
selection.remove(post)
|
||||
}
|
||||
}
|
||||
|
||||
extension PostDetailView: MainContentView {
|
||||
|
@@ -6,6 +6,12 @@ struct TagDetailView: View {
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@EnvironmentObject
|
||||
private var selection: SelectedContent
|
||||
|
||||
@ObservedObject
|
||||
var tag: Tag
|
||||
|
||||
@@ -37,10 +43,20 @@ struct TagDetailView: View {
|
||||
tag: tag.localized(in: language),
|
||||
transferImage: transferImage)
|
||||
.id(tag.id + language.rawValue)
|
||||
DeleteButton(action: deleteTag)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteTag() {
|
||||
guard content.storage.delete(tag: tag.id) else {
|
||||
print("Tag '\(tag.id)': Failed to delete file in content folder")
|
||||
return
|
||||
}
|
||||
content.remove(tag)
|
||||
selection.remove(tag)
|
||||
}
|
||||
}
|
||||
|
||||
extension TagDetailView: MainContentView {
|
||||
|
Reference in New Issue
Block a user