Improve post entry views, add post link data

This commit is contained in:
Christoph Hagen 2024-11-30 08:34:31 +01:00
parent fd1f1f4c40
commit f1e1538167
18 changed files with 567 additions and 101 deletions

View File

@ -20,6 +20,10 @@
E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */; };
E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850222CF10C840090B18B /* TagSelectionView.swift */; };
E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850242CF38BCE0090B18B /* TextEntrySheet.swift */; };
E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850262CF3B42D0090B18B /* PostDetailView.swift */; };
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; };
E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; };
E218502F2CFAF69C0090B18B /* Content+Generate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502E2CFAF6990090B18B /* Content+Generate.swift */; };
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
E24252032C5163CF0029FF16 /* Importer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252022C5163CF0029FF16 /* Importer.swift */; };
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252052C51684E0029FF16 /* GenericMetadata.swift */; };
@ -93,6 +97,10 @@
E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = "<group>"; };
E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = "<group>"; };
E21850242CF38BCE0090B18B /* TextEntrySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntrySheet.swift; sourceTree = "<group>"; };
E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; };
E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = "<group>"; };
E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = "<group>"; };
E218502E2CFAF6990090B18B /* Content+Generate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Generate.swift"; sourceTree = "<group>"; };
E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = "<group>"; };
E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = "<group>"; };
E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.swift"; sourceTree = "<group>"; };
@ -249,13 +257,14 @@
isa = PBXGroup;
children = (
E2E06DFA2CA4A6570019C2AF /* Content.swift */,
E218502E2CFAF6990090B18B /* Content+Generate.swift */,
E24252092C52C9260029FF16 /* ContentLanguage.swift */,
E2A21C502CBBD53C0060935B /* FileResource.swift */,
E2A21C3A2CB9D9A50060935B /* ImageResource.swift */,
E25A0B882CE4021400F33674 /* LocalizedPage.swift */,
E2A21C042CB176670060935B /* LocalizedText.swift */,
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */,
E21850102CEE17010090B18B /* Page+Storage.swift */,
E25A0B882CE4021400F33674 /* LocalizedPage.swift */,
E2B85F3A2C428F0D0047CD0C /* Post.swift */,
E21850122CEE541A0090B18B /* Post+Storage.swift */,
E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */,
@ -304,6 +313,7 @@
E2B85F4B2C4B8B7F0047CD0C /* Posts */ = {
isa = PBXGroup;
children = (
E218502A2CF790AC0090B18B /* PostContentView.swift */,
E21850222CF10C840090B18B /* TagSelectionView.swift */,
E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */,
E21850082CEE01BF0090B18B /* PagePickerView.swift */,
@ -313,6 +323,8 @@
E2A21C002CB16A820060935B /* PostView.swift */,
E2A21C072CB17B810060935B /* TagView.swift */,
E21850242CF38BCE0090B18B /* TextEntrySheet.swift */,
E21850262CF3B42D0090B18B /* PostDetailView.swift */,
E218502C2CF791440090B18B /* PostImagesView.swift */,
);
path = Posts;
sourceTree = "<group>";
@ -455,6 +467,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */,
E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */,
E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */,
@ -472,6 +485,7 @@
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */,
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */,
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
E218502F2CFAF69C0090B18B /* Content+Generate.swift in Sources */,
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */,
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */,
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
@ -484,6 +498,7 @@
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
E218501B2CEE59EC0090B18B /* Tag+Storage.swift in Sources */,
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */,
E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */,
E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */,
E24252032C5163CF0029FF16 /* Importer.swift in Sources */,
@ -515,6 +530,7 @@
E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */,
E2A37D0E2CE527070000979F /* Storage.swift in Sources */,
E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */,
E218502D2CF791440090B18B /* PostImagesView.swift in Sources */,
E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */,
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */,
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */,

View File

@ -11,18 +11,15 @@ enum ContentDisplayType {
@main
struct CHDataManagementApp: App {
var navigationTitle: String {
private var navigationTitle: String {
""
}
@StateObject
var content: Content = .init()
private var content: Content = .init()
@State
var selectedLanguage: ContentLanguage = .english
@State
var contentDisplayType: ContentDisplayType = .markdown
private var selectedLanguage: ContentLanguage = .english
var body: some Scene {
WindowGroup {
@ -62,6 +59,13 @@ struct CHDataManagementApp: App {
.onReceive(Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()) { _ in
save()
}
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: save) {
Text("Save")
}
}
}
}
}

View File

@ -268,7 +268,10 @@ final class Importer {
images: images.sorted(),
title: page.linkPreviewTitle ?? page.title,
content: content,
lastModifiedDate: nil)
lastModifiedDate: nil,
linkPreviewImage: nil,
linkPreviewTitle: nil,
linkPreviewDescription: nil)
}
}

