Improve post entry views, add post link data
This commit is contained in:
parent
fd1f1f4c40
commit
f1e1538167
@ -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 */,
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
8
CHDataManagement/Model/Content+Generate.swift
Normal file
8
CHDataManagement/Model/Content+Generate.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
extension Content {
|
||||
|
||||
func generateWebsite(into folder: URL) throws {
|
||||
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,8 @@ import Foundation
|
||||
|
||||
final class Post: ObservableObject {
|
||||
|
||||
let id: String
|
||||
@Published
|
||||
var id: String
|
||||
|
||||
@Published
|
||||
var isDraft: Bool
|
||||
|
@ -35,6 +35,12 @@ struct LocalizedPostFile {
|
||||
let content: String
|
||||
|
||||
let lastModifiedDate: Date?
|
||||
|
||||
let linkPreviewImage: String?
|
||||
|
||||
let linkPreviewTitle: String?
|
||||
|
||||
let linkPreviewDescription: String?
|
||||
}
|
||||
|
||||
extension LocalizedPostFile: Codable {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
123
CHDataManagement/Views/Posts/PostContentView.swift
Normal file
123
CHDataManagement/Views/Posts/PostContentView.swift
Normal 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)
|
||||
}
|
151
CHDataManagement/Views/Posts/PostDetailView.swift
Normal file
151
CHDataManagement/Views/Posts/PostDetailView.swift
Normal 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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
94
CHDataManagement/Views/Posts/PostImagesView.swift
Normal file
94
CHDataManagement/Views/Posts/PostImagesView.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user