Add tag overview, improve assets
This commit is contained in:
parent
8a3a0f1797
commit
1e67a99866
@ -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 */,
|
||||||
|
89
CHDataManagement/Generator/FeedPageGenerator.swift
Normal file
89
CHDataManagement/Generator/FeedPageGenerator.swift
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
33
CHDataManagement/Generator/HeaderElement.swift
Normal file
33
CHDataManagement/Generator/HeaderElement.swift
Normal 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' />"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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 {
|
||||||
|
@ -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),
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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: "/")
|
|
||||||
}
|
|
||||||
}
|
|
55
CHDataManagement/Model/Item/Item.swift
Normal file
55
CHDataManagement/Model/Item/Item.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
38
CHDataManagement/Model/Item/ItemId.swift
Normal file
38
CHDataManagement/Model/Item/ItemId.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
35
CHDataManagement/Model/Item/ItemType.swift
Normal file
35
CHDataManagement/Model/Item/ItemType.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
19
CHDataManagement/Model/Item/LocalizedItem.swift
Normal file
19
CHDataManagement/Model/Item/LocalizedItem.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
99
CHDataManagement/Model/Item/TagOverviewPage.swift
Normal file
99
CHDataManagement/Model/Item/TagOverviewPage.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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>"
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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>"
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
)
|
)
|
||||||
|
@ -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: [:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
31
CHDataManagement/Storage/Model/TagOverviewFile.swift
Normal file
31
CHDataManagement/Storage/Model/TagOverviewFile.swift
Normal 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 {
|
||||||
|
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
@ -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 {
|
||||||
|
@ -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()
|
||||||
|
19
CHDataManagement/Views/Generic/DetailTitle.swift
Normal file
19
CHDataManagement/Views/Generic/DetailTitle.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
CHDataManagement/Views/Generic/FilePropertyView.swift
Normal file
34
CHDataManagement/Views/Generic/FilePropertyView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
CHDataManagement/Views/Generic/IntegerPropertyView.swift
Normal file
23
CHDataManagement/Views/Generic/IntegerPropertyView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
111
CHDataManagement/Views/ItemSelectionView.swift
Normal file
111
CHDataManagement/Views/ItemSelectionView.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
130
CHDataManagement/Views/Settings/TagOverviewDetailView.swift
Normal file
130
CHDataManagement/Views/Settings/TagOverviewDetailView.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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"))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user