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 */; };
|
||||
E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */; };
|
||||
E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */; };
|
||||
E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */; };
|
||||
E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */; };
|
||||
E22990192D0E3546009F8D77 /* ItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990182D0E3546009F8D77 /* ItemType.swift */; };
|
||||
E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229901D2D0E4362009F8D77 /* LocalizedItem.swift */; };
|
||||
E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */; };
|
||||
E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990212D0ED129009F8D77 /* TagOverviewFile.swift */; };
|
||||
E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990232D0EDBD0009F8D77 /* HeaderElement.swift */; };
|
||||
E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990252D0F5822009F8D77 /* FilePropertyView.swift */; };
|
||||
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */; };
|
||||
E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990292D0F5A10009F8D77 /* DetailTitle.swift */; };
|
||||
E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902B2D0F6FC0009F8D77 /* ItemId.swift */; };
|
||||
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
||||
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; };
|
||||
E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.swift */; };
|
||||
@ -109,7 +120,6 @@
|
||||
E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */; };
|
||||
E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */; };
|
||||
E29D31852D0AE8EE0051B7F4 /* RequiredHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */; };
|
||||
E29D31872D0AE9DE0051B7F4 /* AdditionalPageHeaders.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */; };
|
||||
E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */; };
|
||||
E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318A2D0B07E60051B7F4 /* ContentBox.swift */; };
|
||||
E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318D2D0B2E640051B7F4 /* PageSettingsContentView.swift */; };
|
||||
@ -165,7 +175,7 @@
|
||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; };
|
||||
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; };
|
||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; };
|
||||
E2B85F3D2C4293F80047CD0C /* PageInFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* PageInFeed.swift */; };
|
||||
E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */; };
|
||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F402C4294790047CD0C /* PageHead.swift */; };
|
||||
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F422C4294F60047CD0C /* FeedEntry.swift */; };
|
||||
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F442C429ED60047CD0C /* ImageGallery.swift */; };
|
||||
@ -196,6 +206,17 @@
|
||||
E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -276,7 +297,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -329,7 +349,7 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -361,6 +381,18 @@
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
E229901A2D0E3F09009F8D77 /* Item */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E229902B2D0F6FC0009F8D77 /* ItemId.swift */,
|
||||
E229901D2D0E4362009F8D77 /* LocalizedItem.swift */,
|
||||
E29D31A22D0CC98B0051B7F4 /* Item.swift */,
|
||||
E22990182D0E3546009F8D77 /* ItemType.swift */,
|
||||
E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */,
|
||||
);
|
||||
path = Item;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E25DA5112CFF001900AEF16D /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -371,6 +403,7 @@
|
||||
E21850182CEE561B0090B18B /* PageOnDisk.swift */,
|
||||
E2A37D142CE68BEA0000979F /* PostFile.swift */,
|
||||
E2A37D162CE73F170000979F /* TagFile.swift */,
|
||||
E22990212D0ED129009F8D77 /* TagOverviewFile.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@ -402,6 +435,7 @@
|
||||
E25DA5782D01C56200AEF16D /* Generator */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E22990232D0EDBD0009F8D77 /* HeaderElement.swift */,
|
||||
E29D31B62D0DAC030051B7F4 /* Page Content */,
|
||||
E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */,
|
||||
E29D31842D0AE8EE0051B7F4 /* RequiredHeaders.swift */,
|
||||
@ -410,6 +444,7 @@
|
||||
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
|
||||
E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */,
|
||||
E25DA5982D02401A00AEF16D /* PageGenerator.swift */,
|
||||
E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */,
|
||||
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */,
|
||||
E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */,
|
||||
E29D31252D0370A50051B7F4 /* VideoOption.swift */,
|
||||
@ -439,7 +474,6 @@
|
||||
E29D31932D0B7D250051B7F4 /* SvgImage.swift */,
|
||||
E29D318A2D0B07E60051B7F4 /* ContentBox.swift */,
|
||||
E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */,
|
||||
E29D31862D0AE9D40051B7F4 /* AdditionalPageHeaders.swift */,
|
||||
E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */,
|
||||
E29D31272D0371870051B7F4 /* ContentPageVideo.swift */,
|
||||
E29D31232D0366820051B7F4 /* TagList.swift */,
|
||||
@ -539,6 +573,7 @@
|
||||
E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */,
|
||||
E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */,
|
||||
E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */,
|
||||
E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@ -554,6 +589,9 @@
|
||||
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */,
|
||||
E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */,
|
||||
E2A21C0F2CB18B390060935B /* FlowHStack.swift */,
|
||||
E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */,
|
||||
E22990252D0F5822009F8D77 /* FilePropertyView.swift */,
|
||||
E22990292D0F5A10009F8D77 /* DetailTitle.swift */,
|
||||
);
|
||||
path = Generic;
|
||||
sourceTree = "<group>";
|
||||
@ -599,7 +637,7 @@
|
||||
E2B85F392C428F020047CD0C /* Model */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D31A22D0CC98B0051B7F4 /* Item.swift */,
|
||||
E229901A2D0E3F09009F8D77 /* Item */,
|
||||
E25DA5812D01C79800AEF16D /* Types */,
|
||||
E25DA53B2D0042EA00AEF16D /* Settings */,
|
||||
E2E06DFA2CA4A6570019C2AF /* Content.swift */,
|
||||
@ -626,7 +664,6 @@
|
||||
children = (
|
||||
E25DA5962D023F9900AEF16D /* ContentPage.swift */,
|
||||
E25DA51C2CFF135B00AEF16D /* GenericPage.swift */,
|
||||
E2B85F3C2C4293F80047CD0C /* PageInFeed.swift */,
|
||||
);
|
||||
path = Pages;
|
||||
sourceTree = "<group>";
|
||||
@ -651,6 +688,7 @@
|
||||
E2B85F462C42C7CA0047CD0C /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */,
|
||||
E2A21C372CB9A4F10060935B /* Generic */,
|
||||
E2B85F4B2C4B8B7F0047CD0C /* Posts */,
|
||||
E2A21C322CB5BCAC0060935B /* Pages */,
|
||||
@ -835,6 +873,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E29D31242D0366860051B7F4 /* TagList.swift in Sources */,
|
||||
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */,
|
||||
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
|
||||
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
|
||||
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
|
||||
@ -848,7 +887,6 @@
|
||||
E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */,
|
||||
E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */,
|
||||
E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */,
|
||||
E29D31872D0AE9DE0051B7F4 /* AdditionalPageHeaders.swift in Sources */,
|
||||
E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */,
|
||||
E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */,
|
||||
E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */,
|
||||
@ -881,7 +919,8 @@
|
||||
E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */,
|
||||
E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */,
|
||||
E25DA5972D023F9F00AEF16D /* ContentPage.swift in Sources */,
|
||||
E2B85F3D2C4293F80047CD0C /* PageInFeed.swift in Sources */,
|
||||
E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */,
|
||||
E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */,
|
||||
E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */,
|
||||
E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */,
|
||||
E29D31492D0489BB0051B7F4 /* AddFileView.swift in Sources */,
|
||||
@ -896,14 +935,17 @@
|
||||
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
|
||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
||||
E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */,
|
||||
E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */,
|
||||
E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */,
|
||||
E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */,
|
||||
E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */,
|
||||
E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */,
|
||||
E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */,
|
||||
E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */,
|
||||
E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */,
|
||||
E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */,
|
||||
E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */,
|
||||
E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */,
|
||||
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
|
||||
E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */,
|
||||
E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */,
|
||||
@ -923,6 +965,7 @@
|
||||
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
|
||||
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */,
|
||||
E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */,
|
||||
E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */,
|
||||
E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */,
|
||||
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */,
|
||||
E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */,
|
||||
@ -933,6 +976,7 @@
|
||||
E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */,
|
||||
E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */,
|
||||
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
|
||||
E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */,
|
||||
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
|
||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
||||
E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */,
|
||||
@ -958,12 +1002,16 @@
|
||||
E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */,
|
||||
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */,
|
||||
E29D31C32D0DBEF20051B7F4 /* Song.swift in Sources */,
|
||||
E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */,
|
||||
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */,
|
||||
E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */,
|
||||
E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */,
|
||||
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */,
|
||||
E22990192D0E3546009F8D77 /* ItemType.swift in Sources */,
|
||||
E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */,
|
||||
E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */,
|
||||
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
|
||||
E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */,
|
||||
E29D315F2D06D6F30051B7F4 /* CodeFileType.swift in Sources */,
|
||||
E2A37D0E2CE527070000979F /* Storage.swift in Sources */,
|
||||
E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */,
|
||||
|
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 var navigationBarLinks: [NavigationBar.Link] {
|
||||
content.settings.navigationTags.map {
|
||||
let localized = $0.localized(in: language)
|
||||
return .init(text: localized.name, url: content.absoluteUrlToTag($0, language: language))
|
||||
content.settings.navigationItems.map {
|
||||
.init(text: $0.title(in: language),
|
||||
url: $0.absoluteUrl(in: language))
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ final class LocalizedWebsiteGenerator {
|
||||
return false
|
||||
}
|
||||
#warning("Generate content pages")
|
||||
#warning("Generate tag overview page")
|
||||
guard generateTagPages() else {
|
||||
return false
|
||||
}
|
||||
@ -94,7 +95,7 @@ final class LocalizedWebsiteGenerator {
|
||||
}
|
||||
|
||||
private func generatePagesFolderIfNeeded() -> Bool {
|
||||
let relativePath = content.settings.pages.pageUrlPrefix
|
||||
let relativePath = content.settings.paths.pagesOutputFolderPath
|
||||
|
||||
return content.storage.write(in: .outputPath) { folder in
|
||||
let outputFile = folder.appendingPathComponent(relativePath, isDirectory: true)
|
||||
@ -130,7 +131,7 @@ final class LocalizedWebsiteGenerator {
|
||||
return true
|
||||
}
|
||||
|
||||
let path = page.absoluteUrl(for: language) + ".html"
|
||||
let path = page.absoluteUrl(in: language) + ".html"
|
||||
guard save(content, to: path) else {
|
||||
print("Failed to save page")
|
||||
return false
|
||||
|
@ -78,7 +78,7 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
|
||||
let footerScript = AudioPlayerScript(items: amplitude).content
|
||||
results.requiredFooters.insert(footerScript)
|
||||
results.requiredHeaders.insert(.audioPlayerCss)
|
||||
results.requiredHeaders.insert(.amplitude)
|
||||
results.requiredHeaders.insert(.audioPlayerJs)
|
||||
|
||||
results.requiredIcons.formUnion([
|
||||
.audioPlayerClose,
|
||||
|
@ -101,7 +101,7 @@ final class PageContentParser {
|
||||
return markdown.between("[", and: "]")
|
||||
}
|
||||
results.linkedPages.insert(page)
|
||||
let pagePath = page.absoluteUrl(for: language)
|
||||
let pagePath = page.absoluteUrl(in: language)
|
||||
return html.replacingOccurrences(of: textToChange, with: pagePath)
|
||||
}
|
||||
|
||||
@ -119,12 +119,15 @@ final class PageContentParser {
|
||||
return html.replacingOccurrences(of: textToChange, with: tagPath)
|
||||
}
|
||||
|
||||
private func handleHTML(_: String, markdown: Substring) -> String {
|
||||
let result = String(markdown)
|
||||
findImages(in: result)
|
||||
findLinks(in: result)
|
||||
findSourceSets(in: result)
|
||||
return result
|
||||
private func handleHTML(html: String, _: Substring) -> String {
|
||||
findResourcesInHtml(html: html)
|
||||
return html
|
||||
}
|
||||
|
||||
private func findResourcesInHtml(html: String) {
|
||||
findImages(in: html)
|
||||
findLinks(in: html)
|
||||
findSourceSets(in: html)
|
||||
}
|
||||
|
||||
private func findImages(in markdown: String) {
|
||||
@ -298,7 +301,7 @@ final class PageContentParser {
|
||||
results.files.insert(image)
|
||||
|
||||
let caption = arguments.count == 2 ? arguments[1] : nil
|
||||
let altText = image.getDescription(for: language)
|
||||
let altText = image.localized(in: language)
|
||||
|
||||
let path = image.absoluteUrl
|
||||
|
||||
@ -403,7 +406,9 @@ final class PageContentParser {
|
||||
results.missing(file: fileId, markdown: markdown)
|
||||
return ""
|
||||
}
|
||||
return file.textContent()
|
||||
let content = file.textContent()
|
||||
findResourcesInHtml(html: content)
|
||||
return content
|
||||
}
|
||||
|
||||
/**
|
||||
@ -439,7 +444,7 @@ final class PageContentParser {
|
||||
}
|
||||
|
||||
let localized = page.localized(in: language)
|
||||
let url = page.absoluteUrl(for: language)
|
||||
let url = page.absoluteUrl(in: language)
|
||||
let title = localized.linkPreviewTitle ?? localized.title
|
||||
let description = localized.linkPreviewDescription ?? ""
|
||||
|
||||
@ -450,7 +455,7 @@ final class PageContentParser {
|
||||
|
||||
return RelatedPageLink.Image(
|
||||
url: image.absoluteUrl,
|
||||
description: image.getDescription(for: language),
|
||||
description: image.localized(in: language),
|
||||
size: size)
|
||||
}
|
||||
|
||||
@ -478,7 +483,7 @@ final class PageContentParser {
|
||||
}
|
||||
|
||||
let localized = tag.localized(in: language)
|
||||
let url = tag.absoluteUrl(for: language)
|
||||
let url = tag.absoluteUrl(in: language)
|
||||
let title = localized.name
|
||||
let description = localized.description ?? ""
|
||||
|
||||
@ -489,7 +494,7 @@ final class PageContentParser {
|
||||
|
||||
return RelatedPageLink.Image(
|
||||
url: image.absoluteUrl,
|
||||
description: image.getDescription(for: language),
|
||||
description: image.localized(in: language),
|
||||
size: size)
|
||||
}
|
||||
|
||||
@ -522,7 +527,7 @@ final class PageContentParser {
|
||||
results.files.insert(file)
|
||||
results.requiredHeaders.insert(.modelViewer)
|
||||
|
||||
let description = file.getDescription(for: language)
|
||||
let description = file.localized(in: language)
|
||||
return ModelViewer(file: file.absoluteUrl, description: description).content
|
||||
}
|
||||
|
||||
@ -554,7 +559,7 @@ final class PageContentParser {
|
||||
|
||||
return PartialSvgImage(
|
||||
imagePath: image.absoluteUrl,
|
||||
altText: image.getDescription(for: language),
|
||||
altText: image.localized(in: language),
|
||||
x: x,
|
||||
y: y,
|
||||
width: partWidth,
|
||||
|
@ -12,6 +12,18 @@ final class PageGenerator {
|
||||
self.navigationBarLinks = navigationBarLinks
|
||||
}
|
||||
|
||||
func makeHeaders(requiredItems: [HeaderFile]) -> [HeaderElement] {
|
||||
var result = [HeaderElement]()
|
||||
for item in requiredItems {
|
||||
guard let header = item.header(content: content) else {
|
||||
#warning("Add warning on missing file assignment")
|
||||
continue
|
||||
}
|
||||
result.append(header)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func generate(page: Page, language: ContentLanguage) throws -> (page: String, results: PageGenerationResults) {
|
||||
let contentGenerator = PageContentParser(
|
||||
content: content,
|
||||
@ -30,9 +42,8 @@ final class PageGenerator {
|
||||
url: content.absoluteUrlToTag(tag, language: language))
|
||||
}
|
||||
|
||||
let headers = AdditionalPageHeaders(
|
||||
headers: contentGenerator.results.requiredHeaders,
|
||||
assetPath: content.settings.pages.javascriptFilesPath)
|
||||
let headers = makeHeaders(requiredItems: contentGenerator.results.requiredHeaders.sorted())
|
||||
|
||||
let fullPage = ContentPage(
|
||||
language: language,
|
||||
dateString: page.dateText(in: language),
|
||||
@ -42,7 +53,7 @@ final class PageGenerator {
|
||||
description: localized.linkPreviewDescription ?? "",
|
||||
navigationBarLinks: navigationBarLinks,
|
||||
pageContent: pageContent,
|
||||
headers: headers.content,
|
||||
headers: headers,
|
||||
footers: contentGenerator.results.requiredFooters.sorted(),
|
||||
icons: contentGenerator.results.requiredIcons)
|
||||
.content
|
||||
|
@ -8,6 +8,7 @@ final class PostListPageGenerator {
|
||||
|
||||
private let imageGenerator: ImageGenerator
|
||||
|
||||
#warning("Get from settings")
|
||||
private let navigationBarLinks: [NavigationBar.Link]
|
||||
|
||||
private let showTitle: Bool
|
||||
@ -62,7 +63,7 @@ final class PostListPageGenerator {
|
||||
|
||||
let linkUrl = post.linkedPage.map {
|
||||
FeedEntryData.Link(
|
||||
url: $0.absoluteUrl(for: language),
|
||||
url: $0.absoluteUrl(in: language),
|
||||
text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings
|
||||
}
|
||||
|
||||
@ -81,16 +82,17 @@ final class PostListPageGenerator {
|
||||
images: localized.images.map(createImageSet))
|
||||
}
|
||||
|
||||
let feed = PageInFeed(
|
||||
let feedPageGenerator = FeedPageGenerator(content: content)
|
||||
|
||||
let fileContent = feedPageGenerator.generatePage(
|
||||
language: language,
|
||||
posts: posts,
|
||||
title: pageTitle,
|
||||
showTitle: showTitle,
|
||||
description: pageDescription,
|
||||
navigationBarLinks: bar,
|
||||
showTitle: showTitle,
|
||||
pageNumber: pageIndex,
|
||||
totalPages: pageCount,
|
||||
posts: posts)
|
||||
let fileContent = feed.content
|
||||
totalPages: pageCount)
|
||||
|
||||
if pageIndex == 1 {
|
||||
return save(fileContent, to: "\(pageUrlPrefix).html")
|
||||
} else {
|
||||
@ -107,7 +109,7 @@ final class PostListPageGenerator {
|
||||
rawImagePath: image.absoluteUrl,
|
||||
width: Int(mainContentMaximumWidth),
|
||||
height: Int(mainContentMaximumWidth),
|
||||
altText: image.getDescription(for: language))
|
||||
altText: image.localized(in: language))
|
||||
}
|
||||
|
||||
private func save(_ content: String, to relativePath: String) -> Bool {
|
||||
|
@ -1,22 +1,46 @@
|
||||
|
||||
enum HeaderFile: String {
|
||||
enum HeaderFile: Int {
|
||||
|
||||
case codeHightlighting = "highlight.min.js"
|
||||
|
||||
case modelViewer = "model-viewer.min.js"
|
||||
case codeHightlighting = 4
|
||||
|
||||
case audioPlayerCss = "audio-player.css"
|
||||
case modelViewer = 3
|
||||
|
||||
case amplitude = "amplitude.min.js"
|
||||
/// CSS File to style the audio player
|
||||
case audioPlayerCss = 1
|
||||
|
||||
var asModule: Bool {
|
||||
/// JavaScript file for the audio player
|
||||
case audioPlayerJs = 2
|
||||
|
||||
func header(content: Content) -> HeaderElement? {
|
||||
switch self {
|
||||
case .codeHightlighting: return false
|
||||
case .modelViewer: return true
|
||||
case .amplitude: return false
|
||||
case .audioPlayerCss: return false
|
||||
case .codeHightlighting:
|
||||
if let file = content.settings.pages.codeHighlightingJsFile {
|
||||
return HeaderElement.js(file: file, defer: true)
|
||||
}
|
||||
case .modelViewer:
|
||||
if let file = content.settings.pages.modelViewerJsFile {
|
||||
return HeaderElement.jsModule(file)
|
||||
}
|
||||
case .audioPlayerCss:
|
||||
if let file = content.settings.pages.audioPlayerCssFile {
|
||||
return .css(file)
|
||||
}
|
||||
case .audioPlayerJs:
|
||||
if let file = content.settings.pages.audioPlayerJsFile {
|
||||
return .js(file: file, defer: true)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension HeaderFile: Comparable {
|
||||
|
||||
static func < (lhs: HeaderFile, rhs: HeaderFile) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
typealias RequiredHeaders = Set<HeaderFile>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
|
||||
#warning("Allow selection of pages as navigation bar items")
|
||||
#warning("Show all warnings on page content")
|
||||
#warning("Button to delete file")
|
||||
@ -12,6 +13,7 @@ import SFSafeSymbols
|
||||
#warning("Replace links to files inside pages when id changes")
|
||||
#warning("Calculate file sizes")
|
||||
#warning("Specify image aspect ratio to prevent page jumps")
|
||||
#warning("Add version and source url properties to file resources")
|
||||
|
||||
@main
|
||||
struct MainView: App {
|
||||
|
@ -51,6 +51,7 @@ extension Content {
|
||||
let postsData = try storage.loadAllPosts()
|
||||
let fileList = try storage.loadAllFiles()
|
||||
let externalFiles = try storage.loadExternalFileList()
|
||||
let tagOverviewData = try storage.loadTagOverview()
|
||||
|
||||
var files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in
|
||||
let descriptions = imageDescriptions[fileId]
|
||||
@ -77,6 +78,7 @@ extension Content {
|
||||
let tags = tagData.reduce(into: [:]) { (tags, data) in
|
||||
tags[data.key] = Tag(
|
||||
content: self,
|
||||
id: data.value.id,
|
||||
isVisible: data.value.isVisible,
|
||||
german: convert(data.value.german, images: images),
|
||||
english: convert(data.value.english, images: images))
|
||||
@ -102,28 +104,46 @@ extension Content {
|
||||
linkedPage: linkedPage)
|
||||
}
|
||||
|
||||
let tagOverview = tagOverviewData.map { file in
|
||||
TagOverviewPage(
|
||||
content: self,
|
||||
german: .init(file: file.german, image: file.german.linkPreviewImage.map { files[$0] }),
|
||||
english: .init(file: file.english, image: file.english.linkPreviewImage.map { files[$0] }))
|
||||
}
|
||||
|
||||
self.tags = tags.values.sorted()
|
||||
self.pages = pages.values.sorted(ascending: false) { $0.startDate }
|
||||
self.files = files.values.sorted { $0.id }
|
||||
self.posts = posts.sorted(ascending: false) { $0.startDate }
|
||||
self.settings = makeSettings(settings, tags: tags)
|
||||
self.tagOverview = tagOverview
|
||||
self.settings = makeSettings(settings, tags: tags, pages: pages, files: files)
|
||||
}
|
||||
|
||||
private func makeSettings(_ settings: SettingsFile, tags: [String : Tag]) -> Settings {
|
||||
private func makeSettings(_ settings: SettingsFile, tags: [String : Tag], pages: [String : Page], files: [String : FileResource]) -> Settings {
|
||||
|
||||
let navigationTags = settings.navigationTags.map { tags[$0]! }
|
||||
#warning("Notify about missing links")
|
||||
let navigationItems: [Item] = settings.navigationItems.compactMap {
|
||||
switch $0.type {
|
||||
case .tag:
|
||||
return tags[$0.id]
|
||||
case .page:
|
||||
return pages[$0.id]
|
||||
case .tagOverview:
|
||||
return tagOverview
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
let posts = PostSettings(
|
||||
postsPerPage: settings.posts.postsPerPage,
|
||||
contentWidth: settings.posts.contentWidth)
|
||||
let posts = PostSettings(file: settings.posts, files: files)
|
||||
|
||||
let pages = PageSettings(file: settings.pages)
|
||||
let pages = PageSettings(file: settings.pages, files: files)
|
||||
|
||||
let paths = PathSettings(file: settings.paths)
|
||||
|
||||
return Settings(
|
||||
paths: paths,
|
||||
navigationTags: navigationTags,
|
||||
navigationItems: navigationItems,
|
||||
posts: posts,
|
||||
pages: pages,
|
||||
german: .init(file: settings.german),
|
||||
|
@ -18,16 +18,17 @@ extension Content {
|
||||
try storage.save(settings: settings.file)
|
||||
|
||||
let fileDescriptions: [FileDescriptions] = files.sorted().compactMap { file in
|
||||
guard !file.englishDescription.isEmpty || !file.germanDescription.isEmpty else {
|
||||
guard !file.english.isEmpty || !file.german.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return FileDescriptions(
|
||||
fileId: file.id,
|
||||
german: file.germanDescription.nonEmpty,
|
||||
english: file.englishDescription.nonEmpty)
|
||||
german: file.german.nonEmpty,
|
||||
english: file.english.nonEmpty)
|
||||
}
|
||||
|
||||
try storage.save(fileDescriptions: fileDescriptions)
|
||||
try storage.save(tagOverview: tagOverview?.file)
|
||||
|
||||
let externalFileList = files.filter { $0.isExternallyStored }.map { $0.id }
|
||||
try storage.save(externalFileList: externalFileList)
|
||||
@ -130,7 +131,7 @@ extension Settings {
|
||||
var file: SettingsFile {
|
||||
.init(
|
||||
paths: paths.file,
|
||||
navigationTags: navigationTags.map { $0.id },
|
||||
navigationItems: navigationItems.map { .init(type: $0.itemType, id: $0.id) },
|
||||
posts: posts.file,
|
||||
pages: pages.file,
|
||||
german: german.file,
|
||||
@ -138,34 +139,14 @@ extension Settings {
|
||||
}
|
||||
}
|
||||
|
||||
private extension PathSettings {
|
||||
|
||||
var file: PathSettingsFile {
|
||||
.init(outputDirectoryPath: outputDirectoryPath,
|
||||
pagesOutputFolderPath: pagesOutputFolderPath,
|
||||
imagesOutputFolderPath: imagesOutputFolderPath,
|
||||
filesOutputFolderPath: filesOutputFolderPath,
|
||||
videosOutputFolderPath: videosOutputFolderPath,
|
||||
tagsOutputFolderPath: tagsOutputFolderPath)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PostSettings {
|
||||
|
||||
var file: PostSettingsFile {
|
||||
.init(postsPerPage: postsPerPage,
|
||||
contentWidth: contentWidth)
|
||||
}
|
||||
}
|
||||
|
||||
private extension PageSettings {
|
||||
|
||||
var file: PageSettingsFile {
|
||||
.init(pageUrlPrefix: pageUrlPrefix,
|
||||
contentWidth: contentWidth,
|
||||
largeImageWidth: largeImageWidth,
|
||||
pageLinkImageSize: pageLinkImageSize,
|
||||
javascriptFilesPath: javascriptFilesPath)
|
||||
swiperCssFile: swiperCssFile?.id,
|
||||
swiperJsFile: swiperJsFile?.id,
|
||||
defaultCssFile: defaultCssFile?.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,11 +18,16 @@ extension Content {
|
||||
!posts.contains { $0.id == id }
|
||||
}
|
||||
|
||||
func isValidIdForTagOrTagOrPost(_ id: String) -> Bool {
|
||||
func isValidIdForTagOrPageOrPost(_ id: String) -> Bool {
|
||||
id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil
|
||||
}
|
||||
|
||||
func isValidIdForFile(_ id: String) -> Bool {
|
||||
id.rangeOfCharacter(from: Content.disallowedCharactersInFileIds) == nil
|
||||
}
|
||||
|
||||
func containsTag(withUrlComponent urlComponent: String) -> Bool {
|
||||
(tagOverview?.contains(urlComponent: urlComponent) ?? false) ||
|
||||
tags.contains { $0.contains(urlComponent: urlComponent) }
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,12 @@ final class Content: ObservableObject {
|
||||
@Published
|
||||
var files: [FileResource]
|
||||
|
||||
@Published
|
||||
var tagOverview: TagOverviewPage?
|
||||
|
||||
@Published
|
||||
var results: [ItemId : PageGenerationResults] = [:]
|
||||
|
||||
@AppStorage("contentPath")
|
||||
private var storedContentPath: String = ""
|
||||
|
||||
@ -38,12 +44,14 @@ final class Content: ObservableObject {
|
||||
pages: [Page],
|
||||
tags: [Tag],
|
||||
files: [FileResource],
|
||||
tagOverview: TagOverviewPage?,
|
||||
storedContentPath: String) {
|
||||
self.settings = settings
|
||||
self.posts = posts
|
||||
self.pages = pages
|
||||
self.tags = tags
|
||||
self.files = files
|
||||
self.tagOverview = tagOverview
|
||||
self.storedContentPath = storedContentPath
|
||||
self.contentPath = storedContentPath
|
||||
self.storage = Storage(baseFolder: URL(filePath: storedContentPath))
|
||||
@ -64,6 +72,7 @@ final class Content: ObservableObject {
|
||||
self.pages = []
|
||||
self.tags = []
|
||||
self.files = []
|
||||
self.tagOverview = nil
|
||||
|
||||
contentPath = storedContentPath
|
||||
do {
|
||||
|
@ -14,3 +14,21 @@ extension ContentLanguage: Codable {
|
||||
extension ContentLanguage: CaseIterable {
|
||||
|
||||
}
|
||||
|
||||
extension ContentLanguage: Hashable {
|
||||
|
||||
}
|
||||
|
||||
extension ContentLanguage: Identifiable {
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension ContentLanguage: Comparable {
|
||||
|
||||
static func < (lhs: ContentLanguage, rhs: ContentLanguage) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
@ -5,29 +5,24 @@ final class FileResource: Item {
|
||||
|
||||
let type: FileType
|
||||
|
||||
/// Globally unique id
|
||||
@Published
|
||||
var id: String
|
||||
|
||||
@Published
|
||||
var isExternallyStored: Bool
|
||||
|
||||
@Published
|
||||
var germanDescription: String
|
||||
var german: String
|
||||
|
||||
@Published
|
||||
var englishDescription: String
|
||||
var english: String
|
||||
|
||||
@Published
|
||||
var size: CGSize = .zero
|
||||
|
||||
init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) {
|
||||
self.id = id
|
||||
self.type = FileType(fileExtension: id.fileExtension)
|
||||
self.englishDescription = en
|
||||
self.germanDescription = de
|
||||
self.english = en
|
||||
self.german = de
|
||||
self.isExternallyStored = isExternallyStored
|
||||
super.init(content: content)
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,18 +30,10 @@ final class FileResource: Item {
|
||||
*/
|
||||
init(resourceImage: String, type: ImageFileType) {
|
||||
self.type = .image(type)
|
||||
self.id = resourceImage
|
||||
self.englishDescription = "A test image included in the bundle"
|
||||
self.germanDescription = "Ein Testbild aus dem Bundle"
|
||||
self.english = "A test image included in the bundle"
|
||||
self.german = "Ein Testbild aus dem Bundle"
|
||||
self.isExternallyStored = true
|
||||
super.init(content: .mock) // TODO: Add images to mock
|
||||
}
|
||||
|
||||
func getDescription(for language: ContentLanguage) -> String {
|
||||
switch language {
|
||||
case .english: return englishDescription
|
||||
case .german: return germanDescription
|
||||
}
|
||||
super.init(content: .mock, id: resourceImage) // TODO: Add images to mock
|
||||
}
|
||||
|
||||
// MARK: Text
|
||||
@ -108,6 +95,11 @@ final class FileResource: Item {
|
||||
return makeCleanAbsolutePath(path)
|
||||
}
|
||||
|
||||
var assetUrl: String {
|
||||
let path = content.settings.paths.assetsOutputFolderPath + "/" + id
|
||||
return makeCleanAbsolutePath(path)
|
||||
}
|
||||
|
||||
|
||||
private var pathPrefix: String {
|
||||
switch type {
|
||||
@ -135,27 +127,6 @@ final class FileResource: Item {
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource: Identifiable {
|
||||
extension FileResource: LocalizedItem {
|
||||
|
||||
}
|
||||
|
||||
extension FileResource: Equatable {
|
||||
|
||||
static func == (lhs: FileResource, rhs: FileResource) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource: Comparable {
|
||||
|
||||
static func < (lhs: FileResource, rhs: FileResource) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
/**
|
||||
The unique id of the entry
|
||||
*/
|
||||
@Published
|
||||
var id: String
|
||||
|
||||
/**
|
||||
The external link this page points to.
|
||||
|
||||
@ -59,7 +53,6 @@ final class Page: Item {
|
||||
german: LocalizedPage,
|
||||
english: LocalizedPage,
|
||||
tags: [Tag]) {
|
||||
self.id = id
|
||||
self.externalLink = externalLink
|
||||
self.isDraft = isDraft
|
||||
self.createdDate = createdDate
|
||||
@ -70,14 +63,7 @@ final class Page: Item {
|
||||
self.english = english
|
||||
self.tags = tags
|
||||
|
||||
super.init(content: content)
|
||||
}
|
||||
|
||||
func localized(in language: ContentLanguage) -> LocalizedPage {
|
||||
switch language {
|
||||
case .german: return german
|
||||
case .english: return english
|
||||
}
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
func update(id newId: String) -> Bool {
|
||||
@ -95,7 +81,7 @@ final class Page: Item {
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
func absoluteUrl(for language: ContentLanguage) -> String {
|
||||
override func absoluteUrl(in language: ContentLanguage) -> String {
|
||||
if let url = externalLink {
|
||||
return url
|
||||
}
|
||||
@ -103,40 +89,31 @@ final class Page: Item {
|
||||
return makeCleanAbsolutePath(internalPath(for: language))
|
||||
}
|
||||
|
||||
override func title(in language: ContentLanguage) -> String {
|
||||
localized(in: language).title
|
||||
}
|
||||
|
||||
func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String {
|
||||
makeCleanRelativePath(internalPath(for: language))
|
||||
}
|
||||
|
||||
private func internalPath(for language: ContentLanguage) -> String {
|
||||
content.settings.pages.pageUrlPrefix + "/" + localized(in: language).urlString
|
||||
content.settings.paths.pagesOutputFolderPath + "/" + localized(in: language).urlString
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: Identifiable {
|
||||
|
||||
}
|
||||
|
||||
extension Page: Equatable {
|
||||
|
||||
static func == (lhs: Page, rhs: Page) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
override var itemType: ItemType {
|
||||
.page
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: Comparable {
|
||||
|
||||
static func < (lhs: Page, rhs: Page) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
func contains(urlComponent: String) -> Bool {
|
||||
english.urlString == urlComponent || german.urlString == urlComponent
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: DateItem {
|
||||
|
||||
}
|
||||
|
||||
extension Page: LocalizedItem {
|
||||
|
||||
}
|
||||
|
@ -2,11 +2,6 @@ import Foundation
|
||||
|
||||
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
|
||||
var contentWidth: Int
|
||||
|
||||
@ -17,13 +12,39 @@ final class PageSettings: ObservableObject {
|
||||
var pageLinkImageSize: Int
|
||||
|
||||
@Published
|
||||
var javascriptFilesPath: String
|
||||
var defaultCssFile: FileResource?
|
||||
|
||||
init(file: PageSettingsFile) {
|
||||
self.pageUrlPrefix = file.pageUrlPrefix
|
||||
@Published
|
||||
var codeHighlightingJsFile: FileResource?
|
||||
|
||||
@Published
|
||||
var audioPlayerJsFile: FileResource?
|
||||
|
||||
@Published
|
||||
var audioPlayerCssFile: FileResource?
|
||||
|
||||
@Published
|
||||
var modelViewerJsFile: FileResource?
|
||||
|
||||
init(file: PageSettingsFile, files: [String : FileResource]) {
|
||||
self.contentWidth = file.contentWidth
|
||||
self.largeImageWidth = file.largeImageWidth
|
||||
self.pageLinkImageSize = file.pageLinkImageSize
|
||||
self.javascriptFilesPath = file.javascriptFilesPath
|
||||
self.defaultCssFile = file.defaultCssFile.map { files[$0] }
|
||||
self.codeHighlightingJsFile = file.codeHighlightingJsFile.map { files[$0] }
|
||||
self.audioPlayerJsFile = file.audioPlayerJsFile.map { files[$0] }
|
||||
self.audioPlayerCssFile = file.audioPlayerCssFile.map { files[$0] }
|
||||
self.modelViewerJsFile = file.modelViewerJsFile.map { files[$0] }
|
||||
}
|
||||
|
||||
var file: PageSettingsFile {
|
||||
.init(contentWidth: contentWidth,
|
||||
largeImageWidth: largeImageWidth,
|
||||
pageLinkImageSize: pageLinkImageSize,
|
||||
defaultCssFile: defaultCssFile?.id,
|
||||
codeHighlightingJsFile: codeHighlightingJsFile?.id,
|
||||
audioPlayerJsFile: audioPlayerJsFile?.id,
|
||||
audioPlayerCssFile: audioPlayerJsFile?.id,
|
||||
modelViewerJsFile: modelViewerJsFile?.id)
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ final class PathSettings: ObservableObject {
|
||||
@Published
|
||||
var outputDirectoryPath: String
|
||||
|
||||
@Published
|
||||
var assetsOutputFolderPath: String
|
||||
|
||||
@Published
|
||||
var pagesOutputFolderPath: String
|
||||
|
||||
@ -21,6 +24,7 @@ final class PathSettings: ObservableObject {
|
||||
var tagsOutputFolderPath: String
|
||||
|
||||
init(file: PathSettingsFile) {
|
||||
self.assetsOutputFolderPath = file.assetsOutputFolderPath
|
||||
self.outputDirectoryPath = file.outputDirectoryPath
|
||||
self.pagesOutputFolderPath = file.pagesOutputFolderPath
|
||||
self.imagesOutputFolderPath = file.imagesOutputFolderPath
|
||||
@ -28,4 +32,14 @@ final class PathSettings: ObservableObject {
|
||||
self.videosOutputFolderPath = file.videosOutputFolderPath
|
||||
self.tagsOutputFolderPath = file.tagsOutputFolderPath
|
||||
}
|
||||
|
||||
var file: PathSettingsFile {
|
||||
.init(outputDirectoryPath: outputDirectoryPath,
|
||||
assetsOutputFolderPath: assetsOutputFolderPath,
|
||||
pagesOutputFolderPath: pagesOutputFolderPath,
|
||||
imagesOutputFolderPath: imagesOutputFolderPath,
|
||||
filesOutputFolderPath: filesOutputFolderPath,
|
||||
videosOutputFolderPath: videosOutputFolderPath,
|
||||
tagsOutputFolderPath: tagsOutputFolderPath)
|
||||
}
|
||||
}
|
||||
|
@ -10,13 +10,32 @@ final class PostSettings: ObservableObject {
|
||||
@Published
|
||||
var contentWidth: Int
|
||||
|
||||
init(postsPerPage: Int, contentWidth: Int) {
|
||||
@Published
|
||||
var swiperCssFile: FileResource?
|
||||
|
||||
@Published
|
||||
var swiperJsFile: FileResource?
|
||||
|
||||
@Published
|
||||
var defaultCssFile: FileResource?
|
||||
|
||||
init(postsPerPage: Int,
|
||||
contentWidth: Int,
|
||||
swiperCssFile: FileResource?,
|
||||
swiperJsFile: FileResource?,
|
||||
defaultCssFile: FileResource?) {
|
||||
self.postsPerPage = postsPerPage
|
||||
self.contentWidth = contentWidth
|
||||
self.swiperCssFile = swiperCssFile
|
||||
self.swiperJsFile = swiperJsFile
|
||||
self.defaultCssFile = defaultCssFile
|
||||
}
|
||||
|
||||
init(file: PostSettingsFile) {
|
||||
init(file: PostSettingsFile, files: [String : FileResource]) {
|
||||
self.postsPerPage = file.postsPerPage
|
||||
self.contentWidth = file.contentWidth
|
||||
self.swiperCssFile = file.swiperCssFile.map { files[$0] }
|
||||
self.swiperJsFile = file.swiperJsFile.map { files[$0] }
|
||||
self.defaultCssFile = file.defaultCssFile.map { files[$0] }
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,9 @@ final class Settings: ObservableObject {
|
||||
@Published
|
||||
var paths: PathSettings
|
||||
|
||||
/// The tags to show in the navigation bar
|
||||
/// The items to show in the navigation bar
|
||||
@Published
|
||||
var navigationTags: [Tag]
|
||||
var navigationItems: [Item]
|
||||
|
||||
@Published
|
||||
var posts: PostSettings
|
||||
@ -21,9 +21,9 @@ final class Settings: ObservableObject {
|
||||
@Published
|
||||
var english: LocalizedPostSettings
|
||||
|
||||
init(paths: PathSettings, navigationTags: [Tag], posts: PostSettings, pages: PageSettings, german: LocalizedPostSettings, english: LocalizedPostSettings) {
|
||||
init(paths: PathSettings, navigationItems: [Item], posts: PostSettings, pages: PageSettings, german: LocalizedPostSettings, english: LocalizedPostSettings) {
|
||||
self.paths = paths
|
||||
self.navigationTags = navigationTags
|
||||
self.navigationItems = navigationItems
|
||||
self.posts = posts
|
||||
self.pages = pages
|
||||
self.german = german
|
||||
|
@ -2,10 +2,6 @@ import Foundation
|
||||
|
||||
final class Tag: Item {
|
||||
|
||||
var id: String {
|
||||
english.urlComponent
|
||||
}
|
||||
|
||||
@Published
|
||||
var isVisible: Bool
|
||||
|
||||
@ -15,19 +11,19 @@ final class Tag: Item {
|
||||
@Published
|
||||
var english: LocalizedTag
|
||||
|
||||
init(content: Content, id: String) {
|
||||
override init(content: Content, id: String) {
|
||||
self.isVisible = true
|
||||
self.english = .init(urlComponent: id, name: id)
|
||||
let deId = id + "-" + ContentLanguage.german.rawValue
|
||||
self.german = .init(urlComponent: deId, name: deId)
|
||||
super.init(content: content)
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
init(content: Content, isVisible: Bool = true, german: LocalizedTag, english: LocalizedTag) {
|
||||
init(content: Content, id: String, isVisible: Bool = true, german: LocalizedTag, english: LocalizedTag) {
|
||||
self.isVisible = isVisible
|
||||
self.german = german
|
||||
self.english = english
|
||||
super.init(content: content)
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
var linkName: String {
|
||||
@ -38,49 +34,33 @@ final class Tag: Item {
|
||||
"/tags/\(linkName).html"
|
||||
}
|
||||
|
||||
func localized(in language: ContentLanguage) -> LocalizedTag {
|
||||
switch language {
|
||||
case .english: return english
|
||||
case .german: return german
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
func absoluteUrl(for language: ContentLanguage) -> String {
|
||||
makeCleanAbsolutePath(internalPath(for: language))
|
||||
}
|
||||
|
||||
func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String {
|
||||
makeCleanRelativePath(internalPath(for: language))
|
||||
}
|
||||
|
||||
private func internalPath(for language: ContentLanguage) -> String {
|
||||
content.settings.pages.pageUrlPrefix + "/" + localized(in: language).urlComponent
|
||||
content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlComponent
|
||||
}
|
||||
|
||||
override func absoluteUrl(in language: ContentLanguage) -> String {
|
||||
makeCleanAbsolutePath(internalPath(for: language))
|
||||
}
|
||||
|
||||
override func title(in language: ContentLanguage) -> String {
|
||||
localized(in: language).name
|
||||
}
|
||||
|
||||
override var itemType: ItemType {
|
||||
.tag
|
||||
}
|
||||
|
||||
func contains(urlComponent: String) -> Bool {
|
||||
german.urlComponent == urlComponent || english.urlComponent == urlComponent
|
||||
}
|
||||
}
|
||||
|
||||
extension Tag: Identifiable {
|
||||
|
||||
}
|
||||
|
||||
extension Tag: Equatable {
|
||||
|
||||
static func == (_ lhs: Tag, _ rhs: Tag) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension Tag: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
extension Tag: Comparable {
|
||||
|
||||
static func < (lhs: Tag, rhs: Tag) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
extension Tag: LocalizedItem {
|
||||
|
||||
}
|
||||
|
@ -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 Elementary
|
||||
|
||||
struct PageHead {
|
||||
|
||||
let title: String
|
||||
struct PageHead: HtmlProducer {
|
||||
|
||||
let description: String
|
||||
let items: [HeaderElement]
|
||||
|
||||
let additionalHeaders: String
|
||||
|
||||
var content: String {
|
||||
"""
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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>
|
||||
"""
|
||||
func populate(_ result: inout String) {
|
||||
result += "<head>"
|
||||
for item in items {
|
||||
result += item.content
|
||||
}
|
||||
result += "</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 headers: String
|
||||
private let headers: [HeaderElement]
|
||||
|
||||
private let footers: String
|
||||
|
||||
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.dateString = dateString
|
||||
self.title = title
|
||||
@ -41,7 +41,7 @@ struct ContentPage: HtmlProducer {
|
||||
func populate(_ result: inout String) {
|
||||
// TODO: Add headers and footers from page content
|
||||
result += "<!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 += NavigationBar(links: navigationBarLinks).content
|
||||
|
||||
|
@ -10,25 +10,25 @@ struct GenericPage {
|
||||
|
||||
let links: [NavigationBar.Link]
|
||||
|
||||
let additionalHeaders: String
|
||||
let headers: [HeaderElement]
|
||||
|
||||
let additionalFooter: String
|
||||
|
||||
let insertedContent: (inout String) -> Void
|
||||
|
||||
init(language: ContentLanguage, title: String, description: String, links: [NavigationBar.Link], additionalHeaders: String, additionalFooter: String, insertedContent: @escaping (inout String) -> Void) {
|
||||
init(language: ContentLanguage, title: String, description: String, links: [NavigationBar.Link], headers: [HeaderElement], additionalFooter: String, insertedContent: @escaping (inout String) -> Void) {
|
||||
self.language = language
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.links = links
|
||||
self.additionalHeaders = additionalHeaders
|
||||
self.headers = headers
|
||||
self.additionalFooter = additionalFooter
|
||||
self.insertedContent = insertedContent
|
||||
}
|
||||
var content: String {
|
||||
var result = ""
|
||||
result += "<!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 += NavigationBar(links: links).content
|
||||
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],
|
||||
tags: [.hiking, .mountains, .nature, .sports],
|
||||
files: [],
|
||||
tagOverview: nil,
|
||||
storedContentPath: dbPath)
|
||||
}
|
||||
|
@ -4,29 +4,34 @@ extension Tag {
|
||||
|
||||
static let mock = Tag(
|
||||
content: .mock,
|
||||
id: "electronics",
|
||||
german: .german,
|
||||
english: .english)
|
||||
|
||||
static let nature = Tag(
|
||||
content: .mock,
|
||||
id: "nature",
|
||||
german: .init(urlComponent: "natur", name: "Natur"),
|
||||
english: .init(urlComponent: "nature", name: "Nature")
|
||||
)
|
||||
|
||||
static let sports = Tag(
|
||||
content: .mock,
|
||||
id: "sports",
|
||||
german: .init(urlComponent: "sport", name: "Sport"),
|
||||
english: .init(urlComponent: "sports", name: "Sports")
|
||||
)
|
||||
|
||||
static let hiking = Tag(
|
||||
content: .mock,
|
||||
id: "hiking",
|
||||
german: .init(urlComponent: "wandern", name: "Wandern"),
|
||||
english: .init(urlComponent: "hiking", name: "Hiking")
|
||||
)
|
||||
|
||||
static let mountains = Tag(
|
||||
content: .mock,
|
||||
id: "mountains",
|
||||
german: .init(urlComponent: "berge", name: "Berge"),
|
||||
english: .init(urlComponent: "mountains", name: "Mountains")
|
||||
)
|
||||
|
@ -4,7 +4,7 @@ extension Settings {
|
||||
|
||||
static let mock: Settings = .init(
|
||||
paths: .default,
|
||||
navigationTags: [],
|
||||
navigationItems: [],
|
||||
posts: .default,
|
||||
pages: .default,
|
||||
german: .german,
|
||||
@ -21,14 +21,14 @@ extension PathSettings {
|
||||
extension PostSettings {
|
||||
|
||||
static var `default`: PostSettings {
|
||||
.init(file: .default)
|
||||
.init(file: .default, files: [:])
|
||||
}
|
||||
}
|
||||
|
||||
extension PageSettings {
|
||||
|
||||
static var `default`: PageSettings {
|
||||
.init(file: .default)
|
||||
.init(file: .default, files: [:])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,21 @@
|
||||
|
||||
struct PageSettingsFile {
|
||||
|
||||
let pageUrlPrefix: String
|
||||
|
||||
let contentWidth: Int
|
||||
|
||||
let largeImageWidth: Int
|
||||
|
||||
let pageLinkImageSize: Int
|
||||
|
||||
let javascriptFilesPath: String
|
||||
let defaultCssFile: String?
|
||||
|
||||
let codeHighlightingJsFile: String?
|
||||
|
||||
let audioPlayerJsFile: String?
|
||||
|
||||
let audioPlayerCssFile: String?
|
||||
|
||||
let modelViewerJsFile: String?
|
||||
}
|
||||
|
||||
extension PageSettingsFile: Codable {
|
||||
@ -19,10 +25,13 @@ extension PageSettingsFile: Codable {
|
||||
extension PageSettingsFile {
|
||||
|
||||
static var `default`: PageSettingsFile {
|
||||
.init(pageUrlPrefix: "page",
|
||||
contentWidth: 600,
|
||||
.init(contentWidth: 600,
|
||||
largeImageWidth: 1200,
|
||||
pageLinkImageSize: 180,
|
||||
javascriptFilesPath: "/assets/js")
|
||||
defaultCssFile: nil,
|
||||
codeHighlightingJsFile: nil,
|
||||
audioPlayerJsFile: nil,
|
||||
audioPlayerCssFile: nil,
|
||||
modelViewerJsFile: nil)
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ struct PathSettingsFile {
|
||||
|
||||
let outputDirectoryPath: String
|
||||
|
||||
let assetsOutputFolderPath: String
|
||||
|
||||
let pagesOutputFolderPath: String
|
||||
|
||||
let imagesOutputFolderPath: String
|
||||
@ -13,8 +15,15 @@ struct PathSettingsFile {
|
||||
|
||||
let tagsOutputFolderPath: String
|
||||
|
||||
init(outputDirectoryPath: String, pagesOutputFolderPath: String, imagesOutputFolderPath: String, filesOutputFolderPath: String, videosOutputFolderPath: String, tagsOutputFolderPath: String) {
|
||||
init(outputDirectoryPath: String,
|
||||
assetsOutputFolderPath: String,
|
||||
pagesOutputFolderPath: String,
|
||||
imagesOutputFolderPath: String,
|
||||
filesOutputFolderPath: String,
|
||||
videosOutputFolderPath: String,
|
||||
tagsOutputFolderPath: String) {
|
||||
self.outputDirectoryPath = outputDirectoryPath
|
||||
self.assetsOutputFolderPath = assetsOutputFolderPath
|
||||
self.pagesOutputFolderPath = pagesOutputFolderPath
|
||||
self.imagesOutputFolderPath = imagesOutputFolderPath
|
||||
self.filesOutputFolderPath = filesOutputFolderPath
|
||||
@ -32,6 +41,7 @@ extension PathSettingsFile {
|
||||
static var `default`: PathSettingsFile {
|
||||
PathSettingsFile(
|
||||
outputDirectoryPath: "build",
|
||||
assetsOutputFolderPath: "asset",
|
||||
pagesOutputFolderPath: "page",
|
||||
imagesOutputFolderPath: "image",
|
||||
filesOutputFolderPath: "file",
|
||||
|
@ -7,6 +7,12 @@ struct PostSettingsFile {
|
||||
|
||||
/// The maximum width of the main content
|
||||
let contentWidth: Int
|
||||
|
||||
let swiperCssFile: String?
|
||||
|
||||
let swiperJsFile: String?
|
||||
|
||||
let defaultCssFile: String?
|
||||
}
|
||||
|
||||
extension PostSettingsFile: Codable { }
|
||||
@ -15,6 +21,9 @@ extension PostSettingsFile {
|
||||
|
||||
static var `default`: PostSettingsFile {
|
||||
.init(postsPerPage: 25,
|
||||
contentWidth: 600)
|
||||
contentWidth: 600,
|
||||
swiperCssFile: nil,
|
||||
swiperJsFile: nil,
|
||||
defaultCssFile: nil)
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
import Foundation
|
||||
|
||||
struct NavigationItemReference: Codable {
|
||||
|
||||
let type: ItemType
|
||||
|
||||
let id: String
|
||||
}
|
||||
|
||||
struct SettingsFile {
|
||||
|
||||
let paths: PathSettingsFile
|
||||
|
||||
/// The tags to show in the navigation bar
|
||||
let navigationTags: [String]
|
||||
let navigationItems: [NavigationItemReference]
|
||||
|
||||
let posts: PostSettingsFile
|
||||
|
||||
@ -23,7 +30,7 @@ extension SettingsFile {
|
||||
static var `default`: SettingsFile {
|
||||
.init(
|
||||
paths: .default,
|
||||
navigationTags: [],
|
||||
navigationItems: [],
|
||||
posts: .default,
|
||||
pages: .default,
|
||||
german: .default,
|
||||
|
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)
|
||||
}
|
||||
|
||||
// MARK: Tag overview
|
||||
|
||||
private let tagOverviewFileName = "tag-overview.json"
|
||||
|
||||
func loadTagOverview() throws -> TagOverviewFile? {
|
||||
try read(at: tagOverviewFileName)
|
||||
}
|
||||
|
||||
func save(tagOverview: TagOverviewFile?) throws {
|
||||
try writeIfChanged(tagOverview, to: tagOverviewFileName)
|
||||
}
|
||||
|
||||
// MARK: Files
|
||||
|
||||
private let filesFolderName = "files"
|
||||
@ -499,6 +511,19 @@ final class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Write the data of an encodable value to a relative path in the content folder,
|
||||
or delete the file if nil is passed.
|
||||
- Note: This function requires a security scope for the content path
|
||||
*/
|
||||
private func writeIfChanged<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
|
||||
- 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
|
||||
@ -632,4 +667,13 @@ final class Storage {
|
||||
try fm.copyItem(at: file, to: destination)
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteFile(at relativePath: String) throws {
|
||||
try withScopedContent(file: relativePath) { destination in
|
||||
guard fm.fileExists(atPath: destination.path()) else {
|
||||
return
|
||||
}
|
||||
try fm.removeItem(at: destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,11 +37,11 @@ struct FileDetailView: View {
|
||||
}
|
||||
Text("German Description")
|
||||
.font(.headline)
|
||||
TextField("", text: $file.germanDescription)
|
||||
TextField("", text: $file.german)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("English Description")
|
||||
.font(.headline)
|
||||
TextField("", text: $file.englishDescription)
|
||||
TextField("", text: $file.english)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
if file.type.isImage {
|
||||
Text("Image size")
|
||||
|
@ -73,16 +73,30 @@ struct FileListView: View {
|
||||
guard oldValue != newValue else {
|
||||
return
|
||||
}
|
||||
if let selectedFile,
|
||||
newValue.matches(selectedFile.type) {
|
||||
let newFile = filteredFiles.first
|
||||
|
||||
guard let selectedFile else {
|
||||
if let newFile {
|
||||
DispatchQueue.main.async {
|
||||
selectedFile = newFile
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
selectedFile = filteredFiles.first
|
||||
|
||||
if newValue.matches(selectedFile.type) {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.selectedFile = newFile
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if selectedFile == nil {
|
||||
selectedFile = content.files.first
|
||||
DispatchQueue.main.async {
|
||||
selectedFile = content.files.first
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,17 +10,32 @@ struct FileSelectionView: View {
|
||||
|
||||
init(selectedFile: Binding<FileResource?>) {
|
||||
self._selectedFile = selectedFile
|
||||
self.newSelection = selectedFile.wrappedValue
|
||||
}
|
||||
|
||||
@State
|
||||
private var newSelection: FileResource?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
FileListView(selectedFile: $selectedFile)
|
||||
FileListView(selectedFile: $newSelection)
|
||||
.frame(minHeight: 500, idealHeight: 600)
|
||||
HStack {
|
||||
Button("Cancel") {
|
||||
selectedFile = nil
|
||||
dismiss() }
|
||||
Button("Select") { dismiss() }
|
||||
DispatchQueue.main.async {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Button("Remove") {
|
||||
DispatchQueue.main.async {
|
||||
selectedFile = nil
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
Button("Select") {
|
||||
selectedFile = newSelection
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
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) {
|
||||
guard content.isValidIdForTagOrTagOrPost(pageId) else {
|
||||
guard content.isValidIdForTagOrPageOrPost(pageId) else {
|
||||
show(error: "Invalid page id, can't create page")
|
||||
return
|
||||
}
|
||||
@ -245,7 +245,7 @@ struct PageIssueView: View {
|
||||
}
|
||||
|
||||
private func createTag(tagId: String) {
|
||||
guard content.isValidIdForTagOrTagOrPost(tagId) else {
|
||||
guard content.isValidIdForTagOrPageOrPost(tagId) else {
|
||||
show(error: "Invalid tag id, can't create tag")
|
||||
return
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ struct GenerationContentView: View {
|
||||
|
||||
var body: some View {
|
||||
switch selectedSection {
|
||||
case .folders, .navigationBar, .postFeed:
|
||||
case .folders, .navigationBar, .postFeed, .tagOverview:
|
||||
generationView
|
||||
case .pages:
|
||||
PageSettingsContentView()
|
||||
|
@ -17,6 +17,8 @@ struct GenerationDetailView: View {
|
||||
PostFeedSettingsView()
|
||||
case .pages:
|
||||
PageSettingsDetailView()
|
||||
case .tagOverview:
|
||||
TagOverviewDetailView()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct NavigationBarSettingsView: View {
|
||||
|
||||
@ -9,7 +10,7 @@ struct NavigationBarSettingsView: View {
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var showTagPicker = false
|
||||
private var showItemPicker = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@ -21,14 +22,10 @@ struct NavigationBarSettingsView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 30)
|
||||
|
||||
Text("Visible Tags")
|
||||
.font(.headline)
|
||||
FlowHStack {
|
||||
ForEach(content.settings.navigationTags) { tag in
|
||||
TagView(text: tag.localized(in: language).name)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
Button(action: { showTagPicker = true }) {
|
||||
HStack {
|
||||
Text("Links")
|
||||
.font(.headline)
|
||||
Button(action: { showItemPicker = true }) {
|
||||
Image(systemSymbol: .squareAndPencilCircleFill)
|
||||
.resizable()
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
@ -40,15 +37,19 @@ struct NavigationBarSettingsView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
ForEach(content.settings.navigationItems) { tag in
|
||||
TagView(text: tag.title(in: language))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
Text("Select the tags to show in the navigation bar. The number should be even.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showTagPicker) {
|
||||
TagSelectionView(
|
||||
presented: $showTagPicker,
|
||||
selected: $content.settings.navigationTags,
|
||||
tags: $content.tags)
|
||||
.sheet(isPresented: $showItemPicker) {
|
||||
ItemSelectionView(
|
||||
isPresented: $showItemPicker,
|
||||
selectedItems: $content.settings.navigationItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,51 +11,49 @@ struct PageSettingsDetailView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Page Settings")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Change the way pages are displayed")
|
||||
.padding(.bottom, 30)
|
||||
DetailTitle(
|
||||
title: "Page Settings",
|
||||
text: "Change the way pages are displayed")
|
||||
|
||||
Text("Content Width")
|
||||
.font(.headline)
|
||||
IntegerField("", number: $content.settings.pages.contentWidth)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The maximum width of the content in pages (in pixels)")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.pages.contentWidth,
|
||||
title: "Content Width",
|
||||
footer: "The maximum width of the content in pages (in pixels)")
|
||||
|
||||
Text("Fullscreen Image Width")
|
||||
.font(.headline)
|
||||
IntegerField("", number: $content.settings.pages.largeImageWidth)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The maximum width of images that are diplayed fullscreen")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.pages.largeImageWidth,
|
||||
title: "Fullscreen Image Width",
|
||||
footer: "The maximum width of images that are diplayed fullscreen")
|
||||
|
||||
Text("Page Link Image Width")
|
||||
.font(.headline)
|
||||
IntegerField("", number: $content.settings.pages.pageLinkImageSize)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The maximum width of images diplayed as thumbnails on page links")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.pages.pageLinkImageSize,
|
||||
title: "Page Link Image Width",
|
||||
footer: "The maximum width of images diplayed as thumbnails on page links")
|
||||
|
||||
Text("Page URL Prefix")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.pages.pageUrlPrefix)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The URL prefix used for the links to pages")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
FilePropertyView(
|
||||
title: "Default CSS File",
|
||||
description: "The CSS file containing the styling of all pages",
|
||||
selectedFile: $content.settings.pages.defaultCssFile)
|
||||
|
||||
Text("Javascript Files Path")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.pages.javascriptFilesPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The path to the javascript files in the output folder")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
FilePropertyView(
|
||||
title: "Code Highlighting File",
|
||||
description: "The JavaScript file to provide syntax highlighting of code blocks",
|
||||
selectedFile: $content.settings.pages.codeHighlightingJsFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Audio Player CSS File",
|
||||
description: "The CSS file to provide the style for the audio player",
|
||||
selectedFile: $content.settings.pages.audioPlayerCssFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Audio Player JavaScript File",
|
||||
description: "The CSS file to provide the functionality for the audio player",
|
||||
selectedFile: $content.settings.pages.audioPlayerJsFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "3D Model Viewer File",
|
||||
description: "The JavaScript file to provide the functionality for the 3D model viewer",
|
||||
selectedFile: $content.settings.pages.modelViewerJsFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,14 @@ struct PathSettingsView: View {
|
||||
Text("The path in the output folder where the generated videos are stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Assets output folder")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.paths.assetsOutputFolderPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The path in the output folder where assets are stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,32 +11,36 @@ struct PostFeedSettingsView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Post Feed Settings")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Change the way the posts are displayed")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 30)
|
||||
DetailTitle(title: "Post Feed Settings",
|
||||
text: "Change the way the posts are displayed")
|
||||
|
||||
Text("Content Width")
|
||||
.font(.headline)
|
||||
IntegerField("", number: $content.settings.posts.contentWidth)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 400)
|
||||
Text("The maximum width of the content the post feed (in pixels)")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.posts.contentWidth,
|
||||
title: "Content Width",
|
||||
footer: "The maximum width of the content the post feed (in pixels)")
|
||||
|
||||
Text("Posts Per Page")
|
||||
.font(.headline)
|
||||
IntegerField("", number: $content.settings.posts.postsPerPage)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 400)
|
||||
Text("The maximum number of posts displayed on a single page")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.posts.postsPerPage,
|
||||
title: "Posts Per Page",
|
||||
footer: "The maximum number of posts displayed on a single page")
|
||||
|
||||
LocalizedPostFeedSettingsView(settings: content.settings.localized(in: language))
|
||||
FilePropertyView(
|
||||
title: "Default CSS File",
|
||||
description: "The CSS file containing the styling of all post pages",
|
||||
selectedFile: $content.settings.posts.defaultCssFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Swiper CSS File",
|
||||
description: "The CSS file containing the styling of image galleries in post feeds",
|
||||
selectedFile: $content.settings.posts.swiperCssFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Swiper JavaScript File",
|
||||
description: "The JavaScript file to load the image gallery code in post feeds",
|
||||
selectedFile: $content.settings.posts.swiperJsFile)
|
||||
|
||||
LocalizedPostFeedSettingsView(
|
||||
settings: content.settings.localized(in: language))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,17 +12,19 @@ enum SettingsSection: String {
|
||||
|
||||
case pages = "Pages"
|
||||
|
||||
case tagOverview = "Tag Overview"
|
||||
|
||||
}
|
||||
|
||||
extension SettingsSection {
|
||||
|
||||
var icon: SFSymbol {
|
||||
switch self {
|
||||
//case .generation: return .arrowTriangle2Circlepath
|
||||
case .folders: return .folder
|
||||
case .navigationBar: return .menubarRectangle
|
||||
case .postFeed: return .rectangleGrid1x2
|
||||
case .pages: return .docRichtext
|
||||
case .tagOverview: return .tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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() {
|
||||
let newTag = Tag(
|
||||
content: content,
|
||||
id: "tag",
|
||||
isVisible: true,
|
||||
german: .init(urlComponent: "tag", name: "Neuer Tag"),
|
||||
english: .init(urlComponent: "tag-en", name: "New Tag"))
|
||||
|
Loading…
x
Reference in New Issue
Block a user