Add tag overview, improve assets

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

View File

@ -25,6 +25,17 @@
E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; };
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 */,

View File

@ -0,0 +1,89 @@
import Foundation
final class FeedPageGenerator {
let content: Content
init(content: Content) {
self.content = content
}
func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] {
content.settings.navigationItems.map {
.init(text: $0.title(in: language),
url: $0.absoluteUrl(in: language))
}
}
var swiperIncludes: [HeaderElement] {
var result = [HeaderElement]()
if let swiperCss = content.settings.posts.swiperCssFile {
result.append(.css(swiperCss))
} else {
#warning("Add warning message")
}
if let swiperJs = content.settings.posts.swiperJsFile {
result.append(.js(file: swiperJs, defer: true))
} else {
#warning("Add warning message")
}
return result
}
var defaultHeaders: [HeaderElement] {
if let header = content.settings.posts.defaultCssFile {
return [.css(header)]
} else {
#warning("Add warning message")
return []
}
}
func generatePage(language: ContentLanguage,
posts: [FeedEntryData],
title: String,
description: String,
showTitle: Bool,
pageNumber: Int,
totalPages: Int) -> String {
var headers = defaultHeaders
var footer = ""
if posts.contains(where: { $0.images.count > 1 }) {
headers += swiperIncludes
footer = swiperInitScript(posts: posts)
}
let page = GenericPage(
language: language,
title: title,
description: description,
links: navigationBar(in: language),
headers: headers,
additionalFooter: footer) { content in
if showTitle {
content += "<h1>\(title)</h1>"
}
for post in posts {
content += FeedEntry(data: post).content
}
if totalPages > 1 {
content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content
}
}
return page.content
}
func swiperInitScript(posts: [FeedEntryData]) -> String {
var result = "<script>"
for post in posts {
guard post.images.count > 1 else {
continue
}
result += ImageGallery.swiperInit(id: post.entryId)
}
result += "</script>"
return result
}
}

View File

@ -0,0 +1,33 @@
enum HeaderElement {
case css(FileResource)
case js(file: FileResource, defer: Bool)
case jsModule(FileResource)
case title(String)
case description(String)
case charset
case viewport
}
extension HeaderElement {
var content: String {
switch self {
case .css(let file):
return "<link rel='stylesheet' href='\(file.assetUrl)' />"
case .js(let file, let deferred):
let deferText = deferred ? " defer" : ""
return "<script src='\(file.assetUrl)'\(deferText)></script>"
case .jsModule(let file):
return "<script type='module' src='\(file.assetUrl)'></script>"
case .title(let title):
return "<title>\(title)</title>"
case .description(let description):
return "<meta name='description' content='\(description)'>"
case .charset:
return "<meta charset='utf-8' />"
case .viewport:
return "<meta name='viewport' content='width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1' />"
}
}
}

View File

@ -23,9 +23,9 @@ final class LocalizedWebsiteGenerator {
private let imageGenerator: ImageGenerator
private 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +0,0 @@
import Foundation
class Item: ObservableObject {
unowned let content: Content
init(content: Content) {
self.content = content
}
func makeCleanAbsolutePath(_ path: String) -> String {
"/" + makeCleanRelativePath(path)
}
func makeCleanRelativePath(_ path: String) -> String {
path.components(separatedBy: "/").filter { !$0.isEmpty }.joined(separator: "/")
}
}

View File

@ -0,0 +1,55 @@
import Foundation
class Item: ObservableObject, Identifiable {
unowned let content: Content
@Published
var id: String
init(content: Content, id: String) {
self.content = content
self.id = id
}
func makeCleanAbsolutePath(_ path: String) -> String {
"/" + makeCleanRelativePath(path)
}
func makeCleanRelativePath(_ path: String) -> String {
path.components(separatedBy: "/").filter { !$0.isEmpty }.joined(separator: "/")
}
func title(in language: ContentLanguage) -> String {
fatalError()
}
func absoluteUrl(in language: ContentLanguage) -> String {
fatalError()
}
var itemType: ItemType {
fatalError()
}
}
extension Item: Equatable {
static func == (lhs: Item, rhs: Item) -> Bool {
lhs.id == rhs.id
}
}
extension Item: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension Item: Comparable {
static func < (lhs: Item, rhs: Item) -> Bool {
lhs.id < rhs.id
}
}

View File

@ -0,0 +1,38 @@
struct ItemId {
let itemId: String
let language: ContentLanguage
let itemType: ItemType
}
extension ItemId: Equatable {
static func == (lhs: ItemId, rhs: ItemId) -> Bool {
lhs.itemId == rhs.itemId && lhs.language == rhs.language && lhs.itemType == rhs.itemType
}
}
extension ItemId: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(itemId)
hasher.combine(language)
hasher.combine(itemType)
}
}
extension ItemId: Comparable {
static func < (lhs: ItemId, rhs: ItemId) -> Bool {
guard lhs.itemType == rhs.itemType else {
return lhs.itemType < rhs.itemType
}
guard lhs.itemId == rhs.itemId else {
return lhs.itemId < rhs.itemId
}
return lhs.language < rhs.language
}
}