View File

@ -0,0 +1,8 @@
import Foundation
extension Content {
func generateWebsite(into folder: URL) throws {
}
}

View File

@ -221,17 +221,26 @@ final class Content: ObservableObject {
let posts = postsData.map { postId, post in
let linkedPage = post.linkedPageId.map { pages[$0] }
let german = LocalizedPost(
title: post.german.title,
content: post.german.content,
lastModified: post.german.lastModifiedDate,
images: post.german.images.compactMap { images[$0] })
let germanData = post.german
let german = LocalizedPost(
title: germanData.title,
content: germanData.content,
lastModified: germanData.lastModifiedDate,
images: germanData.images.compactMap { images[$0] },
linkPreviewImage: germanData.linkPreviewImage.map { images[$0] },
linkPreviewTitle: germanData.linkPreviewTitle,
linkPreviewDescription: germanData.linkPreviewDescription)
let englishData = post.english
let english = LocalizedPost(
title: post.english.title,
content: post.english.content,
lastModified: post.english.lastModifiedDate,
images: post.english.images.compactMap { images[$0] })
title: englishData.title,
content: englishData.content,
lastModified: englishData.lastModifiedDate,
images: englishData.images.compactMap { images[$0] },
linkPreviewImage: englishData.linkPreviewImage.map { images[$0] },
linkPreviewTitle: englishData.linkPreviewTitle,
linkPreviewDescription: englishData.linkPreviewDescription)
return Post(
id: postId,

View File

@ -15,14 +15,29 @@ final class LocalizedPost: ObservableObject {
@Published
var images: [ImageResource]
@Published
var linkPreviewImage: ImageResource?
@Published
var linkPreviewTitle: String?
@Published
var linkPreviewDescription: String?
init(title: String? = nil,
content: String,
lastModified: Date? = nil,
images: [ImageResource] = []) {
images: [ImageResource] = [],
linkPreviewImage: ImageResource? = nil,
linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil) {
self.title = title ?? ""
self.content = content
self.lastModified = lastModified
self.images = images
self.linkPreviewImage = linkPreviewImage
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription
}
@MainActor

View File

@ -21,6 +21,9 @@ extension LocalizedPost {
.init(images: images.map { $0.id },
title: title.nonEmpty,
content: content,
lastModifiedDate: lastModified)
lastModifiedDate: lastModified,
linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription)
}
}

View File

@ -2,7 +2,8 @@ import Foundation
final class Post: ObservableObject {
let id: String
@Published
var id: String
@Published
var isDraft: Bool

View File

@ -35,6 +35,12 @@ struct LocalizedPostFile {
let content: String
let lastModifiedDate: Date?
let linkPreviewImage: String?
let linkPreviewTitle: String?
let linkPreviewDescription: String?
}
extension LocalizedPostFile: Codable {

View File

@ -19,6 +19,7 @@ struct PageDetailView: View {
VStack(alignment: .leading) {
TextField("", text: page.localized(in: language).editableTitle())
.font(.title)
.textFieldStyle(.plain)
HStack(alignment: .firstTextBaseline) {
Button(action: loadContent) {

View File

@ -5,8 +5,7 @@ struct ImagePickerView: View {
@Binding
var showImagePicker: Bool
@ObservedObject
var post: LocalizedPost
private let selected: (ImageResource) -> Void
@EnvironmentObject
private var content: Content
@ -14,9 +13,9 @@ struct ImagePickerView: View {
@Environment(\.language)
private var language
init(showImagePicker: Binding<Bool>, post: LocalizedPost) {
init(showImagePicker: Binding<Bool>, selected: @escaping (ImageResource) -> Void) {
self._showImagePicker = showImagePicker
self.post = post
self.selected = selected
}
@State
@ -35,7 +34,7 @@ struct ImagePickerView: View {
DispatchQueue.main.async {
if let selectedImage {
print("Added image")
post.images.append(selectedImage)
selected(selectedImage)
} else {
print("No image to add")
}
@ -48,13 +47,13 @@ struct ImagePickerView: View {
}
}
}
.navigationTitle("Pick a page")
.navigationTitle("Pick an image")
.padding()
}
}
#Preview {
ImagePickerView(showImagePicker: .constant(true),
post: LocalizedPost.english)
ImagePickerView(showImagePicker: .constant(true)) { _ in
}
.environmentObject(Content.mock)
}

View File

@ -0,0 +1,123 @@
import SwiftUI
import HighlightedTextEditor
import SFSafeSymbols
struct PostContentView: View {
@ObservedObject
var post: Post
@Environment(\.language)
private var language
var body: some View {
LocalizedPostContentView(post: post)
}
}
private struct LocalizedTitle: View {
@ObservedObject
private var post: LocalizedPost
init(post: LocalizedPost) {
self.post = post
}
var body: some View {
TextField("", text: $post.title)
.font(.system(size: 24, weight: .bold))
.foregroundStyle(Color.primary)
.textFieldStyle(.plain)
.lineLimit(2)
}
}
private struct LocalizedContentEditor: View {
@ObservedObject
private var post: LocalizedPost
init(post: LocalizedPost) {
self.post = post
}
var body: some View {
TextEditor(text: $post.content)
// HighlightedTextEditor(
// text: $post.content,
// highlightRules: .markdown)
}
}
struct LocalizedPostContentView: View {
@ObservedObject
var post: Post
@State
private var showTagPicker = false
@Environment(\.language)
private var language
@EnvironmentObject
private var content: Content
init(post: Post) {
self.post = post
}
var body: some View {
VStack(alignment: .leading) {
Text("Images")
.font(.headline)
PostImagesView(post: post.localized(in: language))
LocalizedTitle(post: post.localized(in: language))
FlowHStack {
ForEach(post.tags, id: \.id) { tag in
TagView(tag: .init(
en: tag.english.name,
de: tag.german.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)
}
LocalizedContentEditor(post: post.localized(in: language))
}
.padding()
.sheet(isPresented: $showTagPicker) {
TagSelectionView(
presented: $showTagPicker,
selected: $post.tags,
tags: $content.tags)
}
}
private func remove(tag: Tag) {
post.tags = post.tags.filter {$0.id != tag.id }
}
}
#Preview(traits: .fixedLayout(width: 450, height: 600)) {
List {
PostContentView(post: .fullMock)
.listRowSeparator(.hidden)
.environment(\.language, ContentLanguage.german)
PostContentView(post: .mock)
.listRowSeparator(.hidden)
}
.environmentObject(Content.mock)
//.listStyle(.plain)
}

View File

@ -0,0 +1,151 @@
import SwiftUI
private struct DetailListItem<Content>: View where Content: View {
private let alignment: VerticalAlignment
private let spacing: CGFloat?
private let content: Content
init(alignment: VerticalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: () -> Content) {
self.alignment = alignment
self.spacing = spacing
self.content = content()
}
var body: some View {
HStack(alignment: alignment,
spacing: spacing) {
content
}
.padding(.horizontal)
.padding(.vertical)
.background(Color(NSColor.windowBackgroundColor))
.cornerRadius(8)
}
}
struct PostDetailView: View {
@Environment(\.language)
private var language
@ObservedObject
var post: Post
@State
private var showPagePicker = false
init(post: Post) {
self.post = post
}
private var linkedPageText: String {
if let page = post.linkedPage {
return page.localized(in: language).title
}
return "Add"
}
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("Post data")
.font(.headline)
DetailListItem {
Text("ID")
.foregroundStyle(.primary)
TextField("ID", text: $post.id)
.multilineTextAlignment(.trailing)
}
DetailListItem {
Text("Draft")
Spacer()
Toggle(isOn: $post.isDraft) {
Text("")
}.toggleStyle(.switch)
}
DetailListItem {
Text("Start")
Spacer()
DatePicker("", selection: $post.startDate, displayedComponents: .date)
.datePickerStyle(.compact)
}
DetailListItem {
Text("End")
Spacer()
Toggle(isOn: $post.hasEndDate) {
Text("")
}.toggleStyle(.switch)
DatePicker("", selection: $post.endDate, displayedComponents: .date)
.datePickerStyle(.compact)
.disabled(!post.hasEndDate)
}
DetailListItem {
Text("Linked page")
Spacer()
Button(action: { showPagePicker = true }) {
Text(linkedPageText)
}
.buttonStyle(.plain)
.foregroundStyle(.blue)
}
LocalizedPostDetailView(post: post.localized(in: language))
}
.padding()
}
.sheet(isPresented: $showPagePicker) {
PagePickerView(
showPagePicker: $showPagePicker,
selectedPage: $post.linkedPage)
}
}
}
struct LocalizedPostDetailView: View {
@ObservedObject
var post: LocalizedPost
@State
private var showImagePicker = false
var body: some View {
VStack(alignment: .leading) {
Text("Link Preview")
.font(.headline)
DetailListItem {
Text("Title")
Spacer()
OptionalTextField("", text: $post.linkPreviewTitle)
}
DetailListItem {
Text("Image")
Spacer()
Button(action: { showImagePicker = true }) {
Text(post.linkPreviewImage?.id ?? "Select")
}
.buttonStyle(.plain)
.foregroundStyle(.blue)
}
DetailListItem {
Text("Description")
Spacer()
OptionalTextField("", text: $post.linkPreviewDescription)
}
}
.sheet(isPresented: $showImagePicker) {
ImagePickerView(showImagePicker: $showImagePicker) { image in
post.linkPreviewImage = image
}
}
}
}
#Preview(traits: .fixedLayout(width: 270, height: 500)) {
PostDetailView(post: .fullMock)
}

View File

@ -1,7 +1,7 @@
import SwiftUI
import SFSafeSymbols
private struct NavigationIcon: View {
struct NavigationIcon: View {
let symbol: SFSymbol
@ -96,10 +96,9 @@ struct PostImageGalleryView: View {
}
}
.sheet(isPresented: $showImagePicker) {
ImagePickerView(
showImagePicker: $showImagePicker,
post: post
)
ImagePickerView(showImagePicker: $showImagePicker) { image in
post.images.append(image)
}
}
}

View File

@ -0,0 +1,94 @@
import SwiftUI
struct PostImagesView: View {
@ObservedObject
var post: LocalizedPost
@State
private var showImagePicker = false
var body: some View {
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 8) {
ForEach(post.images) { image in
ZStack {
image.imageToDisplay
.resizable()
.aspectRatio(contentMode: .fill)
.frame(maxWidth: 300, maxHeight: 200)
.cornerRadius(8)
.layoutPriority(1)
VStack {
HStack {
Button(action: { remove(image) }) {
NavigationIcon(symbol: .trash, edge: .all)
}
.buttonStyle(.plain)
Spacer()
}
Spacer()
HStack {
Button(action: { shiftLeft(image) }) {
NavigationIcon(symbol: .chevronLeft, edge: .trailing)
}
.buttonStyle(.plain)
Spacer()
Button(action: { shiftRight(image) }) {
NavigationIcon(symbol: .chevronRight, edge: .leading)
}
.buttonStyle(.plain)
}
}
.padding()
}
}
Button(action: { showImagePicker = true }) {
NavigationIcon(symbol: .plus, edge: .all)
}
.buttonStyle(.plain)
.padding()
}
}
.sheet(isPresented: $showImagePicker) {
ImagePickerView(showImagePicker: $showImagePicker) { image in
post.images.append(image)
}
}
}
private func shiftLeft(_ image: ImageResource) {
guard let index = post.images.firstIndex(of: image) else {
return
}
guard index > 0 else {
return
}
post.images.swapAt(index, index - 1)
}
private func shiftRight(_ image: ImageResource) {
guard let index = post.images.firstIndex(of: image) else {
return
}
guard index < post.images.count - 1 else {
return
}
post.images.swapAt(index, index + 1)
}
private func remove(_ image: ImageResource) {
guard let index = post.images.firstIndex(of: image) else {
return
}
post.images.remove(at: index)
}
}
#Preview {
VStack(alignment: .leading) {
Text("Images")
.font(.headline)
PostImagesView(post: .english)
}
}

View File

@ -5,51 +5,96 @@ struct PostList: View {
@EnvironmentObject
private var content: Content
@State
private var showNewPostIdSheet = false
@Environment(\.language)
private var language: ContentLanguage
@State
private var newPostId = ""
@State
private var selected: Post? = nil
@State
private var showNewPostView = false
@State
private var newPostIdIsValid = false
private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted
private var cleanPostId: String {
newPostId.trimmingCharacters(in: .whitespacesAndNewlines)
}
var body: some View {
List {
if content.posts.isEmpty {
HorizontalCenter {
Text("No posts yet.")
.padding()
}
.listRowSeparator(.hidden)
NavigationSplitView {
List(content.posts, selection: $selected) { post in
Text(post.localized(in: language).title)
.tag(post)
}
HorizontalCenter {
Button(action: { showNewPostIdSheet = true }) {
Text("Add post")
.frame(minWidth: 200)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: { showNewPostView = true }) {
Label("New post", systemSymbol: .plus)
}
}
.padding()
.listRowSeparator(.hidden)
}
ForEach(content.posts) { post in
HorizontalCenter {
PostView(post: post)
.frame(maxWidth: 600)
}
.listRowSeparator(.hidden)
.listRowInsets(.init(top: 0, leading: 0, bottom: 30, trailing: 0))
} content: {
if let selected {
PostContentView(post: selected)
.layoutPriority(1)
} else {
HStack {
Spacer()
Text("Select a post to show the content")
.font(.largeTitle)
.foregroundColor(.secondary)
Spacer()
}.layoutPriority(1)
}
} detail: {
if let selected {
PostDetailView(post: selected)
.frame(minWidth: 280)
} else {
Text("No post selected")
.frame(minWidth: 280)
}
}
.listStyle(.plain)
.sheet(isPresented: $showNewPostIdSheet, onDismiss: addNewPost) {
TextEntrySheet(title: "Enter the new post id", text: $newPostId)
.sheet(isPresented: $showNewPostView,
onDismiss: addNewPost) {
TextEntrySheet(
title: "Enter the id for the new post",
text: $newPostId,
isValid: $newPostIdIsValid)
}
.onChange(of: newPostId) { _, newValue in
newPostIdIsValid = isValid(id: newValue)
}
.onAppear {
if selected == nil {
selected = content.posts.first
}
}
}
private func addNewPost() {
let id = newPostId.trimmingCharacters(in: .whitespacesAndNewlines)
private func isValid(id: String) -> Bool {
let id = cleanPostId
guard id != "" else {
return
return false
}
guard !content.posts.contains(where: { $0.id == id }) else {
print("ID \(id) already exists")
return false
}
// Only allow alphanumeric characters and hyphens
return id.rangeOfCharacter(from: allowedCharactersInPostId) == nil
}
private func addNewPost() {
let id = cleanPostId
guard isValid(id: id) else {
return
}
@ -63,6 +108,7 @@ struct PostList: View {
german: .init(title: "Titel", content: "Text"),
english: .init(title: "Title", content: "Text"))
content.posts.insert(post, at: 0)
selected = post
}
}

View File

@ -26,9 +26,6 @@ struct LocalizedPostView: View {
@State
private var showDatePicker = false
@State
private var showPagePicker = false
@State
private var showImagePicker = false
@ -41,13 +38,6 @@ struct LocalizedPostView: View {
@EnvironmentObject
private var content: Content
private var linkedPageText: String {
if let page = post.linkedPage {
return page.localized(in: language).title
}
return "Add linked page"
}
var body: some View {
VStack(alignment: .center) {
if localized.images.isEmpty {
@ -62,14 +52,9 @@ struct LocalizedPostView: View {
.aspectRatio(1.33, contentMode: .fill)
}
VStack(alignment: .leading) {
HStack(alignment: .center, spacing: 0) {
Text(post.dateText(in: language))
.font(.system(size: 19, weight: .semibold))
.onTapGesture { showDatePicker = true }
Spacer()
Toggle("Draft", isOn: $post.isDraft)
}
.foregroundStyle(.secondary)
Text(post.dateText(in: language))
.font(.system(size: 19, weight: .semibold))
.foregroundStyle(.secondary)
TextField("", text: $localized.title)
.font(.system(size: 24, weight: .bold))
.foregroundStyle(Color.primary)
@ -101,13 +86,6 @@ struct LocalizedPostView: View {
.textEditorStyle(.plain)
.padding(.leading, -5)
.scrollDisabled(true)
HorizontalCenter {
Button(action: { showPagePicker = true }) {
Text(linkedPageText)
}
.buttonStyle(.plain)
.foregroundStyle(ColorPalette.link)
}
}
.padding()
}
@ -118,15 +96,10 @@ struct LocalizedPostView: View {
post: post,
showDatePicker: $showDatePicker)
}
.sheet(isPresented: $showPagePicker) {
PagePickerView(
showPagePicker: $showPagePicker,
selectedPage: $post.linkedPage)
}
.sheet(isPresented: $showImagePicker) {
ImagePickerView(
showImagePicker: $showImagePicker,
post: localized)
ImagePickerView(showImagePicker: $showImagePicker) { image in
localized.images.append(image)
}
}
.sheet(isPresented: $showTagPicker) {
TagSelectionView(

View File

@ -7,21 +7,33 @@ struct TextEntrySheet: View {
@Binding
var text: String
@Environment(\.dismiss)
var dismiss: DismissAction
@Binding
var isValid: Bool
@State
private var enteredText: String = ""
@Environment(\.dismiss)
private var dismiss: DismissAction
var body: some View {
VStack {
Text(title)
.foregroundStyle(.secondary)
TextField("Text", text: $enteredText)
TextField("Text", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
.overlay {
if isValid {
EmptyView()
} else {
RoundedRectangle(cornerRadius: 8)
.strokeBorder(lineWidth: 3)
.foregroundStyle(.red)
}
}
.frame(maxWidth: 300)
HStack {
Button(action: submit) {
Text("Submit")
}
.disabled(!isValid)
Button(role: .cancel, action: cancel) {
Text("Cancel")
}
@ -31,15 +43,18 @@ struct TextEntrySheet: View {
}
private func submit() {
text = enteredText
dismiss()
}
private func cancel() {
text = ""
dismiss()
}
}
#Preview {
TextEntrySheet(title: "Enter the id for the new post", text: .constant("new"))
TextEntrySheet(
title: "Enter the id for the new post",
text: .constant("new"),
isValid: .constant(false))
}