Add tag overview, improve assets

This commit is contained in:
Christoph Hagen 2024-12-15 21:20:12 +01:00
parent 8a3a0f1797
commit 1e67a99866
59 changed files with 1301 additions and 480 deletions

View File

@ -25,6 +25,17 @@
E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; }; E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; };
E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850382CFCA6BA0090B18B /* WebsiteData+Mock.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 */; }; 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 */; }; E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; }; E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; };
E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.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 */; }; E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */; };
E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */; }; E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */; };
E29D31852D0AE8EE0051B7F4 /* RequiredHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.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 */; }; E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */; };
E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318A2D0B07E60051B7F4 /* ContentBox.swift */; }; E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318A2D0B07E60051B7F4 /* ContentBox.swift */; };
E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.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 */; }; E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; };
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; }; E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; };
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; }; 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 */; }; E2B85F412C4294790047CD0C /* PageHead.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F402C4294790047CD0C /* PageHead.swift */; };
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F422C4294F60047CD0C /* FeedEntry.swift */; }; E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F422C4294F60047CD0C /* FeedEntry.swift */; };
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F442C429ED60047CD0C /* ImageGallery.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 = "<group>"; }; E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = "<group>"; };
E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Mock.swift"; sourceTree = "<group>"; }; E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Mock.swift"; sourceTree = "<group>"; };
E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostFeedSettingsView.swift; sourceTree = "<group>"; }; E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostFeedSettingsView.swift; sourceTree = "<group>"; };
E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSelectionView.swift; sourceTree = "<group>"; };
E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewPage.swift; sourceTree = "<group>"; };
E22990182D0E3546009F8D77 /* ItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = "<group>"; };
E229901D2D0E4362009F8D77 /* LocalizedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedItem.swift; sourceTree = "<group>"; };
E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewDetailView.swift; sourceTree = "<group>"; };
E22990212D0ED129009F8D77 /* TagOverviewFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewFile.swift; sourceTree = "<group>"; };
E22990232D0EDBD0009F8D77 /* HeaderElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderElement.swift; sourceTree = "<group>"; };
E22990252D0F5822009F8D77 /* FilePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePropertyView.swift; sourceTree = "<group>"; };
E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerPropertyView.swift; sourceTree = "<group>"; };
E22990292D0F5A10009F8D77 /* DetailTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTitle.swift; sourceTree = "<group>"; };
E229902B2D0F6FC0009F8D77 /* ItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemId.swift; sourceTree = "<group>"; };
E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = "<group>"; }; E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = "<group>"; };
E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; }; E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = "<group>"; }; E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = "<group>"; };
@ -276,7 +297,6 @@
E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsIcons.swift; sourceTree = "<group>"; }; E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsIcons.swift; sourceTree = "<group>"; };
E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedPageLink.swift; sourceTree = "<group>"; }; E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedPageLink.swift; sourceTree = "<group>"; };
E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredHeaders.swift; sourceTree = "<group>"; }; E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequiredHeaders.swift; sourceTree = "<group>"; };
E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdditionalPageHeaders.swift; sourceTree = "<group>"; };
E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelViewer.swift; sourceTree = "<group>"; }; E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelViewer.swift; sourceTree = "<group>"; };
E29D318A2D0B07E60051B7F4 /* ContentBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBox.swift; sourceTree = "<group>"; }; E29D318A2D0B07E60051B7F4 /* ContentBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentBox.swift; sourceTree = "<group>"; };
E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsContentView.swift; sourceTree = "<group>"; }; E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsContentView.swift; sourceTree = "<group>"; };
@ -329,7 +349,7 @@
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextField.swift; sourceTree = "<group>"; }; E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextField.swift; sourceTree = "<group>"; };
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; }; E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; }; E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
E2B85F3C2C4293F80047CD0C /* PageInFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInFeed.swift; sourceTree = "<group>"; }; E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedPageGenerator.swift; sourceTree = "<group>"; };
E2B85F402C4294790047CD0C /* PageHead.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHead.swift; sourceTree = "<group>"; }; E2B85F402C4294790047CD0C /* PageHead.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHead.swift; sourceTree = "<group>"; };
E2B85F422C4294F60047CD0C /* FeedEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntry.swift; sourceTree = "<group>"; }; E2B85F422C4294F60047CD0C /* FeedEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntry.swift; sourceTree = "<group>"; };
E2B85F442C429ED60047CD0C /* ImageGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGallery.swift; sourceTree = "<group>"; }; E2B85F442C429ED60047CD0C /* ImageGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGallery.swift; sourceTree = "<group>"; };
@ -361,6 +381,18 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup 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 = "<group>";
};
E25DA5112CFF001900AEF16D /* Model */ = { E25DA5112CFF001900AEF16D /* Model */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -371,6 +403,7 @@
E21850182CEE561B0090B18B /* PageOnDisk.swift */, E21850182CEE561B0090B18B /* PageOnDisk.swift */,
E2A37D142CE68BEA0000979F /* PostFile.swift */, E2A37D142CE68BEA0000979F /* PostFile.swift */,
E2A37D162CE73F170000979F /* TagFile.swift */, E2A37D162CE73F170000979F /* TagFile.swift */,
E22990212D0ED129009F8D77 /* TagOverviewFile.swift */,
); );
path = Model; path = Model;
sourceTree = "<group>"; sourceTree = "<group>";
@ -402,6 +435,7 @@
E25DA5782D01C56200AEF16D /* Generator */ = { E25DA5782D01C56200AEF16D /* Generator */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E22990232D0EDBD0009F8D77 /* HeaderElement.swift */,
E29D31B62D0DAC030051B7F4 /* Page Content */, E29D31B62D0DAC030051B7F4 /* Page Content */,
E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */, E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */,
E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */, E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */,
@ -410,6 +444,7 @@
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */, E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */, E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */,
E25DA5982D02401A00AEF16D /* PageGenerator.swift */, E25DA5982D02401A00AEF16D /* PageGenerator.swift */,
E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */,
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */, E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */,
E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */, E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */,
E29D31252D0370A50051B7F4 /* VideoOption.swift */, E29D31252D0370A50051B7F4 /* VideoOption.swift */,
@ -439,7 +474,6 @@
E29D31932D0B7D250051B7F4 /* SvgImage.swift */, E29D31932D0B7D250051B7F4 /* SvgImage.swift */,
E29D318A2D0B07E60051B7F4 /* ContentBox.swift */, E29D318A2D0B07E60051B7F4 /* ContentBox.swift */,
E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */, E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */,
E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */,
E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */, E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */,
E29D31272D0371870051B7F4 /* ContentPageVideo.swift */, E29D31272D0371870051B7F4 /* ContentPageVideo.swift */,
E29D31232D0366820051B7F4 /* TagList.swift */, E29D31232D0366820051B7F4 /* TagList.swift */,
@ -539,6 +573,7 @@
E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */, E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */,
E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */, E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */,
E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */, E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */,
E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */,
); );
path = Settings; path = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
@ -554,6 +589,9 @@
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */, E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */,
E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */, E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */,
E2A21C0F2CB18B390060935B /* FlowHStack.swift */, E2A21C0F2CB18B390060935B /* FlowHStack.swift */,
E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */,
E22990252D0F5822009F8D77 /* FilePropertyView.swift */,
E22990292D0F5A10009F8D77 /* DetailTitle.swift */,
); );
path = Generic; path = Generic;
sourceTree = "<group>"; sourceTree = "<group>";
@ -599,7 +637,7 @@
E2B85F392C428F020047CD0C /* Model */ = { E2B85F392C428F020047CD0C /* Model */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E29D31A22D0CC98B0051B7F4 /* Item.swift */, E229901A2D0E3F09009F8D77 /* Item */,
E25DA5812D01C79800AEF16D /* Types */, E25DA5812D01C79800AEF16D /* Types */,
E25DA53B2D0042EA00AEF16D /* Settings */, E25DA53B2D0042EA00AEF16D /* Settings */,
E2E06DFA2CA4A6570019C2AF /* Content.swift */, E2E06DFA2CA4A6570019C2AF /* Content.swift */,
@ -626,7 +664,6 @@
children = ( children = (
E25DA5962D023F9900AEF16D /* ContentPage.swift */, E25DA5962D023F9900AEF16D /* ContentPage.swift */,
E25DA51C2CFF135B00AEF16D /* GenericPage.swift */, E25DA51C2CFF135B00AEF16D /* GenericPage.swift */,
E2B85F3C2C4293F80047CD0C /* PageInFeed.swift */,
); );
path = Pages; path = Pages;
sourceTree = "<group>"; sourceTree = "<group>";
@ -651,6 +688,7 @@
E2B85F462C42C7CA0047CD0C /* Views */ = { E2B85F462C42C7CA0047CD0C /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */,
E2A21C372CB9A4F10060935B /* Generic */, E2A21C372CB9A4F10060935B /* Generic */,
E2B85F4B2C4B8B7F0047CD0C /* Posts */, E2B85F4B2C4B8B7F0047CD0C /* Posts */,
E2A21C322CB5BCAC0060935B /* Pages */, E2A21C322CB5BCAC0060935B /* Pages */,
@ -835,6 +873,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
E29D31242D0366860051B7F4 /* TagList.swift in Sources */, E29D31242D0366860051B7F4 /* TagList.swift in Sources */,
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */,
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */, E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */, E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */, E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
@ -848,7 +887,6 @@
E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */,
E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */, E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */,
E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */, E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */,
E29D31872D0AE9DE0051B7F4 /* AdditionalPageHeaders.swift in Sources */,
E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */,
E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */, E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */,
E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */, E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */,
@ -881,7 +919,8 @@
E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */, E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */,
E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */, E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */,
E25DA5972D023F9F00AEF16D /* ContentPage.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 */, E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */,
E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */, E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */,
E29D31492D0489BB0051B7F4 /* AddFileView.swift in Sources */, E29D31492D0489BB0051B7F4 /* AddFileView.swift in Sources */,
@ -896,14 +935,17 @@
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */,
E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */,
E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */, E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */,
E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */, E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */,
E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */, E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */,
E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */, E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */,
E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */,
E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */, E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */,
E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */, E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */,
E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */, E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */,
E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */, E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */,
E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */,
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */, E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */,
E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */, E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */,
@ -923,6 +965,7 @@
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */, E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */,
E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */, E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */,
E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */,
E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */,
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */,
E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */,
@ -933,6 +976,7 @@
E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */, E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */,
E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */, E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */,
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */,
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */, E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */,
@ -958,12 +1002,16 @@
E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */, E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */,
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */, E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */,
E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */, E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */,
E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */,
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */,
E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */,
E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */,
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */, E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */,
E22990192D0E3546009F8D77 /* ItemType.swift in Sources */,
E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */,
E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */, E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */,
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */,
E29D315F2D06D6F30051B7F4 /* CodeFileType.swift in Sources */, E29D315F2D06D6F30051B7F4 /* CodeFileType.swift in Sources */,
E2A37D0E2CE527070000979F /* Storage.swift in Sources */, E2A37D0E2CE527070000979F /* Storage.swift in Sources */,
E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */, E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */,

View File

@ -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 += "<h1>\(title)</h1>"
}
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 = "<script>"
for post in posts {
guard post.images.count > 1 else {
continue
}
result += ImageGallery.swiperInit(id: post.entryId)
}
result += "</script>"
return result
}
}

