Add labels to posts

This commit is contained in:
Christoph Hagen
2025-01-26 20:32:44 +01:00
parent 06b4c1ed76
commit 42fa08b43d
15 changed files with 273 additions and 30 deletions

View File

@ -48,6 +48,10 @@ final class FeedPageGenerator {
let imageUrl = image?.linkPreviewImage(results: results)
let requiredIcons: Set<PageIcon> = 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,

View File

@ -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")

View File

@ -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
}
}

View File

@ -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?

View File

@ -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 = "<div class='labels-container'>"
result += "<div class='labels-container'>"
for label in labels {
result += "<div><svg><use href='#\(label.icon.icon.name)'></use></svg>\(label.value)</div>"
}
result += "</div>"
return result
}
}

View File

@ -0,0 +1,15 @@
protocol HtmlProducer {
func populate(_ result: inout String)
}
extension HtmlProducer {
var content: String {
var result = ""
populate(&result)
return result
}
}

View File

@ -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 {

View File

@ -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]

View File

@ -32,7 +32,8 @@ struct FeedEntry {
if let title = data.title {
result += "<h2>\(title.htmlEscaped())</h2>"
}
result += TagList(tags: data.tags).content
TagList(tags: data.tags).populate(&result)
ContentLabels(labels: data.labels).populate(&result)
for paragraph in data.text {
result += "<p>\(paragraph)</p>"

View File

@ -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
}

View File

@ -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()
}
}
}

View File

@ -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)

View File

@ -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)
}
}