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

View File

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

View File

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

View File

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

View File

@ -99,6 +99,28 @@ final class PageGenerationResults: ObservableObject {
pageIsEmpty = false 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 // MARK: Adding entries
func inaccessibleContent(file: FileResource) { func inaccessibleContent(file: FileResource) {

View File

@ -38,8 +38,7 @@ final class PostListPageGenerator {
} }
} }
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>) { private func makePostData(post: Post) -> FeedEntryData {
let posts: [FeedEntryData] = posts.map { post in
let localized: LocalizedPost = post.localized(in: language) let localized: LocalizedPost = post.localized(in: language)
#warning("Add post link text to settings or to each post") #warning("Add post link text to settings or to each post")
@ -49,7 +48,8 @@ final class PostListPageGenerator {
text: language == .english ? "View" : "Anzeigen") text: language == .english ? "View" : "Anzeigen")
} }
let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in // 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, .init(name: tag.localized(in: language).name,
url: tag.absoluteUrl(in: language)) url: tag.absoluteUrl(in: language))
} }
@ -65,10 +65,13 @@ final class PostListPageGenerator {
textAboveTitle: post.dateText(in: language), textAboveTitle: post.dateText(in: language),
link: linkUrl, link: linkUrl,
tags: tags, tags: tags,
text: localized.text.components(separatedBy: "\n"), text: localized.text.components(separatedBy: "\n\n"),
images: images) 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 feedPageGenerator = FeedPageGenerator(content: source.content, results: source.results)
let languageButtonUrl = "/" + pageUrl(in: language.next, pageNumber: pageIndex) let languageButtonUrl = "/" + pageUrl(in: language.next, pageNumber: pageIndex)

View File