View File

@ -0,0 +1,35 @@
enum ItemType: String, Codable {
case post
case tag
case page
case tagOverview
case file
}
extension ItemType: Equatable {
}
extension ItemType: Hashable {
}
extension ItemType: Identifiable {
var id: String {
rawValue
}
}
extension ItemType: Comparable {
static func < (lhs: ItemType, rhs: ItemType) -> Bool {
lhs.rawValue < rhs.rawValue
}
}

View File

@ -0,0 +1,19 @@
protocol LocalizedItem {
associatedtype Localized
var german: Localized { get }
var english: Localized { get }
}
extension LocalizedItem {
func localized(in language: ContentLanguage) -> Localized {
switch language {
case .german: return german
case .english: return english
}
}
}

View File

@ -0,0 +1,99 @@
import Foundation
final class TagOverviewPage: Item {
static let id = "all-tags"
@Published
var german: LocalizedTagOverviewPage
@Published
var english: LocalizedTagOverviewPage
init(content: Content, german: LocalizedTagOverviewPage, english: LocalizedTagOverviewPage) {
self.german = german
self.english = english
super.init(content: content, id: TagOverviewPage.id)
}
override var itemType: ItemType {
.tagOverview
}
override func title(in language: ContentLanguage) -> String {
localized(in: language).title
}
override func absoluteUrl(in language: ContentLanguage) -> String {
makeCleanAbsolutePath(internalPath(for: language))
}
func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String {
makeCleanRelativePath(internalPath(for: language))
}
private func internalPath(for language: ContentLanguage) -> String {
content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlString
}
func contains(urlComponent: String) -> Bool {
english.urlString == urlComponent || german.urlString == urlComponent
}
var file: TagOverviewFile {
.init(german: german.file,
english: english.file)
}
}
extension TagOverviewPage: LocalizedItem {
}
final class LocalizedTagOverviewPage: ObservableObject {
@Published
var title: String
/**
The string to use when creating the url for the page.
Defaults to ``id`` if unset.
*/
@Published
var urlString: String
@Published
var linkPreviewImage: FileResource?
@Published
var linkPreviewTitle: String?
@Published
var linkPreviewDescription: String?
init(title: String, urlString: String, linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) {
self.title = title
self.urlString = urlString
self.linkPreviewImage = linkPreviewImage
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription
}
init(file: LocalizedTagOverviewFile, image: FileResource?) {
self.title = file.title
self.urlString = file.url
self.linkPreviewImage = image
self.linkPreviewTitle = file.linkPreviewTitle
self.linkPreviewDescription = file.linkPreviewDescription
}
var file: LocalizedTagOverviewFile {
.init(url: urlString,
title: title,
linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription)
}
}

View File

