Add labels to posts
This commit is contained in:
@ -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,
|
||||
|
@ -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")
|
||||
|
49
CHDataManagement/Model/ContentLabel.swift
Normal file
49
CHDataManagement/Model/ContentLabel.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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?
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
|
||||
protocol HtmlProducer {
|
||||
|
||||
func populate(_ result: inout String)
|
||||
}
|
||||
|
||||
extension HtmlProducer {
|
||||
|
||||
var content: String {
|
||||
var result = ""
|
||||
populate(&result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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]
|
||||
|
@ -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>"
|
||||
|
@ -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
|
||||
}
|
||||
|
19
CHDataManagement/Views/Posts/PageIconView.swift
Normal file
19
CHDataManagement/Views/Posts/PageIconView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
113
CHDataManagement/Views/Posts/PostLabelsView.swift
Normal file
113
CHDataManagement/Views/Posts/PostLabelsView.swift
Normal 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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user