diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index c7dc506..67ef3d7 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -27,6 +27,10 @@ E21850312CFAF8880090B18B /* Content+Import.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850302CFAF8840090B18B /* Content+Import.swift */; }; E21850332CFAFA2F0090B18B /* WebsiteData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850322CFAFA200090B18B /* WebsiteData.swift */; }; E21850352CFAFA5A0090B18B /* WebsiteDataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850342CFAFA570090B18B /* WebsiteDataFile.swift */; }; + E21850372CFCA55F0090B18B /* LocalizedWebsiteData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedWebsiteData.swift */; }; + E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */; }; + E218503B2CFCFBE70090B18B /* WebsiteData+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503A2CFCFBDE0090B18B /* WebsiteData+Storage.swift */; }; + E218503D2CFCFD910090B18B /* LocalizedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503C2CFCFD8C0090B18B /* LocalizedSettingsView.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 */; }; @@ -34,6 +38,10 @@ E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; }; E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.swift */; }; E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DF02C7523F400F1F079 /* ImportableTag.swift */; }; + E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5082CFD964E00AEF16D /* TagContentView.swift */; }; + E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */; }; + E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */; }; + E25DA50F2CFDD76B00AEF16D /* ImagesContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50E2CFDD76B00AEF16D /* ImagesContentView.swift */; }; E2A21C012CB16A820060935B /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C002CB16A820060935B /* PostView.swift */; }; E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; }; E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C042CB176670060935B /* LocalizedText.swift */; }; @@ -107,6 +115,10 @@ E21850302CFAF8840090B18B /* Content+Import.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Import.swift"; sourceTree = ""; }; E21850322CFAFA200090B18B /* WebsiteData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteData.swift; sourceTree = ""; }; E21850342CFAFA570090B18B /* WebsiteDataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteDataFile.swift; sourceTree = ""; }; + E21850362CFCA5580090B18B /* LocalizedWebsiteData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedWebsiteData.swift; sourceTree = ""; }; + E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Mock.swift"; sourceTree = ""; }; + E218503A2CFCFBDE0090B18B /* WebsiteData+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Storage.swift"; sourceTree = ""; }; + E218503C2CFCFD8C0090B18B /* LocalizedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedSettingsView.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 = ""; }; @@ -114,6 +126,10 @@ E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; E2581DF02C7523F400F1F079 /* ImportableTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportableTag.swift; sourceTree = ""; }; E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = ""; }; + E25DA5082CFD964E00AEF16D /* TagContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentView.swift; sourceTree = ""; }; + E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTagAssignmentView.swift; sourceTree = ""; }; + E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTagAssignmentView.swift; sourceTree = ""; }; + E25DA50E2CFDD76B00AEF16D /* ImagesContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesContentView.swift; sourceTree = ""; }; E2A21C002CB16A820060935B /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = ""; }; E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = ""; }; E2A21C042CB176670060935B /* LocalizedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedText.swift; sourceTree = ""; }; @@ -202,6 +218,7 @@ E2A21C342CB9A3CA0060935B /* Settings */ = { isa = PBXGroup; children = ( + E218503C2CFCFD8C0090B18B /* LocalizedSettingsView.swift */, E2A21C352CB9A3D70060935B /* SettingsView.swift */, ); path = Settings; @@ -222,6 +239,7 @@ isa = PBXGroup; children = ( E2A21C4C2CBB16B50060935B /* ImagesView.swift */, + E25DA50E2CFDD76B00AEF16D /* ImagesContentView.swift */, E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */, E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */, ); @@ -254,8 +272,11 @@ E2A9CB7F2C7E686C005C89CC /* Tags */ = { isa = PBXGroup; children = ( + E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */, E2A37D282CED2C6A0000979F /* TagsListView.swift */, E2A37D2A2CED2CC30000979F /* TagDetailView.swift */, + E25DA5082CFD964E00AEF16D /* TagContentView.swift */, + E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */, ); path = Tags; sourceTree = ""; @@ -264,6 +285,8 @@ isa = PBXGroup; children = ( E21850322CFAFA200090B18B /* WebsiteData.swift */, + E218503A2CFCFBDE0090B18B /* WebsiteData+Storage.swift */, + E21850362CFCA5580090B18B /* LocalizedWebsiteData.swift */, E2E06DFA2CA4A6570019C2AF /* Content.swift */, E218502E2CFAF6990090B18B /* Content+Generate.swift */, E21850302CFAF8840090B18B /* Content+Import.swift */, @@ -388,6 +411,7 @@ E2DD047C2C276F32003BFF1F /* Preview Content */ = { isa = PBXGroup; children = ( + E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */, E218500A2CEE02FA0090B18B /* Content+Mock.swift */, E2A37D1A2CEA45530000979F /* Tag+Mock.swift */, E2A21C1F2CB28ED20060935B /* MockImage.swift */, @@ -508,6 +532,7 @@ E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E218501B2CEE59EC0090B18B /* Tag+Storage.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, + E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */, E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */, E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */, @@ -522,8 +547,10 @@ E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */, E2A21C202CB28ED20060935B /* MockImage.swift in Sources */, E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */, + E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, E21850112CEE17070090B18B /* Page+Storage.swift in Sources */, E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */, + E218503B2CFCFBE70090B18B /* WebsiteData+Storage.swift in Sources */, E21850352CFAFA5A0090B18B /* WebsiteDataFile.swift in Sources */, E21850132CEE541D0090B18B /* Post+Storage.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, @@ -532,10 +559,13 @@ E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */, E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */, E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */, + E218503D2CFCFD910090B18B /* LocalizedSettingsView.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, + E21850372CFCA55F0090B18B /* LocalizedWebsiteData.swift in Sources */, E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */, E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */, E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */, + E25DA50F2CFDD76B00AEF16D /* ImagesContentView.swift in Sources */, E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */, E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, @@ -545,7 +575,9 @@ E218502D2CF791440090B18B /* PostImagesView.swift in Sources */, E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */, E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */, + E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */, E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */, + E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */, E2A21C362CB9A3D70060935B /* SettingsView.swift in Sources */, E2A21C012CB16A820060935B /* PostView.swift in Sources */, E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */, diff --git a/CHDataManagement/Model/Content+Generate.swift b/CHDataManagement/Model/Content+Generate.swift index b3c8d69..f75a13d 100644 --- a/CHDataManagement/Model/Content+Generate.swift +++ b/CHDataManagement/Model/Content+Generate.swift @@ -4,26 +4,19 @@ extension Content { func generateFeed(for language: ContentLanguage, bookmarkKey: String) { let posts = posts.map { $0.feedEntry(for: language) } - DispatchQueue.global(qos: .userInitiated).async { + let data = websiteData.localized(in: language) + let navigationItems: [FeedNavigationLink] = websiteData.navigationTags.map { + let localized = $0.localized(in: language) + return .init(text: localized.name, url: localized.urlComponent) + } - let navigationItems: [FeedNavigationLink] = [ - .init(text: .init(en: "Projects", de: "Projekte"), - url: .init(en: "/projects", de: "/projekte")), - .init(text: .init(en: "Adventures", de: "Abenteuer"), - url: .init(en: "/adventures", de: "/abenteuer")), - .init(text: .init(en: "Services", de: "Dienste"), - url: .init(en: "/services", de: "/dienste")), - .init(text: .init(en: "Tags", de: "Kategorien"), - url: .init(en: "/tags", de: "/kategorien")), - ] + DispatchQueue.global(qos: .userInitiated).async { let feed = Feed( language: language, - title: .init(en: "Blog | CH", de: "Blog | CH"), - description: .init(en: "The latests posts, projects and adventures", - de: "Die neusten Beiträge, Projekte und Abenteuer"), - iconDescription: .init(en: "An icon consisting of the letters C and H in blue and orange", - de: "Ein Logo aus den Buchstaben C und H in Blau und Orange"), + title: data.title, + description: data.description, + iconDescription: data.iconDescription, navigationItems: navigationItems, posts: posts) let fileContent = feed.content diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index 508970d..f1ed7aa 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -5,19 +5,22 @@ import Combine final class Content: ObservableObject { @Published - var posts: [Post] = [] + var websiteData: WebsiteData @Published - var pages: [Page] = [] + var posts: [Post] @Published - var tags: [Tag] = [] + var pages: [Page] @Published - var images: [ImageResource] = [] + var tags: [Tag] @Published - var files: [FileResource] = [] + var images: [ImageResource] + + @Published + var files: [FileResource] @AppStorage("contentPath") private var storedContentPath: String = "" @@ -33,12 +36,14 @@ final class Content: ObservableObject { private var cancellables = Set() - init(posts: [Post] = [], - pages: [Page] = [], - tags: [Tag] = [], - images: [ImageResource] = [], - files: [FileResource] = [], + init(websiteData: WebsiteData, + posts: [Post], + pages: [Page], + tags: [Tag], + images: [ImageResource], + files: [FileResource], storedContentPath: String) { + self.websiteData = websiteData self.posts = posts self.pages = pages self.tags = tags @@ -59,6 +64,13 @@ final class Content: ObservableObject { init() { self.storage = Storage(baseFolder: URL(filePath: "")) + self.websiteData = .mock + self.posts = [] + self.pages = [] + self.tags = [] + self.images = [] + self.files = [] + contentPath = storedContentPath do { try storage.createFolderStructure() @@ -114,9 +126,17 @@ final class Content: ObservableObject { linkPreviewDescription: page.linkPreviewDescription) } + private func convert(_ websiteData: LocalizedWebsiteDataFile) -> LocalizedWebsiteData { + .init(title: websiteData.title, + description: websiteData.description, + iconDescription: websiteData.iconDescription) + } + func loadFromDisk() throws { let storage = Storage(baseFolder: URL(filePath: contentPath)) + let websiteData = try storage.loadWebsiteData() + let tagData = try storage.loadAllTags() let pagesData = try storage.loadAllPages() let postsData = try storage.loadAllPosts() @@ -170,6 +190,10 @@ final class Content: ObservableObject { self.files = files.sorted { $0.uniqueId } self.images = images.values.sorted { $0.id } self.posts = posts.sorted(ascending: false) { $0.startDate } + self.websiteData = WebsiteData( + navigationTags: websiteData.navigationTags.map { tags[$0]! }, + german: convert(websiteData.german), + english: convert(websiteData.english)) } private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag]) -> [String : Page] { @@ -202,6 +226,8 @@ final class Content: ObservableObject { for tag in tags { storage.save(tagMetadata: tag.tagFile, for: tag.id) } + storage.save(websiteData: websiteData.dataFile) + // TODO: Remove all files that are no longer in use (they belong to deleted items) //print("Finished save") } diff --git a/CHDataManagement/Model/ImageResource.swift b/CHDataManagement/Model/ImageResource.swift index 34ac443..3c95619 100644 --- a/CHDataManagement/Model/ImageResource.swift +++ b/CHDataManagement/Model/ImageResource.swift @@ -80,7 +80,11 @@ extension ImageResource { print("Failed to create image from \(url.path)") return failureImage } - self.size = loadedImage.size + if self.size == .zero && loadedImage.size != .zero { + DispatchQueue.main.async { + self.size = loadedImage.size + } + } return .init(nsImage: loadedImage) } diff --git a/CHDataManagement/Model/LocalizedWebsiteData.swift b/CHDataManagement/Model/LocalizedWebsiteData.swift new file mode 100644 index 0000000..d772bcc --- /dev/null +++ b/CHDataManagement/Model/LocalizedWebsiteData.swift @@ -0,0 +1,19 @@ +import Foundation + +final class LocalizedWebsiteData: ObservableObject { + + @Published + var title: String + + @Published + var description: String + + @Published + var iconDescription: String + + init(title: String, description: String, iconDescription: String) { + self.title = title + self.description = description + self.iconDescription = iconDescription + } +} diff --git a/CHDataManagement/Model/WebsiteData+Storage.swift b/CHDataManagement/Model/WebsiteData+Storage.swift new file mode 100644 index 0000000..7bb26a1 --- /dev/null +++ b/CHDataManagement/Model/WebsiteData+Storage.swift @@ -0,0 +1,20 @@ +import Foundation + +extension WebsiteData { + + var dataFile: WebsiteDataFile { + .init( + navigationTags: navigationTags.map { $0.id }, + german: german.dataFile, + english: english.dataFile) + } +} + +extension LocalizedWebsiteData { + + var dataFile: LocalizedWebsiteDataFile { + .init(title: title, + description: description, + iconDescription: iconDescription) + } +} diff --git a/CHDataManagement/Model/WebsiteData.swift b/CHDataManagement/Model/WebsiteData.swift index 0d3eccb..b11659f 100644 --- a/CHDataManagement/Model/WebsiteData.swift +++ b/CHDataManagement/Model/WebsiteData.swift @@ -1,5 +1,26 @@ import Foundation final class WebsiteData: ObservableObject { - + + @Published + var navigationTags: [Tag] + + @Published + var german: LocalizedWebsiteData + + @Published + var english: LocalizedWebsiteData + + init(navigationTags: [Tag] = [], german: LocalizedWebsiteData, english: LocalizedWebsiteData) { + self.navigationTags = navigationTags + self.german = german + self.english = english + } + + func localized(in language: ContentLanguage) -> LocalizedWebsiteData { + switch language { + case .english: return english + case .german: return german + } + } } diff --git a/CHDataManagement/Pages/Feed.swift b/CHDataManagement/Pages/Feed.swift index e060682..a00c455 100644 --- a/CHDataManagement/Pages/Feed.swift +++ b/CHDataManagement/Pages/Feed.swift @@ -2,9 +2,9 @@ import Foundation struct FeedNavigationLink { - let text: LocalizedText + let text: String - let url: LocalizedText + let url: String } struct Feed { @@ -13,11 +13,11 @@ struct Feed { let language: ContentLanguage - let title: LocalizedText + let title: String - let description: LocalizedText + let description: String - let iconDescription: LocalizedText + let iconDescription: String let navigationItems: [FeedNavigationLink] @@ -28,8 +28,8 @@ struct Feed { var result = "" result += "" let head = PageHead( - title: title.getText(for: language), - description: description.getText(for: language)) + title: title, + description: description) result += head.content result += "" addNavbar(to: &result) @@ -52,14 +52,14 @@ struct Feed { let rightNavigationItems = navigationItems[middleIndex...] for item in leftNavigationItems { - result += "\(item.text.getText(for: language))" + result += "\(item.text)" } result += "" - result += "\"\(iconDescription.getText(for:" + result += "\"\(iconDescription)\"" for item in rightNavigationItems { - result += "\(item.text.getText(for: language))" + result += "\(item.text)" } result += "" // Close nav-center, navbar } diff --git a/CHDataManagement/Preview Content/Content+Mock.swift b/CHDataManagement/Preview Content/Content+Mock.swift index 36118f1..00fd2cf 100644 --- a/CHDataManagement/Preview Content/Content+Mock.swift +++ b/CHDataManagement/Preview Content/Content+Mock.swift @@ -15,6 +15,7 @@ extension Content { private static let dbPath = FileManager.default.documentDirectory.appendingPathComponent("db").path() static let mock: Content = Content( + websiteData: .mock, posts: [.empty, .mock, .fullMock], pages: [.empty], tags: [.hiking, .mountains, .nature, .sports], diff --git a/CHDataManagement/Preview Content/WebsiteData+Mock.swift b/CHDataManagement/Preview Content/WebsiteData+Mock.swift new file mode 100644 index 0000000..a421fce --- /dev/null +++ b/CHDataManagement/Preview Content/WebsiteData+Mock.swift @@ -0,0 +1,25 @@ +import Foundation + +extension WebsiteData { + + static let mock: WebsiteData = .init( + german: .german, + english: .english) +} + +extension LocalizedWebsiteData { + + static var german: LocalizedWebsiteData { + .init( + title: "Titel", + description: "Beschreibung", + iconDescription: "Icon") + } + + static var english: LocalizedWebsiteData { + .init( + title: "A Title", + description: "Description", + iconDescription: "Icon") + } +} diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index 60379da..d897047 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -242,8 +242,26 @@ final class Storage { try fileNames(in: videosFolder) } + // MARK: Website data + + private var websiteDataUrl: URL { + baseFolder.appending(path: "website-data.json", directoryHint: .notDirectory) + } + + func loadWebsiteData() throws -> WebsiteDataFile { + try read(at: websiteDataUrl) + } + + @discardableResult + func save(websiteData: WebsiteDataFile) -> Bool { + write(websiteData, type: "Website Data", id: "-", to: websiteDataUrl) + } + // MARK: Writing files + /** + Encode a value and write it to a file, if the content changed + */ private func write(_ value: T, type: String, id: String, to file: URL) -> Bool where T: Encodable { let content: Data do { @@ -255,6 +273,9 @@ final class Storage { return write(data: content, type: type, id: id, to: file) } + /** + Write data to a file if the content changed + */ private func write(data: Data, type: String, id: String, to file: URL) -> Bool { if fm.fileExists(atPath: file.path()) { // Check if content is the same, to prevent unnecessary writes diff --git a/CHDataManagement/Storage/WebsiteDataFile.swift b/CHDataManagement/Storage/WebsiteDataFile.swift index 5b96926..5c6b6aa 100644 --- a/CHDataManagement/Storage/WebsiteDataFile.swift +++ b/CHDataManagement/Storage/WebsiteDataFile.swift @@ -2,6 +2,8 @@ import Foundation struct WebsiteDataFile { + let navigationTags: [String] + let german: LocalizedWebsiteDataFile let english: LocalizedWebsiteDataFile @@ -24,26 +26,3 @@ struct LocalizedWebsiteDataFile { extension LocalizedWebsiteDataFile: Codable { } - -/* - let navigationItems: [FeedNavigationLink] = [ - .init(text: .init(en: "Projects", de: "Projekte"), - url: .init(en: "/projects", de: "/projekte")), - .init(text: .init(en: "Adventures", de: "Abenteuer"), - url: .init(en: "/adventures", de: "/abenteuer")), - .init(text: .init(en: "Services", de: "Dienste"), - url: .init(en: "/services", de: "/dienste")), - .init(text: .init(en: "Tags", de: "Kategorien"), - url: .init(en: "/tags", de: "/kategorien")), - ] - - let feed = Feed( - language: language, - title: .init(en: "Blog | CH", de: "Blog | CH"), - description: .init(en: "The latests posts, projects and adventures", - de: "Die neusten Beiträge, Projekte und Abenteuer"), - iconDescription: .init(en: "An icon consisting of the letters C and H in blue and orange", - de: "Ein Logo aus den Buchstaben C und H in Blau und Orange"), - navigationItems: navigationItems, - posts: posts) - */ diff --git a/CHDataManagement/Views/Images/ImageDetailsView.swift b/CHDataManagement/Views/Images/ImageDetailsView.swift index 9cf553c..9d136f0 100644 --- a/CHDataManagement/Views/Images/ImageDetailsView.swift +++ b/CHDataManagement/Views/Images/ImageDetailsView.swift @@ -5,7 +5,8 @@ struct ImageDetailsView: View { @Environment(\.language) var language - let image: ImageResource + @ObservedObject + var image: ImageResource @State private var newId: String diff --git a/CHDataManagement/Views/Images/ImagesContentView.swift b/CHDataManagement/Views/Images/ImagesContentView.swift new file mode 100644 index 0000000..da4f769 --- /dev/null +++ b/CHDataManagement/Views/Images/ImagesContentView.swift @@ -0,0 +1,17 @@ +import SwiftUI + +struct ImagesContentView: View { + + @ObservedObject + var image: ImageResource + + var body: some View { + image.imageToDisplay + .resizable() + .aspectRatio(contentMode: .fit) + } +} + +#Preview { + ImagesContentView(image: .init(resourceName: "image1")) +} diff --git a/CHDataManagement/Views/Images/ImagesView.swift b/CHDataManagement/Views/Images/ImagesView.swift index d740f15..7fa79f3 100644 --- a/CHDataManagement/Views/Images/ImagesView.swift +++ b/CHDataManagement/Views/Images/ImagesView.swift @@ -19,38 +19,26 @@ struct ImagesView: View { private var showImageDetails = false var body: some View { - FlexibleColumnView(items: $content.images) { image, width in - let isSelected = selectedImage == image - let borderColor: Color = isSelected ? .accentColor : .clear - return image.imageToDisplay - .resizable() - .aspectRatio(contentMode: .fit) - .border(borderColor, width: 5) - .frame(width: width) - .onTapGesture { didTap(image: image) } - } - .inspector(isPresented: $showImageDetails) { + NavigationSplitView { + List(content.images, selection: $selectedImage) { image in + Text(image.id) + .tag(image) + } + } content: { + if let selectedImage { + ImagesContentView(image: selectedImage) + .layoutPriority(1) + } else { + Text("Select an image in the sidebar") + } + } detail: { if let selectedImage { ImageDetailsView(image: selectedImage) + .frame(maxWidth: 350) } else { - Text("Select an image to show its details") + EmptyView() } } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button(action: { showImageDetails.toggle() }) { - Label("Details", systemSymbol: .infoCircle) - } - } - } - } - - private func didTap(image: ImageResource) { - if selectedImage == image { - selectedImage = nil - } else { - selectedImage = image - } } } diff --git a/CHDataManagement/Views/Settings/LocalizedSettingsView.swift b/CHDataManagement/Views/Settings/LocalizedSettingsView.swift new file mode 100644 index 0000000..78bb568 --- /dev/null +++ b/CHDataManagement/Views/Settings/LocalizedSettingsView.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct LocalizedSettingsView: View { + + @ObservedObject + var settings: LocalizedWebsiteData + + var body: some View { + VStack(alignment: .leading) { + Text("Title") + .font(.headline) + TextField("", text: $settings.title) + Text("Description") + .font(.headline) + TextField("", text: $settings.description) + Text("Icon description") + .font(.headline) + TextField("", text: $settings.iconDescription) + } + } +} + +#Preview { + LocalizedSettingsView(settings: .english) +} diff --git a/CHDataManagement/Views/Settings/SettingsView.swift b/CHDataManagement/Views/Settings/SettingsView.swift index 4815ad3..bcb5b9b 100644 --- a/CHDataManagement/Views/Settings/SettingsView.swift +++ b/CHDataManagement/Views/Settings/SettingsView.swift @@ -20,31 +20,65 @@ struct SettingsView: View { @State private var showFileImporter = false + @State + private var showTagPicker = false + var body: some View { - VStack(alignment: .leading) { - Text("Content Folder") - .font(.headline) - TextField("Content Folder", text: $contentPath) - Button(action: selectContentFolder) { - Text("Select folder") - } - Text("Output Folder") - .font(.headline) - TextField("Output Folder", text: $outputPath) - Button(action: selectOutputFolder) { - Text("Select folder") - } - Text("Feed") - .font(.headline) - Button(action: generateFeed) { - Text("Generate") + ScrollView { + VStack(alignment: .leading) { + Text("Content Folder") + .font(.headline) + TextField("Content Folder", text: $contentPath) + Button(action: selectContentFolder) { + Text("Select folder") + } + Text("Output Folder") + .font(.headline) + TextField("Output Folder", text: $outputPath) + Button(action: selectOutputFolder) { + Text("Select folder") + } + Text("Navigation Bar Items") + .font(.headline) + FlowHStack { + ForEach(content.websiteData.navigationTags, 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) + } + LocalizedSettingsView(settings: content.websiteData.localized(in: language)) + Text("Feed") + .font(.headline) + Button(action: generateFeed) { + Text("Generate") + } } + .padding() } - .padding() .fileImporter( isPresented: $showFileImporter, allowedContentTypes: [.folder], onCompletion: didSelectContentFolder) + .sheet(isPresented: $showTagPicker) { + TagSelectionView( + presented: $showTagPicker, + selected: $content.websiteData.navigationTags, + tags: $content.tags) + } } // MARK: Folder selection @@ -140,4 +174,5 @@ struct SettingsView: View { #Preview { SettingsView() + .environmentObject(Content.mock) } diff --git a/CHDataManagement/Views/Tags/PageTagAssignmentView.swift b/CHDataManagement/Views/Tags/PageTagAssignmentView.swift new file mode 100644 index 0000000..2a82288 --- /dev/null +++ b/CHDataManagement/Views/Tags/PageTagAssignmentView.swift @@ -0,0 +1,68 @@ +import SwiftUI +import SFSafeSymbols + +private struct PageSelectionView: View { + + @ObservedObject + var tag: Tag + + @ObservedObject + var page: Page + + @Environment(\.language) + private var language + + var body: some View { + HStack { + let isSelected = page.tags.contains(tag) + Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle) + .foregroundStyle(Color.blue) + Text(page.localized(in: language).title) + } + .contentShape(Rectangle()) + .onTapGesture { + toggleTagAssignment() + } + } + + private func toggleTagAssignment() { + guard let index = page.tags.firstIndex(of: tag) else { + page.tags.append(tag) + return + } + page.tags.remove(at: index) + } +} + +struct PageTagAssignmentView: View { + + @ObservedObject + var tag: Tag + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @Environment(\.dismiss) + private var dismiss: DismissAction + + var body: some View { + VStack { + List { + ForEach(content.pages) { page in + PageSelectionView(tag: tag, page: page) + } + }.frame(minHeight: 400) + Button("Done") { + dismiss() + }.padding(.bottom) + } + } +} + +#Preview { + PageTagAssignmentView(tag: .hiking) + .environmentObject(Content.mock) +} diff --git a/CHDataManagement/Views/Tags/PostTagAssignmentView.swift b/CHDataManagement/Views/Tags/PostTagAssignmentView.swift new file mode 100644 index 0000000..1351c2a --- /dev/null +++ b/CHDataManagement/Views/Tags/PostTagAssignmentView.swift @@ -0,0 +1,68 @@ +import SwiftUI +import SFSafeSymbols + +private struct PostSelectionView: View { + + @ObservedObject + var tag: Tag + + @ObservedObject + var post: Post + + @Environment(\.language) + private var language + + var body: some View { + HStack { + let isSelected = post.tags.contains(tag) + Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle) + .foregroundStyle(Color.blue) + Text(post.localized(in: language).title) + } + .contentShape(Rectangle()) + .onTapGesture { + toggleTagAssignment() + } + } + + private func toggleTagAssignment() { + guard let index = post.tags.firstIndex(of: tag) else { + post.tags.append(tag) + return + } + post.tags.remove(at: index) + } +} + +struct PostTagAssignmentView: View { + + @ObservedObject + var tag: Tag + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @Environment(\.dismiss) + private var dismiss: DismissAction + + var body: some View { + VStack { + List { + ForEach(content.posts) { post in + PostSelectionView(tag: tag, post: post) + } + }.frame(minHeight: 400) + Button("Done") { + dismiss() + }.padding(.bottom) + } + } +} + +#Preview { + PostTagAssignmentView(tag: .hiking) + .environmentObject(Content.mock) +} diff --git a/CHDataManagement/Views/Tags/TagContentView.swift b/CHDataManagement/Views/Tags/TagContentView.swift new file mode 100644 index 0000000..c6777fa --- /dev/null +++ b/CHDataManagement/Views/Tags/TagContentView.swift @@ -0,0 +1,63 @@ +import SwiftUI + +struct TagContentView: View { + + @ObservedObject + var tag: Tag + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @State + private var showPageSelection = false + + @State + private var showPostSelection = false + + var selectedPages: [Page] { + content.pages.filter { $0.tags.contains(tag) } + } + + var selectedPosts: [Post] { + content.posts.filter { $0.tags.contains(tag) } + } + + var body: some View { + List { + Section("Pages") { + ForEach(selectedPages) { page in + Text(page.localized(in: language).title) + } + Button(action: { showPageSelection = true }) { + Text("Select pages") + } + .buttonStyle(.plain) + .foregroundStyle(Color.blue) + } + Section("Posts") { + ForEach(selectedPosts) { post in + Text(post.localized(in: language).title) + } + Button(action: { showPostSelection = true }) { + Text("Select posts") + } + .buttonStyle(.plain) + .foregroundStyle(Color.blue) + } + } + .sheet(isPresented: $showPageSelection) { + PageTagAssignmentView(tag: tag) + } + .sheet(isPresented: $showPostSelection) { + PostTagAssignmentView(tag: tag) + } + } +} + +#Preview { + TagContentView(tag: .hiking) + .environmentObject(Content.mock) +} diff --git a/CHDataManagement/Views/Tags/TagDetailView.swift b/CHDataManagement/Views/Tags/TagDetailView.swift index c17d2ab..50a231f 100644 --- a/CHDataManagement/Views/Tags/TagDetailView.swift +++ b/CHDataManagement/Views/Tags/TagDetailView.swift @@ -15,49 +15,51 @@ struct TagDetailView: View { private var showImagePicker = false var body: some View { - VStack(alignment: .leading) { - Toggle("Appears in overviews", isOn: $tagIsVisible) - .toggleStyle(.switch) - .font(.callout) - .foregroundStyle(.secondary) + ScrollView { + VStack(alignment: .leading) { + Toggle("Appears in overviews", isOn: $tagIsVisible) + .toggleStyle(.switch) + .font(.callout) + .foregroundStyle(.secondary) - Text("Name") - .font(.callout) - .foregroundStyle(.secondary) - TextField("", text: $tag.name) + Text("Name") + .font(.callout) + .foregroundStyle(.secondary) + TextField("", text: $tag.name) - Text("URL String") - .font(.callout) - .foregroundStyle(.secondary) - TextField("", text: $tag.urlComponent) + 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("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("Subtitle") + .font(.callout) + .foregroundStyle(.secondary) + OptionalTextField("", text: $tag.subtitle) - Text("Description") - .font(.callout) - .foregroundStyle(.secondary) - OptionalTextField("", text: $tag.description) + Text("Description") + .font(.callout) + .foregroundStyle(.secondary) + OptionalTextField("", text: $tag.description) - Text("Thumbnail") - .font(.callout) - .foregroundStyle(.secondary) - Button(action: { showImagePicker = true }) { - Text(tag.thumbnail?.id ?? "Select") + Text("Thumbnail") + .font(.callout) + .foregroundStyle(.secondary) + Button(action: { showImagePicker = true }) { + Text(tag.thumbnail?.id ?? "Select") + } + .buttonStyle(.plain) + .foregroundStyle(.blue) } - .buttonStyle(.plain) - .foregroundStyle(.blue) + .padding() } - .padding() .sheet(isPresented: $showImagePicker) { ImagePickerView(showImagePicker: $showImagePicker) { image in tag.thumbnail = image diff --git a/CHDataManagement/Views/Tags/TagsListView.swift b/CHDataManagement/Views/Tags/TagsListView.swift index e8a3dea..27994c6 100644 --- a/CHDataManagement/Views/Tags/TagsListView.swift +++ b/CHDataManagement/Views/Tags/TagsListView.swift @@ -26,6 +26,7 @@ struct TagsListView: View { @State var selectedTag: Tag? +#warning("TODO: Resort tag list when name changes") var body: some View { NavigationSplitView { List(content.tags, selection: $selectedTag) { tag in @@ -33,20 +34,43 @@ struct TagsListView: View { .tag(tag) } - } detail: { + } content: { if let selectedTag { - SelectedTagView(tag: selectedTag) + TagContentView(tag: selectedTag) + .layoutPriority(1) } else { Text("Select a tag to show the details") .font(.largeTitle) .foregroundColor(.secondary) } + } detail: { + if let selectedTag { + SelectedTagView(tag: selectedTag) + .frame(maxWidth: 350) + } else { + EmptyView() + } } .onAppear { if selectedTag == nil { selectedTag = content.tags.first } } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(action: addNewTag) { + Label("New tag", systemSymbol: .plus) + } + } + } + } + + private func addNewTag() { + let newTag = Tag(isVisible: true, + german: .init(urlComponent: "tag", name: "Neuer Tag"), + english: .init(urlComponent: "tag-en", name: "New Tag")) + // Add to top of the list, and resort when changing the name + content.tags.insert(newTag, at: 0) } }