From 42fa08b43d0fb433cb8394ca6ef785272bbf9316 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Sun, 26 Jan 2025 20:32:44 +0100 Subject: [PATCH] Add labels to posts --- CHDataManagement.xcodeproj/project.pbxproj | 33 +++++ .../xcshareddata/swiftpm/Package.resolved | 11 +- .../Page Generators/FeedPageGenerator.swift | 6 +- .../Post Lists/PostListPageGenerator.swift | 1 + CHDataManagement/Model/ContentLabel.swift | 49 ++++++++ CHDataManagement/Model/LocalizedPost.swift | 9 ++ .../ContentElements/ContentLabels.swift | 16 +-- .../ContentElements/HtmlProducer.swift | 15 +++ .../ContentElements/Icons/PageIcon.swift | 8 ++ .../ContentElements/TagList.swift | 14 --- .../Page Elements/FeedEntry.swift | 3 +- .../Page Elements/FeedEntryData.swift | 5 +- .../Views/Posts/PageIconView.swift | 19 +++ .../Views/Posts/PostContentView.swift | 1 + .../Views/Posts/PostLabelsView.swift | 113 ++++++++++++++++++ 15 files changed, 273 insertions(+), 30 deletions(-) create mode 100644 CHDataManagement/Model/ContentLabel.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/HtmlProducer.swift create mode 100644 CHDataManagement/Views/Posts/PageIconView.swift create mode 100644 CHDataManagement/Views/Posts/PostLabelsView.swift diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 8ffcf60..e661b76 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -191,6 +191,11 @@ E2FD1D372D3BBCCA00B48627 /* Insert+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D362D3BBCB500B48627 /* Insert+Image.swift */; }; E2FD1D392D3BBED300B48627 /* InsertableItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D382D3BBECA00B48627 /* InsertableItemsView.swift */; }; E2FD1D3B2D3BC40500B48627 /* InsertableCommandSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D3A2D3BC40500B48627 /* InsertableCommandSheet.swift */; }; + E2FD1D3D2D463CD800B48627 /* ContentLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D3C2D463CD800B48627 /* ContentLabel.swift */; }; + E2FD1D3F2D46405000B48627 /* PostLabelsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D3E2D46404900B48627 /* PostLabelsView.swift */; }; + E2FD1D462D46428100B48627 /* PageIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D452D46427B00B48627 /* PageIconView.swift */; }; + E2FD1D522D4644B400B48627 /* SVGView in Frameworks */ = {isa = PBXBuildFile; productRef = E2FD1D512D4644B400B48627 /* SVGView */; }; + E2FD1D542D46577700B48627 /* HtmlProducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D532D46577700B48627 /* HtmlProducer.swift */; }; E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */; }; E2FE0EE82D16D4A3002963B7 /* ConvertThrowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */; }; E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */; }; @@ -428,6 +433,10 @@ E2FD1D362D3BBCB500B48627 /* Insert+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Image.swift"; sourceTree = ""; }; E2FD1D382D3BBECA00B48627 /* InsertableItemsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertableItemsView.swift; sourceTree = ""; }; E2FD1D3A2D3BC40500B48627 /* InsertableCommandSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertableCommandSheet.swift; sourceTree = ""; }; + E2FD1D3C2D463CD800B48627 /* ContentLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLabel.swift; sourceTree = ""; }; + E2FD1D3E2D46404900B48627 /* PostLabelsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostLabelsView.swift; sourceTree = ""; }; + E2FD1D452D46427B00B48627 /* PageIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIconView.swift; sourceTree = ""; }; + E2FD1D532D46577700B48627 /* HtmlProducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlProducer.swift; sourceTree = ""; }; E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = ""; }; E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = ""; }; E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = ""; }; @@ -492,6 +501,7 @@ files = ( E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */, E25DA52C2CFFC3EC00AEF16D /* SDWebImageAVIFCoder in Frameworks */, + E2FD1D522D4644B400B48627 /* SVGView in Frameworks */, E25DA57D2D01C67900AEF16D /* Ink in Frameworks */, E25DA52F2CFFC91B00AEF16D /* SDWebImageWebPCoder in Frameworks */, E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */, @@ -569,6 +579,7 @@ E2FD1D312D3AEB6000B48627 /* PostVideo.swift */, E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */, E29D31232D0366820051B7F4 /* TagList.swift */, + E2FD1D532D46577700B48627 /* HtmlProducer.swift */, E2FE0F612D2C0D8D002963B7 /* VersionedVideo.swift */, ); path = ContentElements; @@ -757,6 +768,7 @@ E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */, E25DA5162CFF00F200AEF16D /* Content+Save.swift */, E24252092C52C9260029FF16 /* ContentLanguage.swift */, + E2FD1D3C2D463CD800B48627 /* ContentLabel.swift */, E25DA59A2D024A2900AEF16D /* DateItem.swift */, E21850162CEE55FB0090B18B /* FileType.swift */, E2A21C502CBBD53C0060935B /* FileResource.swift */, @@ -806,6 +818,8 @@ E2B85F4B2C4B8B7F0047CD0C /* Posts */ = { isa = PBXGroup; children = ( + E2FD1D452D46427B00B48627 /* PageIconView.swift */, + E2FD1D3E2D46404900B48627 /* PostLabelsView.swift */, E29D31502D0616890051B7F4 /* PostListView.swift */, E218502A2CF790AC0090B18B /* PostContentView.swift */, E21850262CF3B42D0090B18B /* PostDetailView.swift */, @@ -1031,6 +1045,7 @@ E25DA57C2D01C67900AEF16D /* Ink */, E25DA57F2D01C6AC00AEF16D /* Splash */, E29D31A72D0CDC5D0051B7F4 /* SwiftSoup */, + E2FD1D512D4644B400B48627 /* SVGView */, ); productName = CHDataManagement; productReference = E2DD04702C276F31003BFF1F /* CHDataManagement.app */; @@ -1068,6 +1083,7 @@ E25DA57B2D01C67900AEF16D /* XCRemoteSwiftPackageReference "ink" */, E25DA57E2D01C6AC00AEF16D /* XCRemoteSwiftPackageReference "Splash" */, E29D31A62D0CDC5D0051B7F4 /* XCRemoteSwiftPackageReference "SwiftSoup" */, + E2FD1D502D4644B400B48627 /* XCRemoteSwiftPackageReference "SVGView" */, ); productRefGroup = E2DD04712C276F31003BFF1F /* Products */; projectDirPath = ""; @@ -1148,6 +1164,7 @@ E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */, E2FE0F702D2D5235002963B7 /* TextIndicator.swift in Sources */, E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */, + E2FD1D3D2D463CD800B48627 /* ContentLabel.swift in Sources */, E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */, E29D31942D0B7D280051B7F4 /* SimpleImage.swift in Sources */, E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */, @@ -1188,9 +1205,11 @@ E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */, E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */, E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */, + E2FD1D462D46428100B48627 /* PageIconView.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, E2FE0F172D2698D5002963B7 /* LocalizedPageId.swift in Sources */, E2FD1D2E2D37180900B48627 /* GeneralSettings.swift in Sources */, + E2FD1D542D46577700B48627 /* HtmlProducer.swift in Sources */, E2FE0F0D2D268A09002963B7 /* PostListPageGeneratorSource.swift in Sources */, E2FE0F402D2B45D3002963B7 /* SwiftBlock.swift in Sources */, E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */, @@ -1233,6 +1252,7 @@ E2FD1D212D2EB22900B48627 /* ModelLoader.swift in Sources */, E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */, E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, + E2FD1D3F2D46405000B48627 /* PostLabelsView.swift in Sources */, E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */, @@ -1605,6 +1625,14 @@ minimumVersion = 5.3.0; }; }; + E2FD1D502D4644B400B48627 /* XCRemoteSwiftPackageReference "SVGView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/exyte/SVGView.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.6; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1643,6 +1671,11 @@ package = E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; productName = SFSafeSymbols; }; + E2FD1D512D4644B400B48627 /* SVGView */ = { + isa = XCSwiftPackageProductDependency; + package = E2FD1D502D4644B400B48627 /* XCRemoteSwiftPackageReference "SVGView" */; + productName = SVGView; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = E2DD04682C276F31003BFF1F /* Project object */; diff --git a/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 41c5283..b7a392d 100644 --- a/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CHDataManagement.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "610a80083aa646fbd77d72ddb7dcc16342884551283091b7b1ebf40042816810", + "originHash" : "83059c87de78e5571dba4ab957133083b5c56dff4c72bf5c872969be5ca53685", "pins" : [ { "identity" : "highlightedtexteditor", @@ -100,6 +100,15 @@ "version" : "0.16.0" } }, + { + "identity" : "svgview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/exyte/SVGView.git", + "state" : { + "revision" : "6465962facdd25cb96eaebc35603afa2f15d2c0d", + "version" : "1.0.6" + } + }, { "identity" : "swiftsoup", "kind" : "remoteSourceControl", diff --git a/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift b/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift index 88b3d5e..2d6b69e 100644 --- a/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift +++ b/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift @@ -48,6 +48,10 @@ final class FeedPageGenerator { let imageUrl = image?.linkPreviewImage(results: results) + let requiredIcons: Set = posts.reduce(into: []) { icons, post in + icons.formUnion(post.labels.map { $0.icon }) + } + let pageHeader = PageHeader( language: language, title: title ?? pageTitle, @@ -58,7 +62,7 @@ final class FeedPageGenerator { languageButton: languageButton, links: content.navigationBar(in: language), headers: headers, - icons: []) + icons: requiredIcons) let page = GenericPage( header: pageHeader, diff --git a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift index b1ed7aa..045c951 100644 --- a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift @@ -86,6 +86,7 @@ final class PostListPageGenerator { textAboveTitle: post.dateText(in: language), link: linkUrl, tags: tags, + labels: localized.labels, text: localized.text.components(separatedBy: "\n\n"), media: media) #warning("Treat post text as markdown") diff --git a/CHDataManagement/Model/ContentLabel.swift b/CHDataManagement/Model/ContentLabel.swift new file mode 100644 index 0000000..5a4654d --- /dev/null +++ b/CHDataManagement/Model/ContentLabel.swift @@ -0,0 +1,49 @@ +import Foundation + +final class ContentLabel: ObservableObject { + + @Published + var icon: PageIcon + + @Published + var value: String + + init(icon: PageIcon, value: String) { + self.icon = icon + self.value = value + } +} + +extension ContentLabel: Equatable { + + static func == (lhs: ContentLabel, rhs: ContentLabel) -> Bool { + lhs.icon == rhs.icon && lhs.value == rhs.value + } +} + +extension ContentLabel: Identifiable { + + var id: String { + icon.rawValue + value + } +} + +extension ContentLabel { + + var data: Data { + .init(icon: icon.rawValue, value: value) + } + + convenience init?(context: LoadingContext, data: Data) { + guard let icon = PageIcon(rawValue: data.icon) else { + context.error("Unknown label icon '\(data.icon)'") + return nil + } + self.init(icon: icon, value: data.value) + } + + struct Data: Codable { + let icon: String + let value: String + } +} diff --git a/CHDataManagement/Model/LocalizedPost.swift b/CHDataManagement/Model/LocalizedPost.swift index 9ae1480..09670f8 100644 --- a/CHDataManagement/Model/LocalizedPost.swift +++ b/CHDataManagement/Model/LocalizedPost.swift @@ -17,6 +17,10 @@ final class LocalizedPost: ObservableObject { @Published var images: [FileResource] + /// The labels to show beneath the title + @Published + var labels: [ContentLabel] + /// The text to show for the link to the `linkedPage` @Published var pageLinkText: String? @@ -29,6 +33,7 @@ final class LocalizedPost: ObservableObject { text: String, lastModified: Date? = nil, images: [FileResource] = [], + labels: [ContentLabel] = [], pageLinkText: String? = nil, linkPreview: LinkPreview = .init()) { self.content = content @@ -36,6 +41,7 @@ final class LocalizedPost: ObservableObject { self.text = text self.lastModified = lastModified self.images = images + self.labels = labels self.pageLinkText = pageLinkText self.linkPreview = linkPreview } @@ -86,12 +92,14 @@ extension LocalizedPost { text: data.text, lastModified: data.lastModifiedDate, images: data.images.compactMap(context.postMedia), + labels: data.labels?.compactMap { ContentLabel(context: context, data: $0) } ?? [], pageLinkText: data.pageLinkText, linkPreview: .init(context: context, data: data.linkPreview)) } var data: Data { .init(images: images.map { $0.id }, + labels: labels.map { $0.data }.nonEmpty, title: title, text: text, lastModifiedDate: lastModified, @@ -102,6 +110,7 @@ extension LocalizedPost { /// The structure to store the metadata of a localized post struct Data: Codable { let images: [String] + let labels: [ContentLabel.Data]? let title: String? let text: String let lastModifiedDate: Date? diff --git a/CHDataManagement/Page Elements/ContentElements/ContentLabels.swift b/CHDataManagement/Page Elements/ContentElements/ContentLabels.swift index 539b430..4a004f4 100644 --- a/CHDataManagement/Page Elements/ContentElements/ContentLabels.swift +++ b/CHDataManagement/Page Elements/ContentElements/ContentLabels.swift @@ -1,12 +1,5 @@ -struct ContentLabel { - - let icon: PageIcon - - let value: String -} - -struct ContentLabels { +struct ContentLabels: HtmlProducer { private let labels: [ContentLabel] @@ -14,15 +7,14 @@ struct ContentLabels { self.labels = labels } - var content: String { + func populate(_ result: inout String) { guard !labels.isEmpty else { - return "" + return } - var result = "
" + result += "
" for label in labels { result += "
\(label.value)
" } result += "
" - return result } } diff --git a/CHDataManagement/Page Elements/ContentElements/HtmlProducer.swift b/CHDataManagement/Page Elements/ContentElements/HtmlProducer.swift new file mode 100644 index 0000000..1858b68 --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/HtmlProducer.swift @@ -0,0 +1,15 @@ + +protocol HtmlProducer { + + func populate(_ result: inout String) +} + +extension HtmlProducer { + + var content: String { + var result = "" + populate(&result) + return result + } +} + diff --git a/CHDataManagement/Page Elements/ContentElements/Icons/PageIcon.swift b/CHDataManagement/Page Elements/ContentElements/Icons/PageIcon.swift index da9f7e9..fd62a72 100644 --- a/CHDataManagement/Page Elements/ContentElements/Icons/PageIcon.swift +++ b/CHDataManagement/Page Elements/ContentElements/Icons/PageIcon.swift @@ -84,6 +84,14 @@ enum PageIcon: String, CaseIterable { case .leftRightArrow:return Icon.LeftRightArrow.self } } + + var svgString: String { + icon.content + } + + var name: String { + icon.name + } } extension PageIcon: Hashable { diff --git a/CHDataManagement/Page Elements/ContentElements/TagList.swift b/CHDataManagement/Page Elements/ContentElements/TagList.swift index 048b784..615a27d 100644 --- a/CHDataManagement/Page Elements/ContentElements/TagList.swift +++ b/CHDataManagement/Page Elements/ContentElements/TagList.swift @@ -1,17 +1,3 @@ -protocol HtmlProducer { - - func populate(_ result: inout String) -} - -extension HtmlProducer { - - var content: String { - var result = "" - populate(&result) - return result - } -} - struct TagList: HtmlProducer { let tags: [FeedEntryData.Tag] diff --git a/CHDataManagement/Page Elements/FeedEntry.swift b/CHDataManagement/Page Elements/FeedEntry.swift index a910d6e..c6d1ea1 100644 --- a/CHDataManagement/Page Elements/FeedEntry.swift +++ b/CHDataManagement/Page Elements/FeedEntry.swift @@ -32,7 +32,8 @@ struct FeedEntry { if let title = data.title { result += "

\(title.htmlEscaped())

" } - result += TagList(tags: data.tags).content + TagList(tags: data.tags).populate(&result) + ContentLabels(labels: data.labels).populate(&result) for paragraph in data.text { result += "

\(paragraph)

" diff --git a/CHDataManagement/Page Elements/FeedEntryData.swift b/CHDataManagement/Page Elements/FeedEntryData.swift index a083c27..a814a6b 100644 --- a/CHDataManagement/Page Elements/FeedEntryData.swift +++ b/CHDataManagement/Page Elements/FeedEntryData.swift @@ -11,16 +11,19 @@ struct FeedEntryData { let tags: [Tag] + let labels: [ContentLabel] + let text: [String] let media: Media? - init(entryId: String, title: String?, textAboveTitle: String, link: Link?, tags: [Tag], text: [String], media: Media?) { + init(entryId: String, title: String?, textAboveTitle: String, link: Link?, tags: [Tag], labels: [ContentLabel], text: [String], media: Media?) { self.entryId = entryId self.title = title self.textAboveTitle = textAboveTitle self.link = link self.tags = tags + self.labels = labels self.text = text self.media = media } diff --git a/CHDataManagement/Views/Posts/PageIconView.swift b/CHDataManagement/Views/Posts/PageIconView.swift new file mode 100644 index 0000000..d62bc17 --- /dev/null +++ b/CHDataManagement/Views/Posts/PageIconView.swift @@ -0,0 +1,19 @@ +import SwiftUI +import SVGView + +struct PageIconView: View { + + @Environment(\.colorScheme) + private var colorScheme + + let icon: PageIcon + + var body: some View { + if colorScheme == .light { + SVGView(string: icon.svgString) + } else { + SVGView(string: icon.svgString) + .colorInvert() + } + } +} diff --git a/CHDataManagement/Views/Posts/PostContentView.swift b/CHDataManagement/Views/Posts/PostContentView.swift index 0355b02..64369e9 100644 --- a/CHDataManagement/Views/Posts/PostContentView.swift +++ b/CHDataManagement/Views/Posts/PostContentView.swift @@ -177,6 +177,7 @@ struct LocalizedPostContentView: View { } else { TagDisplayView(tags: $tags) } + PostLabelsView(post: post, other: other) TextEditor(text: $post.text) .font(.body) .frame(minHeight: 150) diff --git a/CHDataManagement/Views/Posts/PostLabelsView.swift b/CHDataManagement/Views/Posts/PostLabelsView.swift new file mode 100644 index 0000000..1ac21a1 --- /dev/null +++ b/CHDataManagement/Views/Posts/PostLabelsView.swift @@ -0,0 +1,113 @@ +import SwiftUI + +struct LabelEditingView: View { + + @ObservedObject + var label: ContentLabel + + @State + private var showIconPicker: Bool = false + + var body: some View { + HStack { + Button(action: { showIconPicker = true }) { + PageIconView(icon: label.icon) + .frame(maxWidth: 20, maxHeight: 20) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + TextField("", text: $label.value) + .textFieldStyle(.plain) + } + .sheet(isPresented: $showIconPicker) { + LabelIconSelectionView(selected: $label.icon) + } + } +} + +private struct LabelIconSelectionView: View { + + @Environment(\.dismiss) + var dismiss + + @Binding + var selected: PageIcon + + var body: some View { + VStack { + List(PageIcon.allCases, id: \.rawValue) { icon in + HStack { + Image(systemSymbol: selected == icon ? .checkmarkCircleFill : .circle) + PageIconView(icon: icon) + .frame(maxWidth: 20, maxHeight: 20) + Text(icon.name) + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + selected = icon + dismiss() + } + }.frame(minHeight: 300) + Button("Done") { + dismiss() + } + }.padding() + } +} + +struct PostLabelsView: View { + + @ObservedObject + var post: LocalizedPost + + @ObservedObject + var other: LocalizedPost + + @Environment(\.colorScheme) + var colorScheme + + var body: some View { + ScrollView(.horizontal) { + HStack(spacing: 5) { + Text("Labels") + .font(.headline) + ForEach(post.labels, id: \.icon) { label in + HStack { + Button(action: { remove(label) }) { + Image(systemSymbol: .minusCircleFill) + .foregroundStyle(.red) + } + .buttonStyle(.plain) + LabelEditingView(label: label) + } + .padding(.vertical, 2) + .padding(.horizontal, 8) + .background(colorScheme == .light ? Color.white : Color.black) + .cornerRadius(8) + } + Button("Add", action: addLabel) + if !other.labels.isEmpty { + Button("Transfer") { + post.labels = other.labels.map { + // Copy instead of reference + ContentLabel(icon: $0.icon, value: $0.value) + } + } + } + } + .padding(.vertical, 2) + } + } + + func addLabel() { + post.labels.append(.init(icon: .clockFill, value: "Value")) + } + + func remove(_ label: ContentLabel) { + guard let index = post.labels.firstIndex(of: label) else { + return + } + post.labels.remove(at: index) + } +}