@ -2,12 +2,6 @@ import Foundation
final class Page: Item {
/**
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 {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
struct AdditionalPageHeaders {
let headers: RequiredHeaders
let assetPath: String
#warning("Provide paths in settings, import files")
var content: String {
headers.map(header).sorted().joined()
}
private func header(for asset: HeaderFile) -> String {
let file = asset.rawValue
guard file.hasSuffix(".js") else {
return "<link rel='stylesheet' type='text/css' href='\(assetPath)/css/\(file)'>"
}
let module = asset.asModule ? " type='module'" : ""
return "<script\(module) src='\(assetPath)/js/\(file)'></script>"
}
}

View File

@ -1,38 +1,15 @@
import Foundation
//import 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"))
}
}
*/

View File

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

View File

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

View File

@ -1,71 +0,0 @@
import Foundation
struct PageInFeed {
private let swiperStyleSheetPath = "/assets/swiper/swiper-bundle.min.css"
private let swiperJsPath = "/assets/swiper/swiper-bundle.min.js"
let language: ContentLanguage
let title: String
let showTitle: Bool
let description: String
let navigationBarLinks: [NavigationBar.Link]
let pageNumber: Int
let totalPages: Int
let posts: [FeedEntryData]
private var swiperHeader: String {
"<link rel='stylesheet' href='\(swiperStyleSheetPath)' />"
}
private var swiperIsNeeded: Bool {
posts.contains(where: { $0.images.count > 1 })
}
private var headers: String {
swiperIsNeeded ? swiperHeader : ""
}
var content: String {
let footer = swiperIsNeeded ? swiperInits : ""
return GenericPage(
language: language,
title: title,
description: description,
links: navigationBarLinks,
additionalHeaders: headers,
additionalFooter: footer) { content in
if showTitle {
content += "<h1>\(title)</h1>"
}
for post in posts {
content += FeedEntry(data: post).content
}
if totalPages > 1 {
content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content
}
}.content
}
private var swiperInits: String {
var result = "<script src='\(swiperJsPath)'></script><script>"
for post in posts {
guard post.images.count > 1 else {
continue
}
result += ImageGallery.swiperInit(id: post.entryId)
}
result += "</script>"
return result
}
}

View File

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

View File

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

View File

@ -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: [:])
}
}

View File

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

View File

@ -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",

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

View File

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

View File

@ -0,0 +1,31 @@
struct TagOverviewFile {
let german: LocalizedTagOverviewFile
let english: LocalizedTagOverviewFile
}
extension TagOverviewFile: Codable {
}
/**
The structure to store the metadata of a localized page
*/
struct LocalizedTagOverviewFile {
let url: String
let title: String
let linkPreviewImage: String?
let linkPreviewTitle: String?
let linkPreviewDescription: String?
}
extension LocalizedTagOverviewFile: Codable {
}

View File

@ -306,6 +306,18 @@ final class Storage {
try writeIfChanged(fileDescriptions, to: fileDescriptionFilename)
}
// 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)
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
import SwiftUI
struct DetailTitle: View {
let title: String
let text: String
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.largeTitle)
.bold()
Text(text)
.foregroundStyle(.secondary)
.padding(.bottom, 30)
}
}
}

View File

@ -0,0 +1,34 @@
import SwiftUI
struct FilePropertyView: View {
let title: String
let description: String
@Binding
var selectedFile: FileResource?
@State
private var showFileSelectionSheet = false
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.headline)
HStack {
Text(selectedFile?.id ?? "No file selected")
Spacer()
Button("Select") {
showFileSelectionSheet = true
}
}
Text(description)
.foregroundStyle(.secondary)
.padding(.bottom)
}
.sheet(isPresented: $showFileSelectionSheet) {
FileSelectionView(selectedFile: $selectedFile)
}
}
}

View File

@ -0,0 +1,23 @@
import SwiftUI
struct IntegerPropertyView: View {
@Binding
var value: Int
let title: String
let footer: String
var body: some View {
VStack(alignment: .leading) {
Text(title)
.font(.headline)
IntegerField("", number: $value)
.textFieldStyle(.roundedBorder)
Text(footer)
.foregroundStyle(.secondary)
.padding(.bottom)
}
}
}

View File

