From 8ae2a237cc5c274dfb9a390f45bd9dcaf1001408 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Wed, 20 Nov 2024 13:53:44 +0100 Subject: [PATCH] Colors, pages, post links --- CHDataManagement.xcodeproj/project.pbxproj | 28 ++++++++ CHDataManagement/CHDataManagementApp.swift | 15 ++--- CHDataManagement/Model/Content.swift | 67 ++++++++++++++++--- CHDataManagement/Model/LocalizedPage.swift | 13 ++++ CHDataManagement/Model/LocalizedPost.swift | 1 + CHDataManagement/Model/Page.swift | 2 +- CHDataManagement/Model/Tag.swift | 7 ++ .../Preview Content/Content+Mock.swift | 24 +++++++ CHDataManagement/Storage/Storage.swift | 14 ++++ CHDataManagement/Views/ColorPalette.swift | 17 +++++ .../Views/Generic/OptionalTextField.swift | 28 ++++++++ .../Views/Pages/PageDetailView.swift | 43 ++++++++++-- .../Views/Pages/PageListView.swift | 38 +++++++++++ .../Views/Posts/PagePickerView.swift | 61 +++++++++++++++++ CHDataManagement/Views/Posts/PostList.swift | 19 +++--- CHDataManagement/Views/Posts/PostView.swift | 38 +++++++++-- CHDataManagement/Views/Posts/TagView.swift | 8 +-- .../Views/Tags/TagDetailView.swift | 53 +++++++++++++++ .../Views/Tags/TagsListView.swift | 36 ++++++++++ 19 files changed, 466 insertions(+), 46 deletions(-) create mode 100644 CHDataManagement/Preview Content/Content+Mock.swift create mode 100644 CHDataManagement/Views/ColorPalette.swift create mode 100644 CHDataManagement/Views/Generic/OptionalTextField.swift create mode 100644 CHDataManagement/Views/Pages/PageListView.swift create mode 100644 CHDataManagement/Views/Posts/PagePickerView.swift create mode 100644 CHDataManagement/Views/Tags/TagDetailView.swift create mode 100644 CHDataManagement/Views/Tags/TagsListView.swift diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index e385e74..5ef56f6 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850082CEE01BF0090B18B /* PagePickerView.swift */; }; + E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500A2CEE02FA0090B18B /* Content+Mock.swift */; }; + E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500C2CEE07140090B18B /* ColorPalette.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 */; }; @@ -47,6 +50,10 @@ E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */; }; E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */; }; E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */; }; + E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D242CEBD7A10000979F /* PageListView.swift */; }; + E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D282CED2C6A0000979F /* TagsListView.swift */; }; + E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D2A2CED2CC30000979F /* TagDetailView.swift */; }; + E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */; }; E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; }; E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; }; E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; }; @@ -63,6 +70,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + E21850082CEE01BF0090B18B /* PagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePickerView.swift; sourceTree = ""; }; + E218500A2CEE02FA0090B18B /* Content+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Mock.swift"; sourceTree = ""; }; + E218500C2CEE07140090B18B /* ColorPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPalette.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 = ""; }; @@ -102,6 +112,10 @@ E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPost.swift; sourceTree = ""; }; E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = ""; }; E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Sorted.swift"; sourceTree = ""; }; + E2A37D242CEBD7A10000979F /* PageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListView.swift; sourceTree = ""; }; + E2A37D282CED2C6A0000979F /* TagsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsListView.swift; sourceTree = ""; }; + E2A37D2A2CED2CC30000979F /* TagDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDetailView.swift; sourceTree = ""; }; + E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextField.swift; sourceTree = ""; }; E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; }; E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; E2B85F3C2C4293F80047CD0C /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; @@ -146,6 +160,7 @@ isa = PBXGroup; children = ( E2A21C312CB5BCAC0060935B /* PageDetailView.swift */, + E2A37D242CEBD7A10000979F /* PageListView.swift */, ); path = Pages; sourceTree = ""; @@ -161,6 +176,7 @@ E2A21C372CB9A4F10060935B /* Generic */ = { isa = PBXGroup; children = ( + E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */, E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */, E2A21C0F2CB18B390060935B /* FlowHStack.swift */, ); @@ -199,6 +215,8 @@ E2A9CB7F2C7E686C005C89CC /* Tags */ = { isa = PBXGroup; children = ( + E2A37D282CED2C6A0000979F /* TagsListView.swift */, + E2A37D2A2CED2CC30000979F /* TagDetailView.swift */, ); path = Tags; sourceTree = ""; @@ -244,6 +262,7 @@ E2B85F462C42C7CA0047CD0C /* Views */ = { isa = PBXGroup; children = ( + E218500C2CEE07140090B18B /* ColorPalette.swift */, E2A21C522CBBF86D0060935B /* Files */, E2A21C492CBB168F0060935B /* Images */, E2A21C372CB9A4F10060935B /* Generic */, @@ -258,6 +277,7 @@ E2B85F4B2C4B8B7F0047CD0C /* Posts */ = { isa = PBXGroup; children = ( + E21850082CEE01BF0090B18B /* PagePickerView.swift */, E2A21C112CB18D520060935B /* DatePickerView.swift */, E2A21C152CB1A3C60060935B /* PostImageGalleryView.swift */, E2A21C2B2CB2BB210060935B /* PostList.swift */, @@ -317,6 +337,7 @@ E2DD047C2C276F32003BFF1F /* Preview Content */ = { isa = PBXGroup; children = ( + E218500A2CEE02FA0090B18B /* Content+Mock.swift */, E2A37D1A2CEA45530000979F /* Tag+Mock.swift */, E2A21C1F2CB28ED20060935B /* MockImage.swift */, E2A21C292CB2AA4C0060935B /* Post+Mock.swift */, @@ -416,11 +437,14 @@ E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */, E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */, E2A21C082CB17B870060935B /* TagView.swift in Sources */, + E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */, + E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */, E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */, E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */, E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */, E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */, E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */, + E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */, E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */, E24252082C5168750029FF16 /* GenericMetadata+Localized.swift in Sources */, E2581DED2C75202400F1F079 /* Tag.swift in Sources */, @@ -428,15 +452,19 @@ E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */, + E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */, E24252032C5163CF0029FF16 /* Importer.swift in Sources */, + E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */, E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */, E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */, E2A21C202CB28ED20060935B /* MockImage.swift in Sources */, E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */, + E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */, + E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */, E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */, diff --git a/CHDataManagement/CHDataManagementApp.swift b/CHDataManagement/CHDataManagementApp.swift index b959f7d..079e975 100644 --- a/CHDataManagement/CHDataManagementApp.swift +++ b/CHDataManagement/CHDataManagementApp.swift @@ -15,7 +15,7 @@ struct CHDataManagementApp: App { "" } - @ObservedObject + @StateObject var content: Content = .init() @State @@ -28,29 +28,26 @@ struct CHDataManagementApp: App { WindowGroup { TabView { Tab("Posts", systemImage: SFSymbol.rectangleAndPencilAndEllipsis.rawValue) { - PostList(posts: $content.posts) - .environment(\.language, selectedLanguage) - .background(Color(r: 2, g: 15, b: 26)) + PostList() } Tab("Pages", systemImage: SFSymbol.textBelowPhoto.rawValue) { - Text("TODO") + PageListView() } Tab("Tags", systemImage: SFSymbol.tag.rawValue) { - Text("TODO") + TagsListView() } Tab("Images", systemImage: SFSymbol.photo.rawValue) { ImagesView() - .environmentObject(content) } Tab("Files", systemImage: SFSymbol.doc.rawValue) { FilesView() - .environmentObject(content) } Tab("Settings", systemImage: SFSymbol.gear.rawValue) { SettingsView() - .environmentObject(content) } } + .environment(\.language, selectedLanguage) + .environmentObject(content) .toolbar { ToolbarItem(placement: .primaryAction) { Picker("", selection: $selectedLanguage) { diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index 22e7c62..777c467 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -1,5 +1,6 @@ import Foundation import SwiftUI +import Combine final class Content: ObservableObject { @@ -19,7 +20,64 @@ final class Content: ObservableObject { var files: [FileResources] = [] @AppStorage("contentPath") - var contentPath: String = "" + private var storedContentPath: String = "" + + @Published + var contentPath: String = "" { + didSet { + storedContentPath = contentPath + } + } + + let storage: Storage + + private var cancellables = Set() + + init(posts: [Post] = [], + pages: [Page] = [], + tags: [Tag] = [], + images: [ImageResource] = [], + files: [FileResources] = [], + storedContentPath: String) { + self.posts = posts + self.pages = pages + self.tags = tags + self.images = images + self.files = files + self.storedContentPath = storedContentPath + self.contentPath = storedContentPath + self.storage = Storage(baseFolder: URL(filePath: storedContentPath)) + do { + try storage.createFolderStructure() + } catch { + print(error) + return + } + observeContentPath() + } + + init() { + self.storage = Storage(baseFolder: URL(filePath: "")) + + contentPath = storedContentPath + do { + try storage.createFolderStructure() + } catch { + print(error) + return + } + + try? storage.update(baseFolder: URL(filePath: contentPath), moveContent: false) + observeContentPath() + } + + private func observeContentPath() { + $contentPath.sink { newValue in + let url = URL(filePath: newValue) + try? self.storage.update(baseFolder: url, moveContent: true) + } + .store(in: &cancellables) + } func generateFeed(for language: ContentLanguage, bookmarkKey: String) { let posts = posts.map { $0.feedEntry(for: language) } @@ -60,13 +118,6 @@ final class Content: ObservableObject { } func importOldContent() { - let storage = Storage(baseFolder: URL(filePath: "/Users/ch/Downloads/Content")) - do { - try storage.createFolderStructure() - } catch { - print(error) - return - } let importer = Importer() do { diff --git a/CHDataManagement/Model/LocalizedPage.swift b/CHDataManagement/Model/LocalizedPage.swift index 970a456..63b1b0d 100644 --- a/CHDataManagement/Model/LocalizedPage.swift +++ b/CHDataManagement/Model/LocalizedPage.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI /** A localized page contains the page content of a single language, @@ -69,4 +70,16 @@ final class LocalizedPage: ObservableObject { self.externalFiles = externalFiles self.requiredFiles = requiredFiles } + + @MainActor + func editableTitle() -> Binding { + Binding( + get: { + self.title + }, + set: { newValue in + self.title = newValue + } + ) + } } diff --git a/CHDataManagement/Model/LocalizedPost.swift b/CHDataManagement/Model/LocalizedPost.swift index 5ff149a..b878485 100644 --- a/CHDataManagement/Model/LocalizedPost.swift +++ b/CHDataManagement/Model/LocalizedPost.swift @@ -41,6 +41,7 @@ final class LocalizedPost: ObservableObject { ) } + @MainActor func editableContent() -> Binding { Binding( get: { diff --git a/CHDataManagement/Model/Page.swift b/CHDataManagement/Model/Page.swift index b04c901..78c8199 100644 --- a/CHDataManagement/Model/Page.swift +++ b/CHDataManagement/Model/Page.swift @@ -59,7 +59,7 @@ final class Page: ObservableObject { self.tags = tags } - func metadata(for language: ContentLanguage) -> LocalizedPage? { + func localized(in language: ContentLanguage) -> LocalizedPage { switch language { case .german: return german case .english: return english diff --git a/CHDataManagement/Model/Tag.swift b/CHDataManagement/Model/Tag.swift index 3a0a094..5b9ba8f 100644 --- a/CHDataManagement/Model/Tag.swift +++ b/CHDataManagement/Model/Tag.swift @@ -24,6 +24,13 @@ final class Tag: ObservableObject { var url: String { "/tags/\(linkName).html" } + + func localized(in language: ContentLanguage) -> LocalizedTag { + switch language { + case .english: return english + case .german: return german + } + } } extension Tag { diff --git a/CHDataManagement/Preview Content/Content+Mock.swift b/CHDataManagement/Preview Content/Content+Mock.swift new file mode 100644 index 0000000..36118f1 --- /dev/null +++ b/CHDataManagement/Preview Content/Content+Mock.swift @@ -0,0 +1,24 @@ +import Foundation + +extension FileManager { + + var documentDirectory: URL { + try! url( + for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, create: true) + } +} + +extension Content { + + private static let dbPath = FileManager.default.documentDirectory.appendingPathComponent("db").path() + + static let mock: Content = Content( + posts: [.empty, .mock, .fullMock], + pages: [.empty], + tags: [.hiking, .mountains, .nature, .sports], + images: [], + files: [], + storedContentPath: dbPath) +} diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index cb77a54..1aed20c 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -122,6 +122,20 @@ final class Storage { try loadAll(in: pagesFolder) } + func pageContent(for pageId: String, language: ContentLanguage) -> String { + let contentUrl = pageContentUrl(pageId: pageId, language: language) + guard fm.fileExists(atPath: contentUrl.path()) else { + print("No file at \(contentUrl.path())") + return "New file" + } + do { + return try String(contentsOf: contentUrl, encoding: .utf8) + } catch { + print("Failed to load page content for \(pageId) (\(language)): \(error)") + return error.localizedDescription + } + } + // MARK: Posts /// The folder path where the markdown files of the posts are stored (by their unique id/url component) diff --git a/CHDataManagement/Views/ColorPalette.swift b/CHDataManagement/Views/ColorPalette.swift new file mode 100644 index 0000000..9af96f6 --- /dev/null +++ b/CHDataManagement/Views/ColorPalette.swift @@ -0,0 +1,17 @@ +import SwiftUI + +enum ColorPalette { + + static let tagBackground = Color(r: 9, g: 62, b: 103) + + static let tagForeground = Color(r: 96, g: 186, b: 255) + + static let listBackground = Color(r: 2, g: 15, b: 26) + + static let postBackground = Color(r: 4, g: 31, b: 52) + + static let postText = Color(r: 221, g: 221, b: 221) + + static let postDate = tagForeground +} + diff --git a/CHDataManagement/Views/Generic/OptionalTextField.swift b/CHDataManagement/Views/Generic/OptionalTextField.swift new file mode 100644 index 0000000..68c9fbf --- /dev/null +++ b/CHDataManagement/Views/Generic/OptionalTextField.swift @@ -0,0 +1,28 @@ +import SwiftUI + +// A reusable component to handle optional strings with a TextField +struct OptionalTextField: View { + + let titleKey: LocalizedStringKey + + // The optional text that will be passed in and out of the component + @Binding var text: String? + + init(_ titleKey: LocalizedStringKey, text: Binding) { + self.titleKey = titleKey + self._text = text + } + + var body: some View { + TextField(titleKey, text: Binding( + get: { + // Convert `nil` to an empty string for display + text ?? "" + }, + set: { newValue in + // Convert an empty string to `nil` + text = newValue.isEmpty ? nil : newValue + } + )) + } +} diff --git a/CHDataManagement/Views/Pages/PageDetailView.swift b/CHDataManagement/Views/Pages/PageDetailView.swift index db8b776..98bbceb 100644 --- a/CHDataManagement/Views/Pages/PageDetailView.swift +++ b/CHDataManagement/Views/Pages/PageDetailView.swift @@ -1,17 +1,50 @@ import SwiftUI +import HighlightedTextEditor struct PageDetailView: View { - @ObservedObject var page: Page + @ObservedObject + var page: Page - @Binding - var language: ContentLanguage + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @State + private var pageContent: String = "" var body: some View { - Text(page.metadata(for: language)?.title ?? "No headline") + VStack(alignment: .leading) { + TextField("", text: page.localized(in: language).editableTitle()) + .font(.title) + + HStack(alignment: .firstTextBaseline) { + Button(action: loadContent) { + Text("Load") + } + Button(action: saveContent) { + Text("Save") + } + Spacer() + } + HighlightedTextEditor( + text: $pageContent, + highlightRules: .markdown) + } + .padding() + } + + private func loadContent() { + pageContent = content.storage.pageContent(for: page.id, language: language) + } + + private func saveContent() { + content.storage.save(pageContent: pageContent, for: page.id, language: language) } } #Preview { - PageDetailView(page: .empty, language: .constant(.english)) + PageDetailView(page: .empty) } diff --git a/CHDataManagement/Views/Pages/PageListView.swift b/CHDataManagement/Views/Pages/PageListView.swift new file mode 100644 index 0000000..4e8bea1 --- /dev/null +++ b/CHDataManagement/Views/Pages/PageListView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct PageListView: View { + + @Environment(\.language) + var language + + @EnvironmentObject + var content: Content + + @State + var selectedPage: Page? + + var body: some View { + NavigationSplitView { + List(content.pages, selection: $selectedPage) { page in + Text(page.localized(in: language).title) + .tag(page) + + } + } detail: { + // Detail view when an item is selected + if let selectedPage { + PageDetailView(page: selectedPage) + } else { + // Fallback if no item is selected + Text("Select a page to show the content.") + .font(.largeTitle) + .foregroundColor(.secondary) + } + } + } +} + +#Preview { + PageListView() + .environmentObject(Content()) +} diff --git a/CHDataManagement/Views/Posts/PagePickerView.swift b/CHDataManagement/Views/Posts/PagePickerView.swift new file mode 100644 index 0000000..3e8f50e --- /dev/null +++ b/CHDataManagement/Views/Posts/PagePickerView.swift @@ -0,0 +1,61 @@ +import SwiftUI + +struct PagePickerView: View { + + @Binding var showPagePicker: Bool + + @Binding var selectedPage: Page? + + @EnvironmentObject + private var content: Content + + @Environment(\.language) + private var language + + @State + private var newSelection: Page? + + init(showPagePicker: Binding, selectedPage: Binding) { + self._showPagePicker = showPagePicker + self._selectedPage = selectedPage + self.newSelection = selectedPage.wrappedValue + // TODO: Fix assignment not working + } + + var body: some View { + VStack { + Text("Select a page to link to") + List(content.pages, selection: $newSelection) { page in + let loc = page.localized(in: language) + Text("\(loc.title) (\(page.id))") + .tag(page) + } + .frame(minHeight: 300) + HStack { + Button("Use selection") { + DispatchQueue.main.async { + self.selectedPage = self.newSelection + } + showPagePicker = false + } + Button("Remove page", role: .destructive) { + DispatchQueue.main.async { + self.selectedPage = nil + } + showPagePicker = false + } + Button("Cancel", role: .cancel) { + showPagePicker = false + } + } + } + .navigationTitle("Pick a page") + .padding() + } +} + +#Preview { + PagePickerView(showPagePicker: .constant(true), + selectedPage: .constant(nil)) + .environmentObject(Content.mock) +} diff --git a/CHDataManagement/Views/Posts/PostList.swift b/CHDataManagement/Views/Posts/PostList.swift index 82c3f9a..edb672c 100644 --- a/CHDataManagement/Views/Posts/PostList.swift +++ b/CHDataManagement/Views/Posts/PostList.swift @@ -13,20 +13,18 @@ private struct CenteredPost: View where Content: View { HorizontalCenter { content } - .listRowBackground(PostList.background) + .listRowBackground(ColorPalette.listBackground) } } struct PostList: View { - static let background = Color(r: 2, g: 15, b: 26) - - @Binding - var posts: [Post] + @EnvironmentObject + private var content: Content var body: some View { List { - if posts.isEmpty { + if content.posts.isEmpty { CenteredPost { Text("No posts yet.") .padding() @@ -40,7 +38,7 @@ struct PostList: View { .padding() .listRowSeparator(.hidden) } - ForEach(posts) { post in + ForEach(content.posts) { post in CenteredPost { PostView(post: post) .frame(maxWidth: 600) @@ -50,7 +48,7 @@ struct PostList: View { } } .listStyle(.plain) - .background(PostList.background) + .background(ColorPalette.listBackground) .scrollContentBackground(.hidden) } @@ -64,10 +62,11 @@ struct PostList: View { tags: [], german: .init(title: "Titel", content: "Text"), english: .init(title: "Title", content: "Text")) - posts.insert(post, at: 0) + content.posts.insert(post, at: 0) } } #Preview { - PostList(posts: .constant([.mock, .fullMock])) + PostList() + .environmentObject(Content()) } diff --git a/CHDataManagement/Views/Posts/PostView.swift b/CHDataManagement/Views/Posts/PostView.swift index ce7b937..82c14ff 100644 --- a/CHDataManagement/Views/Posts/PostView.swift +++ b/CHDataManagement/Views/Posts/PostView.swift @@ -11,6 +11,16 @@ struct PostView: View { @State private var showDatePicker = false + @State + private var showPagePicker = false + + 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 !post.localized(in: language).images.isEmpty { @@ -25,7 +35,7 @@ struct PostView: View { Spacer() Toggle("Draft", isOn: $post.isDraft) } - .foregroundStyle(Color(r: 96, g: 186, b: 255)) + .foregroundStyle(ColorPalette.postDate) TextField("", text: post.localized(in: language).editableTitle()) .font(.system(size: 24, weight: .bold)) .foregroundStyle(Color.white) @@ -46,7 +56,7 @@ struct PostView: View { .resizable() .aspectRatio(1, contentMode: .fit) .frame(height: 18) - .foregroundColor(TagView.foreground) + .foregroundColor(ColorPalette.tagForeground) .opacity(0.7) .padding(.top, 3) } @@ -54,17 +64,31 @@ struct PostView: View { } TextEditor(text: post.localized(in: language).editableContent()) .font(.body) - .foregroundStyle(Color(r: 221, g: 221, b: 221)) + .foregroundStyle(ColorPalette.postText) .textEditorStyle(.plain) .padding(.leading, -5) .scrollDisabled(true) + HorizontalCenter { + Button(action: { showPagePicker = true }) { + Text(linkedPageText) + } + .buttonStyle(.plain) + .foregroundStyle(ColorPalette.postDate) + } } .padding() } - .background(Color(r: 4, g: 31, b: 52)) + .background(ColorPalette.postBackground) .cornerRadius(8) .sheet(isPresented: $showDatePicker) { - DatePickerView(post: post, showDatePicker: $showDatePicker) + DatePickerView( + post: post, + showDatePicker: $showDatePicker) + } + .sheet(isPresented: $showPagePicker) { + PagePickerView( + showPagePicker: $showPagePicker, + selectedPage: $post.linkedPage) } } @@ -81,11 +105,11 @@ struct PostView: View { List { PostView(post: .fullMock) .listRowSeparator(.hidden) - .listRowBackground(Color(r: 2, g: 15, b: 26)) + .listRowBackground(ColorPalette.listBackground) .environment(\.language, ContentLanguage.german) PostView(post: .mock) .listRowSeparator(.hidden) - .listRowBackground(Color(r: 2, g: 15, b: 26)) + .listRowBackground(ColorPalette.listBackground) } .listStyle(.plain) } diff --git a/CHDataManagement/Views/Posts/TagView.swift b/CHDataManagement/Views/Posts/TagView.swift index 11586ee..5fe39bd 100644 --- a/CHDataManagement/Views/Posts/TagView.swift +++ b/CHDataManagement/Views/Posts/TagView.swift @@ -4,10 +4,6 @@ import SFSafeSymbols struct TagView: View { - static let background = Color(r: 9, g: 62, b: 103) - - static let foreground = Color(r: 96, g: 186, b: 255) - @Environment(\.language) var language: ContentLanguage @@ -37,11 +33,11 @@ struct TagView: View { .opacity(0.7) .padding(.leading, -5) } - .foregroundColor(TagView.foreground) + .foregroundColor(ColorPalette.tagForeground) .font(.caption2) .padding(.horizontal, 8) .padding(.vertical, 4) - .background(TagView.background) + .background(ColorPalette.tagBackground) .cornerRadius(8) } } diff --git a/CHDataManagement/Views/Tags/TagDetailView.swift b/CHDataManagement/Views/Tags/TagDetailView.swift new file mode 100644 index 0000000..e2c8a90 --- /dev/null +++ b/CHDataManagement/Views/Tags/TagDetailView.swift @@ -0,0 +1,53 @@ +import SwiftUI + +struct TagDetailView: View { + + @ObservedObject + var tag: LocalizedTag + + @EnvironmentObject + private var content: Content + + var body: some View { + VStack(alignment: .leading) { + Text("Name") + .font(.callout) + .foregroundStyle(.secondary) + TextField("", text: $tag.name) + + Text("URL String") + .font(.callout) + .foregroundStyle(.secondary) + TextField("", text: $tag.urlComponent) + + Text("Original url") + .font(.callout) + .foregroundStyle(.secondary) + Text(tag.originalUrl ?? "-") + .padding(.top, 1) + .padding(.bottom) + + Text("Subtitle") + .font(.callout) + .foregroundStyle(.secondary) + OptionalTextField("", text: $tag.subtitle) + + Text("Description") + .font(.callout) + .foregroundStyle(.secondary) + OptionalTextField("", text: $tag.description) + + Text("Thumbnail") + .font(.callout) + .foregroundStyle(.secondary) + Text(tag.thumbnail ?? "-") + .padding(.top, 1) + .padding(.bottom) + } + .padding() + } +} + +#Preview { + TagDetailView(tag: Tag.mock.english) +} diff --git a/CHDataManagement/Views/Tags/TagsListView.swift b/CHDataManagement/Views/Tags/TagsListView.swift new file mode 100644 index 0000000..fc02b7d --- /dev/null +++ b/CHDataManagement/Views/Tags/TagsListView.swift @@ -0,0 +1,36 @@ +import SwiftUI + +struct TagsListView: View { + + @Environment(\.language) + var language + + @EnvironmentObject + var content: Content + + @State + var selectedTag: Tag? + + var body: some View { + NavigationSplitView { + List(content.tags, selection: $selectedTag) { tag in + Text(tag.localized(in: language).name) + .tag(tag) + + } + } detail: { + if let selectedTag { + TagDetailView(tag: selectedTag.localized(in: language)) + } else { + Text("Select a tag to show the details") + .font(.largeTitle) + .foregroundColor(.secondary) + } + } + } +} + +#Preview { + PageListView() + .environmentObject(Content()) +}