From 1e67a998667b446073b3c8e95e4722cedf8344f7 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Sun, 15 Dec 2024 21:20:12 +0100 Subject: [PATCH] Add tag overview, improve assets --- CHDataManagement.xcodeproj/project.pbxproj | 66 +++++++-- .../Generator/FeedPageGenerator.swift | 89 ++++++++++++ .../Generator/HeaderElement.swift | 33 +++++ .../Generator/LocalizedWebsiteGenerator.swift | 11 +- .../Page Content/AudioPlayerCommand.swift | 2 +- .../Generator/PageContentGenerator.swift | 35 +++-- .../Generator/PageGenerator.swift | 19 ++- .../Generator/PostListPageGenerator.swift | 18 +-- .../Generator/RequiredHeaders.swift | 46 +++++-- CHDataManagement/Main/MainView.swift | 2 + CHDataManagement/Model/Content+Load.swift | 36 +++-- CHDataManagement/Model/Content+Save.swift | 35 ++--- .../Model/Content+Validation.swift | 7 +- CHDataManagement/Model/Content.swift | 9 ++ CHDataManagement/Model/ContentLanguage.swift | 18 +++ CHDataManagement/Model/FileResource.swift | 57 ++------ CHDataManagement/Model/Item.swift | 18 --- CHDataManagement/Model/Item/Item.swift | 55 ++++++++ CHDataManagement/Model/Item/ItemId.swift | 38 +++++ CHDataManagement/Model/Item/ItemType.swift | 35 +++++ .../Model/Item/LocalizedItem.swift | 19 +++ .../Model/Item/TagOverviewPage.swift | 99 +++++++++++++ CHDataManagement/Model/Page.swift | 53 ++----- .../Model/Settings/PageSettings.swift | 39 ++++-- .../Model/Settings/PathSettings.swift | 14 ++ .../Model/Settings/PostSettings.swift | 23 +++- .../Model/Settings/Settings.swift | 8 +- CHDataManagement/Model/Tag.swift | 66 ++++----- .../AdditionalPageHeaders.swift | 21 --- CHDataManagement/Page Elements/PageHead.swift | 39 ++---- CHDataManagement/Pages/ContentPage.swift | 6 +- CHDataManagement/Pages/GenericPage.swift | 8 +- CHDataManagement/Pages/PageInFeed.swift | 71 ---------- .../Preview Content/Content+Mock.swift | 1 + .../Preview Content/Tag+Mock.swift | 5 + .../Preview Content/WebsiteData+Mock.swift | 6 +- .../Model/Settings/PageSettingsFile.swift | 21 ++- .../Model/Settings/PathSettingsFile.swift | 12 +- .../Model/Settings/PostSettingsFile.swift | 11 +- .../Storage/Model/Settings/SettingsFile.swift | 11 +- .../Storage/Model/TagOverviewFile.swift | 31 +++++ CHDataManagement/Storage/Storage.swift | 44 ++++++ .../Views/Files/FileDetailView.swift | 4 +- .../Views/Files/FileListView.swift | 22 ++- .../Views/Files/FileSelectionView.swift | 23 +++- .../Views/Generic/DetailTitle.swift | 19 +++ .../Views/Generic/FilePropertyView.swift | 34 +++++ .../Views/Generic/IntegerPropertyView.swift | 23 ++++ .../Views/ItemSelectionView.swift | 111 +++++++++++++++ .../Content/Pages/PageIssueView.swift | 4 +- .../Settings/GenerationContentView.swift | 2 +- .../Views/Settings/GenerationDetailView.swift | 2 + .../Settings/NavigationBarSettingsView.swift | 29 ++-- .../Settings/PageSettingsDetailView.swift | 78 +++++------ .../Views/Settings/PathSettingsView.swift | 8 ++ .../Views/Settings/PostFeedSettingsView.swift | 50 +++---- .../Views/Settings/SettingsSection.swift | 4 +- .../Settings/TagOverviewDetailView.swift | 130 ++++++++++++++++++ CHDataManagement/Views/Tags/AddTagView.swift | 1 + 59 files changed, 1301 insertions(+), 480 deletions(-) create mode 100644 CHDataManagement/Generator/FeedPageGenerator.swift create mode 100644 CHDataManagement/Generator/HeaderElement.swift delete mode 100644 CHDataManagement/Model/Item.swift create mode 100644 CHDataManagement/Model/Item/Item.swift create mode 100644 CHDataManagement/Model/Item/ItemId.swift create mode 100644 CHDataManagement/Model/Item/ItemType.swift create mode 100644 CHDataManagement/Model/Item/LocalizedItem.swift create mode 100644 CHDataManagement/Model/Item/TagOverviewPage.swift delete mode 100644 CHDataManagement/Page Elements/ContentElements/AdditionalPageHeaders.swift delete mode 100644 CHDataManagement/Pages/PageInFeed.swift create mode 100644 CHDataManagement/Storage/Model/TagOverviewFile.swift create mode 100644 CHDataManagement/Views/Generic/DetailTitle.swift create mode 100644 CHDataManagement/Views/Generic/FilePropertyView.swift create mode 100644 CHDataManagement/Views/Generic/IntegerPropertyView.swift create mode 100644 CHDataManagement/Views/ItemSelectionView.swift create mode 100644 CHDataManagement/Views/Settings/TagOverviewDetailView.swift diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 2f10eb5..cc193d3 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -25,6 +25,17 @@ E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; }; E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */; }; E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */; }; + E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */; }; + E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */; }; + E22990192D0E3546009F8D77 /* ItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990182D0E3546009F8D77 /* ItemType.swift */; }; + E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229901D2D0E4362009F8D77 /* LocalizedItem.swift */; }; + E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */; }; + E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990212D0ED129009F8D77 /* TagOverviewFile.swift */; }; + E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990232D0EDBD0009F8D77 /* HeaderElement.swift */; }; + E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990252D0F5822009F8D77 /* FilePropertyView.swift */; }; + E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */; }; + E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990292D0F5A10009F8D77 /* DetailTitle.swift */; }; + E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902B2D0F6FC0009F8D77 /* ItemId.swift */; }; E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; }; E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; }; E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.swift */; }; @@ -109,7 +120,6 @@ E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */; }; E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */; }; E29D31852D0AE8EE0051B7F4 /* RequiredHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */; }; - E29D31872D0AE9DE0051B7F4 /* AdditionalPageHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */; }; E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */; }; E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318A2D0B07E60051B7F4 /* ContentBox.swift */; }; E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */; }; @@ -165,7 +175,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 /* PageInFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* PageInFeed.swift */; }; + E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.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 */; }; @@ -196,6 +206,17 @@ E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = ""; }; E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Mock.swift"; sourceTree = ""; }; E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostFeedSettingsView.swift; sourceTree = ""; }; + E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSelectionView.swift; sourceTree = ""; }; + E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewPage.swift; sourceTree = ""; }; + E22990182D0E3546009F8D77 /* ItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = ""; }; + E229901D2D0E4362009F8D77 /* LocalizedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedItem.swift; sourceTree = ""; }; + E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewDetailView.swift; sourceTree = ""; }; + E22990212D0ED129009F8D77 /* TagOverviewFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewFile.swift; sourceTree = ""; }; + E22990232D0EDBD0009F8D77 /* HeaderElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderElement.swift; sourceTree = ""; }; + E22990252D0F5822009F8D77 /* FilePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePropertyView.swift; sourceTree = ""; }; + E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerPropertyView.swift; sourceTree = ""; }; + E22990292D0F5A10009F8D77 /* DetailTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTitle.swift; sourceTree = ""; }; + E229902B2D0F6FC0009F8D77 /* ItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemId.swift; sourceTree = ""; }; E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = ""; }; E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = ""; }; @@ -276,7 +297,6 @@ E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsIcons.swift; sourceTree = ""; }; E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedPageLink.swift; sourceTree = ""; }; E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredHeaders.swift; sourceTree = ""; }; - E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalPageHeaders.swift; sourceTree = ""; }; E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelViewer.swift; sourceTree = ""; }; E29D318A2D0B07E60051B7F4 /* ContentBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBox.swift; sourceTree = ""; }; E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsContentView.swift; sourceTree = ""; }; @@ -329,7 +349,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 /* PageInFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInFeed.swift; sourceTree = ""; }; + E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPageGenerator.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 = ""; }; @@ -361,6 +381,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + E229901A2D0E3F09009F8D77 /* Item */ = { + isa = PBXGroup; + children = ( + E229902B2D0F6FC0009F8D77 /* ItemId.swift */, + E229901D2D0E4362009F8D77 /* LocalizedItem.swift */, + E29D31A22D0CC98B0051B7F4 /* Item.swift */, + E22990182D0E3546009F8D77 /* ItemType.swift */, + E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */, + ); + path = Item; + sourceTree = ""; + }; E25DA5112CFF001900AEF16D /* Model */ = { isa = PBXGroup; children = ( @@ -371,6 +403,7 @@ E21850182CEE561B0090B18B /* PageOnDisk.swift */, E2A37D142CE68BEA0000979F /* PostFile.swift */, E2A37D162CE73F170000979F /* TagFile.swift */, + E22990212D0ED129009F8D77 /* TagOverviewFile.swift */, ); path = Model; sourceTree = ""; @@ -402,6 +435,7 @@ E25DA5782D01C56200AEF16D /* Generator */ = { isa = PBXGroup; children = ( + E22990232D0EDBD0009F8D77 /* HeaderElement.swift */, E29D31B62D0DAC030051B7F4 /* Page Content */, E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */, E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */, @@ -410,6 +444,7 @@ E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */, E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */, E25DA5982D02401A00AEF16D /* PageGenerator.swift */, + E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */, E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */, E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */, E29D31252D0370A50051B7F4 /* VideoOption.swift */, @@ -439,7 +474,6 @@ E29D31932D0B7D250051B7F4 /* SvgImage.swift */, E29D318A2D0B07E60051B7F4 /* ContentBox.swift */, E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */, - E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */, E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */, E29D31272D0371870051B7F4 /* ContentPageVideo.swift */, E29D31232D0366820051B7F4 /* TagList.swift */, @@ -539,6 +573,7 @@ E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */, E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */, E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */, + E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */, ); path = Settings; sourceTree = ""; @@ -554,6 +589,9 @@ E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */, E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */, E2A21C0F2CB18B390060935B /* FlowHStack.swift */, + E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */, + E22990252D0F5822009F8D77 /* FilePropertyView.swift */, + E22990292D0F5A10009F8D77 /* DetailTitle.swift */, ); path = Generic; sourceTree = ""; @@ -599,7 +637,7 @@ E2B85F392C428F020047CD0C /* Model */ = { isa = PBXGroup; children = ( - E29D31A22D0CC98B0051B7F4 /* Item.swift */, + E229901A2D0E3F09009F8D77 /* Item */, E25DA5812D01C79800AEF16D /* Types */, E25DA53B2D0042EA00AEF16D /* Settings */, E2E06DFA2CA4A6570019C2AF /* Content.swift */, @@ -626,7 +664,6 @@ children = ( E25DA5962D023F9900AEF16D /* ContentPage.swift */, E25DA51C2CFF135B00AEF16D /* GenericPage.swift */, - E2B85F3C2C4293F80047CD0C /* PageInFeed.swift */, ); path = Pages; sourceTree = ""; @@ -651,6 +688,7 @@ E2B85F462C42C7CA0047CD0C /* Views */ = { isa = PBXGroup; children = ( + E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */, E2A21C372CB9A4F10060935B /* Generic */, E2B85F4B2C4B8B7F0047CD0C /* Posts */, E2A21C322CB5BCAC0060935B /* Pages */, @@ -835,6 +873,7 @@ buildActionMask = 2147483647; files = ( E29D31242D0366860051B7F4 /* TagList.swift in Sources */, + E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */, E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */, E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */, E218502B2CF790B30090B18B /* PostContentView.swift in Sources */, @@ -848,7 +887,6 @@ E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */, E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */, - E29D31872D0AE9DE0051B7F4 /* AdditionalPageHeaders.swift in Sources */, E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */, E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */, @@ -881,7 +919,8 @@ E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */, E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */, E25DA5972D023F9F00AEF16D /* ContentPage.swift in Sources */, - E2B85F3D2C4293F80047CD0C /* PageInFeed.swift in Sources */, + E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */, + E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */, E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */, E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */, E29D31492D0489BB0051B7F4 /* AddFileView.swift in Sources */, @@ -896,14 +935,17 @@ E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, + E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */, E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */, E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */, E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */, E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */, + E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */, E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */, E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */, E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */, E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */, + E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */, E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */, @@ -923,6 +965,7 @@ E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */, E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */, + E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, @@ -933,6 +976,7 @@ E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */, E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */, E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, + E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */, E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */, @@ -958,12 +1002,16 @@ E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */, E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */, E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */, + E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */, E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, + E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */, E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */, + E22990192D0E3546009F8D77 /* ItemType.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, + E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */, E29D315F2D06D6F30051B7F4 /* CodeFileType.swift in Sources */, E2A37D0E2CE527070000979F /* Storage.swift in Sources */, E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */, diff --git a/CHDataManagement/Generator/FeedPageGenerator.swift b/CHDataManagement/Generator/FeedPageGenerator.swift new file mode 100644 index 0000000..7b68daf --- /dev/null +++ b/CHDataManagement/Generator/FeedPageGenerator.swift @@ -0,0 +1,89 @@ +import Foundation + +final class FeedPageGenerator { + + let content: Content + + init(content: Content) { + self.content = content + } + + func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] { + content.settings.navigationItems.map { + .init(text: $0.title(in: language), + url: $0.absoluteUrl(in: language)) + } + } + + var swiperIncludes: [HeaderElement] { + var result = [HeaderElement]() + if let swiperCss = content.settings.posts.swiperCssFile { + result.append(.css(swiperCss)) + } else { + #warning("Add warning message") + } + if let swiperJs = content.settings.posts.swiperJsFile { + result.append(.js(file: swiperJs, defer: true)) + } else { + #warning("Add warning message") + } + return result + } + + var defaultHeaders: [HeaderElement] { + if let header = content.settings.posts.defaultCssFile { + return [.css(header)] + } else { + #warning("Add warning message") + return [] + } + } + + func generatePage(language: ContentLanguage, + posts: [FeedEntryData], + title: String, + description: String, + showTitle: Bool, + pageNumber: Int, + totalPages: Int) -> String { + var headers = defaultHeaders + var footer = "" + if posts.contains(where: { $0.images.count > 1 }) { + headers += swiperIncludes + footer = swiperInitScript(posts: posts) + } + + let page = GenericPage( + language: language, + title: title, + description: description, + links: navigationBar(in: language), + headers: headers, + additionalFooter: footer) { content in + if showTitle { + content += "

