consolidate tags, hide date

This commit is contained in:
Christoph Hagen 2025-01-05 12:19:32 +01:00
parent 93e642c3c9
commit 5ac5a7b000
26 changed files with 284 additions and 129 deletions

View File

@ -213,6 +213,7 @@
E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */; };
E2FE0F202D29A70E002963B7 /* Array+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */; };
E2FE0F222D2A84A0002963B7 /* VideoCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */; };
E2FE0F242D2A8C21002963B7 /* TagDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@ -417,6 +418,7 @@
E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewGenerator.swift; sourceTree = "<group>"; };
E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = "<group>"; };
E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCommandProcessor.swift; sourceTree = "<group>"; };
E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDisplayView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -638,6 +640,7 @@
E2A21C372CB9A4F10060935B /* Generic */ = {
isa = PBXGroup;
children = (
E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */,
E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */,
E22990312D0F7678009F8D77 /* DatePropertyView.swift */,
E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */,
@ -998,6 +1001,7 @@
E229904E2D13535C009F8D77 /* SecurityBookmark.swift in Sources */,
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */,
E2FE0F242D2A8C21002963B7 /* TagDisplayView.swift in Sources */,
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */,
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */,
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,

View File

@ -58,7 +58,7 @@ final class FeedPageGenerator {
header: pageHeader,
additionalFooter: footer) { content in
if showTitle {
content += "<h1>\(title)</h1>"
content += "<h1 class='separated-headline'>\(title)</h1>"
}
for post in posts {
content += FeedEntry(data: post).content

View File

@ -80,7 +80,9 @@ final class PageGenerator {
additionalFooter: results.requiredFooters.sorted().joined()) { content in
content += "<article>"
if !localized.hideTitle {
content += "<h3>\(page.dateText(in: language))</h3>"
if !page.hideDate {
content += "<h3>\(page.dateText(in: language))</h3>"
}
content += "<h1>\(localized.title)</h1>"
content += TagList(tags: tags).content
}

View File

@ -121,7 +121,7 @@ final class TagOverviewGenerator {
let page = GenericPage(
header: pageHeader,
additionalFooter: "") { content in
content += "<h1>\(header.title)</h1>"
content += "<h1 class='separated-headline'>\(header.title)</h1>"
for tag in tags {
let description = tag.localized.description ?? ""
let image = self.makePageImage(item: tag.localized)

View File

@ -99,6 +99,28 @@ final class PageGenerationResults: ObservableObject {
pageIsEmpty = false
}
func reset() {
inaccessibleFiles = []
unparsableFiles = [:]
missingFiles = [:]
missingLinkedFiles = [:]
missingLinkedTags = [:]
missingLinkedPages = [:]
requiredHeaders = []
requiredFooters = []
requiredIcons = []
linkedPages = []
linkedTags = []
externalLinks = []
usedFiles = []
requiredFiles = []
imagesToGenerate = []
invalidCommands = []
warnings = []
unsavedOutputFiles = [:]
pageIsEmpty = false
}
// MARK: Adding entries
func inaccessibleContent(file: FileResource) {

View File

@ -38,37 +38,40 @@ final class PostListPageGenerator {
}
}
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>) {
let posts: [FeedEntryData] = posts.map { post in
let localized: LocalizedPost = post.localized(in: language)
private func makePostData(post: Post) -> FeedEntryData {
let localized: LocalizedPost = post.localized(in: language)
#warning("Add post link text to settings or to each post")
let linkUrl = post.linkedPage.map {
FeedEntryData.Link(
url: $0.absoluteUrl(in: language),
text: language == .english ? "View" : "Anzeigen")
}
let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in
.init(name: tag.localized(in: language).name,
url: tag.absoluteUrl(in: language))
}
let images = localized.images.map { image in
image.imageSet(width: mainContentMaximumWidth, height: mainContentMaximumWidth, language: language)
}
images.forEach(source.results.require)
return FeedEntryData(
entryId: post.id,
title: localized.title,
textAboveTitle: post.dateText(in: language),
link: linkUrl,
tags: tags,
text: localized.text.components(separatedBy: "\n"),
images: images)
#warning("Add post link text to settings or to each post")
let linkUrl = post.linkedPage.map {
FeedEntryData.Link(
url: $0.absoluteUrl(in: language),
text: language == .english ? "View" : "Anzeigen")
}
// Use the tags of the page if one is linked
let tags: [FeedEntryData.Tag] = post.usedTags().filter { $0.isVisible }.map { tag in
.init(name: tag.localized(in: language).name,
url: tag.absoluteUrl(in: language))
}
let images = localized.images.map { image in
image.imageSet(width: mainContentMaximumWidth, height: mainContentMaximumWidth, language: language)
}
images.forEach(source.results.require)
return FeedEntryData(
entryId: post.id,
title: localized.title,
textAboveTitle: post.dateText(in: language),
link: linkUrl,
tags: tags,
text: localized.text.components(separatedBy: "\n\n"),
images: images)
}
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>) {
let posts: [FeedEntryData] = posts.map(makePostData)
let feedPageGenerator = FeedPageGenerator(content: source.content, results: source.results)
let languageButtonUrl = "/" + pageUrl(in: language.next, pageNumber: pageIndex)

View File

@ -1,29 +1,40 @@
import SwiftUI
import SFSafeSymbols
#warning("Fix podcast")
#warning("Fix endeavor basics (image compare)")
#warning("Fix cap mosaic GIF")
/**
**Content**
- Podcast: Fix audio player, preview image
- Endeavor Basics: -> image compare command
- Article Cap Mosaic: -> GIF feature
- iPhone Backgrounds: Add page, html
#warning("Add custom url string to external files (optional)")
#warning("Show all warnings on page content")
#warning("Button to delete file, show in finder, replace, mark changed (-> images)")
#warning("Transfer images of posts to other language")
#warning("Show tag selection view for pages")
#warning("Button to replace files")
#warning("Replace links to files inside pages when id changes")
#warning("Calculate file sizes")
#warning("Specify image aspect ratio to prevent page jumps")
#warning("Add version and source url properties to file resources")
#warning("Show errors during loading of content")
#warning("Generate pages for posts")
#warning("Clean up mock content")
#warning("Show posts linking to a page")
#warning("Add author to settings and page headers")
#warning("Check for files in output folder not generated by app")
#warning("Fix GIFs: Don't rescale, don't use image set")
#warning("Add view to browse images")
#warning("Show warnings for empty item properties")
**UI**
- Image search: Add view to see all images and filter
- Pages: Show linking posts
- Page Content: Show all results of `PageGenerationResults`
- Files: Show usages of file
- Images: Show list of generated versions
**Features**
- GIF Support (No image set, don't rescale)
- Image compare command `![compare](image1;image2)`
- Files: Optional Property `customFilePath` for external files to place them in another location
- Files: Property `version` and `sourceUrl` to track asset files
- Posts: Generate separate pages for posts to link to
- Settings: Introduce `Authors` (`name`, `image`, `description`)
- Page: Property `author`
- Video: Specify versions
**Generation**
- ImageSet: Specify image aspect ratio (width, height) to prevent page jumps
- Consistency: Check output folder for unused files
- Empty properties: Show warnings for empty link previews, etc.
**Fixes**
- Files: Id change: Check all page contents for links to the renamed file and replace occurences
- Database: Show errors during loading
- Mock content: Clean and improve
*/
@main
struct MainView: App {

View File

@ -34,8 +34,10 @@ extension Content {
func check(content: String, of page: Page, for language: ContentLanguage, onComplete: @escaping (PageGenerationResults) -> Void) {
performGenerationIfIdle {
let results = self.results.makeResults(for: page, in: language)
results.reset()
let generator = PageContentParser(content: page.content, language: language, results: results)
_ = generator.generatePage(from: content)
self.results.recalculate()
DispatchQueue.main.async {
onComplete(results)
}
@ -227,7 +229,7 @@ extension Content {
for language in ContentLanguage.allCases {
let results = results.makeResults(for: tag, in: language)
let posts = posts.filter { $0.tags.contains(tag) }
let posts = posts.filter { $0.contains(tag) }
guard posts.count > 0 else { continue }
let source = TagPageGeneratorSource(

View File

@ -180,6 +180,7 @@ extension Content {
externalLink: page.externalLink,
isDraft: page.isDraft,
createdDate: page.createdDate,
hideDate: page.hideDate ?? false,
startDate: page.startDate,
endDate: page.endDate,
german: convert(page.german, images: files),

View File

@ -59,6 +59,7 @@ private extension Page {
.init(isDraft: isDraft,
externalLink: externalLink,
tags: tags.map { $0.id },
hideDate: hideDate ? true : nil,
createdDate: createdDate,
startDate: startDate,
endDate: hasEndDate ? endDate : nil,

View File

@ -49,6 +49,11 @@ final class LocalizedTag: ObservableObject {
content.isValidIdForTagOrPageOrPost(urlComponent) &&
!content.containsTag(withUrlComponent: urlComponent)
}
/// The title to display when considering multiple items of this tag
var title: String {
linkPreviewTitle ?? name
}
}
extension LocalizedTag: LinkPreviewItem {

View File

@ -17,6 +17,9 @@ final class Page: Item {
@Published
var createdDate: Date
@Published
var hideDate: Bool
@Published
var startDate: Date
@ -46,6 +49,7 @@ final class Page: Item {
externalLink: String?,
isDraft: Bool,
createdDate: Date,
hideDate: Bool,
startDate: Date,
endDate: Date?,
german: LocalizedPage,
@ -55,6 +59,7 @@ final class Page: Item {
self.externalLink = externalLink
self.isDraft = isDraft
self.createdDate = createdDate
self.hideDate = hideDate
self.startDate = startDate
self.hasEndDate = endDate != nil
self.endDate = endDate ?? startDate
@ -86,6 +91,34 @@ final class Page: Item {
externalLink != nil
}
// MARK: Tags
/**
Check if a tag is associated with this page
*/
func contains(_ tag: Tag) -> Bool {
tags.contains(tag)
}
func toggle(_ tag: Tag) {
guard let index = tags.firstIndex(of: tag) else {
tags.append(tag)
return
}
tags.remove(at: index)
}
func remove(_ tag: Tag) {
tags.remove(tag)
}
func associate(_ tag: Tag) {
guard !tags.contains(tag) else {
return
}
tags.append(tag)
}
// MARK: Paths
override func absoluteUrl(in language: ContentLanguage) -> String {

View File

@ -17,6 +17,12 @@ final class Post: Item {
@Published
var endDate: Date
/**
The tags associated with the post
This list is only used if ``linkedPage`` is `nil`,
otherwise the tag list of the linked page is used.
*/
@Published
var tags: [Tag]
@ -52,6 +58,55 @@ final class Post: Item {
super.init(content: content, id: id)
}
// MARK: Tags
func usedTags() -> [Tag] {
linkedPage?.tags ?? tags
}
/**
Check if a tag is associated with this post.
If the post is linked to a page, then the tags of the page are checked.
*/
func contains(_ tag: Tag) -> Bool {
guard let linkedPage else {
return tags.contains(tag)
}
return linkedPage.tags.contains(tag)
}
func toggle(_ tag: Tag) {
if let linkedPage {
linkedPage.toggle(tag)
return
}
guard let index = tags.firstIndex(of: tag) else {
tags.append(tag)
return
}
tags.remove(at: index)
}
func remove(_ tag: Tag) {
if let linkedPage {
linkedPage.remove(tag)
return
}
tags.remove(tag)
}
func associate(_ tag: Tag) {
if let linkedPage {
linkedPage.associate(tag)
return
}
guard !tags.contains(tag) else {
return
}
tags.append(tag)
}
func localized(in language: ContentLanguage) -> LocalizedPost {
switch language {
case .english: return english

View File

@ -56,7 +56,7 @@ final class Tag: Item {
}
override func title(in language: ContentLanguage) -> String {
localized(in: language).name
localized(in: language).title
}
override var itemType: ItemType {

View File

@ -9,6 +9,7 @@ extension Page {
externalLink: nil,
isDraft: true,
createdDate: Date(),
hideDate: false,
startDate: Date().addingTimeInterval(-86400),
endDate: nil,
german: .german,

View File

@ -8,6 +8,8 @@ struct PageFile {
let tags: [String]
let hideDate: Bool?
let createdDate: Date
let startDate: Date

View File

@ -0,0 +1,42 @@
import SwiftUI
struct TagDisplayView: View {
@Environment(\.language)
private var language
@EnvironmentObject
private var content: Content
@Binding
var tags: [Tag]
@State
private var showTagPicker = false
var body: some View {
FlowHStack {
ForEach(tags, id: \.id) { tag in
TagView(text: tag.localized(in: language).name)
.foregroundStyle(.white)
}
Button(action: { showTagPicker = true }) {
Image(systemSymbol: .squareAndPencilCircleFill)
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(height: 22)
.foregroundColor(Color.gray)
.background(Circle()
.fill(Color.white)
.padding(1))
}
.buttonStyle(.plain)
}
.sheet(isPresented: $showTagPicker) {
TagSelectionView(
presented: $showTagPicker,
selected: $tags,
tags: $content.tags)
}
}
}

View File

@ -72,6 +72,7 @@ struct AddPageView: View {
externalLink: nil,
isDraft: true,
createdDate: .now,
hideDate: false,
startDate: .now,
endDate: nil,
german: .init(content: content,

View File

@ -24,9 +24,6 @@ struct PageContentView: View {
@EnvironmentObject
private var content: Content
@State
private var showTagPicker = false
init(page: Page) {
self.page = page
}
@ -44,36 +41,13 @@ struct PageContentView: View {
}.padding()
} else {
VStack(alignment: .leading) {
FlowHStack {
ForEach(page.tags, id: \.id) { tag in
TagView(text: tag.localized(in: language).name)
.foregroundStyle(.white)
}
Button(action: { showTagPicker = true }) {
Image(systemSymbol: .squareAndPencilCircleFill)
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(height: 22)
.foregroundColor(Color.gray)
.background(Circle()
.fill(Color.white)
.padding(1))
}
.buttonStyle(.plain)
}
TagDisplayView(tags: $page.tags)
LocalizedPageContentView(pageId: page.id, page: page.localized(in: language), language: language)
.id(page.id + language.rawValue)
}.padding()
.sheet(isPresented: $showTagPicker) {
TagSelectionView(
presented: $showTagPicker,
selected: $page.tags,
tags: $content.tags)
}
.padding()
}
}
}
extension PageContentView: MainContentView {

View File

@ -67,6 +67,18 @@ struct PageDetailView: View {
value: $page.isDraft,
footer: "Indicate a page as a draft to hide it from the website")
.disabled(page.isExternalUrl)
.onChange(of: page.externalLink) { _, newValue in
// Ensure that external pages are not drafts
if newValue != nil && page.isDraft {
page.isDraft = false
}
}
BoolPropertyView(
title: "Hide date",
value: $page.hideDate,
footer: "Do not show the date string on the page")
.disabled(page.isExternalUrl)
DatePropertyView(
title: "Start date",

View File

@ -68,14 +68,21 @@ private struct LocalizedContentEditor: View {
}
}
private struct LinkedPageTagView: View {
@ObservedObject
var page: Page
var body: some View {
TagDisplayView(tags: $page.tags)
}
}
struct LocalizedPostContentView: View {
@ObservedObject
var post: Post
@State
private var showTagPicker = false
@Environment(\.language)
private var language
@ -95,32 +102,14 @@ struct LocalizedPostContentView: View {
}
PostImagesView(post: post.localized(in: language))
LocalizedTitle(post: post.localized(in: language))
FlowHStack {
ForEach(post.tags, id: \.id) { tag in
TagView(text: tag.localized(in: language).name)
.foregroundStyle(.white)
}
Button(action: { showTagPicker = true }) {
Image(systemSymbol: .squareAndPencilCircleFill)
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(height: 22)
.foregroundColor(Color.gray)
.background(Circle()
.fill(Color.white)
.padding(1))
}
.buttonStyle(.plain)
if let page = post.linkedPage {
LinkedPageTagView(page: page)
} else {
TagDisplayView(tags: $post.tags)
}
LocalizedContentEditor(post: post.localized(in: language))
}
.padding()
.sheet(isPresented: $showTagPicker) {
TagSelectionView(
presented: $showTagPicker,
selected: $post.tags,
tags: $content.tags)
}
}
private func copyImagesFromOtherLanguage() {

View File

@ -76,6 +76,16 @@ struct PostDetailView: View {
title: "Linked page",
selectedPage: $post.linkedPage,
footer: "The page to open when clicking on the post")
.onChange(of: post.linkedPage) { oldValue, newValue in
if newValue != nil {
post.tags = []
} else {
// Link removed, so copy tags from previous link
if let oldValue {
post.tags = oldValue.tags
}
}
}
LocalizedPostDetailView(post: post.localized(in: language))
}

View File

@ -223,6 +223,7 @@ struct PageIssueView: View {
externalLink: nil,
isDraft: true,
createdDate: .now,
hideDate: false,
startDate: .now,
endDate: nil,
german: .init(content: content,

View File

@ -14,24 +14,16 @@ private struct PageSelectionView: View {
var body: some View {
HStack {
let isSelected = page.tags.contains(tag)
let isSelected = page.contains(tag)
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
.foregroundStyle(Color.blue)
Text(page.localized(in: language).title)
}
.contentShape(Rectangle())
.onTapGesture {
toggleTagAssignment()
page.toggle(tag)
}
}
private func toggleTagAssignment() {
guard let index = page.tags.firstIndex(of: tag) else {
page.tags.append(tag)
return
}
page.tags.remove(at: index)
}
}
struct PageTagAssignmentView: View {

View File

@ -14,24 +14,16 @@ private struct PostSelectionView: View {
var body: some View {
HStack {
let isSelected = post.tags.contains(tag)
let isSelected = post.contains(tag)
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
.foregroundStyle(Color.blue)
Text(post.localized(in: language).title)
}
.contentShape(Rectangle())
.onTapGesture {
toggleTagAssignment()
post.toggle(tag)
}
}
private func toggleTagAssignment() {
guard let index = post.tags.firstIndex(of: tag) else {
post.tags.append(tag)
return
}
post.tags.remove(at: index)
}
}
struct PostTagAssignmentView: View {

View File

@ -22,7 +22,7 @@ struct TagContentView: View {
}
var selectedPosts: [Post] {
content.posts.filter { $0.tags.contains(tag) }
content.posts.filter { $0.contains(tag) }
}
var body: some View {