From 508483071aa3d02e884336f14cb5056ef07e65f1 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Tue, 7 Jan 2025 14:03:07 +0100 Subject: [PATCH] Add draft indicator, filter drafts, show issue count --- CHDataManagement.xcodeproj/project.pbxproj | 4 ++ .../Extensions/String+Extensions.swift | 9 +++ .../Generator/Commands/HtmlCommand.swift | 66 +++++++++++++++---- .../Post Lists/PostListPageGenerator.swift | 7 +- CHDataManagement/Model/FileResource.swift | 2 +- CHDataManagement/Model/FileType.swift | 9 +++ .../Views/Generic/DraftIndicator.swift | 15 +++++ .../Views/Pages/PageListView.swift | 8 ++- .../Views/Posts/PostListView.swift | 8 ++- .../Content/GenerationContentView.swift | 28 ++++---- 10 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 CHDataManagement/Views/Generic/DraftIndicator.swift diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index ab18bb4..bd896d8 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -247,6 +247,7 @@ E2FE0F6A2D2D2D55002963B7 /* LocalizedPageSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F692D2D2D4F002963B7 /* LocalizedPageSettingsFile.swift */; }; E2FE0F6C2D2D335E002963B7 /* LocalizedPageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F6B2D2D3358002963B7 /* LocalizedPageSettingsView.swift */; }; E2FE0F6E2D2D3689002963B7 /* LocalizedAudioPlayerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F6D2D2D3685002963B7 /* LocalizedAudioPlayerSettings.swift */; }; + E2FE0F702D2D5235002963B7 /* DraftIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F6F2D2D5231002963B7 /* DraftIndicator.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -485,6 +486,7 @@ E2FE0F692D2D2D4F002963B7 /* LocalizedPageSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageSettingsFile.swift; sourceTree = ""; }; E2FE0F6B2D2D3358002963B7 /* LocalizedPageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageSettingsView.swift; sourceTree = ""; }; E2FE0F6D2D2D3685002963B7 /* LocalizedAudioPlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedAudioPlayerSettings.swift; sourceTree = ""; }; + E2FE0F6F2D2D5231002963B7 /* DraftIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftIndicator.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -699,6 +701,7 @@ E2A21C372CB9A4F10060935B /* Generic */ = { isa = PBXGroup; children = ( + E2FE0F6F2D2D5231002963B7 /* DraftIndicator.swift */, E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */, E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */, E22990312D0F7678009F8D77 /* DatePropertyView.swift */, @@ -1151,6 +1154,7 @@ E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */, E2FE0F242D2A8C21002963B7 /* TagDisplayView.swift in Sources */, E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */, + E2FE0F702D2D5235002963B7 /* DraftIndicator.swift in Sources */, E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */, E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */, E29D31942D0B7D280051B7F4 /* SimpleImage.swift in Sources */, diff --git a/CHDataManagement/Extensions/String+Extensions.swift b/CHDataManagement/Extensions/String+Extensions.swift index 42ad981..c3eb3ac 100644 --- a/CHDataManagement/Extensions/String+Extensions.swift +++ b/CHDataManagement/Extensions/String+Extensions.swift @@ -63,6 +63,15 @@ extension String { return components(separatedBy: separator).dropLast().joined(separator: separator) } + /** + Remove everything before the last separator. + + Also removes the separator itself. If the separator is not contained in the string, then the full string is returned. + */ + func dropBeforeLast(_ separator: T) -> String where T: StringProtocol { + components(separatedBy: separator).last! + } + func dropBeforeFirst(_ separator: String) -> String { guard contains(separator) else { return self diff --git a/CHDataManagement/Generator/Commands/HtmlCommand.swift b/CHDataManagement/Generator/Commands/HtmlCommand.swift index 9a1c3de..f979e10 100644 --- a/CHDataManagement/Generator/Commands/HtmlCommand.swift +++ b/CHDataManagement/Generator/Commands/HtmlCommand.swift @@ -72,7 +72,7 @@ struct HtmlCommand: CommandProcessor { } for src in srcAttributes { - results.warning("Found image in html: \(src)") + findFile(path: src, source: "src of ") } } @@ -93,7 +93,7 @@ struct HtmlCommand: CommandProcessor { if url.hasPrefix("http://") || url.hasPrefix("https://") { results.externalLink(to: url) } else { - results.warning("Relative link in HTML: \(url)") + findFile(path: url, source: "href of ") } } } @@ -122,7 +122,7 @@ struct HtmlCommand: CommandProcessor { } for src in srcSets { - results.warning("Found source set in html: \(src)") + findFile(path: src, source: "srcset of ") } } @@ -138,17 +138,57 @@ struct HtmlCommand: CommandProcessor { } for src in srcAttributes { - guard content.isValidIdForFile(src) else { - results.warning("Found source in html: \(src)") - continue - } - guard let file = content.file(src) else { - results.warning("Found source in html: \(src)") - continue - } - #warning("Either find files by their full path, or replace file id with full path") - results.require(file: file) + findFile(path: src, source: "src of ") } } + private func findFile(path: String, source: String) { + let type = FileType(fileExtension: path.fileExtension) + + guard path.hasPrefix("/") else { + findFileWith(relativePath: path, type: type, source: source) + return + } + + guard !type.generatesImageVersions else { + // Try to determine image version needed + findImageVersion(path: path, type: type, source: source) + return + } + if findFile(withAbsolutePath: path) { + return + } + + let fileId = path.dropBeforeLast("/") + if content.isValidIdForFile(fileId) { + results.missing(file: fileId, source: "HTML: \(source)") + } else { + results.warning("Could not find file '\(path)' for \(source)") + } + } + + private func findFile(withAbsolutePath absolutePath: String) -> Bool { + guard let file = content.file(withOutputPath: absolutePath) else { + return false + } + results.require(file: file) + return true + } + + private func findImageVersion(path: String, type: FileType, source: String) { + // First check if image original should be used + if findFile(withAbsolutePath: path) { + return + } + let fileId = path.dropAfterLast("/").dropBeforeLast("/") + guard let file = content.file(fileId) else { + results.missing(file: fileId, source: "HTML: \(source)") + return + } + results.warning("Could not determine image version for file '\(file.id)' for \(source)") + } + + private func findFileWith(relativePath: String, type: FileType, source: String) { + results.warning("Could not determine relative file '\(relativePath)' for \(source)") + } } diff --git a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift index 07ab27e..dd4bc32 100644 --- a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift @@ -21,8 +21,11 @@ final class PostListPageGenerator { } func createPages(for posts: [Post]) { - // Sort by newest first - let posts = posts.sorted(ascending: false) { $0.startDate } + // Sort by newest first, filter drafts + let posts = posts + .filter { !$0.isDraft } + .sorted(ascending: false) { $0.startDate } + let totalCount = posts.count guard totalCount > 0 else { // Create one empty page diff --git a/CHDataManagement/Model/FileResource.swift b/CHDataManagement/Model/FileResource.swift index e141798..bcc250d 100644 --- a/CHDataManagement/Model/FileResource.swift +++ b/CHDataManagement/Model/FileResource.swift @@ -246,7 +246,7 @@ final class FileResource: Item { /** Get the url path to a file in the output folder. - The result is an absolute path from the output folder for use in HTML. + The result is an absolute path from the output folder for use in HTML, including a leading slash */ var absoluteUrl: String { if let customOutputPath { diff --git a/CHDataManagement/Model/FileType.swift b/CHDataManagement/Model/FileType.swift index 4e96315..31e5318 100644 --- a/CHDataManagement/Model/FileType.swift +++ b/CHDataManagement/Model/FileType.swift @@ -180,6 +180,15 @@ enum FileType: String { category == .image } + var generatesImageVersions: Bool { + switch self { + case .jpg, .png, .avif, .webp, .tiff: + return true + default: + return false + } + } + var isVideo: Bool { category == .video } diff --git a/CHDataManagement/Views/Generic/DraftIndicator.swift b/CHDataManagement/Views/Generic/DraftIndicator.swift new file mode 100644 index 0000000..30ca1c4 --- /dev/null +++ b/CHDataManagement/Views/Generic/DraftIndicator.swift @@ -0,0 +1,15 @@ +import SwiftUI + +struct DraftIndicator: View { + + var body: some View { + Text("Draft") + .foregroundStyle(.white) + .padding(.vertical, 2) + .padding(.horizontal, 5) + .background( + RoundedRectangle(cornerRadius: 5, style: .circular) + .foregroundStyle(Color.gray) + ) + } +} diff --git a/CHDataManagement/Views/Pages/PageListView.swift b/CHDataManagement/Views/Pages/PageListView.swift index dfc9a39..074afa1 100644 --- a/CHDataManagement/Views/Pages/PageListView.swift +++ b/CHDataManagement/Views/Pages/PageListView.swift @@ -31,7 +31,13 @@ struct PageListView: View { .textFieldStyle(.roundedBorder) .padding(.horizontal, 8) List(filteredPages, selection: $selectedPage) { page in - Text(page.localized(in: language).title).tag(page) + HStack { + Text(page.title(in: language)) + Spacer() + if page.isDraft { + DraftIndicator() + } + }.tag(page) } } .onAppear { diff --git a/CHDataManagement/Views/Posts/PostListView.swift b/CHDataManagement/Views/Posts/PostListView.swift index 9582e80..ecea9ef 100644 --- a/CHDataManagement/Views/Posts/PostListView.swift +++ b/CHDataManagement/Views/Posts/PostListView.swift @@ -31,7 +31,13 @@ struct PostListView: View { .textFieldStyle(.roundedBorder) .padding(.horizontal, 8) List(filteredPosts, selection: $selectedPost) { post in - Text(post.title(in: language)).tag(post) + HStack { + Text(post.title(in: language)) + Spacer() + if post.isDraft { + DraftIndicator() + } + }.tag(post) } }.onAppear { if selectedPost == nil { diff --git a/CHDataManagement/Views/Settings/Content/GenerationContentView.swift b/CHDataManagement/Views/Settings/Content/GenerationContentView.swift index cf47d1e..2fc466c 100644 --- a/CHDataManagement/Views/Settings/Content/GenerationContentView.swift +++ b/CHDataManagement/Views/Settings/Content/GenerationContentView.swift @@ -61,56 +61,56 @@ struct GenerationContentView: View { Text("\(content.results.requiredFiles.count) files") } List { - Section("Empty pages") { - ForEach(content.results.emptyPages.sorted()) { id in - Text("\(id.pageId) (\(id.language))") - } - } - Section("Inaccessible files") { + Section("Inaccessible files (\(content.results.inaccessibleFiles.count))") { ForEach(content.results.inaccessibleFiles.sorted()) { file in Text(file.id) } } - Section("Unparsable files") { + Section("Unparsable files (\(content.results.unparsableFiles.count))") { ForEach(content.results.unparsableFiles.sorted()) { file in Text(file.id) } } - Section("Missing files") { + Section("Missing files (\(content.results.missingFiles.count))") { ForEach(content.results.missingFiles.sorted(), id: \.self) { file in Text(file) } } - Section("Missing tags") { + Section("Missing tags (\(content.results.missingTags.count))") { ForEach(content.results.missingTags.sorted(), id: \.self) { tag in Text(tag) } } - Section("Missing pages") { + Section("Missing pages (\(content.results.missingPages.count))") { ForEach(content.results.missingPages.sorted(), id: \.self) { page in Text(page) } } - Section("Invalid commands") { + Section("Invalid commands (\(content.results.invalidCommands.count))") { ForEach(content.results.invalidCommands.sorted(), id: \.self) { markdown in Text(markdown) } } - Section("Invalid blocks") { + Section("Invalid blocks (\(content.results.invalidBlocks.count))") { ForEach(content.results.invalidBlocks.sorted(), id: \.self) { markdown in Text(markdown) } } - Section("Warnings") { + Section("Warnings (\(content.results.warnings.count))") { ForEach(content.results.warnings.sorted(), id: \.self) { warning in Text(warning) } } - Section("Unsaved output files") { + Section("Unsaved output files (\(content.results.unsavedOutputFiles.count))") { ForEach(content.results.unsavedOutputFiles.sorted(), id: \.self) { file in Text(file) } } + Section("Empty pages (\(content.results.emptyPages.count))") { + ForEach(content.results.emptyPages.sorted()) { id in + Text("\(id.pageId) (\(id.language))") + } + } } }.padding() }