@ -1,29 +1,40 @@
import SwiftUI import SwiftUI
import SFSafeSymbols import SFSafeSymbols
#warning("Fix podcast") /**
#warning("Fix endeavor basics (image compare)") **Content**
#warning("Fix cap mosaic GIF") - 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)") **UI**
#warning("Show all warnings on page content") - Image search: Add view to see all images and filter
#warning("Button to delete file, show in finder, replace, mark changed (-> images)") - Pages: Show linking posts
#warning("Transfer images of posts to other language") - Page Content: Show all results of `PageGenerationResults`
#warning("Show tag selection view for pages") - Files: Show usages of file
#warning("Button to replace files") - Images: Show list of generated versions
#warning("Replace links to files inside pages when id changes")
#warning("Calculate file sizes") **Features**
#warning("Specify image aspect ratio to prevent page jumps") - GIF Support (No image set, don't rescale)
#warning("Add version and source url properties to file resources") - Image compare command `![compare](image1;image2)`
#warning("Show errors during loading of content") - Files: Optional Property `customFilePath` for external files to place them in another location
#warning("Generate pages for posts") - Files: Property `version` and `sourceUrl` to track asset files
#warning("Clean up mock content") - Posts: Generate separate pages for posts to link to
#warning("Show posts linking to a page") - Settings: Introduce `Authors` (`name`, `image`, `description`)
#warning("Add author to settings and page headers") - Page: Property `author`
#warning("Check for files in output folder not generated by app") - Video: Specify versions
#warning("Fix GIFs: Don't rescale, don't use image set")
#warning("Add view to browse images") **Generation**
#warning("Show warnings for empty item properties") - 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 @main
struct MainView: App { 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) { func check(content: String, of page: Page, for language: ContentLanguage, onComplete: @escaping (PageGenerationResults) -> Void) {
performGenerationIfIdle { performGenerationIfIdle {
let results = self.results.makeResults(for: page, in: language) let results = self.results.makeResults(for: page, in: language)
results.reset()
let generator = PageContentParser(content: page.content, language: language, results: results) let generator = PageContentParser(content: page.content, language: language, results: results)
_ = generator.generatePage(from: content) _ = generator.generatePage(from: content)
self.results.recalculate()
DispatchQueue.main.async { DispatchQueue.main.async {
onComplete(results) onComplete(results)
} }
@ -227,7 +229,7 @@ extension Content {
for language in ContentLanguage.allCases { for language in ContentLanguage.allCases {
let results = results.makeResults(for: tag, in: language) 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 } guard posts.count > 0 else { continue }
let source = TagPageGeneratorSource( let source = TagPageGeneratorSource(

View File

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

View File

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

View File

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

View File

@ -17,6 +17,9 @@ final class Page: Item {
@Published @Published
var createdDate: Date var createdDate: Date
@Published
var hideDate: Bool
@Published @Published
var startDate: Date var startDate: Date
@ -46,6 +49,7 @@ final class Page: Item {
externalLink: String?, externalLink: String?,
isDraft: Bool, isDraft: Bool,
createdDate: Date, createdDate: Date,
hideDate: Bool,
startDate: Date, startDate: Date,
endDate: Date?, endDate: Date?,
german: LocalizedPage, german: LocalizedPage,
@ -55,6 +59,7 @@ final class Page: Item {
self.externalLink = externalLink self.externalLink = externalLink
self.isDraft = isDraft self.isDraft = isDraft
self.createdDate = createdDate self.createdDate = createdDate
self.hideDate = hideDate
self.startDate = startDate self.startDate = startDate
self.hasEndDate = endDate != nil self.hasEndDate = endDate != nil
self.endDate = endDate ?? startDate self.endDate = endDate ?? startDate
@ -86,6 +91,34 @@ final class Page: Item {
externalLink != nil 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 // MARK: Paths
override func absoluteUrl(in language: ContentLanguage) -> String { override func absoluteUrl(in language: ContentLanguage) -> String {

View File

@ -17,6 +17,12 @@ final class Post: Item {
@Published @Published
var endDate: Date 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 @Published
var tags: [Tag] var tags: [Tag]
@ -52,6 +58,55 @@ final class Post: Item {
super.init(content: content, id: id) 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 { func localized(in language: ContentLanguage) -> LocalizedPost {
switch language { switch language {
case .english: return english case .english: return english

View File

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

View File

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

View File

@ -8,6 +8,8 @@ struct PageFile {
let tags: [String] let tags: [String]
let hideDate: Bool?
let createdDate: Date let createdDate: Date
let startDate: 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, externalLink: nil,
isDraft: true, isDraft: true,
createdDate: .now, createdDate: .now,
hideDate: false,
startDate: .now, startDate: .now,
endDate: nil, endDate: nil,
german: .init(content: content, german: .init(content: content,

View File

@ -24,9 +24,6 @@ struct PageContentView: View {
@EnvironmentObject @EnvironmentObject
private var content: Content private var content: Content
@State
private var showTagPicker = false
init(page: Page) { init(page: Page) {
self.page = page self.page = page
} }
@ -44,38 +41,15 @@ struct PageContentView: View {
}.padding() }.padding()
} else { } else {
VStack(alignment: .leading) { VStack(alignment: .leading) {
FlowHStack { TagDisplayView(tags: $page.tags)
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)
}
LocalizedPageContentView(pageId: page.id, page: page.localized(in: language), language: language) LocalizedPageContentView(pageId: page.id, page: page.localized(in: language), language: language)
.id(page.id + language.rawValue) .id(page.id + language.rawValue)
}.padding() }
.sheet(isPresented: $showTagPicker) { .padding()
TagSelectionView(
presented: $showTagPicker,
selected: $page.tags,
tags: $content.tags)
} }
} }
} }
}
extension PageContentView: MainContentView { extension PageContentView: MainContentView {
init(item: Page) { init(item: Page) {

View File

@ -67,6 +67,18 @@ struct PageDetailView: View {
value: $page.isDraft, value: $page.isDraft,
footer: "Indicate a page as a draft to hide it from the website") footer: "Indicate a page as a draft to hide it from the website")
.disabled(page.isExternalUrl) .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( DatePropertyView(
title: "Start date", 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 { struct LocalizedPostContentView: View {
@ObservedObject @ObservedObject
var post: Post var post: Post
@State
private var showTagPicker = false
@Environment(\.language) @Environment(\.language)
private var language private var language
@ -95,32 +102,14 @@ struct LocalizedPostContentView: View {
} }
PostImagesView(post: post.localized(in: language)) PostImagesView(post: post.localized(in: language))
LocalizedTitle(post: post.localized(in: language)) LocalizedTitle(post: post.localized(in: language))
FlowHStack { if let page = post.linkedPage {
ForEach(post.tags, id: \.id) { tag in LinkedPageTagView(page: page)
TagView(text: tag.localized(in: language).name) } else {
.foregroundStyle(.white) TagDisplayView(tags: $post.tags)
}
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)
} }
LocalizedContentEditor(post: post.localized(in: language)) LocalizedContentEditor(post: post.localized(in: language))
} }
.padding() .padding()
.sheet(isPresented: $showTagPicker) {
TagSelectionView(
presented: $showTagPicker,
selected: $post.tags,
tags: $content.tags)
}
} }
private func copyImagesFromOtherLanguage() { private func copyImagesFromOtherLanguage() {

View File

@ -76,6 +76,16 @@ struct PostDetailView: View {
title: "Linked page", title: "Linked page",
selectedPage: $post.linkedPage, selectedPage: $post.linkedPage,
footer: "The page to open when clicking on the post") 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)) LocalizedPostDetailView(post: post.localized(in: language))
} }

View File

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

View File

@ -14,24 +14,16 @@ private struct PageSelectionView: View {
var body: some View { var body: some View {
HStack { HStack {
let isSelected = page.tags.contains(tag) let isSelected = page.contains(tag)
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle) Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
.foregroundStyle(Color.blue) .foregroundStyle(Color.blue)
Text(page.localized(in: language).title) Text(page.localized(in: language).title)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .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 { struct PageTagAssignmentView: View {

View File

@ -14,24 +14,16 @@ private struct PostSelectionView: View {
var body: some View { var body: some View {
HStack { HStack {
let isSelected = post.tags.contains(tag) let isSelected = post.contains(tag)
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle) Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
.foregroundStyle(Color.blue) .foregroundStyle(Color.blue)
Text(post.localized(in: language).title) Text(post.localized(in: language).title)
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.onTapGesture { .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 { struct PostTagAssignmentView: View {

View File

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