diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index fc8da88..b400eac 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -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 = ""; }; E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = ""; }; E21850242CF38BCE0090B18B /* TextEntrySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntrySheet.swift; sourceTree = ""; }; + E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = ""; }; + E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = ""; }; + E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = ""; }; + E218502E2CFAF6990090B18B /* Content+Generate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Generate.swift"; sourceTree = ""; }; E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = ""; }; E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = ""; }; E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.swift"; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/CHDataManagement/CHDataManagementApp.swift b/CHDataManagement/CHDataManagementApp.swift index c39c551..eabca7a 100644 --- a/CHDataManagement/CHDataManagementApp.swift +++ b/CHDataManagement/CHDataManagementApp.swift @@ -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") + } + } + } } } diff --git a/CHDataManagement/Import/Importer.swift b/CHDataManagement/Import/Importer.swift index 54ae1ed..508ffdc 100644 --- a/CHDataManagement/Import/Importer.swift +++ b/CHDataManagement/Import/Importer.swift @@ -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) } } diff --git a/CHDataManagement/Model/Content+Generate.swift b/CHDataManagement/Model/Content+Generate.swift new file mode 100644 index 0000000..8e0016d --- /dev/null +++ b/CHDataManagement/Model/Content+Generate.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Content { + + func generateWebsite(into folder: URL) throws { + + } +} diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index 1bc77b4..d7a58eb 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -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, diff --git a/CHDataManagement/Model/LocalizedPost.swift b/CHDataManagement/Model/LocalizedPost.swift index fab9f47..6992b96 100644 --- a/CHDataManagement/Model/LocalizedPost.swift +++ b/CHDataManagement/Model/LocalizedPost.swift @@ -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 diff --git a/CHDataManagement/Model/Post+Storage.swift b/CHDataManagement/Model/Post+Storage.swift index 3e090de..c6aef99 100644 --- a/CHDataManagement/Model/Post+Storage.swift +++ b/CHDataManagement/Model/Post+Storage.swift @@ -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) } } diff --git a/CHDataManagement/Model/Post.swift b/CHDataManagement/Model/Post.swift index c0f962b..d2d400c 100644 --- a/CHDataManagement/Model/Post.swift +++ b/CHDataManagement/Model/Post.swift @@ -2,7 +2,8 @@ import Foundation final class Post: ObservableObject { - let id: String + @Published + var id: String @Published var isDraft: Bool diff --git a/CHDataManagement/Storage/PostFile.swift b/CHDataManagement/Storage/PostFile.swift index 8fea7a3..c2464df 100644 --- a/CHDataManagement/Storage/PostFile.swift +++ b/CHDataManagement/Storage/PostFile.swift @@ -35,6 +35,12 @@ struct LocalizedPostFile { let content: String let lastModifiedDate: Date? + + let linkPreviewImage: String? + + let linkPreviewTitle: String? + + let linkPreviewDescription: String? } extension LocalizedPostFile: Codable { diff --git a/CHDataManagement/Views/Pages/PageDetailView.swift b/CHDataManagement/Views/Pages/PageDetailView.swift index 98bbceb..16f4706 100644 --- a/CHDataManagement/Views/Pages/PageDetailView.swift +++ b/CHDataManagement/Views/Pages/PageDetailView.swift @@ -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) { diff --git a/CHDataManagement/Views/Posts/ImagePickerView.swift b/CHDataManagement/Views/Posts/ImagePickerView.swift index 9f94973..9938096 100644 --- a/CHDataManagement/Views/Posts/ImagePickerView.swift +++ b/CHDataManagement/Views/Posts/ImagePickerView.swift @@ -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, post: LocalizedPost) { + init(showImagePicker: Binding, 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) } diff --git a/CHDataManagement/Views/Posts/PostContentView.swift b/CHDataManagement/Views/Posts/PostContentView.swift new file mode 100644 index 0000000..26ac8e5 --- /dev/null +++ b/CHDataManagement/Views/Posts/PostContentView.swift @@ -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) +} diff --git a/CHDataManagement/Views/Posts/PostDetailView.swift b/CHDataManagement/Views/Posts/PostDetailView.swift new file mode 100644 index 0000000..95f5725 --- /dev/null +++ b/CHDataManagement/Views/Posts/PostDetailView.swift @@ -0,0 +1,151 @@ +import SwiftUI + +private struct DetailListItem: 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) +} diff --git a/CHDataManagement/Views/Posts/PostImageGalleryView.swift b/CHDataManagement/Views/Posts/PostImageGalleryView.swift index e9359f0..bcb65fd 100644 --- a/CHDataManagement/Views/Posts/PostImageGalleryView.swift +++ b/CHDataManagement/Views/Posts/PostImageGalleryView.swift @@ -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) + } } } diff --git a/CHDataManagement/Views/Posts/PostImagesView.swift b/CHDataManagement/Views/Posts/PostImagesView.swift new file mode 100644 index 0000000..9c075e6 --- /dev/null +++ b/CHDataManagement/Views/Posts/PostImagesView.swift @@ -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) + } +} diff --git a/CHDataManagement/Views/Posts/PostList.swift b/CHDataManagement/Views/Posts/PostList.swift index 4b7f4bb..462d330 100644 --- a/CHDataManagement/Views/Posts/PostList.swift +++ b/CHDataManagement/Views/Posts/PostList.swift @@ -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 } } diff --git a/CHDataManagement/Views/Posts/PostView.swift b/CHDataManagement/Views/Posts/PostView.swift index 3976688..dfd7249 100644 --- a/CHDataManagement/Views/Posts/PostView.swift +++ b/CHDataManagement/Views/Posts/PostView.swift @@ -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( diff --git a/CHDataManagement/Views/Posts/TextEntrySheet.swift b/CHDataManagement/Views/Posts/TextEntrySheet.swift index 10bb674..2129019 100644 --- a/CHDataManagement/Views/Posts/TextEntrySheet.swift +++ b/CHDataManagement/Views/Posts/TextEntrySheet.swift @@ -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)) }