View File

@ -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 "<link rel='stylesheet' href='\(file.assetUrl)' />"
case .js(let file, let deferred):
let deferText = deferred ? " defer" : ""
return "<script src='\(file.assetUrl)'\(deferText)></script>"
case .jsModule(let file):
return "<script type='module' src='\(file.assetUrl)'></script>"
case .title(let title):
return "<title>\(title)</title>"
case .description(let description):
return "<meta name='description' content='\(description)'>"
case .charset:
return "<meta charset='utf-8' />"
case .viewport:
return "<meta name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1' />"
}
}
}

View File

@ -23,9 +23,9 @@ final class LocalizedWebsiteGenerator {
private let imageGenerator: ImageGenerator private let imageGenerator: ImageGenerator
private var navigationBarLinks: [NavigationBar.Link] { private var navigationBarLinks: [NavigationBar.Link] {
content.settings.navigationTags.map { content.settings.navigationItems.map {
let localized = $0.localized(in: language) .init(text: $0.title(in: language),
return .init(text: localized.name, url: content.absoluteUrlToTag($0, language: language)) url: $0.absoluteUrl(in: language))
} }
} }
@ -46,6 +46,7 @@ final class LocalizedWebsiteGenerator {
return false return false
} }
#warning("Generate content pages") #warning("Generate content pages")
#warning("Generate tag overview page")
guard generateTagPages() else { guard generateTagPages() else {
return false return false
} }
@ -94,7 +95,7 @@ final class LocalizedWebsiteGenerator {
} }
private func generatePagesFolderIfNeeded() -> Bool { private func generatePagesFolderIfNeeded() -> Bool {
let relativePath = content.settings.pages.pageUrlPrefix let relativePath = content.settings.paths.pagesOutputFolderPath
return content.storage.write(in: .outputPath) { folder in return content.storage.write(in: .outputPath) { folder in
let outputFile = folder.appendingPathComponent(relativePath, isDirectory: true) let outputFile = folder.appendingPathComponent(relativePath, isDirectory: true)
@ -130,7 +131,7 @@ final class LocalizedWebsiteGenerator {
return true return true
} }
let path = page.absoluteUrl(for: language) + ".html" let path = page.absoluteUrl(in: language) + ".html"
guard save(content, to: path) else { guard save(content, to: path) else {
print("Failed to save page") print("Failed to save page")
return false return false

View File

@ -78,7 +78,7 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
let footerScript = AudioPlayerScript(items: amplitude).content let footerScript = AudioPlayerScript(items: amplitude).content
results.requiredFooters.insert(footerScript) results.requiredFooters.insert(footerScript)
results.requiredHeaders.insert(.audioPlayerCss) results.requiredHeaders.insert(.audioPlayerCss)
results.requiredHeaders.insert(.amplitude) results.requiredHeaders.insert(.audioPlayerJs)
results.requiredIcons.formUnion([ results.requiredIcons.formUnion([
.audioPlayerClose, .audioPlayerClose,

View File

@ -101,7 +101,7 @@ final class PageContentParser {
return markdown.between("[", and: "]") return markdown.between("[", and: "]")
} }
results.linkedPages.insert(page) results.linkedPages.insert(page)
let pagePath = page.absoluteUrl(for: language) let pagePath = page.absoluteUrl(in: language)
return html.replacingOccurrences(of: textToChange, with: pagePath) return html.replacingOccurrences(of: textToChange, with: pagePath)
} }
@ -119,12 +119,15 @@ final class PageContentParser {
return html.replacingOccurrences(of: textToChange, with: tagPath) return html.replacingOccurrences(of: textToChange, with: tagPath)
} }
private func handleHTML(_: String, markdown: Substring) -> String { private func handleHTML(html: String, _: Substring) -> String {
let result = String(markdown) findResourcesInHtml(html: html)
findImages(in: result) return html
findLinks(in: result) }
findSourceSets(in: result)
return result private func findResourcesInHtml(html: String) {
findImages(in: html)
findLinks(in: html)
findSourceSets(in: html)
} }
private func findImages(in markdown: String) { private func findImages(in markdown: String) {
@ -298,7 +301,7 @@ final class PageContentParser {
results.files.insert(image) results.files.insert(image)
let caption = arguments.count == 2 ? arguments[1] : nil let caption = arguments.count == 2 ? arguments[1] : nil
let altText = image.getDescription(for: language) let altText = image.localized(in: language)
let path = image.absoluteUrl let path = image.absoluteUrl
@ -403,7 +406,9 @@ final class PageContentParser {
results.missing(file: fileId, markdown: markdown) results.missing(file: fileId, markdown: markdown)
return "" 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 localized = page.localized(in: language)
let url = page.absoluteUrl(for: language) let url = page.absoluteUrl(in: language)
let title = localized.linkPreviewTitle ?? localized.title let title = localized.linkPreviewTitle ?? localized.title
let description = localized.linkPreviewDescription ?? "" let description = localized.linkPreviewDescription ?? ""
@ -450,7 +455,7 @@ final class PageContentParser {
return RelatedPageLink.Image( return RelatedPageLink.Image(
url: image.absoluteUrl, url: image.absoluteUrl,
description: image.getDescription(for: language), description: image.localized(in: language),
size: size) size: size)
} }
@ -478,7 +483,7 @@ final class PageContentParser {
} }
let localized = tag.localized(in: language) let localized = tag.localized(in: language)
let url = tag.absoluteUrl(for: language) let url = tag.absoluteUrl(in: language)
let title = localized.name let title = localized.name
let description = localized.description ?? "" let description = localized.description ?? ""
@ -489,7 +494,7 @@ final class PageContentParser {
return RelatedPageLink.Image( return RelatedPageLink.Image(
url: image.absoluteUrl, url: image.absoluteUrl,
description: image.getDescription(for: language), description: image.localized(in: language),
size: size) size: size)
} }
@ -522,7 +527,7 @@ final class PageContentParser {
results.files.insert(file) results.files.insert(file)
results.requiredHeaders.insert(.modelViewer) results.requiredHeaders.insert(.modelViewer)
let description = file.getDescription(for: language) let description = file.localized(in: language)
return ModelViewer(file: file.absoluteUrl, description: description).content return ModelViewer(file: file.absoluteUrl, description: description).content
} }
@ -554,7 +559,7 @@ final class PageContentParser {
return PartialSvgImage( return PartialSvgImage(
imagePath: image.absoluteUrl, imagePath: image.absoluteUrl,
altText: image.getDescription(for: language), altText: image.localized(in: language),
x: x, x: x,
y: y, y: y,
width: partWidth, width: partWidth,

View File

@ -12,6 +12,18 @@ final class PageGenerator {
self.navigationBarLinks = navigationBarLinks 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) { func generate(page: Page, language: ContentLanguage) throws -> (page: String, results: PageGenerationResults) {
let contentGenerator = PageContentParser( let contentGenerator = PageContentParser(
content: content, content: content,
@ -30,9 +42,8 @@ final class PageGenerator {
url: content.absoluteUrlToTag(tag, language: language)) url: content.absoluteUrlToTag(tag, language: language))
} }
let headers = AdditionalPageHeaders( let headers = makeHeaders(requiredItems: contentGenerator.results.requiredHeaders.sorted())
headers: contentGenerator.results.requiredHeaders,
assetPath: content.settings.pages.javascriptFilesPath)
let fullPage = ContentPage( let fullPage = ContentPage(
language: language, language: language,
dateString: page.dateText(in: language), dateString: page.dateText(in: language),
@ -42,7 +53,7 @@ final class PageGenerator {
description: localized.linkPreviewDescription ?? "", description: localized.linkPreviewDescription ?? "",
navigationBarLinks: navigationBarLinks, navigationBarLinks: navigationBarLinks,
pageContent: pageContent, pageContent: pageContent,
headers: headers.content, headers: headers,
footers: contentGenerator.results.requiredFooters.sorted(), footers: contentGenerator.results.requiredFooters.sorted(),
icons: contentGenerator.results.requiredIcons) icons: contentGenerator.results.requiredIcons)
.content .content

View File

@ -8,6 +8,7 @@ final class PostListPageGenerator {
private let imageGenerator: ImageGenerator private let imageGenerator: ImageGenerator
#warning("Get from settings")
private let navigationBarLinks: [NavigationBar.Link] private let navigationBarLinks: [NavigationBar.Link]
private let showTitle: Bool private let showTitle: Bool
@ -62,7 +63,7 @@ final class PostListPageGenerator {
let linkUrl = post.linkedPage.map { let linkUrl = post.linkedPage.map {
FeedEntryData.Link( FeedEntryData.Link(
url: $0.absoluteUrl(for: language), url: $0.absoluteUrl(in: language),
text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings
} }
@ -81,16 +82,17 @@ final class PostListPageGenerator {
images: localized.images.map(createImageSet)) images: localized.images.map(createImageSet))
} }
let feed = PageInFeed( let feedPageGenerator = FeedPageGenerator(content: content)
let fileContent = feedPageGenerator.generatePage(
language: language, language: language,
posts: posts,
title: pageTitle, title: pageTitle,
showTitle: showTitle,
description: pageDescription, description: pageDescription,
navigationBarLinks: bar, showTitle: showTitle,
pageNumber: pageIndex, pageNumber: pageIndex,
totalPages: pageCount, totalPages: pageCount)
posts: posts)
let fileContent = feed.content
if pageIndex == 1 { if pageIndex == 1 {
return save(fileContent, to: "\(pageUrlPrefix).html") return save(fileContent, to: "\(pageUrlPrefix).html")
} else { } else {
@ -107,7 +109,7 @@ final class PostListPageGenerator {
rawImagePath: image.absoluteUrl, rawImagePath: image.absoluteUrl,
width: Int(mainContentMaximumWidth), width: Int(mainContentMaximumWidth),
height: Int(mainContentMaximumWidth), height: Int(mainContentMaximumWidth),
altText: image.getDescription(for: language)) altText: image.localized(in: language))
} }
private func save(_ content: String, to relativePath: String) -> Bool { private func save(_ content: String, to relativePath: String) -> Bool {

View File

@ -1,22 +1,46 @@
enum HeaderFile: String { enum HeaderFile: Int {
case codeHightlighting = "highlight.min.js" case codeHightlighting = 4
case modelViewer = "model-viewer.min.js" case modelViewer = 3
case audioPlayerCss = "audio-player.css" /// CSS File to style the audio player
case audioPlayerCss = 1
case amplitude = "amplitude.min.js" /// JavaScript file for the audio player
case audioPlayerJs = 2
var asModule: Bool { func header(content: Content) -> HeaderElement? {
switch self { switch self {
case .codeHightlighting: return false case .codeHightlighting:
case .modelViewer: return true if let file = content.settings.pages.codeHighlightingJsFile {
case .amplitude: return false return HeaderElement.js(file: file, defer: true)
case .audioPlayerCss: return false }
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<HeaderFile> typealias RequiredHeaders = Set<HeaderFile>

View File

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
import SFSafeSymbols import SFSafeSymbols
#warning("Allow selection of pages as navigation bar items") #warning("Allow selection of pages as navigation bar items")
#warning("Show all warnings on page content") #warning("Show all warnings on page content")
#warning("Button to delete file") #warning("Button to delete file")
@ -12,6 +13,7 @@ import SFSafeSymbols
#warning("Replace links to files inside pages when id changes") #warning("Replace links to files inside pages when id changes")
#warning("Calculate file sizes") #warning("Calculate file sizes")
#warning("Specify image aspect ratio to prevent page jumps") #warning("Specify image aspect ratio to prevent page jumps")
#warning("Add version and source url properties to file resources")
@main @main
struct MainView: App { struct MainView: App {

View File

@ -51,6 +51,7 @@ extension Content {
let postsData = try storage.loadAllPosts() let postsData = try storage.loadAllPosts()
let fileList = try storage.loadAllFiles() let fileList = try storage.loadAllFiles()
let externalFiles = try storage.loadExternalFileList() let externalFiles = try storage.loadExternalFileList()
let tagOverviewData = try storage.loadTagOverview()
var files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in var files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in
let descriptions = imageDescriptions[fileId] let descriptions = imageDescriptions[fileId]
@ -77,6 +78,7 @@ extension Content {
let tags = tagData.reduce(into: [:]) { (tags, data) in let tags = tagData.reduce(into: [:]) { (tags, data) in
tags[data.key] = Tag( tags[data.key] = Tag(
content: self, content: self,
id: data.value.id,
isVisible: data.value.isVisible, isVisible: data.value.isVisible,
german: convert(data.value.german, images: images), german: convert(data.value.german, images: images),
english: convert(data.value.english, images: images)) english: convert(data.value.english, images: images))
@ -102,28 +104,46 @@ extension Content {
linkedPage: linkedPage) 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.tags = tags.values.sorted()
self.pages = pages.values.sorted(ascending: false) { $0.startDate } self.pages = pages.values.sorted(ascending: false) { $0.startDate }
self.files = files.values.sorted { $0.id } self.files = files.values.sorted { $0.id }
self.posts = posts.sorted(ascending: false) { $0.startDate } 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( let posts = PostSettings(file: settings.posts, files: files)
postsPerPage: settings.posts.postsPerPage,
contentWidth: settings.posts.contentWidth)
let pages = PageSettings(file: settings.pages) let pages = PageSettings(file: settings.pages, files: files)
let paths = PathSettings(file: settings.paths) let paths = PathSettings(file: settings.paths)
return Settings( return Settings(
paths: paths, paths: paths,
navigationTags: navigationTags, navigationItems: navigationItems,
posts: posts, posts: posts,
pages: pages, pages: pages,
german: .init(file: settings.german), german: .init(file: settings.german),

View File

@ -18,16 +18,17 @@ extension Content {
try storage.save(settings: settings.file) try storage.save(settings: settings.file)
let fileDescriptions: [FileDescriptions] = files.sorted().compactMap { file in 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 nil
} }
return FileDescriptions( return FileDescriptions(
fileId: file.id, fileId: file.id,
german: file.germanDescription.nonEmpty, german: file.german.nonEmpty,
english: file.englishDescription.nonEmpty) english: file.english.nonEmpty)
} }
try storage.save(fileDescriptions: fileDescriptions) try storage.save(fileDescriptions: fileDescriptions)
try storage.save(tagOverview: tagOverview?.file)
let externalFileList = files.filter { $0.isExternallyStored }.map { $0.id } let externalFileList = files.filter { $0.isExternallyStored }.map { $0.id }
try storage.save(externalFileList: externalFileList) try storage.save(externalFileList: externalFileList)
@ -130,7 +131,7 @@ extension Settings {
var file: SettingsFile { var file: SettingsFile {
.init( .init(
paths: paths.file, paths: paths.file,
navigationTags: navigationTags.map { $0.id }, navigationItems: navigationItems.map { .init(type: $0.itemType, id: $0.id) },
posts: posts.file, posts: posts.file,
pages: pages.file, pages: pages.file,
german: german.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 { private extension PostSettings {
var file: PostSettingsFile { var file: PostSettingsFile {
.init(postsPerPage: postsPerPage, .init(postsPerPage: postsPerPage,
contentWidth: contentWidth)
}
}
private extension PageSettings {
var file: PageSettingsFile {
.init(pageUrlPrefix: pageUrlPrefix,
contentWidth: contentWidth, contentWidth: contentWidth,
largeImageWidth: largeImageWidth, swiperCssFile: swiperCssFile?.id,
pageLinkImageSize: pageLinkImageSize, swiperJsFile: swiperJsFile?.id,
javascriptFilesPath: javascriptFilesPath) defaultCssFile: defaultCssFile?.id)
} }
} }

View File

@ -18,11 +18,16 @@ extension Content {
!posts.contains { $0.id == id } !posts.contains { $0.id == id }
} }
func isValidIdForTagOrTagOrPost(_ id: String) -> Bool { func isValidIdForTagOrPageOrPost(_ id: String) -> Bool {
id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil
} }
func isValidIdForFile(_ id: String) -> Bool { func isValidIdForFile(_ id: String) -> Bool {
id.rangeOfCharacter(from: Content.disallowedCharactersInFileIds) == nil id.rangeOfCharacter(from: Content.disallowedCharactersInFileIds) == nil
} }
func containsTag(withUrlComponent urlComponent: String) -> Bool {
(tagOverview?.contains(urlComponent: urlComponent) ?? false) ||
tags.contains { $0.contains(urlComponent: urlComponent) }
}
} }

View File

@ -19,6 +19,12 @@ final class Content: ObservableObject {
@Published @Published
var files: [FileResource] var files: [FileResource]
@Published
var tagOverview: TagOverviewPage?
@Published
var results: [ItemId : PageGenerationResults] = [:]
@AppStorage("contentPath") @AppStorage("contentPath")
private var storedContentPath: String = "" private var storedContentPath: String = ""
@ -38,12 +44,14 @@ final class Content: ObservableObject {
pages: [Page], pages: [Page],
tags: [Tag], tags: [Tag],
files: [FileResource], files: [FileResource],
tagOverview: TagOverviewPage?,
storedContentPath: String) { storedContentPath: String) {
self.settings = settings self.settings = settings
self.posts = posts self.posts = posts
self.pages = pages self.pages = pages
self.tags = tags self.tags = tags
self.files = files self.files = files
self.tagOverview = tagOverview
self.storedContentPath = storedContentPath self.storedContentPath = storedContentPath
self.contentPath = storedContentPath self.contentPath = storedContentPath
self.storage = Storage(baseFolder: URL(filePath: storedContentPath)) self.storage = Storage(baseFolder: URL(filePath: storedContentPath))
@ -64,6 +72,7 @@ final class Content: ObservableObject {
self.pages = [] self.pages = []
self.tags = [] self.tags = []
self.files = [] self.files = []
self.tagOverview = nil
contentPath = storedContentPath contentPath = storedContentPath
do { do {

View File

@ -14,3 +14,21 @@ extension ContentLanguage: Codable {
extension ContentLanguage: CaseIterable { 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
}
}

View File

@ -5,29 +5,24 @@ final class FileResource: Item {
let type: FileType let type: FileType
/// Globally unique id
@Published
var id: String
@Published @Published
var isExternallyStored: Bool var isExternallyStored: Bool
@Published @Published
var germanDescription: String var german: String
@Published @Published
var englishDescription: String var english: String
@Published @Published
var size: CGSize = .zero var size: CGSize = .zero
init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) { init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) {
self.id = id
self.type = FileType(fileExtension: id.fileExtension) self.type = FileType(fileExtension: id.fileExtension)
self.englishDescription = en self.english = en
self.germanDescription = de self.german = de
self.isExternallyStored = isExternallyStored 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) { init(resourceImage: String, type: ImageFileType) {
self.type = .image(type) self.type = .image(type)
self.id = resourceImage self.english = "A test image included in the bundle"
self.englishDescription = "A test image included in the bundle" self.german = "Ein Testbild aus dem Bundle"
self.germanDescription = "Ein Testbild aus dem Bundle"
self.isExternallyStored = true self.isExternallyStored = true
super.init(content: .mock) // TODO: Add images to mock super.init(content: .mock, id: resourceImage) // TODO: Add images to mock
}
func getDescription(for language: ContentLanguage) -> String {
switch language {
case .english: return englishDescription
case .german: return germanDescription
}
} }
// MARK: Text // MARK: Text
@ -108,6 +95,11 @@ final class FileResource: Item {
return makeCleanAbsolutePath(path) return makeCleanAbsolutePath(path)
} }
var assetUrl: String {
let path = content.settings.paths.assetsOutputFolderPath + "/" + id
return makeCleanAbsolutePath(path)
}
private var pathPrefix: String { private var pathPrefix: String {
switch type { 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
}
}

View File

@ -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: "/")
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -2,12 +2,6 @@ import Foundation
final class Page: Item { final class Page: Item {
/**
The unique id of the entry
*/
@Published
var id: String
/** /**
The external link this page points to. The external link this page points to.
@ -59,7 +53,6 @@ final class Page: Item {
german: LocalizedPage, german: LocalizedPage,
english: LocalizedPage, english: LocalizedPage,
tags: [Tag]) { tags: [Tag]) {
self.id = id
self.externalLink = externalLink self.externalLink = externalLink
self.isDraft = isDraft self.isDraft = isDraft
self.createdDate = createdDate self.createdDate = createdDate
@ -70,14 +63,7 @@ final class Page: Item {
self.english = english self.english = english
self.tags = tags self.tags = tags
super.init(content: content) super.init(content: content, id: id)
}
func localized(in language: ContentLanguage) -> LocalizedPage {
switch language {
case .german: return german
case .english: return english
}
} }
func update(id newId: String) -> Bool { func update(id newId: String) -> Bool {
@ -95,7 +81,7 @@ final class Page: Item {
// MARK: Paths // MARK: Paths
func absoluteUrl(for language: ContentLanguage) -> String { override func absoluteUrl(in language: ContentLanguage) -> String {
if let url = externalLink { if let url = externalLink {
return url return url
} }
@ -103,40 +89,31 @@ final class Page: Item {
return makeCleanAbsolutePath(internalPath(for: language)) return makeCleanAbsolutePath(internalPath(for: language))
} }
override func title(in language: ContentLanguage) -> String {
localized(in: language).title
}
func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String { func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String {
makeCleanRelativePath(internalPath(for: language)) makeCleanRelativePath(internalPath(for: language))
} }
private func internalPath(for language: ContentLanguage) -> String { 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 { override var itemType: ItemType {
.page
} }
extension Page: Equatable { func contains(urlComponent: String) -> Bool {
english.urlString == urlComponent || german.urlString == urlComponent
static func == (lhs: Page, rhs: Page) -> Bool {
lhs.id == rhs.id
}
}
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
} }
} }
extension Page: DateItem { extension Page: DateItem {
} }
extension Page: LocalizedItem {
}

View File

@ -2,11 +2,6 @@ import Foundation
final class PageSettings: ObservableObject { final class PageSettings: ObservableObject {
/// The prefix of the urls for all pages
/// The full path will be `<pagePrefix>/<page-url-component>`
@Published
var pageUrlPrefix: String
@Published @Published
var contentWidth: Int var contentWidth: Int
@ -17,13 +12,39 @@ final class PageSettings: ObservableObject {
var pageLinkImageSize: Int var pageLinkImageSize: Int
@Published @Published
var javascriptFilesPath: String var defaultCssFile: FileResource?
init(file: PageSettingsFile) { @Published
self.pageUrlPrefix = file.pageUrlPrefix 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.contentWidth = file.contentWidth
self.largeImageWidth = file.largeImageWidth self.largeImageWidth = file.largeImageWidth
self.pageLinkImageSize = file.pageLinkImageSize 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)
} }
} }

View File

@ -5,6 +5,9 @@ final class PathSettings: ObservableObject {
@Published @Published
var outputDirectoryPath: String var outputDirectoryPath: String
@Published
var assetsOutputFolderPath: String
@Published @Published
var pagesOutputFolderPath: String var pagesOutputFolderPath: String
@ -21,6 +24,7 @@ final class PathSettings: ObservableObject {
var tagsOutputFolderPath: String var tagsOutputFolderPath: String
init(file: PathSettingsFile) { init(file: PathSettingsFile) {
self.assetsOutputFolderPath = file.assetsOutputFolderPath
self.outputDirectoryPath = file.outputDirectoryPath self.outputDirectoryPath = file.outputDirectoryPath
self.pagesOutputFolderPath = file.pagesOutputFolderPath self.pagesOutputFolderPath = file.pagesOutputFolderPath
self.imagesOutputFolderPath = file.imagesOutputFolderPath self.imagesOutputFolderPath = file.imagesOutputFolderPath
@ -28,4 +32,14 @@ final class PathSettings: ObservableObject {
self.videosOutputFolderPath = file.videosOutputFolderPath self.videosOutputFolderPath = file.videosOutputFolderPath
self.tagsOutputFolderPath = file.tagsOutputFolderPath self.tagsOutputFolderPath = file.tagsOutputFolderPath
} }
var file: PathSettingsFile {
.init(outputDirectoryPath: outputDirectoryPath,
assetsOutputFolderPath: assetsOutputFolderPath,
pagesOutputFolderPath: pagesOutputFolderPath,
imagesOutputFolderPath: imagesOutputFolderPath,
filesOutputFolderPath: filesOutputFolderPath,
videosOutputFolderPath: videosOutputFolderPath,
tagsOutputFolderPath: tagsOutputFolderPath)
}
} }

View File

@ -10,13 +10,32 @@ final class PostSettings: ObservableObject {
@Published @Published
var contentWidth: Int 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.postsPerPage = postsPerPage
self.contentWidth = contentWidth 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.postsPerPage = file.postsPerPage
self.contentWidth = file.contentWidth 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] }
} }
} }

View File

@ -5,9 +5,9 @@ final class Settings: ObservableObject {
@Published @Published
var paths: PathSettings var paths: PathSettings
/// The tags to show in the navigation bar /// The items to show in the navigation bar
@Published @Published
var navigationTags: [Tag] var navigationItems: [Item]
@Published @Published
var posts: PostSettings var posts: PostSettings
@ -21,9 +21,9 @@ final class Settings: ObservableObject {
@Published @Published
var english: LocalizedPostSettings 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.paths = paths
self.navigationTags = navigationTags self.navigationItems = navigationItems
self.posts = posts self.posts = posts
self.pages = pages self.pages = pages
self.german = german self.german = german

View File

@ -2,10 +2,6 @@ import Foundation
final class Tag: Item { final class Tag: Item {
var id: String {
english.urlComponent
}
@Published @Published
var isVisible: Bool var isVisible: Bool
@ -15,19 +11,19 @@ final class Tag: Item {
@Published @Published
var english: LocalizedTag var english: LocalizedTag
init(content: Content, id: String) { override init(content: Content, id: String) {
self.isVisible = true self.isVisible = true
self.english = .init(urlComponent: id, name: id) self.english = .init(urlComponent: id, name: id)
let deId = id + "-" + ContentLanguage.german.rawValue let deId = id + "-" + ContentLanguage.german.rawValue
self.german = .init(urlComponent: deId, name: deId) 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.isVisible = isVisible
self.german = german self.german = german
self.english = english self.english = english
super.init(content: content) super.init(content: content, id: id)
} }
var linkName: String { var linkName: String {
@ -38,49 +34,33 @@ final class Tag: Item {
"/tags/\(linkName).html" "/tags/\(linkName).html"
} }
func localized(in language: ContentLanguage) -> LocalizedTag {
switch language {
case .english: return english
case .german: return german
}
}
// MARK: Paths // MARK: Paths
func absoluteUrl(for language: ContentLanguage) -> String {
makeCleanAbsolutePath(internalPath(for: language))
}
func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String { func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String {
makeCleanRelativePath(internalPath(for: language)) makeCleanRelativePath(internalPath(for: language))
} }
private func internalPath(for language: ContentLanguage) -> String { 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: LocalizedItem {
} }
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
}
}

View File

@ -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 "<link rel='stylesheet' type='text/css' href='\(assetPath)/css/\(file)'>"
}
let module = asset.asModule ? " type='module'" : ""
return "<script\(module) src='\(assetPath)/js/\(file)'></script>"
}
}

View File

@ -1,38 +1,15 @@
import Foundation import Foundation
//import Elementary
struct PageHead {
let title: String struct PageHead: HtmlProducer {
let description: String let items: [HeaderElement]
let additionalHeaders: String func populate(_ result: inout String) {
result += "<head>"
var content: String { for item in items {
""" result += item.content
<head> }
<meta charset="utf-8" /> result += "</head>"
<title>\(title)</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
<meta name="description" content="\(description)">
\(additionalHeaders)
<link rel="stylesheet" href="/assets/css/style.css" />
</head>
"""
} }
} }
/*
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"))
}
}
*/

View File

@ -18,13 +18,13 @@ struct ContentPage: HtmlProducer {
private let pageContent: String private let pageContent: String
private let headers: String private let headers: [HeaderElement]
private let footers: String private let footers: String
private let icons: Set<PageIcon> private let icons: Set<PageIcon>
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<PageIcon>) { 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<PageIcon>) {
self.language = language self.language = language
self.dateString = dateString self.dateString = dateString
self.title = title self.title = title
@ -41,7 +41,7 @@ struct ContentPage: HtmlProducer {
func populate(_ result: inout String) { func populate(_ result: inout String) {
// TODO: Add headers and footers from page content // TODO: Add headers and footers from page content
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">" result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
result += PageHead(title: title, description: description, additionalHeaders: headers).content result += PageHead(items: [.charset, .viewport] + headers).content
result += "<body>" result += "<body>"
result += NavigationBar(links: navigationBarLinks).content result += NavigationBar(links: navigationBarLinks).content

View File

@ -10,25 +10,25 @@ struct GenericPage {
let links: [NavigationBar.Link] let links: [NavigationBar.Link]
let additionalHeaders: String let headers: [HeaderElement]
let additionalFooter: String let additionalFooter: String
let insertedContent: (inout String) -> Void 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.language = language
self.title = title self.title = title
self.description = description self.description = description
self.links = links self.links = links
self.additionalHeaders = additionalHeaders self.headers = headers
self.additionalFooter = additionalFooter self.additionalFooter = additionalFooter
self.insertedContent = insertedContent self.insertedContent = insertedContent
} }
var content: String { var content: String {
var result = "" var result = ""
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">" result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
result += PageHead(title: title, description: description, additionalHeaders: additionalHeaders).content result += PageHead(items: [.charset, .viewport] + headers).content
result += "<body>" result += "<body>"
result += NavigationBar(links: links).content result += NavigationBar(links: links).content
result += "<div class=\"content\"><div style=\"height: 70px;\"></div>" result += "<div class=\"content\"><div style=\"height: 70px;\"></div>"

View File

@ -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 {
"<link rel='stylesheet' href='\(swiperStyleSheetPath)' />"
}
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 += "<h1>\(title)</h1>"
}
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 = "<script src='\(swiperJsPath)'></script><script>"
for post in posts {
guard post.images.count > 1 else {
continue
}
result += ImageGallery.swiperInit(id: post.entryId)
}
result += "</script>"
return result
}
}

View File

@ -20,5 +20,6 @@ extension Content {
pages: [.empty], pages: [.empty],
tags: [.hiking, .mountains, .nature, .sports], tags: [.hiking, .mountains, .nature, .sports],
files: [], files: [],
tagOverview: nil,
storedContentPath: dbPath) storedContentPath: dbPath)
} }

View File

@ -4,29 +4,34 @@ extension Tag {
static let mock = Tag( static let mock = Tag(
content: .mock, content: .mock,
id: "electronics",
german: .german, german: .german,
english: .english) english: .english)
static let nature = Tag( static let nature = Tag(
content: .mock, content: .mock,
id: "nature",
german: .init(urlComponent: "natur", name: "Natur"), german: .init(urlComponent: "natur", name: "Natur"),
english: .init(urlComponent: "nature", name: "Nature") english: .init(urlComponent: "nature", name: "Nature")
) )
static let sports = Tag( static let sports = Tag(
content: .mock, content: .mock,
id: "sports",
german: .init(urlComponent: "sport", name: "Sport"), german: .init(urlComponent: "sport", name: "Sport"),
english: .init(urlComponent: "sports", name: "Sports") english: .init(urlComponent: "sports", name: "Sports")
) )
static let hiking = Tag( static let hiking = Tag(
content: .mock, content: .mock,
id: "hiking",
german: .init(urlComponent: "wandern", name: "Wandern"), german: .init(urlComponent: "wandern", name: "Wandern"),
english: .init(urlComponent: "hiking", name: "Hiking") english: .init(urlComponent: "hiking", name: "Hiking")
) )
static let mountains = Tag( static let mountains = Tag(
content: .mock, content: .mock,
id: "mountains",
german: .init(urlComponent: "berge", name: "Berge"), german: .init(urlComponent: "berge", name: "Berge"),
english: .init(urlComponent: "mountains", name: "Mountains") english: .init(urlComponent: "mountains", name: "Mountains")
) )

View File

@ -4,7 +4,7 @@ extension Settings {
static let mock: Settings = .init( static let mock: Settings = .init(
paths: .default, paths: .default,
navigationTags: [], navigationItems: [],
posts: .default, posts: .default,
pages: .default, pages: .default,
german: .german, german: .german,
@ -21,14 +21,14 @@ extension PathSettings {
extension PostSettings { extension PostSettings {
static var `default`: PostSettings { static var `default`: PostSettings {
.init(file: .default) .init(file: .default, files: [:])
} }
} }
extension PageSettings { extension PageSettings {
static var `default`: PageSettings { static var `default`: PageSettings {
.init(file: .default) .init(file: .default, files: [:])
} }
} }

View File

@ -1,15 +1,21 @@
struct PageSettingsFile { struct PageSettingsFile {
let pageUrlPrefix: String
let contentWidth: Int let contentWidth: Int
let largeImageWidth: Int let largeImageWidth: Int
let pageLinkImageSize: 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 { extension PageSettingsFile: Codable {
@ -19,10 +25,13 @@ extension PageSettingsFile: Codable {
extension PageSettingsFile { extension PageSettingsFile {
static var `default`: PageSettingsFile { static var `default`: PageSettingsFile {
.init(pageUrlPrefix: "page", .init(contentWidth: 600,
contentWidth: 600,
largeImageWidth: 1200, largeImageWidth: 1200,
pageLinkImageSize: 180, pageLinkImageSize: 180,
javascriptFilesPath: "/assets/js") defaultCssFile: nil,
codeHighlightingJsFile: nil,
audioPlayerJsFile: nil,
audioPlayerCssFile: nil,
modelViewerJsFile: nil)
} }
} }

View File

@ -3,6 +3,8 @@ struct PathSettingsFile {
let outputDirectoryPath: String let outputDirectoryPath: String
let assetsOutputFolderPath: String
let pagesOutputFolderPath: String let pagesOutputFolderPath: String
let imagesOutputFolderPath: String let imagesOutputFolderPath: String
@ -13,8 +15,15 @@ struct PathSettingsFile {
let tagsOutputFolderPath: String 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.outputDirectoryPath = outputDirectoryPath
self.assetsOutputFolderPath = assetsOutputFolderPath
self.pagesOutputFolderPath = pagesOutputFolderPath self.pagesOutputFolderPath = pagesOutputFolderPath
self.imagesOutputFolderPath = imagesOutputFolderPath self.imagesOutputFolderPath = imagesOutputFolderPath
self.filesOutputFolderPath = filesOutputFolderPath self.filesOutputFolderPath = filesOutputFolderPath
@ -32,6 +41,7 @@ extension PathSettingsFile {
static var `default`: PathSettingsFile { static var `default`: PathSettingsFile {
PathSettingsFile( PathSettingsFile(
outputDirectoryPath: "build", outputDirectoryPath: "build",
assetsOutputFolderPath: "asset",
pagesOutputFolderPath: "page", pagesOutputFolderPath: "page",
imagesOutputFolderPath: "image", imagesOutputFolderPath: "image",
filesOutputFolderPath: "file", filesOutputFolderPath: "file",

View File

@ -7,6 +7,12 @@ struct PostSettingsFile {
/// The maximum width of the main content /// The maximum width of the main content
let contentWidth: Int let contentWidth: Int
let swiperCssFile: String?
let swiperJsFile: String?
let defaultCssFile: String?
} }
extension PostSettingsFile: Codable { } extension PostSettingsFile: Codable { }
@ -15,6 +21,9 @@ extension PostSettingsFile {
static var `default`: PostSettingsFile { static var `default`: PostSettingsFile {
.init(postsPerPage: 25, .init(postsPerPage: 25,
contentWidth: 600) contentWidth: 600,
swiperCssFile: nil,
swiperJsFile: nil,
defaultCssFile: nil)
} }
} }

View File

@ -1,11 +1,18 @@
import Foundation import Foundation
struct NavigationItemReference: Codable {
let type: ItemType
let id: String
}
struct SettingsFile { struct SettingsFile {
let paths: PathSettingsFile let paths: PathSettingsFile
/// The tags to show in the navigation bar /// The tags to show in the navigation bar
let navigationTags: [String] let navigationItems: [NavigationItemReference]
let posts: PostSettingsFile let posts: PostSettingsFile
@ -23,7 +30,7 @@ extension SettingsFile {
static var `default`: SettingsFile { static var `default`: SettingsFile {
.init( .init(
paths: .default, paths: .default,
navigationTags: [], navigationItems: [],
posts: .default, posts: .default,
pages: .default, pages: .default,
german: .default, german: .default,

View File

@ -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 {
}

View File

@ -306,6 +306,18 @@ final class Storage {
try writeIfChanged(fileDescriptions, to: fileDescriptionFilename) 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 // MARK: Files
private let filesFolderName = "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<T>(_ 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 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 - 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<T>(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 - 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) 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)
}
}
} }

View File

@ -37,11 +37,11 @@ struct FileDetailView: View {
} }
Text("German Description") Text("German Description")
.font(.headline) .font(.headline)
TextField("", text: $file.germanDescription) TextField("", text: $file.german)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
Text("English Description") Text("English Description")
.font(.headline) .font(.headline)
TextField("", text: $file.englishDescription) TextField("", text: $file.english)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
if file.type.isImage { if file.type.isImage {
Text("Image size") Text("Image size")

View File

@ -73,20 +73,34 @@ struct FileListView: View {
guard oldValue != newValue else { guard oldValue != newValue else {
return return
} }
if let selectedFile, let newFile = filteredFiles.first
newValue.matches(selectedFile.type) {
guard let selectedFile else {
if let newFile {
DispatchQueue.main.async {
selectedFile = newFile
}
}
return return
} }
selectedFile = filteredFiles.first
if newValue.matches(selectedFile.type) {
return
}
DispatchQueue.main.async {
self.selectedFile = newFile
}
} }
} }
.onAppear { .onAppear {
if selectedFile == nil { if selectedFile == nil {
DispatchQueue.main.async {
selectedFile = content.files.first selectedFile = content.files.first
} }
} }
} }
} }
}
#Preview { #Preview {
NavigationSplitView { NavigationSplitView {

View File

@ -10,17 +10,32 @@ struct FileSelectionView: View {
init(selectedFile: Binding<FileResource?>) { init(selectedFile: Binding<FileResource?>) {
self._selectedFile = selectedFile self._selectedFile = selectedFile
self.newSelection = selectedFile.wrappedValue
} }
@State
private var newSelection: FileResource?
var body: some View { var body: some View {
VStack { VStack {
FileListView(selectedFile: $selectedFile) FileListView(selectedFile: $newSelection)
.frame(minHeight: 500, idealHeight: 600) .frame(minHeight: 500, idealHeight: 600)
HStack { HStack {
Button("Cancel") { Button("Cancel") {
DispatchQueue.main.async {
dismiss()
}
}
Button("Remove") {
DispatchQueue.main.async {
selectedFile = nil selectedFile = nil
dismiss() } dismiss()
Button("Select") { dismiss() } }
}
Button("Select") {
selectedFile = newSelection
dismiss()
}
} }
} }
.padding() .padding()

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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<Bool>, 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
}
}

View File

@ -210,7 +210,7 @@ struct PageIssueView: View {
} }
private func createPage(pageId: String) { private func createPage(pageId: String) {
guard content.isValidIdForTagOrTagOrPost(pageId) else { guard content.isValidIdForTagOrPageOrPost(pageId) else {
show(error: "Invalid page id, can't create page") show(error: "Invalid page id, can't create page")
return return
} }
@ -245,7 +245,7 @@ struct PageIssueView: View {
} }
private func createTag(tagId: String) { private func createTag(tagId: String) {
guard content.isValidIdForTagOrTagOrPost(tagId) else { guard content.isValidIdForTagOrPageOrPost(tagId) else {
show(error: "Invalid tag id, can't create tag") show(error: "Invalid tag id, can't create tag")
return return
} }

View File

@ -23,7 +23,7 @@ struct GenerationContentView: View {
var body: some View { var body: some View {
switch selectedSection { switch selectedSection {
case .folders, .navigationBar, .postFeed: case .folders, .navigationBar, .postFeed, .tagOverview:
generationView generationView
case .pages: case .pages:
PageSettingsContentView() PageSettingsContentView()

View File

@ -17,6 +17,8 @@ struct GenerationDetailView: View {
PostFeedSettingsView() PostFeedSettingsView()
case .pages: case .pages:
PageSettingsDetailView() PageSettingsDetailView()
case .tagOverview:
TagOverviewDetailView()
} }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)

View File

@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
struct NavigationBarSettingsView: View { struct NavigationBarSettingsView: View {
@ -9,7 +10,7 @@ struct NavigationBarSettingsView: View {
private var content: Content private var content: Content
@State @State
private var showTagPicker = false private var showItemPicker = false
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -21,14 +22,10 @@ struct NavigationBarSettingsView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.padding(.bottom, 30) .padding(.bottom, 30)
Text("Visible Tags") HStack {
Text("Links")
.font(.headline) .font(.headline)
FlowHStack { Button(action: { showItemPicker = true }) {
ForEach(content.settings.navigationTags) { tag in
TagView(text: tag.localized(in: language).name)
.foregroundStyle(.white)
}
Button(action: { showTagPicker = true }) {
Image(systemSymbol: .squareAndPencilCircleFill) Image(systemSymbol: .squareAndPencilCircleFill)
.resizable() .resizable()
.aspectRatio(1, contentMode: .fit) .aspectRatio(1, contentMode: .fit)
@ -40,15 +37,19 @@ struct NavigationBarSettingsView: View {
} }
.buttonStyle(.plain) .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.") Text("Select the tags to show in the navigation bar. The number should be even.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
.sheet(isPresented: $showTagPicker) { .sheet(isPresented: $showItemPicker) {
TagSelectionView( ItemSelectionView(
presented: $showTagPicker, isPresented: $showItemPicker,
selected: $content.settings.navigationTags, selectedItems: $content.settings.navigationItems)
tags: $content.tags)
} }
} }
} }

View File

@ -11,51 +11,49 @@ struct PageSettingsDetailView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Page Settings") DetailTitle(
.font(.largeTitle) title: "Page Settings",
.bold() text: "Change the way pages are displayed")
Text("Change the way pages are displayed")
.padding(.bottom, 30)
Text("Content Width") IntegerPropertyView(
.font(.headline) value: $content.settings.pages.contentWidth,
IntegerField("", number: $content.settings.pages.contentWidth) title: "Content Width",
.textFieldStyle(.roundedBorder) footer: "The maximum width of the content in pages (in pixels)")
Text("The maximum width of the content in pages (in pixels)")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Fullscreen Image Width") IntegerPropertyView(
.font(.headline) value: $content.settings.pages.largeImageWidth,
IntegerField("", number: $content.settings.pages.largeImageWidth) title: "Fullscreen Image Width",
.textFieldStyle(.roundedBorder) footer: "The maximum width of images that are diplayed fullscreen")
Text("The maximum width of images that are diplayed fullscreen")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Page Link Image Width") IntegerPropertyView(
.font(.headline) value: $content.settings.pages.pageLinkImageSize,
IntegerField("", number: $content.settings.pages.pageLinkImageSize) title: "Page Link Image Width",
.textFieldStyle(.roundedBorder) footer: "The maximum width of images diplayed as thumbnails on page links")
Text("The maximum width of images diplayed as thumbnails on page links")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Page URL Prefix") FilePropertyView(
.font(.headline) title: "Default CSS File",
TextField("", text: $content.settings.pages.pageUrlPrefix) description: "The CSS file containing the styling of all pages",
.textFieldStyle(.roundedBorder) selectedFile: $content.settings.pages.defaultCssFile)
Text("The URL prefix used for the links to pages")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Javascript Files Path") FilePropertyView(
.font(.headline) title: "Code Highlighting File",
TextField("", text: $content.settings.pages.javascriptFilesPath) description: "The JavaScript file to provide syntax highlighting of code blocks",
.textFieldStyle(.roundedBorder) selectedFile: $content.settings.pages.codeHighlightingJsFile)
Text("The path to the javascript files in the output folder")
.foregroundStyle(.secondary) FilePropertyView(
.padding(.bottom) 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)
} }
} }
} }

View File

@ -85,6 +85,14 @@ struct PathSettingsView: View {
Text("The path in the output folder where the generated videos are stored") Text("The path in the output folder where the generated videos are stored")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.padding(.bottom) .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)
} }
} }
} }

View File

@ -11,32 +11,36 @@ struct PostFeedSettingsView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Post Feed Settings") DetailTitle(title: "Post Feed Settings",
.font(.largeTitle) text: "Change the way the posts are displayed")
.bold()
Text("Change the way the posts are displayed")
.foregroundStyle(.secondary)
.padding(.bottom, 30)
Text("Content Width") IntegerPropertyView(
.font(.headline) value: $content.settings.posts.contentWidth,
IntegerField("", number: $content.settings.posts.contentWidth) title: "Content Width",
.textFieldStyle(.roundedBorder) footer: "The maximum width of the content the post feed (in pixels)")
.frame(maxWidth: 400)
Text("The maximum width of the content the post feed (in pixels)")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Posts Per Page") IntegerPropertyView(
.font(.headline) value: $content.settings.posts.postsPerPage,
IntegerField("", number: $content.settings.posts.postsPerPage) title: "Posts Per Page",
.textFieldStyle(.roundedBorder) footer: "The maximum number of posts displayed on a single page")
.frame(maxWidth: 400)
Text("The maximum number of posts displayed on a single page")
.foregroundStyle(.secondary)
.padding(.bottom)
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))
} }
} }
} }

View File

@ -12,17 +12,19 @@ enum SettingsSection: String {
case pages = "Pages" case pages = "Pages"
case tagOverview = "Tag Overview"
} }
extension SettingsSection { extension SettingsSection {
var icon: SFSymbol { var icon: SFSymbol {
switch self { switch self {
//case .generation: return .arrowTriangle2Circlepath
case .folders: return .folder case .folders: return .folder
case .navigationBar: return .menubarRectangle case .navigationBar: return .menubarRectangle
case .postFeed: return .rectangleGrid1x2 case .postFeed: return .rectangleGrid1x2
case .pages: return .docRichtext case .pages: return .docRichtext
case .tagOverview: return .tag
} }
} }
} }

View File

@ -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
}
}

View File

@ -26,6 +26,7 @@ struct AddTagView: View {
private func addNewTag() { private func addNewTag() {
let newTag = Tag( let newTag = Tag(
content: content, content: content,
id: "tag",
isVisible: true, isVisible: true,
german: .init(urlComponent: "tag", name: "Neuer Tag"), german: .init(urlComponent: "tag", name: "Neuer Tag"),
english: .init(urlComponent: "tag-en", name: "New Tag")) english: .init(urlComponent: "tag-en", name: "New Tag"))