From dc7b7a0e90477363cf83c1ead893b30153e7fb26 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Tue, 3 Dec 2024 13:19:50 +0100 Subject: [PATCH] Reorganize saving, generate feed --- CHDataManagement.xcodeproj/project.pbxproj | 84 ++++++---- CHDataManagement/Extensions/Array+Split.swift | 9 ++ CHDataManagement/Model/Content+Generate.swift | 35 ---- CHDataManagement/Model/Content+Load.swift | 128 +++++++++++++++ CHDataManagement/Model/Content+Save.swift | 133 +++++++++++++++ CHDataManagement/Model/Content.swift | 153 ------------------ CHDataManagement/Model/Page+Storage.swift | 30 ---- CHDataManagement/Model/Post+Storage.swift | 29 ---- CHDataManagement/Model/Tag+Storage.swift | 23 --- .../Model/WebsiteData+Storage.swift | 20 --- CHDataManagement/Model/WebsiteGenerator.swift | 102 ++++++++++++ .../Page Elements/NavigationBar.swift | 52 ++++++ CHDataManagement/Page Elements/PageHead.swift | 4 +- .../PostFeedPageNavigation.swift | 88 ++++++++++ CHDataManagement/Pages/Feed.swift | 79 --------- CHDataManagement/Pages/GenericPage.swift | 36 +++++ CHDataManagement/Pages/PageInFeed.swift | 58 +++++++ .../WebsiteGenerator+Mock.swift | 19 +++ .../Storage/{ => Model}/FileOnDisk.swift | 0 .../Storage/{ => Model}/FileType.swift | 0 .../Storage/{ => Model}/PageFile.swift | 0 .../Storage/{ => Model}/PageOnDisk.swift | 0 .../Storage/{ => Model}/PostFile.swift | 0 .../Storage/{ => Model}/TagFile.swift | 0 .../Storage/{ => Model}/WebsiteDataFile.swift | 0 CHDataManagement/Storage/Storage.swift | 5 +- .../Views/Settings/SettingsView.swift | 41 ++++- 27 files changed, 717 insertions(+), 411 deletions(-) create mode 100644 CHDataManagement/Extensions/Array+Split.swift delete mode 100644 CHDataManagement/Model/Content+Generate.swift create mode 100644 CHDataManagement/Model/Content+Load.swift create mode 100644 CHDataManagement/Model/Content+Save.swift delete mode 100644 CHDataManagement/Model/Page+Storage.swift delete mode 100644 CHDataManagement/Model/Post+Storage.swift delete mode 100644 CHDataManagement/Model/Tag+Storage.swift delete mode 100644 CHDataManagement/Model/WebsiteData+Storage.swift create mode 100644 CHDataManagement/Model/WebsiteGenerator.swift create mode 100644 CHDataManagement/Page Elements/NavigationBar.swift create mode 100644 CHDataManagement/Page Elements/PostFeedPageNavigation.swift delete mode 100644 CHDataManagement/Pages/Feed.swift create mode 100644 CHDataManagement/Pages/GenericPage.swift create mode 100644 CHDataManagement/Pages/PageInFeed.swift create mode 100644 CHDataManagement/Preview Content/WebsiteGenerator+Mock.swift rename CHDataManagement/Storage/{ => Model}/FileOnDisk.swift (100%) rename CHDataManagement/Storage/{ => Model}/FileType.swift (100%) rename CHDataManagement/Storage/{ => Model}/PageFile.swift (100%) rename CHDataManagement/Storage/{ => Model}/PageOnDisk.swift (100%) rename CHDataManagement/Storage/{ => Model}/PostFile.swift (100%) rename CHDataManagement/Storage/{ => Model}/TagFile.swift (100%) rename CHDataManagement/Storage/{ => Model}/WebsiteDataFile.swift (100%) diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 67ef3d7..67180c1 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -10,12 +10,9 @@ 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 */; }; - E21850112CEE17070090B18B /* Page+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850102CEE17010090B18B /* Page+Storage.swift */; }; - E21850132CEE541D0090B18B /* Post+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850122CEE541A0090B18B /* Post+Storage.swift */; }; E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850142CEE55D40090B18B /* FileOnDisk.swift */; }; E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; }; E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850182CEE561B0090B18B /* PageOnDisk.swift */; }; - E218501B2CEE59EC0090B18B /* Tag+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501A2CEE59E80090B18B /* Tag+Storage.swift */; }; E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501C2CEE6CB30090B18B /* VerticalCenter.swift */; }; E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */; }; E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850222CF10C840090B18B /* TagSelectionView.swift */; }; @@ -23,13 +20,12 @@ 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 */; }; + E218502F2CFAF69C0090B18B /* WebsiteGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */; }; 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 */; }; @@ -42,6 +38,13 @@ 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 */; }; + E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5142CFF00B900AEF16D /* Content+Load.swift */; }; + E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5162CFF00F200AEF16D /* Content+Save.swift */; }; + E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5182CFF035200AEF16D /* Array+Split.swift */; }; + E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */; }; + E25DA51D2CFF135E00AEF16D /* GenericPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA51C2CFF135B00AEF16D /* GenericPage.swift */; }; + E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */; }; + E25DA5212CFF1B9300AEF16D /* WebsiteGenerator+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5202CFF1B8900AEF16D /* WebsiteGenerator+Mock.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 */; }; @@ -82,7 +85,7 @@ 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 */; }; - E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* Feed.swift */; }; + E2B85F3D2C4293F80047CD0C /* PageInFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* PageInFeed.swift */; }; E2B85F412C4294790047CD0C /* PageHead.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F402C4294790047CD0C /* PageHead.swift */; }; E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F422C4294F60047CD0C /* FeedEntry.swift */; }; E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F442C429ED60047CD0C /* ImageGallery.swift */; }; @@ -98,12 +101,9 @@ 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 = ""; }; - E21850102CEE17010090B18B /* Page+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Page+Storage.swift"; sourceTree = ""; }; - E21850122CEE541A0090B18B /* Post+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+Storage.swift"; sourceTree = ""; }; E21850142CEE55D40090B18B /* FileOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOnDisk.swift; sourceTree = ""; }; E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = ""; }; E21850182CEE561B0090B18B /* PageOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageOnDisk.swift; sourceTree = ""; }; - E218501A2CEE59E80090B18B /* Tag+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tag+Storage.swift"; sourceTree = ""; }; E218501C2CEE6CB30090B18B /* VerticalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCenter.swift; sourceTree = ""; }; E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = ""; }; E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = ""; }; @@ -111,13 +111,12 @@ 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 = ""; }; + E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteGenerator.swift; sourceTree = ""; }; 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 = ""; }; @@ -130,6 +129,13 @@ 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 = ""; }; + E25DA5142CFF00B900AEF16D /* Content+Load.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Load.swift"; sourceTree = ""; }; + E25DA5162CFF00F200AEF16D /* Content+Save.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Save.swift"; sourceTree = ""; }; + E25DA5182CFF035200AEF16D /* Array+Split.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Split.swift"; sourceTree = ""; }; + E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedPageNavigation.swift; sourceTree = ""; }; + E25DA51C2CFF135B00AEF16D /* GenericPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPage.swift; sourceTree = ""; }; + E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; + E25DA5202CFF1B8900AEF16D /* WebsiteGenerator+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteGenerator+Mock.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 = ""; }; @@ -168,7 +174,7 @@ 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 = ""; }; + E2B85F3C2C4293F80047CD0C /* PageInFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInFeed.swift; sourceTree = ""; }; E2B85F402C4294790047CD0C /* PageHead.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHead.swift; sourceTree = ""; }; E2B85F422C4294F60047CD0C /* FeedEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntry.swift; sourceTree = ""; }; E2B85F442C429ED60047CD0C /* ImageGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGallery.swift; sourceTree = ""; }; @@ -206,6 +212,20 @@ path = Import; sourceTree = ""; }; + E25DA5112CFF001900AEF16D /* Model */ = { + isa = PBXGroup; + children = ( + E21850342CFAFA570090B18B /* WebsiteDataFile.swift */, + E21850142CEE55D40090B18B /* FileOnDisk.swift */, + E21850162CEE55FB0090B18B /* FileType.swift */, + E2A37D102CE537670000979F /* PageFile.swift */, + E21850182CEE561B0090B18B /* PageOnDisk.swift */, + E2A37D142CE68BEA0000979F /* PostFile.swift */, + E2A37D162CE73F170000979F /* TagFile.swift */, + ); + path = Model; + sourceTree = ""; + }; E2A21C322CB5BCAC0060935B /* Pages */ = { isa = PBXGroup; children = ( @@ -257,14 +277,8 @@ E2A37D0F2CE5375E0000979F /* Storage */ = { isa = PBXGroup; children = ( - E21850342CFAFA570090B18B /* WebsiteDataFile.swift */, - E21850142CEE55D40090B18B /* FileOnDisk.swift */, - E21850162CEE55FB0090B18B /* FileType.swift */, - E2A37D102CE537670000979F /* PageFile.swift */, - E21850182CEE561B0090B18B /* PageOnDisk.swift */, - E2A37D142CE68BEA0000979F /* PostFile.swift */, + E25DA5112CFF001900AEF16D /* Model */, E2A37D0D2CE527040000979F /* Storage.swift */, - E2A37D162CE73F170000979F /* TagFile.swift */, ); path = Storage; sourceTree = ""; @@ -285,24 +299,22 @@ isa = PBXGroup; children = ( E21850322CFAFA200090B18B /* WebsiteData.swift */, - E218503A2CFCFBDE0090B18B /* WebsiteData+Storage.swift */, E21850362CFCA5580090B18B /* LocalizedWebsiteData.swift */, E2E06DFA2CA4A6570019C2AF /* Content.swift */, - E218502E2CFAF6990090B18B /* Content+Generate.swift */, + E25DA5162CFF00F200AEF16D /* Content+Save.swift */, + E25DA5142CFF00B900AEF16D /* Content+Load.swift */, + E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */, E21850302CFAF8840090B18B /* Content+Import.swift */, E24252092C52C9260029FF16 /* ContentLanguage.swift */, E2A21C502CBBD53C0060935B /* FileResource.swift */, E2A21C3A2CB9D9A50060935B /* ImageResource.swift */, E2A21C042CB176670060935B /* LocalizedText.swift */, - E2A9CB7D2C7BCF2A005C89CC /* Page.swift */, - E21850102CEE17010090B18B /* Page+Storage.swift */, E25A0B882CE4021400F33674 /* LocalizedPage.swift */, E2B85F3A2C428F0D0047CD0C /* Post.swift */, - E21850122CEE541A0090B18B /* Post+Storage.swift */, E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */, E2581DEC2C75202400F1F079 /* Tag.swift */, - E218501A2CEE59E80090B18B /* Tag+Storage.swift */, E2A37D182CEA36A40000979F /* LocalizedTag.swift */, + E2A9CB7D2C7BCF2A005C89CC /* Page.swift */, ); path = Model; sourceTree = ""; @@ -310,7 +322,8 @@ E2B85F3E2C4293FF0047CD0C /* Pages */ = { isa = PBXGroup; children = ( - E2B85F3C2C4293F80047CD0C /* Feed.swift */, + E25DA51C2CFF135B00AEF16D /* GenericPage.swift */, + E2B85F3C2C4293F80047CD0C /* PageInFeed.swift */, ); path = Pages; sourceTree = ""; @@ -318,6 +331,8 @@ E2B85F3F2C42946E0047CD0C /* Page Elements */ = { isa = PBXGroup; children = ( + E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */, + E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */, E2B85F422C4294F60047CD0C /* FeedEntry.swift */, E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */, E2A21C272CB29B290060935B /* FeedEntryData.swift */, @@ -364,6 +379,7 @@ E2B85F552C4BD0AD0047CD0C /* Extensions */ = { isa = PBXGroup; children = ( + E25DA5182CFF035200AEF16D /* Array+Split.swift */, E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */, E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */, E2A21C472CBAF8830060935B /* String+Extensions.swift */, @@ -411,6 +427,7 @@ E2DD047C2C276F32003BFF1F /* Preview Content */ = { isa = PBXGroup; children = ( + E25DA5202CFF1B8900AEF16D /* WebsiteGenerator+Mock.swift */, E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */, E218500A2CEE02FA0090B18B /* Content+Mock.swift */, E2A37D1A2CEA45530000979F /* Tag+Mock.swift */, @@ -505,8 +522,10 @@ E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */, E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */, E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, + E25DA5212CFF1B9300AEF16D /* WebsiteGenerator+Mock.swift in Sources */, E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */, E21850312CFAF8880090B18B /* Content+Import.swift in Sources */, + E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */, E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, @@ -515,22 +534,22 @@ E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */, E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */, E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */, + E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */, E2A21C082CB17B870060935B /* TagView.swift in Sources */, E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */, E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */, E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */, - E218502F2CFAF69C0090B18B /* Content+Generate.swift in Sources */, + E218502F2CFAF69C0090B18B /* WebsiteGenerator.swift in Sources */, E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */, E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */, E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */, - E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */, + E2B85F3D2C4293F80047CD0C /* PageInFeed.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 */, E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */, 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 */, @@ -548,15 +567,14 @@ 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 */, + E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */, + E25DA51D2CFF135E00AEF16D /* GenericPage.swift in Sources */, E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */, E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */, E218503D2CFCFD910090B18B /* LocalizedSettingsView.swift in Sources */, @@ -568,6 +586,7 @@ E25DA50F2CFDD76B00AEF16D /* ImagesContentView.swift in Sources */, E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */, E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */, + E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */, E2A37D0E2CE527070000979F /* Storage.swift in Sources */, @@ -577,6 +596,7 @@ E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */, E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */, E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */, + E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */, E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */, E2A21C362CB9A3D70060935B /* SettingsView.swift in Sources */, E2A21C012CB16A820060935B /* PostView.swift in Sources */, diff --git a/CHDataManagement/Extensions/Array+Split.swift b/CHDataManagement/Extensions/Array+Split.swift new file mode 100644 index 0000000..bb08208 --- /dev/null +++ b/CHDataManagement/Extensions/Array+Split.swift @@ -0,0 +1,9 @@ +extension Array { + + func split(into size: Int) -> [[Element]] { + guard size > 0 else { return [] } + return stride(from: 0, to: count, by: size).map { + Array(self[$0.. LocalizedTag { + LocalizedTag( + urlComponent: tag.urlComponent, + name: tag.name, + subtitle: tag.subtitle, + description: tag.description, + thumbnail: tag.thumbnail.map { images[$0] }, + originalUrl: tag.originalURL) + } + + private func convert(_ post: LocalizedPostFile, images: [String : ImageResource]) -> LocalizedPost { + LocalizedPost( + title: post.title, + content: post.content, + lastModified: post.lastModifiedDate, + images: post.images.compactMap { images[$0] }, + linkPreviewImage: post.linkPreviewImage.map { images[$0] }, + linkPreviewTitle: post.linkPreviewTitle, + linkPreviewDescription: post.linkPreviewDescription) + } + + private func convert(_ page: LocalizedPageFile) -> LocalizedPage { + LocalizedPage( + urlString: page.url, + title: page.title, + lastModified: page.lastModifiedDate, + originalUrl: page.originalURL, + files: Set(page.files), + externalFiles: Set(page.externalFiles), + requiredFiles: Set(page.requiredFiles), + linkPreviewImage: page.linkPreviewImage, + linkPreviewTitle: page.linkPreviewTitle, + 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() + let filesData = try storage.loadAllFiles() + + var images: [String : ImageResource] = [:] + var files: [FileResource] = [] + var videos: [String] = [] + + for (file, url) in filesData { + let ext = file.components(separatedBy: ".").last!.lowercased() + let type = FileType(fileExtension: ext) + switch type { + case .image: + images[file] = ImageResource(uniqueId: file, altText: .init(en: "", de: ""), fileUrl: url) + case .file: + files.append(FileResource(uniqueId: file, description: "")) + case .video: + videos.append(file) + case .resource: + break + } + } + + let tags = tagData.reduce(into: [:]) { (tags, data) in + tags[data.key] = Tag( + isVisible: data.value.isVisible, + german: convert(data.value.german, images: images), + english: convert(data.value.english, images: images)) + } + + let pages: [String : Page] = loadPages(pagesData, tags: tags) + + let posts = postsData.map { postId, post in + let linkedPage = post.linkedPageId.map { pages[$0] } + let german = convert(post.german, images: images) + let english = convert(post.english, images: images) + + return Post( + id: postId, + isDraft: post.isDraft, + createdDate: post.createdDate, + startDate: post.startDate, + endDate: post.endDate, + tags: post.tags.map { tags[$0]! }, + german: german, + english: english, + linkedPage: linkedPage) + } + + self.tags = tags.values.sorted() + self.pages = pages.values.sorted(ascending: false) { $0.startDate } + self.files = files.sorted { $0.uniqueId } + self.images = images.values.sorted { $0.id } + self.videos = videos + 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] { + pagesData.reduce(into: [:]) { pages, data in + let (pageId, page) = data + pages[pageId] = Page( + id: pageId, + isDraft: page.isDraft, + createdDate: page.createdDate, + startDate: page.startDate, + endDate: page.endDate, + german: convert(page.german), + english: convert(page.english), + tags: page.tags.map { tags[$0]! }) + } + } + +} diff --git a/CHDataManagement/Model/Content+Save.swift b/CHDataManagement/Model/Content+Save.swift new file mode 100644 index 0000000..ebd93c3 --- /dev/null +++ b/CHDataManagement/Model/Content+Save.swift @@ -0,0 +1,133 @@ +import Foundation + +extension Content { + + func saveToDisk() { + //print("Starting save") + for page in pages { + storage.save(pageMetadata: page.pageFile, for: page.id) + } + + for post in posts { + storage.save(post: post.postFile, for: post.id) + } + + for tag in tags { + storage.save(tagMetadata: tag.tagFile, for: tag.id) + } + storage.save(websiteData: websiteData.dataFile) + + do { + try storage.deletePostFiles(notIn: posts.map { $0.id }) + try storage.deletePageFiles(notIn: pages.map { $0.id }) + try storage.deleteTagFiles(notIn: tags.map { $0.id }) + let allFiles = files.map { $0.uniqueId } + images.map { $0.id } + videos + try storage.deleteFiles(notIn: allFiles) + } catch { + print("Failed to remove unused files: \(error)") + } + // TODO: Remove all files that are no longer in use (they belong to deleted items) + //print("Finished save") + } +} + + +private extension Page { + + var pageFile: PageFile { + .init(isDraft: isDraft, + tags: tags.map { $0.id }, + createdDate: createdDate, + startDate: startDate, + endDate: hasEndDate ? endDate : nil, + german: german.pageFile, + english: english.pageFile) + } +} + +private extension LocalizedPage { + + var pageFile: LocalizedPageFile { + .init(url: urlString, + files: files.sorted(), + externalFiles: externalFiles.sorted(), + requiredFiles: requiredFiles.sorted(), + title: title, + linkPreviewImage: linkPreviewImage, + linkPreviewTitle: linkPreviewTitle, + linkPreviewDescription: linkPreviewDescription, + lastModifiedDate: lastModified, + originalURL: originalUrl) + } +} + + +private extension Post { + + var postFile: PostFile { + .init( + isDraft: isDraft, + createdDate: createdDate, + startDate: startDate, + endDate: hasEndDate ? endDate : nil, + tags: tags.map { $0.id }, + german: german.postFile, + english: english.postFile, + linkedPageId: linkedPage?.id) + } +} + +private extension LocalizedPost { + + var postFile: LocalizedPostFile { + .init(images: images.map { $0.id }, + title: title.nonEmpty, + content: content, + lastModifiedDate: lastModified, + linkPreviewImage: linkPreviewImage?.id, + linkPreviewTitle: linkPreviewTitle, + linkPreviewDescription: linkPreviewDescription) + } +} + + +private extension Tag { + + var tagFile: TagFile { + .init(id: id, + isVisible: isVisible, + german: german.tagFile, + english: english.tagFile) + } +} + +private extension LocalizedTag { + + var tagFile: LocalizedTagFile { + .init(urlComponent: urlComponent, + name: name, + subtitle: subtitle, + description: description, + thumbnail: thumbnail?.id, + originalURL: originalUrl) + } +} + +private extension WebsiteData { + + var dataFile: WebsiteDataFile { + .init( + navigationTags: navigationTags.map { $0.id }, + german: german.dataFile, + english: english.dataFile) + } +} + +private extension LocalizedWebsiteData { + + var dataFile: LocalizedWebsiteDataFile { + .init(title: title, + description: description, + iconDescription: iconDescription) + } +} diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index 1d36cbd..8cf3b23 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -97,159 +97,6 @@ final class Content: ObservableObject { .store(in: &cancellables) } - private func convert(_ tag: LocalizedTagFile, images: [String : ImageResource]) -> LocalizedTag { - LocalizedTag( - urlComponent: tag.urlComponent, - name: tag.name, - subtitle: tag.subtitle, - description: tag.description, - thumbnail: tag.thumbnail.map { images[$0] }, - originalUrl: tag.originalURL) - } - - private func convert(_ post: LocalizedPostFile, images: [String : ImageResource]) -> LocalizedPost { - LocalizedPost( - title: post.title, - content: post.content, - lastModified: post.lastModifiedDate, - images: post.images.compactMap { images[$0] }, - linkPreviewImage: post.linkPreviewImage.map { images[$0] }, - linkPreviewTitle: post.linkPreviewTitle, - linkPreviewDescription: post.linkPreviewDescription) - } - - private func convert(_ page: LocalizedPageFile) -> LocalizedPage { - LocalizedPage( - urlString: page.url, - title: page.title, - lastModified: page.lastModifiedDate, - originalUrl: page.originalURL, - files: Set(page.files), - externalFiles: Set(page.externalFiles), - requiredFiles: Set(page.requiredFiles), - linkPreviewImage: page.linkPreviewImage, - linkPreviewTitle: page.linkPreviewTitle, - 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() - let filesData = try storage.loadAllFiles() - - var images: [String : ImageResource] = [:] - var files: [FileResource] = [] - var videos: [String] = [] - - for (file, url) in filesData { - let ext = file.components(separatedBy: ".").last!.lowercased() - let type = FileType(fileExtension: ext) - switch type { - case .image: - images[file] = ImageResource(uniqueId: file, altText: .init(en: "", de: ""), fileUrl: url) - case .file: - files.append(FileResource(uniqueId: file, description: "")) - case .video: - videos.append(file) - case .resource: - break - } - } - - let tags = tagData.reduce(into: [:]) { (tags, data) in - tags[data.key] = Tag( - isVisible: data.value.isVisible, - german: convert(data.value.german, images: images), - english: convert(data.value.english, images: images)) - } - - let pages: [String : Page] = loadPages(pagesData, tags: tags) - - let posts = postsData.map { postId, post in - let linkedPage = post.linkedPageId.map { pages[$0] } - let german = convert(post.german, images: images) - let english = convert(post.english, images: images) - - return Post( - id: postId, - isDraft: post.isDraft, - createdDate: post.createdDate, - startDate: post.startDate, - endDate: post.endDate, - tags: post.tags.map { tags[$0]! }, - german: german, - english: english, - linkedPage: linkedPage) - } - - self.tags = tags.values.sorted() - self.pages = pages.values.sorted(ascending: false) { $0.startDate } - self.files = files.sorted { $0.uniqueId } - self.images = images.values.sorted { $0.id } - self.videos = videos - 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] { - pagesData.reduce(into: [:]) { pages, data in - let (pageId, page) = data - pages[pageId] = Page( - id: pageId, - isDraft: page.isDraft, - createdDate: page.createdDate, - startDate: page.startDate, - endDate: page.endDate, - german: convert(page.german), - english: convert(page.english), - tags: page.tags.map { tags[$0]! }) - } - } - - // MARK: Saving - - func saveToDisk() { - //print("Starting save") - for page in pages { - storage.save(pageMetadata: page.pageFile, for: page.id) - } - - for post in posts { - storage.save(post: post.postFile, for: post.id) - } - - for tag in tags { - storage.save(tagMetadata: tag.tagFile, for: tag.id) - } - storage.save(websiteData: websiteData.dataFile) - - do { - try storage.deletePostFiles(notIn: posts.map { $0.id }) - try storage.deletePageFiles(notIn: pages.map { $0.id }) - try storage.deleteTagFiles(notIn: tags.map { $0.id }) - let allFiles = files.map { $0.uniqueId } + images.map { $0.id } + videos - try storage.deleteFiles(notIn: allFiles) - } catch { - print("Failed to remove unused files: \(error)") - } - // TODO: Remove all files that are no longer in use (they belong to deleted items) - //print("Finished save") - } - // MARK: Folder access static func accessFolderFromBookmark(key: String, operation: (URL) -> Void) { diff --git a/CHDataManagement/Model/Page+Storage.swift b/CHDataManagement/Model/Page+Storage.swift deleted file mode 100644 index c5ff903..0000000 --- a/CHDataManagement/Model/Page+Storage.swift +++ /dev/null @@ -1,30 +0,0 @@ -import Foundation - -extension Page { - - var pageFile: PageFile { - .init(isDraft: isDraft, - tags: tags.map { $0.id }, - createdDate: createdDate, - startDate: startDate, - endDate: hasEndDate ? endDate : nil, - german: german.pageFile, - english: english.pageFile) - } -} - -extension LocalizedPage { - - var pageFile: LocalizedPageFile { - .init(url: urlString, - files: files.sorted(), - externalFiles: externalFiles.sorted(), - requiredFiles: requiredFiles.sorted(), - title: title, - linkPreviewImage: linkPreviewImage, - linkPreviewTitle: linkPreviewTitle, - linkPreviewDescription: linkPreviewDescription, - lastModifiedDate: lastModified, - originalURL: originalUrl) - } -} diff --git a/CHDataManagement/Model/Post+Storage.swift b/CHDataManagement/Model/Post+Storage.swift deleted file mode 100644 index c6aef99..0000000 --- a/CHDataManagement/Model/Post+Storage.swift +++ /dev/null @@ -1,29 +0,0 @@ -import Foundation - -extension Post { - - var postFile: PostFile { - .init( - isDraft: isDraft, - createdDate: createdDate, - startDate: startDate, - endDate: hasEndDate ? endDate : nil, - tags: tags.map { $0.id }, - german: german.postFile, - english: english.postFile, - linkedPageId: linkedPage?.id) - } -} - -extension LocalizedPost { - - var postFile: LocalizedPostFile { - .init(images: images.map { $0.id }, - title: title.nonEmpty, - content: content, - lastModifiedDate: lastModified, - linkPreviewImage: linkPreviewImage?.id, - linkPreviewTitle: linkPreviewTitle, - linkPreviewDescription: linkPreviewDescription) - } -} diff --git a/CHDataManagement/Model/Tag+Storage.swift b/CHDataManagement/Model/Tag+Storage.swift deleted file mode 100644 index 1dba3c0..0000000 --- a/CHDataManagement/Model/Tag+Storage.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation - -extension Tag { - - var tagFile: TagFile { - .init(id: id, - isVisible: isVisible, - german: german.tagFile, - english: english.tagFile) - } -} - -extension LocalizedTag { - - var tagFile: LocalizedTagFile { - .init(urlComponent: urlComponent, - name: name, - subtitle: subtitle, - description: description, - thumbnail: thumbnail?.id, - originalURL: originalUrl) - } -} diff --git a/CHDataManagement/Model/WebsiteData+Storage.swift b/CHDataManagement/Model/WebsiteData+Storage.swift deleted file mode 100644 index 7bb26a1..0000000 --- a/CHDataManagement/Model/WebsiteData+Storage.swift +++ /dev/null @@ -1,20 +0,0 @@ -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/WebsiteGenerator.swift b/CHDataManagement/Model/WebsiteGenerator.swift new file mode 100644 index 0000000..4874514 --- /dev/null +++ b/CHDataManagement/Model/WebsiteGenerator.swift @@ -0,0 +1,102 @@ +import Foundation + +struct WebsiteGeneratorConfiguration { + + let language: ContentLanguage + + let postsPerPage: Int + + let postFeedTitle: String + + let postFeedDescription: String + + let postFeedUrlPrefix: String +} + +final class WebsiteGenerator { + + let language: ContentLanguage + + let content: Content + + let postsPerPage: Int + + let postFeedTitle: String + + let postFeedDescription: String + + let postFeedUrlPrefix: String + + init(content: Content, configuration: WebsiteGeneratorConfiguration) { + self.content = content + self.language = configuration.language + self.postsPerPage = configuration.postsPerPage + self.postFeedTitle = configuration.postFeedTitle + self.postFeedDescription = configuration.postFeedDescription + self.postFeedUrlPrefix = configuration.postFeedUrlPrefix + } + + func generateWebsite() { + createPostFeedPages() + } + + private func createPostFeedPages() { + let totalCount = content.posts.count + guard totalCount > 0 else { return } + + let navBarData = createNavigationBarData() + + let numberOfPages = (totalCount + postsPerPage - 1) / postsPerPage // Round up + for pageIndex in 1...numberOfPages { + let startIndex = (pageIndex - 1) * postsPerPage + let endIndex = min(pageIndex * postsPerPage, totalCount) + let postsOnPage = content.posts[startIndex.. NavigationBarData { + let data = content.websiteData.localized(in: language) + let navigationItems: [NavigationBarLink] = content.websiteData.navigationTags.map { + let localized = $0.localized(in: language) + return .init(text: localized.name, url: localized.urlComponent) + } + return NavigationBarData( + navigationIconPath: "/assets/icons/ch.svg", + iconDescription: data.iconDescription, + navigationItems: navigationItems) + } + + private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice, bar: NavigationBarData) { + let posts = posts.map { $0.feedEntry(for: language) } + + let feed = PageInFeed( + language: language, + title: postFeedTitle, + description: postFeedDescription, + navigationBarData: bar, + pageNumber: pageIndex, + totalPages: pageCount, + posts: posts) + let fileContent = feed.content + + if pageIndex == 1 { + save(fileContent, to: "\(postFeedUrlPrefix).html") + } else { + save(fileContent, to: "\(postFeedUrlPrefix)-\(pageIndex).html") + } + } + + private func save(_ content: String, to relativePath: String) { + Content.accessFolderFromBookmark(key: Storage.outputPathBookmarkKey) { folder in + let outputFile = folder.appendingPathComponent(relativePath, isDirectory: false) + do { + try content + .data(using: .utf8)! + .write(to: outputFile) + } catch { + print("Failed to save: \(error)") + } + } + } +} diff --git a/CHDataManagement/Page Elements/NavigationBar.swift b/CHDataManagement/Page Elements/NavigationBar.swift new file mode 100644 index 0000000..f7f23da --- /dev/null +++ b/CHDataManagement/Page Elements/NavigationBar.swift @@ -0,0 +1,52 @@ +import Foundation + +struct NavigationBarLink { + + let text: String + + let url: String +} + + +struct NavigationBarData { + + let navigationIconPath: String + + let iconDescription: String + + let navigationItems: [NavigationBarLink] +} + + +struct NavigationBar { + + let data: NavigationBarData + + init(data: NavigationBarData) { + self.data = data + } + + private var items: [NavigationBarLink] { + data.navigationItems + } + + var content: String { + var result = "" // Close nav-center, navbar + return result + } +} diff --git a/CHDataManagement/Page Elements/PageHead.swift b/CHDataManagement/Page Elements/PageHead.swift index f9e1a47..ca2f645 100644 --- a/CHDataManagement/Page Elements/PageHead.swift +++ b/CHDataManagement/Page Elements/PageHead.swift @@ -7,6 +7,8 @@ struct PageHead { let description: String + let additionalHeaders: String + var content: String { """ @@ -14,7 +16,7 @@ struct PageHead { \(title) - + \(additionalHeaders) """ diff --git a/CHDataManagement/Page Elements/PostFeedPageNavigation.swift b/CHDataManagement/Page Elements/PostFeedPageNavigation.swift new file mode 100644 index 0000000..3406b74 --- /dev/null +++ b/CHDataManagement/Page Elements/PostFeedPageNavigation.swift @@ -0,0 +1,88 @@ +import Foundation + +struct PostFeedPageNavigation { + + let language: ContentLanguage + + let currentPage: Int + + let numberOfPages: Int + + init(currentPage: Int, numberOfPages: Int, language: ContentLanguage) { + self.currentPage = currentPage + self.numberOfPages = numberOfPages + self.language = language + } + + private func pageLink(_ page: Int) -> String { + guard page > 1 else { return "href='/feed'" } + return "href='/feed-\(page)'" + } + + private func previousText() -> String { + switch language { + case .english: + return "Previous" + case .german: + return "Zurück" + } + } + + private func addPreviousButton(to result: inout String) { + if currentPage == 1 { + // Disable the previous button if we are on the first page + result += "" + } + + private func addNextButton(to result: inout String) { + if currentPage == numberOfPages { + // Disable the previous button if we are on the first page + result += "" + } + + private func addLink(page: Int, to result: inout String) { + result += "\(page)" + } + + var content: String { + var result = "" // Close pagination + return result + } +} diff --git a/CHDataManagement/Pages/Feed.swift b/CHDataManagement/Pages/Feed.swift deleted file mode 100644 index a00c455..0000000 --- a/CHDataManagement/Pages/Feed.swift +++ /dev/null @@ -1,79 +0,0 @@ -import Foundation - -struct FeedNavigationLink { - - let text: String - - let url: String -} - -struct Feed { - - private let navigationIconPath = "/assets/icons/ch.svg" - - let language: ContentLanguage - - let title: String - - let description: String - - let iconDescription: String - - let navigationItems: [FeedNavigationLink] - - let posts: [FeedEntryData] - - var content: String { - #warning("TODO: Split feed into multiple pages") - var result = "" - result += "" - let head = PageHead( - title: title, - description: description) - result += head.content - result += "" - addNavbar(to: &result) - result += "
" - for post in posts { - FeedEntry(data: post) - .addContent(to: &result) - } - - addSwiperInits(to: &result) - result += "
" // Close content - return result - } - - #warning("TODO: Set correct navigation links and texts") - private func addNavbar(to result: inout String) { - result += "" // Close nav-center, navbar - } - - private func addSwiperInits(to result: inout String) { - if posts.contains(where: { $0.images.count > 1 }) { - result += "" - } - } -} diff --git a/CHDataManagement/Pages/GenericPage.swift b/CHDataManagement/Pages/GenericPage.swift new file mode 100644 index 0000000..4820eb6 --- /dev/null +++ b/CHDataManagement/Pages/GenericPage.swift @@ -0,0 +1,36 @@ +import Foundation + +struct GenericPage { + + let language: ContentLanguage + + let title: String + + let description: String + + let data: NavigationBarData + + let additionalHeaders: String + + let insertedContent: (inout String) -> Void + + init(language: ContentLanguage, title: String, description: String, data: NavigationBarData, additionalHeaders: String, insertedContent: @escaping (inout String) -> Void) { + self.language = language + self.title = title + self.description = description + self.data = data + self.additionalHeaders = additionalHeaders + self.insertedContent = insertedContent + } + var content: String { + var result = "" + result += "" + result += PageHead(title: title, description: description, additionalHeaders: additionalHeaders).content + result += "" + result += NavigationBar(data: data).content + result += "
" + insertedContent(&result) + result += "
" // Close content + return result + } +} diff --git a/CHDataManagement/Pages/PageInFeed.swift b/CHDataManagement/Pages/PageInFeed.swift new file mode 100644 index 0000000..60c503f --- /dev/null +++ b/CHDataManagement/Pages/PageInFeed.swift @@ -0,0 +1,58 @@ +import Foundation + +struct PageInFeed { + + private let swiperStyleSheetPath = "/assets/swiper/swiper-bundle.min.css" + + private let swiperJsPath = "/assets/swiper/swiper-bundle.min.js" + + let language: ContentLanguage + + let title: String + + let description: String + + let navigationBarData: NavigationBarData + + let pageNumber: Int + + let totalPages: Int + + let posts: [FeedEntryData] + + private var swiperHeader: String { + "" + } + + private var swiperIsNeeded: Bool { + posts.contains(where: { $0.images.count > 1 }) + } + + private var headers: String { + swiperIsNeeded ? swiperHeader : "" + } + + var content: String { + GenericPage(language: language, title: title, description: description, data: navigationBarData, additionalHeaders: headers) { content in + for post in posts { + FeedEntry(data: post) + .addContent(to: &content) + } + content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content + if swiperIsNeeded { + addSwiperInits(to: &content) + } + }.content + } + + private func addSwiperInits(to result: inout String) { + result += "" + } +} diff --git a/CHDataManagement/Preview Content/WebsiteGenerator+Mock.swift b/CHDataManagement/Preview Content/WebsiteGenerator+Mock.swift new file mode 100644 index 0000000..27abf99 --- /dev/null +++ b/CHDataManagement/Preview Content/WebsiteGenerator+Mock.swift @@ -0,0 +1,19 @@ +import Foundation + +extension WebsiteGeneratorConfiguration { + + static let english = WebsiteGeneratorConfiguration( + language: .english, + postsPerPage: 20, + postFeedTitle: "Posts", + postFeedDescription: "The most recent posts on christophhagen.de", + postFeedUrlPrefix: "feed") + + static let german = WebsiteGeneratorConfiguration( + language: .german, + postsPerPage: 20, + postFeedTitle: "Beiträge", + postFeedDescription: "Die neusten Beiträge auf christophhagen.de", + postFeedUrlPrefix: "beiträge") + +} diff --git a/CHDataManagement/Storage/FileOnDisk.swift b/CHDataManagement/Storage/Model/FileOnDisk.swift similarity index 100% rename from CHDataManagement/Storage/FileOnDisk.swift rename to CHDataManagement/Storage/Model/FileOnDisk.swift diff --git a/CHDataManagement/Storage/FileType.swift b/CHDataManagement/Storage/Model/FileType.swift similarity index 100% rename from CHDataManagement/Storage/FileType.swift rename to CHDataManagement/Storage/Model/FileType.swift diff --git a/CHDataManagement/Storage/PageFile.swift b/CHDataManagement/Storage/Model/PageFile.swift similarity index 100% rename from CHDataManagement/Storage/PageFile.swift rename to CHDataManagement/Storage/Model/PageFile.swift diff --git a/CHDataManagement/Storage/PageOnDisk.swift b/CHDataManagement/Storage/Model/PageOnDisk.swift similarity index 100% rename from CHDataManagement/Storage/PageOnDisk.swift rename to CHDataManagement/Storage/Model/PageOnDisk.swift diff --git a/CHDataManagement/Storage/PostFile.swift b/CHDataManagement/Storage/Model/PostFile.swift similarity index 100% rename from CHDataManagement/Storage/PostFile.swift rename to CHDataManagement/Storage/Model/PostFile.swift diff --git a/CHDataManagement/Storage/TagFile.swift b/CHDataManagement/Storage/Model/TagFile.swift similarity index 100% rename from CHDataManagement/Storage/TagFile.swift rename to CHDataManagement/Storage/Model/TagFile.swift diff --git a/CHDataManagement/Storage/WebsiteDataFile.swift b/CHDataManagement/Storage/Model/WebsiteDataFile.swift similarity index 100% rename from CHDataManagement/Storage/WebsiteDataFile.swift rename to CHDataManagement/Storage/Model/WebsiteDataFile.swift diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index faeda47..7a18d4d 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -13,6 +13,9 @@ import Foundation */ final class Storage { + static let outputPathBookmarkKey = "outputPathBookmark" + static let contentPathBookmarkKey = "contentPathBookmark" + private(set) var baseFolder: URL private let encoder = JSONEncoder() @@ -252,7 +255,7 @@ final class Storage { private func deleteFiles(in folder: URL, notIn fileSet: Set) throws { let filesToDelete = try files(in: folder) .filter { !fileSet.contains($0.lastPathComponent) } - + for file in filesToDelete { try fm.removeItem(at: file) print("Deleted \(file.path())") diff --git a/CHDataManagement/Views/Settings/SettingsView.swift b/CHDataManagement/Views/Settings/SettingsView.swift index bcb5b9b..b450fa9 100644 --- a/CHDataManagement/Views/Settings/SettingsView.swift +++ b/CHDataManagement/Views/Settings/SettingsView.swift @@ -23,6 +23,9 @@ struct SettingsView: View { @State private var showTagPicker = false + @State + private var isGeneratingWebsite = false + var body: some View { ScrollView { VStack(alignment: .leading) { @@ -63,8 +66,16 @@ struct SettingsView: View { LocalizedSettingsView(settings: content.websiteData.localized(in: language)) Text("Feed") .font(.headline) - Button(action: generateFeed) { - Text("Generate") + HStack { + Button(action: generateFeed) { + Text("Generate") + } + if isGeneratingWebsite { + ProgressView() + .progressViewStyle(.circular) + .frame(height: 25) + } + Spacer() } } .padding() @@ -86,7 +97,7 @@ struct SettingsView: View { private func selectContentFolder() { isSelectingContentFolder = true //showFileImporter = true - guard let url = savePanelUsingOpenPanel(key: "contentPathBookmark") else { + guard let url = savePanelUsingOpenPanel(key: Storage.contentPathBookmarkKey) else { return } self.contentPath = url.path() @@ -94,8 +105,7 @@ struct SettingsView: View { private func selectOutputFolder() { isSelectingContentFolder = false - //showFileImporter = true - guard let url = savePanelUsingOpenPanel(key: "outputPathBookmark") else { + guard let url = savePanelUsingOpenPanel(key: Storage.outputPathBookmarkKey) else { return } self.outputPath = url.path() @@ -115,10 +125,10 @@ struct SettingsView: View { .replacingOccurrences(of: "file://", with: "") if isSelectingContentFolder { self.contentPath = path - saveSecurityScopedBookmark(folder, key: "contentPathBookmark") + saveSecurityScopedBookmark(folder, key: Storage.contentPathBookmarkKey) } else { self.outputPath = path - saveSecurityScopedBookmark(folder, key: "outputPathBookmark") + saveSecurityScopedBookmark(folder, key: Storage.outputPathBookmarkKey) } } @@ -135,8 +145,23 @@ struct SettingsView: View { print("Missing output folder") return } + isGeneratingWebsite = true + DispatchQueue.global(qos: .userInitiated).async { + let generator = WebsiteGenerator( + content: content, + configuration: configuration) + generator.generateWebsite() + DispatchQueue.main.async { + isGeneratingWebsite = false + } + } + } - content.generateFeed(for: language, bookmarkKey: "outputPathBookmark") + private var configuration: WebsiteGeneratorConfiguration { + switch language { + case .english: return .english + case .german: return .german + } } func savePanelUsingOpenPanel(key: String) -> URL? {