@ -0,0 +1,111 @@
import SwiftUI
import SFSafeSymbols
struct ItemSelectionView: View {
@Binding
var isPresented: Bool
@Binding
var selectedItems: [Item]
@Environment(\.language)
private var language
@EnvironmentObject
private var content: Content
init(isPresented: Binding<Bool>, selectedItems: Binding<[Item]>) {
self._isPresented = isPresented
self._selectedItems = selectedItems
}
var body: some View {
VStack {
List {
Section("Selected") {
ForEach(selectedItems) { item in
HStack {
Image(systemSymbol: .minusCircleFill)
.foregroundStyle(.red)
.onTapGesture {
deselect(item: item)
}
Text(item.title(in: language))
Spacer()
}
}
.onMove(perform: moveItem)
}
if let tagOverview = content.tagOverview {
Section("Special Pages") {
HStack {
Image(systemSymbol: .plusCircleFill)
.foregroundStyle(.green)
Text("Tags Overview")
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
if !selectedItems.contains(where: { $0 is TagOverviewPage }) {
selectedItems.append(tagOverview)
}
}
}
}
Section("Tags") {
ForEach(content.tags) { item in
HStack {
Image(systemSymbol: .plusCircleFill)
.foregroundStyle(.green)
Text(item.title(in: language))
Spacer()
}
.contentShape(Rectangle())
.onTapGesture {
select(item: item)
}
}
}
Section("Pages") {
ForEach(content.pages) { item in
HStack {
Image(systemSymbol: .plusCircleFill)
.foregroundStyle(.green)
Text(item.title(in: language))
}
.contentShape(Rectangle())
.onTapGesture {
select(item: item)
}
}
}
}
Button("Dismiss", action: dismiss)
}
.frame(minHeight: 500)
.padding()
}
private func moveItem(from source: IndexSet, to destination: Int) {
selectedItems.move(fromOffsets: source, toOffset: destination)
}
private func deselect(item: Item) {
guard let index = selectedItems.firstIndex(of: item) else {
return
}
selectedItems.remove(at: index)
}
private func select(item: Item) {
if selectedItems.contains(item) {
return
}
selectedItems.append(item)
}
private func dismiss() {
isPresented = false
}
}

View File

@ -210,7 +210,7 @@ struct PageIssueView: View {
}
private func createPage(pageId: String) {
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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,130 @@
import SwiftUI
struct TagOverviewDetailView: View {
@Environment(\.language)
private var language
@EnvironmentObject
private var content: Content
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("Tag Overview")
.font(.largeTitle)
.bold()
Text("Configure the page showing all tags")
.foregroundStyle(.secondary)
.padding(.bottom, 30)
if let page = content.tagOverview?.localized(in: language) {
TagOverviewDetails(page: page)
} else {
Button("Create", action: createTagOverviewPage)
}
}
}
}
private func createTagOverviewPage() {
content.tagOverview = TagOverviewPage(
content: content,
german: .init(title: "Alle Tags", urlString: "alle"),
english: .init(title: "All tags", urlString: "all"))
}
}
private struct TagOverviewDetails: View {
@ObservedObject
var page: LocalizedTagOverviewPage
@EnvironmentObject
var content: Content
@State
private var showImagePicker = false
@State
private var newUrlString: String = ""
init(page: LocalizedTagOverviewPage) {
self.page = page
}
private var newUrlCanBeUpdated: Bool {
guard !newUrlString.isEmpty else { return false }
guard content.isValidIdForTagOrPageOrPost(newUrlString) else { return false }
return !content.containsTag(withUrlComponent: newUrlString)
}
var body: some View {
VStack(alignment: .leading) {
Text("Title")
.font(.headline)
TextField("", text: $page.title)
.textFieldStyle(.roundedBorder)
HStack {
Text("Page URL String")
.font(.headline)
TextField("", text: $newUrlString)
.textFieldStyle(.roundedBorder)
Button("Update", action: setNewId)
.disabled(!newUrlCanBeUpdated)
}
.padding(.bottom)
Text("Link Preview Title")
.font(.headline)
OptionalTextField("", text: $page.linkPreviewTitle,
prompt: page.title)
.textFieldStyle(.roundedBorder)
.padding(.bottom)
HStack {
Text("Link Preview Image")
.font(.headline)
IconButton(symbol: .squareAndPencilCircleFill,
size: 22,
color: .blue) {
showImagePicker = true
}
IconButton(symbol: .trashCircleFill,
size: 22,
color: .red) {
page.linkPreviewImage = nil
}.disabled(page.linkPreviewImage == nil)
}
.buttonStyle(.plain)
if let image = page.linkPreviewImage {
image.imageToDisplay
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 400, maxHeight: 300)
.cornerRadius(8)
Text(image.id)
.font(.headline)
}
Text("Link Preview Description")
.font(.headline)
.padding(.top)
OptionalDescriptionField(text: $page.linkPreviewDescription)
.textFieldStyle(.roundedBorder)
.padding(.bottom)
}
.sheet(isPresented: $showImagePicker) {
ImagePickerView(showImagePicker: $showImagePicker) { image in
page.linkPreviewImage = image
}
}
}
private func setNewId() {
page.urlString = newUrlString
}
}

View File

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