\(title)

" + } + for post in posts { + content += FeedEntry(data: post).content + } + if totalPages > 1 { + content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content + } + + } + return page.content + } + + func swiperInitScript(posts: [FeedEntryData]) -> String { + var result = "" + return result + + } +} diff --git a/CHDataManagement/Generator/HeaderElement.swift b/CHDataManagement/Generator/HeaderElement.swift new file mode 100644 index 0000000..c00bf6c --- /dev/null +++ b/CHDataManagement/Generator/HeaderElement.swift @@ -0,0 +1,33 @@ + +enum HeaderElement { + case css(FileResource) + case js(file: FileResource, defer: Bool) + case jsModule(FileResource) + case title(String) + case description(String) + case charset + case viewport +} + +extension HeaderElement { + + var content: String { + switch self { + case .css(let file): + return "" + case .js(let file, let deferred): + let deferText = deferred ? " defer" : "" + return "" + case .jsModule(let file): + return "" + case .title(let title): + return "\(title)" + case .description(let description): + return "" + case .charset: + return "" + case .viewport: + return "" + } + } +} diff --git a/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift b/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift index 19594ea..2def2d1 100644 --- a/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift +++ b/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift @@ -23,9 +23,9 @@ final class LocalizedWebsiteGenerator { private let imageGenerator: ImageGenerator private var navigationBarLinks: [NavigationBar.Link] { - content.settings.navigationTags.map { - let localized = $0.localized(in: language) - return .init(text: localized.name, url: content.absoluteUrlToTag($0, language: language)) + content.settings.navigationItems.map { + .init(text: $0.title(in: language), + url: $0.absoluteUrl(in: language)) } } @@ -46,6 +46,7 @@ final class LocalizedWebsiteGenerator { return false } #warning("Generate content pages") + #warning("Generate tag overview page") guard generateTagPages() else { return false } @@ -94,7 +95,7 @@ final class LocalizedWebsiteGenerator { } private func generatePagesFolderIfNeeded() -> Bool { - let relativePath = content.settings.pages.pageUrlPrefix + let relativePath = content.settings.paths.pagesOutputFolderPath return content.storage.write(in: .outputPath) { folder in let outputFile = folder.appendingPathComponent(relativePath, isDirectory: true) @@ -130,7 +131,7 @@ final class LocalizedWebsiteGenerator { return true } - let path = page.absoluteUrl(for: language) + ".html" + let path = page.absoluteUrl(in: language) + ".html" guard save(content, to: path) else { print("Failed to save page") return false diff --git a/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift b/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift index 275f337..00ca317 100644 --- a/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift +++ b/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift @@ -78,7 +78,7 @@ struct AudioPlayerCommandProcessor: CommandProcessor { let footerScript = AudioPlayerScript(items: amplitude).content results.requiredFooters.insert(footerScript) results.requiredHeaders.insert(.audioPlayerCss) - results.requiredHeaders.insert(.amplitude) + results.requiredHeaders.insert(.audioPlayerJs) results.requiredIcons.formUnion([ .audioPlayerClose, diff --git a/CHDataManagement/Generator/PageContentGenerator.swift b/CHDataManagement/Generator/PageContentGenerator.swift index 7de945d..be7588c 100644 --- a/CHDataManagement/Generator/PageContentGenerator.swift +++ b/CHDataManagement/Generator/PageContentGenerator.swift @@ -101,7 +101,7 @@ final class PageContentParser { return markdown.between("[", and: "]") } results.linkedPages.insert(page) - let pagePath = page.absoluteUrl(for: language) + let pagePath = page.absoluteUrl(in: language) return html.replacingOccurrences(of: textToChange, with: pagePath) } @@ -119,12 +119,15 @@ final class PageContentParser { return html.replacingOccurrences(of: textToChange, with: tagPath) } - private func handleHTML(_: String, markdown: Substring) -> String { - let result = String(markdown) - findImages(in: result) - findLinks(in: result) - findSourceSets(in: result) - return result + private func handleHTML(html: String, _: Substring) -> String { + findResourcesInHtml(html: html) + return html + } + + private func findResourcesInHtml(html: String) { + findImages(in: html) + findLinks(in: html) + findSourceSets(in: html) } private func findImages(in markdown: String) { @@ -298,7 +301,7 @@ final class PageContentParser { results.files.insert(image) let caption = arguments.count == 2 ? arguments[1] : nil - let altText = image.getDescription(for: language) + let altText = image.localized(in: language) let path = image.absoluteUrl @@ -403,7 +406,9 @@ final class PageContentParser { results.missing(file: fileId, markdown: markdown) return "" } - return file.textContent() + let content = file.textContent() + findResourcesInHtml(html: content) + return content } /** @@ -439,7 +444,7 @@ final class PageContentParser { } let localized = page.localized(in: language) - let url = page.absoluteUrl(for: language) + let url = page.absoluteUrl(in: language) let title = localized.linkPreviewTitle ?? localized.title let description = localized.linkPreviewDescription ?? "" @@ -450,7 +455,7 @@ final class PageContentParser { return RelatedPageLink.Image( url: image.absoluteUrl, - description: image.getDescription(for: language), + description: image.localized(in: language), size: size) } @@ -478,7 +483,7 @@ final class PageContentParser { } let localized = tag.localized(in: language) - let url = tag.absoluteUrl(for: language) + let url = tag.absoluteUrl(in: language) let title = localized.name let description = localized.description ?? "" @@ -489,7 +494,7 @@ final class PageContentParser { return RelatedPageLink.Image( url: image.absoluteUrl, - description: image.getDescription(for: language), + description: image.localized(in: language), size: size) } @@ -522,7 +527,7 @@ final class PageContentParser { results.files.insert(file) results.requiredHeaders.insert(.modelViewer) - let description = file.getDescription(for: language) + let description = file.localized(in: language) return ModelViewer(file: file.absoluteUrl, description: description).content } @@ -554,7 +559,7 @@ final class PageContentParser { return PartialSvgImage( imagePath: image.absoluteUrl, - altText: image.getDescription(for: language), + altText: image.localized(in: language), x: x, y: y, width: partWidth, diff --git a/CHDataManagement/Generator/PageGenerator.swift b/CHDataManagement/Generator/PageGenerator.swift index e3faf4a..05b6880 100644 --- a/CHDataManagement/Generator/PageGenerator.swift +++ b/CHDataManagement/Generator/PageGenerator.swift @@ -12,6 +12,18 @@ final class PageGenerator { self.navigationBarLinks = navigationBarLinks } + func makeHeaders(requiredItems: [HeaderFile]) -> [HeaderElement] { + var result = [HeaderElement]() + for item in requiredItems { + guard let header = item.header(content: content) else { + #warning("Add warning on missing file assignment") + continue + } + result.append(header) + } + return result + } + func generate(page: Page, language: ContentLanguage) throws -> (page: String, results: PageGenerationResults) { let contentGenerator = PageContentParser( content: content, @@ -30,9 +42,8 @@ final class PageGenerator { url: content.absoluteUrlToTag(tag, language: language)) } - let headers = AdditionalPageHeaders( - headers: contentGenerator.results.requiredHeaders, - assetPath: content.settings.pages.javascriptFilesPath) + let headers = makeHeaders(requiredItems: contentGenerator.results.requiredHeaders.sorted()) + let fullPage = ContentPage( language: language, dateString: page.dateText(in: language), @@ -42,7 +53,7 @@ final class PageGenerator { description: localized.linkPreviewDescription ?? "", navigationBarLinks: navigationBarLinks, pageContent: pageContent, - headers: headers.content, + headers: headers, footers: contentGenerator.results.requiredFooters.sorted(), icons: contentGenerator.results.requiredIcons) .content diff --git a/CHDataManagement/Generator/PostListPageGenerator.swift b/CHDataManagement/Generator/PostListPageGenerator.swift index f682e4f..61d6993 100644 --- a/CHDataManagement/Generator/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/PostListPageGenerator.swift @@ -8,6 +8,7 @@ final class PostListPageGenerator { private let imageGenerator: ImageGenerator + #warning("Get from settings") private let navigationBarLinks: [NavigationBar.Link] private let showTitle: Bool @@ -62,7 +63,7 @@ final class PostListPageGenerator { let linkUrl = post.linkedPage.map { FeedEntryData.Link( - url: $0.absoluteUrl(for: language), + url: $0.absoluteUrl(in: language), text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings } @@ -81,16 +82,17 @@ final class PostListPageGenerator { images: localized.images.map(createImageSet)) } - let feed = PageInFeed( + let feedPageGenerator = FeedPageGenerator(content: content) + + let fileContent = feedPageGenerator.generatePage( language: language, + posts: posts, title: pageTitle, - showTitle: showTitle, description: pageDescription, - navigationBarLinks: bar, + showTitle: showTitle, pageNumber: pageIndex, - totalPages: pageCount, - posts: posts) - let fileContent = feed.content + totalPages: pageCount) + if pageIndex == 1 { return save(fileContent, to: "\(pageUrlPrefix).html") } else { @@ -107,7 +109,7 @@ final class PostListPageGenerator { rawImagePath: image.absoluteUrl, width: Int(mainContentMaximumWidth), height: Int(mainContentMaximumWidth), - altText: image.getDescription(for: language)) + altText: image.localized(in: language)) } private func save(_ content: String, to relativePath: String) -> Bool { diff --git a/CHDataManagement/Generator/RequiredHeaders.swift b/CHDataManagement/Generator/RequiredHeaders.swift index 90b15f2..b3c291a 100644 --- a/CHDataManagement/Generator/RequiredHeaders.swift +++ b/CHDataManagement/Generator/RequiredHeaders.swift @@ -1,22 +1,46 @@ -enum HeaderFile: String { +enum HeaderFile: Int { - case codeHightlighting = "highlight.min.js" - - case modelViewer = "model-viewer.min.js" + case codeHightlighting = 4 - case audioPlayerCss = "audio-player.css" + case modelViewer = 3 - case amplitude = "amplitude.min.js" + /// CSS File to style the audio player + case audioPlayerCss = 1 - var asModule: Bool { + /// JavaScript file for the audio player + case audioPlayerJs = 2 + + func header(content: Content) -> HeaderElement? { switch self { - case .codeHightlighting: return false - case .modelViewer: return true - case .amplitude: return false - case .audioPlayerCss: return false + case .codeHightlighting: + if let file = content.settings.pages.codeHighlightingJsFile { + return HeaderElement.js(file: file, defer: true) + } + case .modelViewer: + if let file = content.settings.pages.modelViewerJsFile { + return HeaderElement.jsModule(file) + } + case .audioPlayerCss: + if let file = content.settings.pages.audioPlayerCssFile { + return .css(file) + } + case .audioPlayerJs: + if let file = content.settings.pages.audioPlayerJsFile { + return .js(file: file, defer: true) + } } + return nil } } +extension HeaderFile: Comparable { + + static func < (lhs: HeaderFile, rhs: HeaderFile) -> Bool { + lhs.rawValue < rhs.rawValue + } + + +} + typealias RequiredHeaders = Set diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index 7cc6e2d..0da3cee 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -1,6 +1,7 @@ import SwiftUI import SFSafeSymbols + #warning("Allow selection of pages as navigation bar items") #warning("Show all warnings on page content") #warning("Button to delete file") @@ -12,6 +13,7 @@ import SFSafeSymbols #warning("Replace links to files inside pages when id changes") #warning("Calculate file sizes") #warning("Specify image aspect ratio to prevent page jumps") +#warning("Add version and source url properties to file resources") @main struct MainView: App { diff --git a/CHDataManagement/Model/Content+Load.swift b/CHDataManagement/Model/Content+Load.swift index d57cd6f..1096238 100644 --- a/CHDataManagement/Model/Content+Load.swift +++ b/CHDataManagement/Model/Content+Load.swift @@ -51,6 +51,7 @@ extension Content { let postsData = try storage.loadAllPosts() let fileList = try storage.loadAllFiles() let externalFiles = try storage.loadExternalFileList() + let tagOverviewData = try storage.loadTagOverview() var files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in let descriptions = imageDescriptions[fileId] @@ -77,6 +78,7 @@ extension Content { let tags = tagData.reduce(into: [:]) { (tags, data) in tags[data.key] = Tag( content: self, + id: data.value.id, isVisible: data.value.isVisible, german: convert(data.value.german, images: images), english: convert(data.value.english, images: images)) @@ -102,28 +104,46 @@ extension Content { linkedPage: linkedPage) } + let tagOverview = tagOverviewData.map { file in + TagOverviewPage( + content: self, + german: .init(file: file.german, image: file.german.linkPreviewImage.map { files[$0] }), + english: .init(file: file.english, image: file.english.linkPreviewImage.map { files[$0] })) + } + self.tags = tags.values.sorted() self.pages = pages.values.sorted(ascending: false) { $0.startDate } self.files = files.values.sorted { $0.id } self.posts = posts.sorted(ascending: false) { $0.startDate } - self.settings = makeSettings(settings, tags: tags) + self.tagOverview = tagOverview + self.settings = makeSettings(settings, tags: tags, pages: pages, files: files) } - private func makeSettings(_ settings: SettingsFile, tags: [String : Tag]) -> Settings { + private func makeSettings(_ settings: SettingsFile, tags: [String : Tag], pages: [String : Page], files: [String : FileResource]) -> Settings { - let navigationTags = settings.navigationTags.map { tags[$0]! } + #warning("Notify about missing links") + let navigationItems: [Item] = settings.navigationItems.compactMap { + switch $0.type { + case .tag: + return tags[$0.id] + case .page: + return pages[$0.id] + case .tagOverview: + return tagOverview + default: + return nil + } + } - let posts = PostSettings( - postsPerPage: settings.posts.postsPerPage, - contentWidth: settings.posts.contentWidth) + let posts = PostSettings(file: settings.posts, files: files) - let pages = PageSettings(file: settings.pages) + let pages = PageSettings(file: settings.pages, files: files) let paths = PathSettings(file: settings.paths) return Settings( paths: paths, - navigationTags: navigationTags, + navigationItems: navigationItems, posts: posts, pages: pages, german: .init(file: settings.german), diff --git a/CHDataManagement/Model/Content+Save.swift b/CHDataManagement/Model/Content+Save.swift index 3520a8a..07ee177 100644 --- a/CHDataManagement/Model/Content+Save.swift +++ b/CHDataManagement/Model/Content+Save.swift @@ -18,16 +18,17 @@ extension Content { try storage.save(settings: settings.file) let fileDescriptions: [FileDescriptions] = files.sorted().compactMap { file in - guard !file.englishDescription.isEmpty || !file.germanDescription.isEmpty else { + guard !file.english.isEmpty || !file.german.isEmpty else { return nil } return FileDescriptions( fileId: file.id, - german: file.germanDescription.nonEmpty, - english: file.englishDescription.nonEmpty) + german: file.german.nonEmpty, + english: file.english.nonEmpty) } try storage.save(fileDescriptions: fileDescriptions) + try storage.save(tagOverview: tagOverview?.file) let externalFileList = files.filter { $0.isExternallyStored }.map { $0.id } try storage.save(externalFileList: externalFileList) @@ -130,7 +131,7 @@ extension Settings { var file: SettingsFile { .init( paths: paths.file, - navigationTags: navigationTags.map { $0.id }, + navigationItems: navigationItems.map { .init(type: $0.itemType, id: $0.id) }, posts: posts.file, pages: pages.file, german: german.file, @@ -138,34 +139,14 @@ extension Settings { } } -private extension PathSettings { - - var file: PathSettingsFile { - .init(outputDirectoryPath: outputDirectoryPath, - pagesOutputFolderPath: pagesOutputFolderPath, - imagesOutputFolderPath: imagesOutputFolderPath, - filesOutputFolderPath: filesOutputFolderPath, - videosOutputFolderPath: videosOutputFolderPath, - tagsOutputFolderPath: tagsOutputFolderPath) - } -} - private extension PostSettings { var file: PostSettingsFile { .init(postsPerPage: postsPerPage, - contentWidth: contentWidth) - } -} - -private extension PageSettings { - - var file: PageSettingsFile { - .init(pageUrlPrefix: pageUrlPrefix, contentWidth: contentWidth, - largeImageWidth: largeImageWidth, - pageLinkImageSize: pageLinkImageSize, - javascriptFilesPath: javascriptFilesPath) + swiperCssFile: swiperCssFile?.id, + swiperJsFile: swiperJsFile?.id, + defaultCssFile: defaultCssFile?.id) } } diff --git a/CHDataManagement/Model/Content+Validation.swift b/CHDataManagement/Model/Content+Validation.swift index 6fcfdb5..0afb0f8 100644 --- a/CHDataManagement/Model/Content+Validation.swift +++ b/CHDataManagement/Model/Content+Validation.swift @@ -18,11 +18,16 @@ extension Content { !posts.contains { $0.id == id } } - func isValidIdForTagOrTagOrPost(_ id: String) -> Bool { + func isValidIdForTagOrPageOrPost(_ id: String) -> Bool { id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil } func isValidIdForFile(_ id: String) -> Bool { id.rangeOfCharacter(from: Content.disallowedCharactersInFileIds) == nil } + + func containsTag(withUrlComponent urlComponent: String) -> Bool { + (tagOverview?.contains(urlComponent: urlComponent) ?? false) || + tags.contains { $0.contains(urlComponent: urlComponent) } + } } diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index 40aa7a5..9ee9fb9 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -19,6 +19,12 @@ final class Content: ObservableObject { @Published var files: [FileResource] + @Published + var tagOverview: TagOverviewPage? + + @Published + var results: [ItemId : PageGenerationResults] = [:] + @AppStorage("contentPath") private var storedContentPath: String = "" @@ -38,12 +44,14 @@ final class Content: ObservableObject { pages: [Page], tags: [Tag], files: [FileResource], + tagOverview: TagOverviewPage?, storedContentPath: String) { self.settings = settings self.posts = posts self.pages = pages self.tags = tags self.files = files + self.tagOverview = tagOverview self.storedContentPath = storedContentPath self.contentPath = storedContentPath self.storage = Storage(baseFolder: URL(filePath: storedContentPath)) @@ -64,6 +72,7 @@ final class Content: ObservableObject { self.pages = [] self.tags = [] self.files = [] + self.tagOverview = nil contentPath = storedContentPath do { diff --git a/CHDataManagement/Model/ContentLanguage.swift b/CHDataManagement/Model/ContentLanguage.swift index 004b7d7..1caba02 100644 --- a/CHDataManagement/Model/ContentLanguage.swift +++ b/CHDataManagement/Model/ContentLanguage.swift @@ -14,3 +14,21 @@ extension ContentLanguage: Codable { extension ContentLanguage: CaseIterable { } + +extension ContentLanguage: Hashable { + +} + +extension ContentLanguage: Identifiable { + + var id: String { + rawValue + } +} + +extension ContentLanguage: Comparable { + + static func < (lhs: ContentLanguage, rhs: ContentLanguage) -> Bool { + lhs.rawValue < rhs.rawValue + } +} diff --git a/CHDataManagement/Model/FileResource.swift b/CHDataManagement/Model/FileResource.swift index ce5d64d..4c9c5a5 100644 --- a/CHDataManagement/Model/FileResource.swift +++ b/CHDataManagement/Model/FileResource.swift @@ -5,29 +5,24 @@ final class FileResource: Item { let type: FileType - /// Globally unique id - @Published - var id: String - @Published var isExternallyStored: Bool @Published - var germanDescription: String + var german: String @Published - var englishDescription: String + var english: String @Published var size: CGSize = .zero init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) { - self.id = id self.type = FileType(fileExtension: id.fileExtension) - self.englishDescription = en - self.germanDescription = de + self.english = en + self.german = de self.isExternallyStored = isExternallyStored - super.init(content: content) + super.init(content: content, id: id) } /** @@ -35,18 +30,10 @@ final class FileResource: Item { */ init(resourceImage: String, type: ImageFileType) { self.type = .image(type) - self.id = resourceImage - self.englishDescription = "A test image included in the bundle" - self.germanDescription = "Ein Testbild aus dem Bundle" + self.english = "A test image included in the bundle" + self.german = "Ein Testbild aus dem Bundle" self.isExternallyStored = true - super.init(content: .mock) // TODO: Add images to mock - } - - func getDescription(for language: ContentLanguage) -> String { - switch language { - case .english: return englishDescription - case .german: return germanDescription - } + super.init(content: .mock, id: resourceImage) // TODO: Add images to mock } // MARK: Text @@ -108,6 +95,11 @@ final class FileResource: Item { return makeCleanAbsolutePath(path) } + var assetUrl: String { + let path = content.settings.paths.assetsOutputFolderPath + "/" + id + return makeCleanAbsolutePath(path) + } + private var pathPrefix: String { switch type { @@ -135,27 +127,6 @@ final class FileResource: Item { } } -extension FileResource: Identifiable { +extension FileResource: LocalizedItem { } - -extension FileResource: Equatable { - - static func == (lhs: FileResource, rhs: FileResource) -> Bool { - lhs.id == rhs.id - } -} - -extension FileResource: Hashable { - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - -extension FileResource: Comparable { - - static func < (lhs: FileResource, rhs: FileResource) -> Bool { - lhs.id < rhs.id - } -} diff --git a/CHDataManagement/Model/Item.swift b/CHDataManagement/Model/Item.swift deleted file mode 100644 index e0f61a9..0000000 --- a/CHDataManagement/Model/Item.swift +++ /dev/null @@ -1,18 +0,0 @@ -import Foundation - -class Item: ObservableObject { - - unowned let content: Content - - init(content: Content) { - self.content = content - } - - func makeCleanAbsolutePath(_ path: String) -> String { - "/" + makeCleanRelativePath(path) - } - - func makeCleanRelativePath(_ path: String) -> String { - path.components(separatedBy: "/").filter { !$0.isEmpty }.joined(separator: "/") - } -} diff --git a/CHDataManagement/Model/Item/Item.swift b/CHDataManagement/Model/Item/Item.swift new file mode 100644 index 0000000..df3b66e --- /dev/null +++ b/CHDataManagement/Model/Item/Item.swift @@ -0,0 +1,55 @@ +import Foundation + +class Item: ObservableObject, Identifiable { + + unowned let content: Content + + @Published + var id: String + + init(content: Content, id: String) { + self.content = content + self.id = id + } + + func makeCleanAbsolutePath(_ path: String) -> String { + "/" + makeCleanRelativePath(path) + } + + func makeCleanRelativePath(_ path: String) -> String { + path.components(separatedBy: "/").filter { !$0.isEmpty }.joined(separator: "/") + } + + func title(in language: ContentLanguage) -> String { + fatalError() + } + + func absoluteUrl(in language: ContentLanguage) -> String { + fatalError() + } + + var itemType: ItemType { + fatalError() + } +} + +extension Item: Equatable { + + static func == (lhs: Item, rhs: Item) -> Bool { + lhs.id == rhs.id + } +} + +extension Item: Hashable { + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } +} + +extension Item: Comparable { + + static func < (lhs: Item, rhs: Item) -> Bool { + lhs.id < rhs.id + } +} diff --git a/CHDataManagement/Model/Item/ItemId.swift b/CHDataManagement/Model/Item/ItemId.swift new file mode 100644 index 0000000..5a86f7c --- /dev/null +++ b/CHDataManagement/Model/Item/ItemId.swift @@ -0,0 +1,38 @@ + +struct ItemId { + + let itemId: String + + let language: ContentLanguage + + let itemType: ItemType +} + +extension ItemId: Equatable { + + static func == (lhs: ItemId, rhs: ItemId) -> Bool { + lhs.itemId == rhs.itemId && lhs.language == rhs.language && lhs.itemType == rhs.itemType + } +} + +extension ItemId: Hashable { + + func hash(into hasher: inout Hasher) { + hasher.combine(itemId) + hasher.combine(language) + hasher.combine(itemType) + } +} + +extension ItemId: Comparable { + + static func < (lhs: ItemId, rhs: ItemId) -> Bool { + guard lhs.itemType == rhs.itemType else { + return lhs.itemType < rhs.itemType + } + guard lhs.itemId == rhs.itemId else { + return lhs.itemId < rhs.itemId + } + return lhs.language < rhs.language + } +} diff --git a/CHDataManagement/Model/Item/ItemType.swift b/CHDataManagement/Model/Item/ItemType.swift new file mode 100644 index 0000000..a3f3b12 --- /dev/null +++ b/CHDataManagement/Model/Item/ItemType.swift @@ -0,0 +1,35 @@ + +enum ItemType: String, Codable { + + case post + + case tag + + case page + + case tagOverview + + case file +} + +extension ItemType: Equatable { + +} + +extension ItemType: Hashable { + +} + +extension ItemType: Identifiable { + + var id: String { + rawValue + } +} + +extension ItemType: Comparable { + + static func < (lhs: ItemType, rhs: ItemType) -> Bool { + lhs.rawValue < rhs.rawValue + } +} diff --git a/CHDataManagement/Model/Item/LocalizedItem.swift b/CHDataManagement/Model/Item/LocalizedItem.swift new file mode 100644 index 0000000..3ee80e3 --- /dev/null +++ b/CHDataManagement/Model/Item/LocalizedItem.swift @@ -0,0 +1,19 @@ + +protocol LocalizedItem { + + associatedtype Localized + + var german: Localized { get } + + var english: Localized { get } +} + +extension LocalizedItem { + + func localized(in language: ContentLanguage) -> Localized { + switch language { + case .german: return german + case .english: return english + } + } +} diff --git a/CHDataManagement/Model/Item/TagOverviewPage.swift b/CHDataManagement/Model/Item/TagOverviewPage.swift new file mode 100644 index 0000000..cc6ca45 --- /dev/null +++ b/CHDataManagement/Model/Item/TagOverviewPage.swift @@ -0,0 +1,99 @@ +import Foundation + +final class TagOverviewPage: Item { + + static let id = "all-tags" + + @Published + var german: LocalizedTagOverviewPage + + @Published + var english: LocalizedTagOverviewPage + + + init(content: Content, german: LocalizedTagOverviewPage, english: LocalizedTagOverviewPage) { + self.german = german + self.english = english + super.init(content: content, id: TagOverviewPage.id) + } + + override var itemType: ItemType { + .tagOverview + } + + override func title(in language: ContentLanguage) -> String { + localized(in: language).title + } + + override func absoluteUrl(in language: ContentLanguage) -> String { + makeCleanAbsolutePath(internalPath(for: language)) + } + + func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String { + makeCleanRelativePath(internalPath(for: language)) + } + + private func internalPath(for language: ContentLanguage) -> String { + content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlString + } + + func contains(urlComponent: String) -> Bool { + english.urlString == urlComponent || german.urlString == urlComponent + } + + var file: TagOverviewFile { + .init(german: german.file, + english: english.file) + } +} + +extension TagOverviewPage: LocalizedItem { + +} + +final class LocalizedTagOverviewPage: ObservableObject { + + @Published + var title: String + + /** + The string to use when creating the url for the page. + + Defaults to ``id`` if unset. + */ + @Published + var urlString: String + + @Published + var linkPreviewImage: FileResource? + + @Published + var linkPreviewTitle: String? + + @Published + var linkPreviewDescription: String? + + init(title: String, urlString: String, linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) { + self.title = title + self.urlString = urlString + self.linkPreviewImage = linkPreviewImage + self.linkPreviewTitle = linkPreviewTitle + self.linkPreviewDescription = linkPreviewDescription + } + + init(file: LocalizedTagOverviewFile, image: FileResource?) { + self.title = file.title + self.urlString = file.url + self.linkPreviewImage = image + self.linkPreviewTitle = file.linkPreviewTitle + self.linkPreviewDescription = file.linkPreviewDescription + } + + var file: LocalizedTagOverviewFile { + .init(url: urlString, + title: title, + linkPreviewImage: linkPreviewImage?.id, + linkPreviewTitle: linkPreviewTitle, + linkPreviewDescription: linkPreviewDescription) + } +} diff --git a/CHDataManagement/Model/Page.swift b/CHDataManagement/Model/Page.swift index 6c32418..779fe27 100644 --- a/CHDataManagement/Model/Page.swift +++ b/CHDataManagement/Model/Page.swift @@ -2,12 +2,6 @@ import Foundation final class Page: Item { - /** - The unique id of the entry - */ - @Published - var id: String - /** The external link this page points to. @@ -59,7 +53,6 @@ final class Page: Item { german: LocalizedPage, english: LocalizedPage, tags: [Tag]) { - self.id = id self.externalLink = externalLink self.isDraft = isDraft self.createdDate = createdDate @@ -70,14 +63,7 @@ final class Page: Item { self.english = english self.tags = tags - super.init(content: content) - } - - func localized(in language: ContentLanguage) -> LocalizedPage { - switch language { - case .german: return german - case .english: return english - } + super.init(content: content, id: id) } func update(id newId: String) -> Bool { @@ -95,7 +81,7 @@ final class Page: Item { // MARK: Paths - func absoluteUrl(for language: ContentLanguage) -> String { + override func absoluteUrl(in language: ContentLanguage) -> String { if let url = externalLink { return url } @@ -103,40 +89,31 @@ final class Page: Item { return makeCleanAbsolutePath(internalPath(for: language)) } + override func title(in language: ContentLanguage) -> String { + localized(in: language).title + } + func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String { makeCleanRelativePath(internalPath(for: language)) } private func internalPath(for language: ContentLanguage) -> String { - content.settings.pages.pageUrlPrefix + "/" + localized(in: language).urlString + content.settings.paths.pagesOutputFolderPath + "/" + localized(in: language).urlString } -} -extension Page: Identifiable { - -} - -extension Page: Equatable { - - static func == (lhs: Page, rhs: Page) -> Bool { - lhs.id == rhs.id + override var itemType: ItemType { + .page } -} -extension Page: Hashable { - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - -extension Page: Comparable { - - static func < (lhs: Page, rhs: Page) -> Bool { - lhs.id < rhs.id + func contains(urlComponent: String) -> Bool { + english.urlString == urlComponent || german.urlString == urlComponent } } extension Page: DateItem { } + +extension Page: LocalizedItem { + +} diff --git a/CHDataManagement/Model/Settings/PageSettings.swift b/CHDataManagement/Model/Settings/PageSettings.swift index 7bc0b29..2aa6f24 100644 --- a/CHDataManagement/Model/Settings/PageSettings.swift +++ b/CHDataManagement/Model/Settings/PageSettings.swift @@ -2,11 +2,6 @@ import Foundation final class PageSettings: ObservableObject { - /// The prefix of the urls for all pages - /// The full path will be `/` - @Published - var pageUrlPrefix: String - @Published var contentWidth: Int @@ -17,13 +12,39 @@ final class PageSettings: ObservableObject { var pageLinkImageSize: Int @Published - var javascriptFilesPath: String + var defaultCssFile: FileResource? - init(file: PageSettingsFile) { - self.pageUrlPrefix = file.pageUrlPrefix + @Published + var codeHighlightingJsFile: FileResource? + + @Published + var audioPlayerJsFile: FileResource? + + @Published + var audioPlayerCssFile: FileResource? + + @Published + var modelViewerJsFile: FileResource? + + init(file: PageSettingsFile, files: [String : FileResource]) { self.contentWidth = file.contentWidth self.largeImageWidth = file.largeImageWidth self.pageLinkImageSize = file.pageLinkImageSize - self.javascriptFilesPath = file.javascriptFilesPath + self.defaultCssFile = file.defaultCssFile.map { files[$0] } + self.codeHighlightingJsFile = file.codeHighlightingJsFile.map { files[$0] } + self.audioPlayerJsFile = file.audioPlayerJsFile.map { files[$0] } + self.audioPlayerCssFile = file.audioPlayerCssFile.map { files[$0] } + self.modelViewerJsFile = file.modelViewerJsFile.map { files[$0] } + } + + var file: PageSettingsFile { + .init(contentWidth: contentWidth, + largeImageWidth: largeImageWidth, + pageLinkImageSize: pageLinkImageSize, + defaultCssFile: defaultCssFile?.id, + codeHighlightingJsFile: codeHighlightingJsFile?.id, + audioPlayerJsFile: audioPlayerJsFile?.id, + audioPlayerCssFile: audioPlayerJsFile?.id, + modelViewerJsFile: modelViewerJsFile?.id) } } diff --git a/CHDataManagement/Model/Settings/PathSettings.swift b/CHDataManagement/Model/Settings/PathSettings.swift index 2588cf6..e8081a1 100644 --- a/CHDataManagement/Model/Settings/PathSettings.swift +++ b/CHDataManagement/Model/Settings/PathSettings.swift @@ -5,6 +5,9 @@ final class PathSettings: ObservableObject { @Published var outputDirectoryPath: String + @Published + var assetsOutputFolderPath: String + @Published var pagesOutputFolderPath: String @@ -21,6 +24,7 @@ final class PathSettings: ObservableObject { var tagsOutputFolderPath: String init(file: PathSettingsFile) { + self.assetsOutputFolderPath = file.assetsOutputFolderPath self.outputDirectoryPath = file.outputDirectoryPath self.pagesOutputFolderPath = file.pagesOutputFolderPath self.imagesOutputFolderPath = file.imagesOutputFolderPath @@ -28,4 +32,14 @@ final class PathSettings: ObservableObject { self.videosOutputFolderPath = file.videosOutputFolderPath self.tagsOutputFolderPath = file.tagsOutputFolderPath } + + var file: PathSettingsFile { + .init(outputDirectoryPath: outputDirectoryPath, + assetsOutputFolderPath: assetsOutputFolderPath, + pagesOutputFolderPath: pagesOutputFolderPath, + imagesOutputFolderPath: imagesOutputFolderPath, + filesOutputFolderPath: filesOutputFolderPath, + videosOutputFolderPath: videosOutputFolderPath, + tagsOutputFolderPath: tagsOutputFolderPath) + } } diff --git a/CHDataManagement/Model/Settings/PostSettings.swift b/CHDataManagement/Model/Settings/PostSettings.swift index 53030c5..3656ea6 100644 --- a/CHDataManagement/Model/Settings/PostSettings.swift +++ b/CHDataManagement/Model/Settings/PostSettings.swift @@ -10,13 +10,32 @@ final class PostSettings: ObservableObject { @Published var contentWidth: Int - init(postsPerPage: Int, contentWidth: Int) { + @Published + var swiperCssFile: FileResource? + + @Published + var swiperJsFile: FileResource? + + @Published + var defaultCssFile: FileResource? + + init(postsPerPage: Int, + contentWidth: Int, + swiperCssFile: FileResource?, + swiperJsFile: FileResource?, + defaultCssFile: FileResource?) { self.postsPerPage = postsPerPage self.contentWidth = contentWidth + self.swiperCssFile = swiperCssFile + self.swiperJsFile = swiperJsFile + self.defaultCssFile = defaultCssFile } - init(file: PostSettingsFile) { + init(file: PostSettingsFile, files: [String : FileResource]) { self.postsPerPage = file.postsPerPage self.contentWidth = file.contentWidth + self.swiperCssFile = file.swiperCssFile.map { files[$0] } + self.swiperJsFile = file.swiperJsFile.map { files[$0] } + self.defaultCssFile = file.defaultCssFile.map { files[$0] } } } diff --git a/CHDataManagement/Model/Settings/Settings.swift b/CHDataManagement/Model/Settings/Settings.swift index eab50ad..3d8a36b 100644 --- a/CHDataManagement/Model/Settings/Settings.swift +++ b/CHDataManagement/Model/Settings/Settings.swift @@ -5,9 +5,9 @@ final class Settings: ObservableObject { @Published var paths: PathSettings - /// The tags to show in the navigation bar + /// The items to show in the navigation bar @Published - var navigationTags: [Tag] + var navigationItems: [Item] @Published var posts: PostSettings @@ -21,9 +21,9 @@ final class Settings: ObservableObject { @Published var english: LocalizedPostSettings - init(paths: PathSettings, navigationTags: [Tag], posts: PostSettings, pages: PageSettings, german: LocalizedPostSettings, english: LocalizedPostSettings) { + init(paths: PathSettings, navigationItems: [Item], posts: PostSettings, pages: PageSettings, german: LocalizedPostSettings, english: LocalizedPostSettings) { self.paths = paths - self.navigationTags = navigationTags + self.navigationItems = navigationItems self.posts = posts self.pages = pages self.german = german diff --git a/CHDataManagement/Model/Tag.swift b/CHDataManagement/Model/Tag.swift index 4cd93ce..70d8b4a 100644 --- a/CHDataManagement/Model/Tag.swift +++ b/CHDataManagement/Model/Tag.swift @@ -2,10 +2,6 @@ import Foundation final class Tag: Item { - var id: String { - english.urlComponent - } - @Published var isVisible: Bool @@ -15,19 +11,19 @@ final class Tag: Item { @Published var english: LocalizedTag - init(content: Content, id: String) { + override init(content: Content, id: String) { self.isVisible = true self.english = .init(urlComponent: id, name: id) let deId = id + "-" + ContentLanguage.german.rawValue self.german = .init(urlComponent: deId, name: deId) - super.init(content: content) + super.init(content: content, id: id) } - init(content: Content, isVisible: Bool = true, german: LocalizedTag, english: LocalizedTag) { + init(content: Content, id: String, isVisible: Bool = true, german: LocalizedTag, english: LocalizedTag) { self.isVisible = isVisible self.german = german self.english = english - super.init(content: content) + super.init(content: content, id: id) } var linkName: String { @@ -38,49 +34,33 @@ final class Tag: Item { "/tags/\(linkName).html" } - func localized(in language: ContentLanguage) -> LocalizedTag { - switch language { - case .english: return english - case .german: return german - } - } - // MARK: Paths - func absoluteUrl(for language: ContentLanguage) -> String { - makeCleanAbsolutePath(internalPath(for: language)) - } - func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String { makeCleanRelativePath(internalPath(for: language)) } private func internalPath(for language: ContentLanguage) -> String { - content.settings.pages.pageUrlPrefix + "/" + localized(in: language).urlComponent + content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlComponent + } + + override func absoluteUrl(in language: ContentLanguage) -> String { + makeCleanAbsolutePath(internalPath(for: language)) + } + + override func title(in language: ContentLanguage) -> String { + localized(in: language).name + } + + override var itemType: ItemType { + .tag + } + + func contains(urlComponent: String) -> Bool { + german.urlComponent == urlComponent || english.urlComponent == urlComponent } } -extension Tag: Identifiable { - -} - -extension Tag: Equatable { - - static func == (_ lhs: Tag, _ rhs: Tag) -> Bool { - lhs.id == rhs.id - } -} - -extension Tag: Hashable { - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - -extension Tag: Comparable { - - static func < (lhs: Tag, rhs: Tag) -> Bool { - lhs.id < rhs.id - } +extension Tag: LocalizedItem { + } diff --git a/CHDataManagement/Page Elements/ContentElements/AdditionalPageHeaders.swift b/CHDataManagement/Page Elements/ContentElements/AdditionalPageHeaders.swift deleted file mode 100644 index 05e4a23..0000000 --- a/CHDataManagement/Page Elements/ContentElements/AdditionalPageHeaders.swift +++ /dev/null @@ -1,21 +0,0 @@ - -struct AdditionalPageHeaders { - - let headers: RequiredHeaders - - let assetPath: String - - #warning("Provide paths in settings, import files") - var content: String { - headers.map(header).sorted().joined() - } - - private func header(for asset: HeaderFile) -> String { - let file = asset.rawValue - guard file.hasSuffix(".js") else { - return "" - } - let module = asset.asModule ? " type='module'" : "" - return "" - } -} diff --git a/CHDataManagement/Page Elements/PageHead.swift b/CHDataManagement/Page Elements/PageHead.swift index ca2f645..46b55dd 100644 --- a/CHDataManagement/Page Elements/PageHead.swift +++ b/CHDataManagement/Page Elements/PageHead.swift @@ -1,38 +1,15 @@ import Foundation -//import Elementary -struct PageHead { - let title: String +struct PageHead: HtmlProducer { - let description: String + let items: [HeaderElement] - let additionalHeaders: String - - var content: String { - """ - - - \(title) - - - \(additionalHeaders) - - - """ + func populate(_ result: inout String) { + result += "" + for item in items { + result += item.content + } + result += "" } } - -/* -extension PageHead: HTML { - - var content: some HTML { - meta(.charset(.utf8)) - meta(.title(title)) - meta(.name(.viewport), .content("width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1")) - meta(.name(.description), .content(description)) - link(.rel(.stylesheet), .href("style.css")) - link(.rel(.stylesheet), .href("swiper.css")) - } -} -*/ diff --git a/CHDataManagement/Pages/ContentPage.swift b/CHDataManagement/Pages/ContentPage.swift index a49ea8b..59d3df5 100644 --- a/CHDataManagement/Pages/ContentPage.swift +++ b/CHDataManagement/Pages/ContentPage.swift @@ -18,13 +18,13 @@ struct ContentPage: HtmlProducer { private let pageContent: String - private let headers: String + private let headers: [HeaderElement] private let footers: String private let icons: Set - init(language: ContentLanguage, dateString: String, title: String, tags: [FeedEntryData.Tag], linkTitle: String, description: String, navigationBarLinks: [NavigationBar.Link], pageContent: String, headers: String, footers: [String], icons: Set) { + init(language: ContentLanguage, dateString: String, title: String, tags: [FeedEntryData.Tag], linkTitle: String, description: String, navigationBarLinks: [NavigationBar.Link], pageContent: String, headers: [HeaderElement], footers: [String], icons: Set) { self.language = language self.dateString = dateString self.title = title @@ -41,7 +41,7 @@ struct ContentPage: HtmlProducer { func populate(_ result: inout String) { // TODO: Add headers and footers from page content result += "" - result += PageHead(title: title, description: description, additionalHeaders: headers).content + result += PageHead(items: [.charset, .viewport] + headers).content result += "" result += NavigationBar(links: navigationBarLinks).content diff --git a/CHDataManagement/Pages/GenericPage.swift b/CHDataManagement/Pages/GenericPage.swift index 5e9828f..a0100df 100644 --- a/CHDataManagement/Pages/GenericPage.swift +++ b/CHDataManagement/Pages/GenericPage.swift @@ -10,25 +10,25 @@ struct GenericPage { let links: [NavigationBar.Link] - let additionalHeaders: String + let headers: [HeaderElement] let additionalFooter: String let insertedContent: (inout String) -> Void - init(language: ContentLanguage, title: String, description: String, links: [NavigationBar.Link], additionalHeaders: String, additionalFooter: String, insertedContent: @escaping (inout String) -> Void) { + init(language: ContentLanguage, title: String, description: String, links: [NavigationBar.Link], headers: [HeaderElement], additionalFooter: String, insertedContent: @escaping (inout String) -> Void) { self.language = language self.title = title self.description = description self.links = links - self.additionalHeaders = additionalHeaders + self.headers = headers self.additionalFooter = additionalFooter self.insertedContent = insertedContent } var content: String { var result = "" result += "" - result += PageHead(title: title, description: description, additionalHeaders: additionalHeaders).content + result += PageHead(items: [.charset, .viewport] + headers).content result += "" result += NavigationBar(links: links).content result += "
" diff --git a/CHDataManagement/Pages/PageInFeed.swift b/CHDataManagement/Pages/PageInFeed.swift deleted file mode 100644 index dc4b3d3..0000000 --- a/CHDataManagement/Pages/PageInFeed.swift +++ /dev/null @@ -1,71 +0,0 @@ -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 showTitle: Bool - - let description: String - - let navigationBarLinks: [NavigationBar.Link] - - 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 { - let footer = swiperIsNeeded ? swiperInits : "" - - return GenericPage( - language: language, - title: title, - description: description, - links: navigationBarLinks, - additionalHeaders: headers, - additionalFooter: footer) { content in - if showTitle { - content += "

\(title)

" - } - for post in posts { - content += FeedEntry(data: post).content - } - if totalPages > 1 { - content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content - } - - }.content - } - - private var swiperInits: String { - var result = "" - return result - } -} diff --git a/CHDataManagement/Preview Content/Content+Mock.swift b/CHDataManagement/Preview Content/Content+Mock.swift index 609c46e..b65cd70 100644 --- a/CHDataManagement/Preview Content/Content+Mock.swift +++ b/CHDataManagement/Preview Content/Content+Mock.swift @@ -20,5 +20,6 @@ extension Content { pages: [.empty], tags: [.hiking, .mountains, .nature, .sports], files: [], + tagOverview: nil, storedContentPath: dbPath) } diff --git a/CHDataManagement/Preview Content/Tag+Mock.swift b/CHDataManagement/Preview Content/Tag+Mock.swift index a51e670..797b940 100644 --- a/CHDataManagement/Preview Content/Tag+Mock.swift +++ b/CHDataManagement/Preview Content/Tag+Mock.swift @@ -4,29 +4,34 @@ extension Tag { static let mock = Tag( content: .mock, + id: "electronics", german: .german, english: .english) static let nature = Tag( content: .mock, + id: "nature", german: .init(urlComponent: "natur", name: "Natur"), english: .init(urlComponent: "nature", name: "Nature") ) static let sports = Tag( content: .mock, + id: "sports", german: .init(urlComponent: "sport", name: "Sport"), english: .init(urlComponent: "sports", name: "Sports") ) static let hiking = Tag( content: .mock, + id: "hiking", german: .init(urlComponent: "wandern", name: "Wandern"), english: .init(urlComponent: "hiking", name: "Hiking") ) static let mountains = Tag( content: .mock, + id: "mountains", german: .init(urlComponent: "berge", name: "Berge"), english: .init(urlComponent: "mountains", name: "Mountains") ) diff --git a/CHDataManagement/Preview Content/WebsiteData+Mock.swift b/CHDataManagement/Preview Content/WebsiteData+Mock.swift index ad8a94f..b8c17bb 100644 --- a/CHDataManagement/Preview Content/WebsiteData+Mock.swift +++ b/CHDataManagement/Preview Content/WebsiteData+Mock.swift @@ -4,7 +4,7 @@ extension Settings { static let mock: Settings = .init( paths: .default, - navigationTags: [], + navigationItems: [], posts: .default, pages: .default, german: .german, @@ -21,14 +21,14 @@ extension PathSettings { extension PostSettings { static var `default`: PostSettings { - .init(file: .default) + .init(file: .default, files: [:]) } } extension PageSettings { static var `default`: PageSettings { - .init(file: .default) + .init(file: .default, files: [:]) } } diff --git a/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift index 9972bf8..88ea923 100644 --- a/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift +++ b/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift @@ -1,15 +1,21 @@ struct PageSettingsFile { - let pageUrlPrefix: String - let contentWidth: Int let largeImageWidth: Int let pageLinkImageSize: Int - let javascriptFilesPath: String + let defaultCssFile: String? + + let codeHighlightingJsFile: String? + + let audioPlayerJsFile: String? + + let audioPlayerCssFile: String? + + let modelViewerJsFile: String? } extension PageSettingsFile: Codable { @@ -19,10 +25,13 @@ extension PageSettingsFile: Codable { extension PageSettingsFile { static var `default`: PageSettingsFile { - .init(pageUrlPrefix: "page", - contentWidth: 600, + .init(contentWidth: 600, largeImageWidth: 1200, pageLinkImageSize: 180, - javascriptFilesPath: "/assets/js") + defaultCssFile: nil, + codeHighlightingJsFile: nil, + audioPlayerJsFile: nil, + audioPlayerCssFile: nil, + modelViewerJsFile: nil) } } diff --git a/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift index 8a554ce..fc052a4 100644 --- a/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift +++ b/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift @@ -3,6 +3,8 @@ struct PathSettingsFile { let outputDirectoryPath: String + let assetsOutputFolderPath: String + let pagesOutputFolderPath: String let imagesOutputFolderPath: String @@ -13,8 +15,15 @@ struct PathSettingsFile { let tagsOutputFolderPath: String - init(outputDirectoryPath: String, pagesOutputFolderPath: String, imagesOutputFolderPath: String, filesOutputFolderPath: String, videosOutputFolderPath: String, tagsOutputFolderPath: String) { + init(outputDirectoryPath: String, + assetsOutputFolderPath: String, + pagesOutputFolderPath: String, + imagesOutputFolderPath: String, + filesOutputFolderPath: String, + videosOutputFolderPath: String, + tagsOutputFolderPath: String) { self.outputDirectoryPath = outputDirectoryPath + self.assetsOutputFolderPath = assetsOutputFolderPath self.pagesOutputFolderPath = pagesOutputFolderPath self.imagesOutputFolderPath = imagesOutputFolderPath self.filesOutputFolderPath = filesOutputFolderPath @@ -32,6 +41,7 @@ extension PathSettingsFile { static var `default`: PathSettingsFile { PathSettingsFile( outputDirectoryPath: "build", + assetsOutputFolderPath: "asset", pagesOutputFolderPath: "page", imagesOutputFolderPath: "image", filesOutputFolderPath: "file", diff --git a/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift index 022abe1..122ad6e 100644 --- a/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift +++ b/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift @@ -7,6 +7,12 @@ struct PostSettingsFile { /// The maximum width of the main content let contentWidth: Int + + let swiperCssFile: String? + + let swiperJsFile: String? + + let defaultCssFile: String? } extension PostSettingsFile: Codable { } @@ -15,6 +21,9 @@ extension PostSettingsFile { static var `default`: PostSettingsFile { .init(postsPerPage: 25, - contentWidth: 600) + contentWidth: 600, + swiperCssFile: nil, + swiperJsFile: nil, + defaultCssFile: nil) } } diff --git a/CHDataManagement/Storage/Model/Settings/SettingsFile.swift b/CHDataManagement/Storage/Model/Settings/SettingsFile.swift index 284a381..c1d2cfb 100644 --- a/CHDataManagement/Storage/Model/Settings/SettingsFile.swift +++ b/CHDataManagement/Storage/Model/Settings/SettingsFile.swift @@ -1,11 +1,18 @@ import Foundation +struct NavigationItemReference: Codable { + + let type: ItemType + + let id: String +} + struct SettingsFile { let paths: PathSettingsFile /// The tags to show in the navigation bar - let navigationTags: [String] + let navigationItems: [NavigationItemReference] let posts: PostSettingsFile @@ -23,7 +30,7 @@ extension SettingsFile { static var `default`: SettingsFile { .init( paths: .default, - navigationTags: [], + navigationItems: [], posts: .default, pages: .default, german: .default, diff --git a/CHDataManagement/Storage/Model/TagOverviewFile.swift b/CHDataManagement/Storage/Model/TagOverviewFile.swift new file mode 100644 index 0000000..857408d --- /dev/null +++ b/CHDataManagement/Storage/Model/TagOverviewFile.swift @@ -0,0 +1,31 @@ + +struct TagOverviewFile { + + let german: LocalizedTagOverviewFile + + let english: LocalizedTagOverviewFile +} + +extension TagOverviewFile: Codable { + +} + +/** + The structure to store the metadata of a localized page + */ +struct LocalizedTagOverviewFile { + + let url: String + + let title: String + + let linkPreviewImage: String? + + let linkPreviewTitle: String? + + let linkPreviewDescription: String? +} + +extension LocalizedTagOverviewFile: Codable { + +} diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index 87ac580..d6675c6 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -306,6 +306,18 @@ final class Storage { try writeIfChanged(fileDescriptions, to: fileDescriptionFilename) } + // MARK: Tag overview + + private let tagOverviewFileName = "tag-overview.json" + + func loadTagOverview() throws -> TagOverviewFile? { + try read(at: tagOverviewFileName) + } + + func save(tagOverview: TagOverviewFile?) throws { + try writeIfChanged(tagOverview, to: tagOverviewFileName) + } + // MARK: Files private let filesFolderName = "files" @@ -499,6 +511,19 @@ final class Storage { } } + /** + Write the data of an encodable value to a relative path in the content folder, + or delete the file if nil is passed. + - Note: This function requires a security scope for the content path + */ + private func writeIfChanged(_ value: T?, to relativePath: String) throws where T: Encodable { + guard let value else { + try deleteFile(at: relativePath) + return + } + return try writeIfChanged(value, to: relativePath) + } + /** Write the data of an encodable value to a relative path in the content folder - Note: This function requires a security scope for the content path @@ -547,6 +572,16 @@ final class Storage { } } + /** + Read an object from a file, if the file exists + */ + private func read(at relativePath: String) throws -> T? where T: Decodable { + guard let data = try readData(at: relativePath) else { + return nil + } + return try decoder.decode(T.self, from: data) + } + /** - Note: This function requires a security scope for the content path @@ -632,4 +667,13 @@ final class Storage { try fm.copyItem(at: file, to: destination) } } + + private func deleteFile(at relativePath: String) throws { + try withScopedContent(file: relativePath) { destination in + guard fm.fileExists(atPath: destination.path()) else { + return + } + try fm.removeItem(at: destination) + } + } } diff --git a/CHDataManagement/Views/Files/FileDetailView.swift b/CHDataManagement/Views/Files/FileDetailView.swift index ac81c89..db4754a 100644 --- a/CHDataManagement/Views/Files/FileDetailView.swift +++ b/CHDataManagement/Views/Files/FileDetailView.swift @@ -37,11 +37,11 @@ struct FileDetailView: View { } Text("German Description") .font(.headline) - TextField("", text: $file.germanDescription) + TextField("", text: $file.german) .textFieldStyle(.roundedBorder) Text("English Description") .font(.headline) - TextField("", text: $file.englishDescription) + TextField("", text: $file.english) .textFieldStyle(.roundedBorder) if file.type.isImage { Text("Image size") diff --git a/CHDataManagement/Views/Files/FileListView.swift b/CHDataManagement/Views/Files/FileListView.swift index 6737541..84054a0 100644 --- a/CHDataManagement/Views/Files/FileListView.swift +++ b/CHDataManagement/Views/Files/FileListView.swift @@ -73,16 +73,30 @@ struct FileListView: View { guard oldValue != newValue else { return } - if let selectedFile, - newValue.matches(selectedFile.type) { + let newFile = filteredFiles.first + + guard let selectedFile else { + if let newFile { + DispatchQueue.main.async { + selectedFile = newFile + } + } return } - selectedFile = filteredFiles.first + + if newValue.matches(selectedFile.type) { + return + } + DispatchQueue.main.async { + self.selectedFile = newFile + } } } .onAppear { if selectedFile == nil { - selectedFile = content.files.first + DispatchQueue.main.async { + selectedFile = content.files.first + } } } } diff --git a/CHDataManagement/Views/Files/FileSelectionView.swift b/CHDataManagement/Views/Files/FileSelectionView.swift index e748a59..4944732 100644 --- a/CHDataManagement/Views/Files/FileSelectionView.swift +++ b/CHDataManagement/Views/Files/FileSelectionView.swift @@ -10,17 +10,32 @@ struct FileSelectionView: View { init(selectedFile: Binding) { self._selectedFile = selectedFile + self.newSelection = selectedFile.wrappedValue } + @State + private var newSelection: FileResource? + var body: some View { VStack { - FileListView(selectedFile: $selectedFile) + FileListView(selectedFile: $newSelection) .frame(minHeight: 500, idealHeight: 600) HStack { Button("Cancel") { - selectedFile = nil - dismiss() } - Button("Select") { dismiss() } + DispatchQueue.main.async { + dismiss() + } + } + Button("Remove") { + DispatchQueue.main.async { + selectedFile = nil + dismiss() + } + } + Button("Select") { + selectedFile = newSelection + dismiss() + } } } .padding() diff --git a/CHDataManagement/Views/Generic/DetailTitle.swift b/CHDataManagement/Views/Generic/DetailTitle.swift new file mode 100644 index 0000000..f4bab6c --- /dev/null +++ b/CHDataManagement/Views/Generic/DetailTitle.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct DetailTitle: View { + + let title: String + + let text: String + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.largeTitle) + .bold() + Text(text) + .foregroundStyle(.secondary) + .padding(.bottom, 30) + } + } +} diff --git a/CHDataManagement/Views/Generic/FilePropertyView.swift b/CHDataManagement/Views/Generic/FilePropertyView.swift new file mode 100644 index 0000000..b254669 --- /dev/null +++ b/CHDataManagement/Views/Generic/FilePropertyView.swift @@ -0,0 +1,34 @@ +import SwiftUI + +struct FilePropertyView: View { + + let title: String + + let description: String + + @Binding + var selectedFile: FileResource? + + @State + private var showFileSelectionSheet = false + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.headline) + HStack { + Text(selectedFile?.id ?? "No file selected") + Spacer() + Button("Select") { + showFileSelectionSheet = true + } + } + Text(description) + .foregroundStyle(.secondary) + .padding(.bottom) + } + .sheet(isPresented: $showFileSelectionSheet) { + FileSelectionView(selectedFile: $selectedFile) + } + } +} diff --git a/CHDataManagement/Views/Generic/IntegerPropertyView.swift b/CHDataManagement/Views/Generic/IntegerPropertyView.swift new file mode 100644 index 0000000..222face --- /dev/null +++ b/CHDataManagement/Views/Generic/IntegerPropertyView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct IntegerPropertyView: View { + + @Binding + var value: Int + + let title: String + + let footer: String + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.headline) + IntegerField("", number: $value) + .textFieldStyle(.roundedBorder) + Text(footer) + .foregroundStyle(.secondary) + .padding(.bottom) + } + } +} diff --git a/CHDataManagement/Views/ItemSelectionView.swift b/CHDataManagement/Views/ItemSelectionView.swift new file mode 100644 index 0000000..ecbb16b --- /dev/null +++ b/CHDataManagement/Views/ItemSelectionView.swift @@ -0,0 +1,111 @@ +import SwiftUI +import SFSafeSymbols + +struct ItemSelectionView: View { + + @Binding + var isPresented: Bool + + @Binding + var selectedItems: [Item] + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + init(isPresented: Binding, selectedItems: Binding<[Item]>) { + self._isPresented = isPresented + self._selectedItems = selectedItems + } + + var body: some View { + VStack { + List { + Section("Selected") { + ForEach(selectedItems) { item in + HStack { + Image(systemSymbol: .minusCircleFill) + .foregroundStyle(.red) + .onTapGesture { + deselect(item: item) + } + Text(item.title(in: language)) + Spacer() + } + } + .onMove(perform: moveItem) + } + if let tagOverview = content.tagOverview { + Section("Special Pages") { + HStack { + Image(systemSymbol: .plusCircleFill) + .foregroundStyle(.green) + Text("Tags Overview") + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + if !selectedItems.contains(where: { $0 is TagOverviewPage }) { + selectedItems.append(tagOverview) + } + } + } + } + Section("Tags") { + ForEach(content.tags) { item in + HStack { + Image(systemSymbol: .plusCircleFill) + .foregroundStyle(.green) + Text(item.title(in: language)) + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + select(item: item) + } + } + } + Section("Pages") { + ForEach(content.pages) { item in + HStack { + Image(systemSymbol: .plusCircleFill) + .foregroundStyle(.green) + Text(item.title(in: language)) + } + .contentShape(Rectangle()) + .onTapGesture { + select(item: item) + } + } + } + } + Button("Dismiss", action: dismiss) + } + .frame(minHeight: 500) + .padding() + } + + private func moveItem(from source: IndexSet, to destination: Int) { + selectedItems.move(fromOffsets: source, toOffset: destination) + } + + private func deselect(item: Item) { + guard let index = selectedItems.firstIndex(of: item) else { + return + } + selectedItems.remove(at: index) + } + + private func select(item: Item) { + if selectedItems.contains(item) { + return + } + selectedItems.append(item) + } + + private func dismiss() { + isPresented = false + } +} diff --git a/CHDataManagement/Views/Settings/Content/Pages/PageIssueView.swift b/CHDataManagement/Views/Settings/Content/Pages/PageIssueView.swift index 4cf9b01..6ac7093 100644 --- a/CHDataManagement/Views/Settings/Content/Pages/PageIssueView.swift +++ b/CHDataManagement/Views/Settings/Content/Pages/PageIssueView.swift @@ -210,7 +210,7 @@ struct PageIssueView: View { } private func createPage(pageId: String) { - guard content.isValidIdForTagOrTagOrPost(pageId) else { + guard content.isValidIdForTagOrPageOrPost(pageId) else { show(error: "Invalid page id, can't create page") return } @@ -245,7 +245,7 @@ struct PageIssueView: View { } private func createTag(tagId: String) { - guard content.isValidIdForTagOrTagOrPost(tagId) else { + guard content.isValidIdForTagOrPageOrPost(tagId) else { show(error: "Invalid tag id, can't create tag") return } diff --git a/CHDataManagement/Views/Settings/GenerationContentView.swift b/CHDataManagement/Views/Settings/GenerationContentView.swift index ef00e47..66b83bc 100644 --- a/CHDataManagement/Views/Settings/GenerationContentView.swift +++ b/CHDataManagement/Views/Settings/GenerationContentView.swift @@ -23,7 +23,7 @@ struct GenerationContentView: View { var body: some View { switch selectedSection { - case .folders, .navigationBar, .postFeed: + case .folders, .navigationBar, .postFeed, .tagOverview: generationView case .pages: PageSettingsContentView() diff --git a/CHDataManagement/Views/Settings/GenerationDetailView.swift b/CHDataManagement/Views/Settings/GenerationDetailView.swift index 4837b50..a39b9bc 100644 --- a/CHDataManagement/Views/Settings/GenerationDetailView.swift +++ b/CHDataManagement/Views/Settings/GenerationDetailView.swift @@ -17,6 +17,8 @@ struct GenerationDetailView: View { PostFeedSettingsView() case .pages: PageSettingsDetailView() + case .tagOverview: + TagOverviewDetailView() } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) diff --git a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift index b6369b7..e01f4e0 100644 --- a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift +++ b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import SFSafeSymbols struct NavigationBarSettingsView: View { @@ -9,7 +10,7 @@ struct NavigationBarSettingsView: View { private var content: Content @State - private var showTagPicker = false + private var showItemPicker = false var body: some View { ScrollView { @@ -21,14 +22,10 @@ struct NavigationBarSettingsView: View { .foregroundStyle(.secondary) .padding(.bottom, 30) - Text("Visible Tags") - .font(.headline) - FlowHStack { - ForEach(content.settings.navigationTags) { tag in - TagView(text: tag.localized(in: language).name) - .foregroundStyle(.white) - } - Button(action: { showTagPicker = true }) { + HStack { + Text("Links") + .font(.headline) + Button(action: { showItemPicker = true }) { Image(systemSymbol: .squareAndPencilCircleFill) .resizable() .aspectRatio(1, contentMode: .fit) @@ -40,15 +37,19 @@ struct NavigationBarSettingsView: View { } .buttonStyle(.plain) } + + ForEach(content.settings.navigationItems) { tag in + TagView(text: tag.title(in: language)) + .foregroundStyle(.white) + } Text("Select the tags to show in the navigation bar. The number should be even.") .foregroundStyle(.secondary) } } - .sheet(isPresented: $showTagPicker) { - TagSelectionView( - presented: $showTagPicker, - selected: $content.settings.navigationTags, - tags: $content.tags) + .sheet(isPresented: $showItemPicker) { + ItemSelectionView( + isPresented: $showItemPicker, + selectedItems: $content.settings.navigationItems) } } } diff --git a/CHDataManagement/Views/Settings/PageSettingsDetailView.swift b/CHDataManagement/Views/Settings/PageSettingsDetailView.swift index b30fe19..8275e93 100644 --- a/CHDataManagement/Views/Settings/PageSettingsDetailView.swift +++ b/CHDataManagement/Views/Settings/PageSettingsDetailView.swift @@ -11,51 +11,49 @@ struct PageSettingsDetailView: View { var body: some View { ScrollView { VStack(alignment: .leading) { - Text("Page Settings") - .font(.largeTitle) - .bold() - Text("Change the way pages are displayed") - .padding(.bottom, 30) + DetailTitle( + title: "Page Settings", + text: "Change the way pages are displayed") - Text("Content Width") - .font(.headline) - IntegerField("", number: $content.settings.pages.contentWidth) - .textFieldStyle(.roundedBorder) - Text("The maximum width of the content in pages (in pixels)") - .foregroundStyle(.secondary) - .padding(.bottom) + IntegerPropertyView( + value: $content.settings.pages.contentWidth, + title: "Content Width", + footer: "The maximum width of the content in pages (in pixels)") - Text("Fullscreen Image Width") - .font(.headline) - IntegerField("", number: $content.settings.pages.largeImageWidth) - .textFieldStyle(.roundedBorder) - Text("The maximum width of images that are diplayed fullscreen") - .foregroundStyle(.secondary) - .padding(.bottom) + IntegerPropertyView( + value: $content.settings.pages.largeImageWidth, + title: "Fullscreen Image Width", + footer: "The maximum width of images that are diplayed fullscreen") - Text("Page Link Image Width") - .font(.headline) - IntegerField("", number: $content.settings.pages.pageLinkImageSize) - .textFieldStyle(.roundedBorder) - Text("The maximum width of images diplayed as thumbnails on page links") - .foregroundStyle(.secondary) - .padding(.bottom) + IntegerPropertyView( + value: $content.settings.pages.pageLinkImageSize, + title: "Page Link Image Width", + footer: "The maximum width of images diplayed as thumbnails on page links") - Text("Page URL Prefix") - .font(.headline) - TextField("", text: $content.settings.pages.pageUrlPrefix) - .textFieldStyle(.roundedBorder) - Text("The URL prefix used for the links to pages") - .foregroundStyle(.secondary) - .padding(.bottom) + FilePropertyView( + title: "Default CSS File", + description: "The CSS file containing the styling of all pages", + selectedFile: $content.settings.pages.defaultCssFile) - Text("Javascript Files Path") - .font(.headline) - TextField("", text: $content.settings.pages.javascriptFilesPath) - .textFieldStyle(.roundedBorder) - Text("The path to the javascript files in the output folder") - .foregroundStyle(.secondary) - .padding(.bottom) + FilePropertyView( + title: "Code Highlighting File", + description: "The JavaScript file to provide syntax highlighting of code blocks", + selectedFile: $content.settings.pages.codeHighlightingJsFile) + + FilePropertyView( + title: "Audio Player CSS File", + description: "The CSS file to provide the style for the audio player", + selectedFile: $content.settings.pages.audioPlayerCssFile) + + FilePropertyView( + title: "Audio Player JavaScript File", + description: "The CSS file to provide the functionality for the audio player", + selectedFile: $content.settings.pages.audioPlayerJsFile) + + FilePropertyView( + title: "3D Model Viewer File", + description: "The JavaScript file to provide the functionality for the 3D model viewer", + selectedFile: $content.settings.pages.modelViewerJsFile) } } } diff --git a/CHDataManagement/Views/Settings/PathSettingsView.swift b/CHDataManagement/Views/Settings/PathSettingsView.swift index ad91814..0c64e85 100644 --- a/CHDataManagement/Views/Settings/PathSettingsView.swift +++ b/CHDataManagement/Views/Settings/PathSettingsView.swift @@ -85,6 +85,14 @@ struct PathSettingsView: View { Text("The path in the output folder where the generated videos are stored") .foregroundStyle(.secondary) .padding(.bottom) + + Text("Assets output folder") + .font(.headline) + TextField("", text: $content.settings.paths.assetsOutputFolderPath) + .textFieldStyle(.roundedBorder) + Text("The path in the output folder where assets are stored") + .foregroundStyle(.secondary) + .padding(.bottom) } } } diff --git a/CHDataManagement/Views/Settings/PostFeedSettingsView.swift b/CHDataManagement/Views/Settings/PostFeedSettingsView.swift index 50fa4fa..7557712 100644 --- a/CHDataManagement/Views/Settings/PostFeedSettingsView.swift +++ b/CHDataManagement/Views/Settings/PostFeedSettingsView.swift @@ -11,32 +11,36 @@ struct PostFeedSettingsView: View { var body: some View { ScrollView { VStack(alignment: .leading) { - Text("Post Feed Settings") - .font(.largeTitle) - .bold() - Text("Change the way the posts are displayed") - .foregroundStyle(.secondary) - .padding(.bottom, 30) + DetailTitle(title: "Post Feed Settings", + text: "Change the way the posts are displayed") - Text("Content Width") - .font(.headline) - IntegerField("", number: $content.settings.posts.contentWidth) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: 400) - Text("The maximum width of the content the post feed (in pixels)") - .foregroundStyle(.secondary) - .padding(.bottom) + IntegerPropertyView( + value: $content.settings.posts.contentWidth, + title: "Content Width", + footer: "The maximum width of the content the post feed (in pixels)") - Text("Posts Per Page") - .font(.headline) - IntegerField("", number: $content.settings.posts.postsPerPage) - .textFieldStyle(.roundedBorder) - .frame(maxWidth: 400) - Text("The maximum number of posts displayed on a single page") - .foregroundStyle(.secondary) - .padding(.bottom) + IntegerPropertyView( + value: $content.settings.posts.postsPerPage, + title: "Posts Per Page", + footer: "The maximum number of posts displayed on a single page") - LocalizedPostFeedSettingsView(settings: content.settings.localized(in: language)) + FilePropertyView( + title: "Default CSS File", + description: "The CSS file containing the styling of all post pages", + selectedFile: $content.settings.posts.defaultCssFile) + + FilePropertyView( + title: "Swiper CSS File", + description: "The CSS file containing the styling of image galleries in post feeds", + selectedFile: $content.settings.posts.swiperCssFile) + + FilePropertyView( + title: "Swiper JavaScript File", + description: "The JavaScript file to load the image gallery code in post feeds", + selectedFile: $content.settings.posts.swiperJsFile) + + LocalizedPostFeedSettingsView( + settings: content.settings.localized(in: language)) } } } diff --git a/CHDataManagement/Views/Settings/SettingsSection.swift b/CHDataManagement/Views/Settings/SettingsSection.swift index f6dc78a..22c669b 100644 --- a/CHDataManagement/Views/Settings/SettingsSection.swift +++ b/CHDataManagement/Views/Settings/SettingsSection.swift @@ -12,17 +12,19 @@ enum SettingsSection: String { case pages = "Pages" + case tagOverview = "Tag Overview" + } extension SettingsSection { var icon: SFSymbol { switch self { - //case .generation: return .arrowTriangle2Circlepath case .folders: return .folder case .navigationBar: return .menubarRectangle case .postFeed: return .rectangleGrid1x2 case .pages: return .docRichtext + case .tagOverview: return .tag } } } diff --git a/CHDataManagement/Views/Settings/TagOverviewDetailView.swift b/CHDataManagement/Views/Settings/TagOverviewDetailView.swift new file mode 100644 index 0000000..86e8742 --- /dev/null +++ b/CHDataManagement/Views/Settings/TagOverviewDetailView.swift @@ -0,0 +1,130 @@ +import SwiftUI + +struct TagOverviewDetailView: View { + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + Text("Tag Overview") + .font(.largeTitle) + .bold() + Text("Configure the page showing all tags") + .foregroundStyle(.secondary) + .padding(.bottom, 30) + + if let page = content.tagOverview?.localized(in: language) { + TagOverviewDetails(page: page) + } else { + Button("Create", action: createTagOverviewPage) + } + } + } + } + + private func createTagOverviewPage() { + content.tagOverview = TagOverviewPage( + content: content, + german: .init(title: "Alle Tags", urlString: "alle"), + english: .init(title: "All tags", urlString: "all")) + } +} + +private struct TagOverviewDetails: View { + + @ObservedObject + var page: LocalizedTagOverviewPage + + @EnvironmentObject + var content: Content + + @State + private var showImagePicker = false + + @State + private var newUrlString: String = "" + + init(page: LocalizedTagOverviewPage) { + self.page = page + } + + private var newUrlCanBeUpdated: Bool { + guard !newUrlString.isEmpty else { return false } + guard content.isValidIdForTagOrPageOrPost(newUrlString) else { return false } + return !content.containsTag(withUrlComponent: newUrlString) + } + + var body: some View { + VStack(alignment: .leading) { + Text("Title") + .font(.headline) + TextField("", text: $page.title) + .textFieldStyle(.roundedBorder) + + HStack { + Text("Page URL String") + .font(.headline) + TextField("", text: $newUrlString) + .textFieldStyle(.roundedBorder) + Button("Update", action: setNewId) + .disabled(!newUrlCanBeUpdated) + } + .padding(.bottom) + + Text("Link Preview Title") + .font(.headline) + OptionalTextField("", text: $page.linkPreviewTitle, + prompt: page.title) + .textFieldStyle(.roundedBorder) + .padding(.bottom) + + HStack { + Text("Link Preview Image") + .font(.headline) + IconButton(symbol: .squareAndPencilCircleFill, + size: 22, + color: .blue) { + showImagePicker = true + } + + IconButton(symbol: .trashCircleFill, + size: 22, + color: .red) { + page.linkPreviewImage = nil + }.disabled(page.linkPreviewImage == nil) + } + + .buttonStyle(.plain) + if let image = page.linkPreviewImage { + image.imageToDisplay + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 400, maxHeight: 300) + .cornerRadius(8) + Text(image.id) + .font(.headline) + } + + Text("Link Preview Description") + .font(.headline) + .padding(.top) + OptionalDescriptionField(text: $page.linkPreviewDescription) + .textFieldStyle(.roundedBorder) + .padding(.bottom) + } + .sheet(isPresented: $showImagePicker) { + ImagePickerView(showImagePicker: $showImagePicker) { image in + page.linkPreviewImage = image + } + } + } + + private func setNewId() { + page.urlString = newUrlString + } +} diff --git a/CHDataManagement/Views/Tags/AddTagView.swift b/CHDataManagement/Views/Tags/AddTagView.swift index a3f9614..9d48a4e 100644 --- a/CHDataManagement/Views/Tags/AddTagView.swift +++ b/CHDataManagement/Views/Tags/AddTagView.swift @@ -26,6 +26,7 @@ struct AddTagView: View { private func addNewTag() { let newTag = Tag( content: content, + id: "tag", isVisible: true, german: .init(urlComponent: "tag", name: "Neuer Tag"), english: .init(urlComponent: "tag-en", name: "New Tag"))