External files, improve page generation

This commit is contained in:
Christoph Hagen 2024-12-10 15:21:28 +01:00
parent 8183bc4903
commit efc9234917
50 changed files with 1069 additions and 424 deletions

View File

@ -19,7 +19,7 @@
E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850262CF3B42D0090B18B /* PostDetailView.swift */; };
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; };
E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; };
E218502F2CFAF69C0090B18B /* WebsiteGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */; };
E218502F2CFAF69C0090B18B /* LocalizedWebsiteGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */; };
E21850332CFAFA2F0090B18B /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850322CFAFA200090B18B /* Settings.swift */; };
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850342CFAFA570090B18B /* SettingsFile.swift */; };
E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; };
@ -31,7 +31,6 @@
E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5082CFD964E00AEF16D /* TagContentView.swift */; };
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */; };
E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */; };
E25DA50F2CFDD76B00AEF16D /* ImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50E2CFDD76B00AEF16D /* ImageContentView.swift */; };
E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5142CFF00B900AEF16D /* Content+Load.swift */; };
E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5162CFF00F200AEF16D /* Content+Save.swift */; };
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5182CFF035200AEF16D /* Array+Split.swift */; };
@ -44,7 +43,6 @@
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */; };
E25DA52C2CFFC3EC00AEF16D /* SDWebImageAVIFCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52B2CFFC3EC00AEF16D /* SDWebImageAVIFCoder */; };
E25DA52F2CFFC91B00AEF16D /* SDWebImageWebPCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52E2CFFC91B00AEF16D /* SDWebImageWebPCoder */; };
E25DA5312D003FCB00AEF16D /* SectionedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5302D003FC000AEF16D /* SectionedSettingsView.swift */; };
E25DA5342D0041CB00AEF16D /* NavigationBarSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5332D0041CB00AEF16D /* NavigationBarSettingsFile.swift */; };
E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */; };
E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */; };
@ -52,11 +50,10 @@
E25DA53D2D0043E600AEF16D /* LocalizedSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA53C2D0043E200AEF16D /* LocalizedSettings.swift */; };
E25DA53F2D00441F00AEF16D /* NavigationBarSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA53E2D00441C00AEF16D /* NavigationBarSettings.swift */; };
E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5402D00446700AEF16D /* PostSettings.swift */; };
E25DA5432D0094A900AEF16D /* SettingsSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5422D0094A400AEF16D /* SettingsSidebar.swift */; };
E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5442D00952D00AEF16D /* SettingsSection.swift */; };
E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */; };
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */; };
E25DA5712D01015400AEF16D /* GenerationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5702D01015400AEF16D /* GenerationSettingsView.swift */; };
E25DA5712D01015400AEF16D /* GenerationContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5702D01015400AEF16D /* GenerationContentView.swift */; };
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5722D018AA100AEF16D /* FileContentView.swift */; };
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5742D018B6100AEF16D /* FileDetailView.swift */; };
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5762D018B9500AEF16D /* File+Mock.swift */; };
@ -77,7 +74,7 @@
E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5982D02401A00AEF16D /* PageGenerator.swift */; };
E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA59A2D024A2900AEF16D /* DateItem.swift */; };
E29D31202D0320E70051B7F4 /* HikingStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D311F2D0320E20051B7F4 /* HikingStatistics.swift */; };
E29D31222D0363FD0051B7F4 /* DownloadButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31212D0363FA0051B7F4 /* DownloadButtons.swift */; };
E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31212D0363FA0051B7F4 /* ContentButtons.swift */; };
E29D31242D0366860051B7F4 /* TagList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31232D0366820051B7F4 /* TagList.swift */; };
E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31252D0370A50051B7F4 /* VideoOption.swift */; };
E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31272D0371870051B7F4 /* ContentPageVideo.swift */; };
@ -109,6 +106,12 @@
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */; };
E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31682D0702670051B7F4 /* TextFileContentView.swift */; };
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */; };
E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */; };
E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D316E2D0822720051B7F4 /* SettingsListView.swift */; };
E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */; };
E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */; };
E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317C2D086AAE0051B7F4 /* Int+Random.swift */; };
E29D317F2D086F4C0051B7F4 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317E2D086F490051B7F4 /* Icons.swift */; };
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; };
E2A21C082CB17B870060935B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C072CB17B810060935B /* TagView.swift */; };
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0D2CB189D70060935B /* Color+RGB.swift */; };
@ -164,7 +167,7 @@
E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; };
E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = "<group>"; };
E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = "<group>"; };
E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteGenerator.swift; sourceTree = "<group>"; };
E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedWebsiteGenerator.swift; sourceTree = "<group>"; };
E21850322CFAFA200090B18B /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
E21850342CFAFA570090B18B /* SettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFile.swift; sourceTree = "<group>"; };
E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = "<group>"; };
@ -176,7 +179,6 @@
E25DA5082CFD964E00AEF16D /* TagContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentView.swift; sourceTree = "<group>"; };
E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTagAssignmentView.swift; sourceTree = "<group>"; };
E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTagAssignmentView.swift; sourceTree = "<group>"; };
E25DA50E2CFDD76B00AEF16D /* ImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContentView.swift; sourceTree = "<group>"; };
E25DA5142CFF00B900AEF16D /* Content+Load.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Load.swift"; sourceTree = "<group>"; };
E25DA5162CFF00F200AEF16D /* Content+Save.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Save.swift"; sourceTree = "<group>"; };
E25DA5182CFF035200AEF16D /* Array+Split.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Split.swift"; sourceTree = "<group>"; };
@ -187,7 +189,6 @@
E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSSize+Scaling.swift"; sourceTree = "<group>"; };
E25DA5262CFF745200AEF16D /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFileType.swift; sourceTree = "<group>"; };
E25DA5302D003FC000AEF16D /* SectionedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionedSettingsView.swift; sourceTree = "<group>"; };
E25DA5332D0041CB00AEF16D /* NavigationBarSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsFile.swift; sourceTree = "<group>"; };
E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsFile.swift; sourceTree = "<group>"; };
E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettingsFile.swift; sourceTree = "<group>"; };
@ -195,11 +196,10 @@
E25DA53C2D0043E200AEF16D /* LocalizedSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedSettings.swift; sourceTree = "<group>"; };
E25DA53E2D00441C00AEF16D /* NavigationBarSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettings.swift; sourceTree = "<group>"; };
E25DA5402D00446700AEF16D /* PostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettings.swift; sourceTree = "<group>"; };
E25DA5422D0094A400AEF16D /* SettingsSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSidebar.swift; sourceTree = "<group>"; };
E25DA5442D00952D00AEF16D /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsView.swift; sourceTree = "<group>"; };
E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedSettingsView.swift; sourceTree = "<group>"; };
E25DA5702D01015400AEF16D /* GenerationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationSettingsView.swift; sourceTree = "<group>"; };
E25DA5702D01015400AEF16D /* GenerationContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationContentView.swift; sourceTree = "<group>"; };
E25DA5722D018AA100AEF16D /* FileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileContentView.swift; sourceTree = "<group>"; };
E25DA5742D018B6100AEF16D /* FileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDetailView.swift; sourceTree = "<group>"; };
E25DA5762D018B9500AEF16D /* File+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+Mock.swift"; sourceTree = "<group>"; };
@ -218,7 +218,7 @@
E25DA5982D02401A00AEF16D /* PageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerator.swift; sourceTree = "<group>"; };
E25DA59A2D024A2900AEF16D /* DateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateItem.swift; sourceTree = "<group>"; };
E29D311F2D0320E20051B7F4 /* HikingStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HikingStatistics.swift; sourceTree = "<group>"; };
E29D31212D0363FA0051B7F4 /* DownloadButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadButtons.swift; sourceTree = "<group>"; };
E29D31212D0363FA0051B7F4 /* ContentButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentButtons.swift; sourceTree = "<group>"; };
E29D31232D0366820051B7F4 /* TagList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagList.swift; sourceTree = "<group>"; };
E29D31252D0370A50051B7F4 /* VideoOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOption.swift; sourceTree = "<group>"; };
E29D31272D0371870051B7F4 /* ContentPageVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPageVideo.swift; sourceTree = "<group>"; };
@ -250,6 +250,12 @@
E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationIcon.swift; sourceTree = "<group>"; };
E29D31682D0702670051B7F4 /* TextFileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileContentView.swift; sourceTree = "<group>"; };
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListPageGenerator.swift; sourceTree = "<group>"; };
E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerationResults.swift; sourceTree = "<group>"; };
E29D316E2D0822720051B7F4 /* SettingsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListView.swift; sourceTree = "<group>"; };
E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationDetailView.swift; sourceTree = "<group>"; };
E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentResultsView.swift; sourceTree = "<group>"; };
E29D317C2D086AAE0051B7F4 /* Int+Random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Random.swift"; sourceTree = "<group>"; };
E29D317E2D086F490051B7F4 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = "<group>"; };
E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = "<group>"; };
E2A21C072CB17B810060935B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
E2A21C0D2CB189D70060935B /* Color+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+RGB.swift"; sourceTree = "<group>"; };
@ -352,14 +358,15 @@
E25DA5782D01C56200AEF16D /* Generator */ = {
isa = PBXGroup;
children = (
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */,
E29D31252D0370A50051B7F4 /* VideoOption.swift */,
E25DA5862D01CA8D00AEF16D /* GenerationResultsHandler.swift */,
E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */,
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */,
E25DA5982D02401A00AEF16D /* PageGenerator.swift */,
E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */,
E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */,
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */,
E25DA5982D02401A00AEF16D /* PageGenerator.swift */,
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */,
E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */,
E29D31252D0370A50051B7F4 /* VideoOption.swift */,
);
path = Generator;
sourceTree = "<group>";
@ -381,9 +388,10 @@
E29D311E2D0320D90051B7F4 /* ContentElements */ = {
isa = PBXGroup;
children = (
E29D317E2D086F490051B7F4 /* Icons.swift */,
E29D31272D0371870051B7F4 /* ContentPageVideo.swift */,
E29D31232D0366820051B7F4 /* TagList.swift */,
E29D31212D0363FA0051B7F4 /* DownloadButtons.swift */,
E29D31212D0363FA0051B7F4 /* ContentButtons.swift */,
E29D311F2D0320E20051B7F4 /* HikingStatistics.swift */,
);
path = ContentElements;
@ -404,6 +412,7 @@
E2A21C322CB5BCAC0060935B /* Pages */ = {
isa = PBXGroup;
children = (
E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */,
E2A37D242CEBD7A10000979F /* PageListView.swift */,
E2A21C312CB5BCAC0060935B /* PageContentView.swift */,
E29D312B2D039DB30051B7F4 /* PageDetailView.swift */,
@ -417,15 +426,15 @@
E2A21C342CB9A3CA0060935B /* Settings */ = {
isa = PBXGroup;
children = (
E29D316E2D0822720051B7F4 /* SettingsListView.swift */,
E25DA5702D01015400AEF16D /* GenerationContentView.swift */,
E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */,
E25DA5942D023BCC00AEF16D /* PageSettingsView.swift */,
E25DA5442D00952D00AEF16D /* SettingsSection.swift */,
E25DA5422D0094A400AEF16D /* SettingsSidebar.swift */,
E25DA5302D003FC000AEF16D /* SectionedSettingsView.swift */,
E2A21C352CB9A3D70060935B /* FolderSettingsView.swift */,
E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */,
E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */,
E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */,
E25DA5702D01015400AEF16D /* GenerationSettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
@ -445,14 +454,6 @@
path = Generic;
sourceTree = "<group>";
};
E2A21C492CBB168F0060935B /* Images */ = {
isa = PBXGroup;
children = (
E25DA50E2CFDD76B00AEF16D /* ImageContentView.swift */,
);
path = Images;
sourceTree = "<group>";
};
E2A21C522CBBF86D0060935B /* Files */ = {
isa = PBXGroup;
children = (
@ -547,7 +548,6 @@
E2A21C322CB5BCAC0060935B /* Pages */,
E2A9CB7F2C7E686C005C89CC /* Tags */,
E2A21C522CBBF86D0060935B /* Files */,
E2A21C492CBB168F0060935B /* Images */,
E2A21C342CB9A3CA0060935B /* Settings */,
);
path = Views;
@ -575,6 +575,7 @@
E2B85F552C4BD0AD0047CD0C /* Extensions */ = {
isa = PBXGroup;
children = (
E29D317C2D086AAE0051B7F4 /* Int+Random.swift */,
E25DA5262CFF745200AEF16D /* URL+Extensions.swift */,
E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */,
E25DA5182CFF035200AEF16D /* Array+Split.swift */,
@ -727,6 +728,7 @@
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */,
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */,
E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */,
E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */,
@ -747,13 +749,14 @@
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */,
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */,
E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */,
E29D317F2D086F4C0051B7F4 /* Icons.swift in Sources */,
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */,
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */,
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */,
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
E25DA53F2D00441F00AEF16D /* NavigationBarSettings.swift in Sources */,
E218502F2CFAF69C0090B18B /* WebsiteGenerator.swift in Sources */,
E218502F2CFAF69C0090B18B /* LocalizedWebsiteGenerator.swift in Sources */,
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */,
E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */,
E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */,
@ -799,12 +802,12 @@
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */,
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */,
E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */,
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */,
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */,
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
E25DA5432D0094A900AEF16D /* SettingsSidebar.swift in Sources */,
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
E29D31202D0320E70051B7F4 /* HikingStatistics.swift in Sources */,
@ -821,9 +824,9 @@
E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */,
E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */,
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
E25DA50F2CFDD76B00AEF16D /* ImageContentView.swift in Sources */,
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
E29D31322D03B5680051B7F4 /* LocalizedPostDetailView.swift in Sources */,
E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */,
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */,
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */,
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */,
@ -842,20 +845,21 @@
E25DA5272CFF745700AEF16D /* URL+Extensions.swift in Sources */,
E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */,
E29D31342D03B5D50051B7F4 /* IconButton.swift in Sources */,
E25DA5712D01015400AEF16D /* GenerationSettingsView.swift in Sources */,
E25DA5712D01015400AEF16D /* GenerationContentView.swift in Sources */,
E25DA5872D01CA9300AEF16D /* GenerationResultsHandler.swift in Sources */,
E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */,
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */,
E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */,
E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */,
E25DA5312D003FCB00AEF16D /* SectionedSettingsView.swift in Sources */,
E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */,
E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */,
E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */,
E29D31222D0363FD0051B7F4 /* DownloadButtons.swift in Sources */,
E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */,
E2A21C362CB9A3D70060935B /* FolderSettingsView.swift in Sources */,
E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */,
E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */,
E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */,
E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */,
E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -0,0 +1,7 @@
extension Int {
static func random() -> Int {
random(in: Int.min...Int.max)
}
}

View File

@ -1,6 +1,6 @@
import Foundation
extension Sequence {
extension Collection {
func sorted<T>(ascending: Bool = true, using conversion: (Element) -> T) -> [Element] where T: Comparable {
guard ascending else {

View File

@ -9,6 +9,16 @@ extension String {
.replacingOccurrences(of: ">", with: "&gt;")
}
var removingSurroundingQuotes: String {
if hasPrefix("\"") && hasSuffix("\"") {
return dropBeforeFirst("\"").dropAfterLast("\"")
}
if hasPrefix("'") && hasSuffix("'") {
return dropBeforeFirst("'").dropAfterLast("'")
}
return self
}
var nonEmpty: String? {
isEmpty ? nil : self
}
@ -30,7 +40,7 @@ extension String {
/**
Remove the part after the last occurence of the separator (including the separator itself).
The string is left unchanges, if it does not contain the separator.
The string is left unchanged, if it does not contain the separator.
*/
func dropAfterLast(_ separator: String) -> String {
guard contains(separator) else {

View File

@ -7,6 +7,8 @@ final class GenerationResultsHandler {
/// Generic warnings for pages
private var pageWarnings: [(message: String, source: String)] = []
private var missingPages: [String : [String]] = [:]
func warning(_ message: String, page: Page) {
pageWarnings.append((message, page.id))
print("Page: \(page.id): \(message)")
@ -15,4 +17,8 @@ final class GenerationResultsHandler {
func addRequiredVideoFile(fileId: String) {
requiredVideoFiles.insert(fileId)
}
func missing(page: String, linkedBy source: String) {
missingPages[page, default: []].append(source)
}
}

View File

@ -54,6 +54,9 @@ final class ImageGenerator {
}
func runJobs(callback: (String) -> Void) -> Bool {
guard !jobs.isEmpty else {
return true
}
print("Generating \(jobs.count) images...")
for job in jobs {
callback("Generating image \(job.version)")
@ -80,7 +83,7 @@ final class ImageGenerator {
return "\(prefix).\(type.fileExtension)"
}
func generateImageSet(for image: String, maxWidth: CGFloat, maxHeight: CGFloat, altText: String) -> FeedEntryData.Image {
func generateImageSet(for image: String, maxWidth: CGFloat, maxHeight: CGFloat) {
let type = ImageFileType(fileExtension: image.fileExtension!)!
let width2x = maxWidth * 2
@ -94,12 +97,6 @@ final class ImageGenerator {
_ = generateVersion(for: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight)
_ = generateVersion(for: image, type: type, maximumWidth: width2x, maximumHeight: height2x)
let path = "/" + relativeImageOutputPath + "/" + image
return .init(rawImagePath: path,
width: Int(maxWidth),
height: Int(maxHeight),
altText: altText)
}
func generateVersion(for image: String, type: ImageFileType, maximumWidth: CGFloat, maximumHeight: CGFloat) -> String {
@ -133,6 +130,10 @@ final class ImageGenerator {
return versions.contains(version)
}
private func exists(imageVersion version: String) -> Bool {
inOutputImagesFolder { $0.appendingPathComponent(version).exists }
}
private func hasNowGenerated(version: String, for image: String) {
guard var versions = generatedImages[image] else {
generatedImages[image] = [version]
@ -149,7 +150,8 @@ final class ImageGenerator {
// MARK: Image operations
private func generate(job: ImageJob) -> Bool {
if hasPreviouslyGenerated(version: job.version, for: job.image), exists(job.version) {
if hasPreviouslyGenerated(version: job.version, for: job.image), exists(job.version),
exists(imageVersion: job.version) {
return true
}
@ -201,7 +203,7 @@ final class ImageGenerator {
let url = folder.appendingPathComponent(job.version)
if job.type == .avif {
let out = url.path()
let input = out.replacingOccurrences(of: ".avif", with: ".jpg")
let input = url.deletingPathExtension().appendingPathExtension(job.image.fileExtension!).path()
print("avifenc -q 70 \(input) \(out)")
return true
}

View File

@ -1,6 +1,6 @@
import Foundation
final class WebsiteGenerator {
final class LocalizedWebsiteGenerator {
let language: ContentLanguage
@ -38,7 +38,7 @@ final class WebsiteGenerator {
self.localizedSettings = content.settings.localized(in: language)
self.imageGenerator = ImageGenerator(
storage: content.storage,
relativeImageOutputPath: "images")
relativeImageOutputPath: "images") // TODO: Get from settings
}
func generateWebsite(callback: (String) -> Void) -> Bool {
@ -48,6 +48,7 @@ final class WebsiteGenerator {
guard createMainPostFeedPages() else {
return false
}
#warning("Generate content pages")
guard generateTagPages() else {
return false
}
@ -127,12 +128,17 @@ final class WebsiteGenerator {
let pageGenerator = PageGenerator(content: content, imageGenerator: imageGenerator, navigationBarData: navigationBarData)
let content: String
let results: PageGenerationResults
do {
content = try pageGenerator.generate(page: page, language: language)
(content, results) = try pageGenerator.generate(page: page, language: language)
} catch {
print("Failed to generate page \(page.id) in language \(language): \(error)")
return false
}
guard !content.trimmed.isEmpty else {
#warning("Generate page with placeholder content")
return true
}
let path = self.content.pageLink(page, language: language) + ".html"
guard save(content, to: path) else {
@ -142,22 +148,32 @@ final class WebsiteGenerator {
guard imageGenerator.runJobs(callback: { _ in }) else {
return false
}
guard copy(requiredVideoFiles: pageGenerator.results.requiredVideoFiles) else {
guard copy(requiredFiles: results.files) else {
return false
}
return true
}
private func copy(requiredVideoFiles: Set<String>) -> Bool {
print("Copying \(requiredVideoFiles.count) videos...")
for fileId in requiredVideoFiles {
guard let outputPath = content.pathToFile(fileId) else {
return false
private func copy(requiredFiles: Set<FileResource>) -> Bool {
//print("Copying \(requiredVideoFiles.count) files...")
for file in requiredFiles {
guard !file.isExternallyStored else {
continue
}
let outputPath: String
switch file.type {
case .video:
outputPath = content.pathToVideo(file)
case .image:
outputPath = content.pathToImage(file)
default:
outputPath = content.pathToFile(file)
}
do {
try content.storage.copy(file: fileId, to: outputPath)
try content.storage.copy(file: file.id, to: outputPath)
} catch {
print("Failed to copy video file: \(error)")
print("Failed to copy file \(file.id): \(error)")
return false
}
}

View File

@ -8,68 +8,86 @@ final class PageContentParser {
private let pageLinkMarker = "page:"
private let largeImageIndicator = "*large*"
private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
private let results: GenerationResultsHandler
let results = PageGenerationResults()
private let content: Content
private let imageGenerator: ImageGenerator
private let page: Page
private let language: ContentLanguage
private var largeImageCount: Int = 0
init(page: Page, content: Content, language: ContentLanguage, results: GenerationResultsHandler, imageGenerator: ImageGenerator) {
self.page = page
var largeImageWidth: Int {
content.settings.pages.largeImageWidth
}
var thumbnailWidth: Int {
content.settings.pages.contentWidth
}
init(content: Content, language: ContentLanguage) {
self.content = content
self.language = language
self.results = results
self.imageGenerator = imageGenerator
}
func requestImages(_ generator: ImageGenerator) {
let thumbnailWidth = CGFloat(thumbnailWidth)
let largeImageWidth = CGFloat(largeImageWidth)
for image in results.files {
guard case .image = image.type else {
continue
}
generator.generateImageSet(
for: image.id,
maxWidth: thumbnailWidth, maxHeight: thumbnailWidth)
generator.generateImageSet(
for: image.id,
maxWidth: largeImageWidth, maxHeight: largeImageWidth)
}
}
func reset() {
results.reset()
largeImageCount = 0
}
func generatePage(from content: String) -> String {
let imageModifier = Modifier(target: .images) { html, markdown in
self.processMarkdownImage(markdown: markdown, html: html)
}
let codeModifier = Modifier(target: .codeBlocks) { html, markdown in
if markdown.starts(with: "```swift") {
let code = markdown.between("```swift", and: "```").trimmed
return "<pre><code>" + self.swift.highlight(code) + "</pre></code>"
}
return html
}
let linkModifier = Modifier(target: .links) { html, markdown in
self.handleLink(html: html, markdown: markdown)
}
let htmlModifier = Modifier(target: .html) { html, markdown in
self.handleHTML(html: html, markdown: markdown)
}
let headlinesModifier = Modifier(target: .headings) { html, markdown in
self.handleHeadlines(html: html, markdown: markdown)
}
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier, linkModifier, htmlModifier, headlinesModifier])
reset()
let parser = MarkdownParser(modifiers: [
Modifier(target: .images, closure: processMarkdownImage),
Modifier(target: .codeBlocks, closure: handleCode),
Modifier(target: .links, closure: handleLink),
Modifier(target: .html, closure: handleHTML),
Modifier(target: .headings, closure: handleHeadlines)
])
return parser.html(from: content)
}
private func handleCode(html: String, markdown: Substring) -> String {
guard markdown.starts(with: "```swift") else {
return html // Just use normal code highlighting
}
// Highlight swift code using Splash
let code = markdown.between("```swift", and: "```").trimmed
return "<pre><code>" + swift.highlight(code) + "</pre></code>"
}
private func handleLink(html: String, markdown: Substring) -> String {
let file = markdown.between("(", and: ")")
if file.hasPrefix(pageLinkMarker) {
// Retain links pointing to elements within a page
let textToChange = file.dropAfterFirst("#")
let pageId = textToChange.replacingOccurrences(of: pageLinkMarker, with: "")
guard let pagePath = content.pageLink(pageId: pageId, language: language) else {
guard let page = content.page(pageId) else {
results.missingPages.insert(pageId)
// Remove link since the page can't be found
return markdown.between("[", and: "]")
}
// Adjust file path to get the page url
// TODO: Calculate relative links to make pages more portable
results.linkedPages.insert(page)
let pagePath = content.pageLink(page, language: language)
return html.replacingOccurrences(of: textToChange, with: pagePath)
}
@ -92,6 +110,13 @@ final class PageContentParser {
return html
}
/**
Modify headlines by extracting an id from the headline and adding it into the html element
Format: ##<title>#<id>
The id is created by lowercasing the string, removing all special characters, and replacing spaces with scores
*/
private func handleHeadlines(html: String, markdown: Substring) -> String {
let id = markdown
.last(after: "#")
@ -101,18 +126,18 @@ final class PageContentParser {
.components(separatedBy: " ")
.filter { $0 != "" }
.joined(separator: "-")
let parts = html.components(separatedBy: ">")
let parts = html.components(separatedBy: ">")
return parts[0] + " id=\"\(id)\">" + parts.dropFirst().joined(separator: ">")
}
private func processMarkdownImage(markdown: Substring, html: String) -> String {
private func processMarkdownImage(html: String, markdown: Substring) -> String {
// First, check the content type, then parse the remaining arguments
// Notation:
// <abc?> -> Optional argument
// <abc...> -> Repeated argument (0 or more)
// ![url](<url>;<text>)
// ![image](<imageId>;<caption?>]
// ![video](<fileId>;<alt>;<option1...>]
// ![video](<fileId>;<option1...>]
// ![svg](<fileId>;<<x>;<y>;<width>;<height>?>)
// ![download](<<fileId>,<text>,<download-filename?>;...)
// ![box](<title>;<body>)
@ -120,12 +145,11 @@ final class PageContentParser {
// ![page](<pageId>)
// ![external](<<url>;<text>...>
// ![html](<fileId>)
guard let argumentList = markdown.between(first: "](", andLast: ")").removingPercentEncoding else {
results.warning("Invalid percent encoding for markdown image", page: page)
return ""
}
let argumentList = markdown.between(first: "](", andLast: ")").removingPercentEncoding ?? markdown.between(first: "](", andLast: ")")
let arguments = argumentList.components(separatedBy: ";")
let rawCommand = markdown.between("![", and: "]").trimmed
guard rawCommand != "" else {
return handleImage(arguments)
@ -134,7 +158,7 @@ final class PageContentParser {
guard let convertedCommand = rawCommand.removingPercentEncoding,
let command = ShorthandMarkdownKey(rawValue: convertedCommand) else {
// Treat unknown commands as normal links
print("Unknown markdown command: \(rawCommand)")
results.warnings.append("Unknown markdown command '\(rawCommand)'")
return html
}
@ -147,12 +171,9 @@ final class PageContentParser {
return handleDownloadButtons(arguments)
case .video:
return handleVideo(arguments)
default:
print("Unhandled markdown command: \(command)")
return ""
/*
case .externalLink:
return handleExternalButtons(content: content)
return handleExternalButtons(arguments)
/*
case .includedHtml:
return handleExternalHTML(file: content)
case .box:
@ -162,35 +183,42 @@ final class PageContentParser {
case .model:
return handle3dModel(content: content)
*/
default:
results.warnings.append("Unhandled command '\(command.rawValue)'")
return ""
}
}
private func handleImage(_ arguments: [String]) -> String {
// [image](<imageId>;<caption?>]
guard (1...2).contains(arguments.count) else {
results.warning("Invalid image arguments: \(arguments)", page: page)
results.invalidCommandArguments.append((.image , arguments))
return ""
}
let imageId = arguments[0]
guard let image = content.image(imageId) else {
results.warning("Missing image \(imageId)", page: page)
results.missingFiles.insert(imageId)
return ""
}
results.files.insert(image)
let caption = arguments.count == 2 ? arguments[1] : nil
let altText = image.getDescription(for: language)
let thumbnailWidth = CGFloat(content.settings.pages.contentWidth)
let thumbnail = imageGenerator.generateImageSet(
for: imageId,
maxWidth: thumbnailWidth, maxHeight: thumbnailWidth,
let path = content.pathToImage(image)
let thumbnail = FeedEntryData.Image(
rawImagePath: path,
width: thumbnailWidth,
height: thumbnailWidth,
altText: altText)
let largeImageWidth = CGFloat(1200) // TODO: Move to settings
let largeImage = imageGenerator.generateImageSet(
for: imageId,
maxWidth: largeImageWidth, maxHeight: largeImageWidth,
let largeImage = FeedEntryData.Image(
rawImagePath: path,
width: largeImageWidth,
height: largeImageWidth,
altText: altText)
return PageImage(
@ -202,7 +230,7 @@ final class PageContentParser {
private func handleHikingStatistics(_ arguments: [String]) -> String {
guard (1...5).contains(arguments.count) else {
results.warning("Invalid hiking statistic arguments: \(arguments)", page: page)
results.invalidCommandArguments.append((.hikingStatistics, arguments))
return ""
}
@ -222,10 +250,11 @@ final class PageContentParser {
}
private func handleDownloadButtons(_ arguments: [String]) -> String {
let buttons: [DownloadButtons.Item] = arguments.compactMap { button in
// ![download](<<fileId>,<text>,<download-filename?>;...)
let buttons: [ContentButtons.Item] = arguments.compactMap { button in
let parts = button.components(separatedBy: ",")
guard (2...3).contains(parts.count) else {
results.warning("Invalid download definition with \(parts)", page: page)
results.invalidCommandArguments.append((.downloadButtons, parts))
return nil
}
let file = parts[0].trimmed
@ -234,44 +263,36 @@ final class PageContentParser {
// Ensure that file is available
guard let filePath = content.pathToFile(file) else {
results.warning("Missing download file \(file)", page: page)
results.missingFiles.insert(file)
return nil
}
return DownloadButtons.Item(filePath: filePath, text: title, downloadFileName: downloadName)
return ContentButtons.Item(icon: .download, filePath: filePath, text: title, downloadFileName: downloadName)
}
return DownloadButtons(items: buttons).content
return ContentButtons(items: buttons).content
}
private func handleVideo(_ arguments: [String]) -> String {
// ![video](<fileId>;<option1...>]
guard arguments.count >= 1 else {
results.invalidCommandArguments.append((.video, arguments))
return ""
}
let fileId = arguments[0].trimmed
let options: [VideoOption] = arguments.dropFirst().compactMap { optionText in
guard let optionText = optionText.trimmed.nonEmpty else {
return nil
}
guard let option = VideoOption(rawValue: optionText) else {
results.warning("Unknown video option \(optionText)", page: page)
return nil
}
return option
}
let options = arguments.dropFirst().compactMap(convertVideoOption)
guard let filePath = content.pathToFile(fileId),
let file = content.file(id: fileId) else {
results.warning("Missing video file \(fileId)", page: page)
guard let file = content.file(id: fileId) else {
results.missingFiles.insert(fileId)
return ""
}
results.files.insert(file)
guard let videoType = file.type.videoType?.htmlType else {
results.warning("Unknown video file type for \(fileId)", page: page)
results.warnings.append("Unknown video file type for \(fileId)")
return ""
}
results.addRequiredVideoFile(fileId: fileId)
let filePath = content.pathToFile(file)
return ContentPageVideo(
filePath: filePath,
videoType: videoType,
@ -279,6 +300,40 @@ final class PageContentParser {
.content
}
private func convertVideoOption(_ videoOption: String) -> VideoOption? {
guard let optionText = videoOption.trimmed.nonEmpty else {
return nil
}
guard let option = VideoOption(rawValue: optionText) else {
results.invalidCommandArguments.append((.video, [optionText]))
return nil
}
if case let .poster(imageId) = option {
if let image = content.image(imageId) {
results.files.insert(image)
let link = content.pathToImage(image)
let width = 2*thumbnailWidth
let fullLink = WebsiteImage.imagePath(source: link, width: width, height: width)
return .poster(image: fullLink)
} else {
results.missingFiles.insert(imageId)
return nil // Image file not present, so skip the option
}
}
if case let .src(videoId) = option {
if let video = content.video(videoId) {
results.files.insert(video)
let link = content.pathToVideo(video)
// TODO: Set correct video path?
return .src(link)
} else {
results.missingFiles.insert(videoId)
return nil // Video file not present, so skip the option
}
}
return option
}
/*
private func handleGif(file: String, altText: String) -> String {
@ -334,27 +389,34 @@ final class PageContentParser {
results.warning("Unhandled file \(file) with extension \(fileExtension)", source: page.path)
return ""
}
private func handleExternalButtons(content: String) -> String {
let buttons = content
.components(separatedBy: ";")
.compactMap { button -> (url: String, text: String)? in
let parts = button.components(separatedBy: ",")
guard parts.count == 2 else {
results.warning("Invalid external link definition", page: page)
return nil
}
guard let url = parts[0].trimmed.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
results.warning("Invalid external link \(parts[0].trimmed)", source: page.path)
return nil
}
let title = parts[1].trimmed
return (url, title)
*/
private func handleExternalButtons(_ arguments: [String]) -> String {
// ![external](<<url>;<text>...>
guard arguments.count >= 1 else {
results.invalidCommandArguments.append((.externalLink, arguments))
return ""
}
let buttons: [ContentButtons.Item] = arguments.compactMap { button in
let parts = button.components(separatedBy: ",")
guard parts.count == 2 else {
results.invalidCommandArguments.append((.externalLink, parts))
return nil
}
return factory.html.externalButtons(buttons)
}
let rawUrl = parts[0].trimmed
guard let url = rawUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
results.invalidCommandArguments.append((.externalLink, parts))
return nil
}
let title = parts[1].trimmed
return .init(
icon: .externalLink,
filePath: url,
text: title)
}
return ContentButtons(items: buttons).content
}
/*
private func handleExternalHTML(file: String) -> String {
let path = page.pathRelativeToRootForContainedInputFile(file)
return results.getContentOfRequiredFile(at: path, source: page.path) ?? ""

View File

@ -0,0 +1,32 @@
import Foundation
final class PageGenerationResults: ObservableObject {
@Published
var linkedPages: Set<Page> = []
@Published
var files: Set<FileResource> = []
@Published
var missingPages: Set<String> = []
@Published
var missingFiles: Set<String> = []
@Published
var invalidCommandArguments: [(command: ShorthandMarkdownKey, arguments: [String])] = []
@Published
var warnings: [String] = []
func reset() {
linkedPages = []
files = []
missingPages = []
missingFiles = []
invalidCommandArguments = []
warnings = []
}
}

View File

@ -6,26 +6,23 @@ final class PageGenerator {
private let navigationBarData: NavigationBarData
let results = GenerationResultsHandler()
init(content: Content, imageGenerator: ImageGenerator, navigationBarData: NavigationBarData) {
self.content = content
self.imageGenerator = imageGenerator
self.navigationBarData = navigationBarData
}
func generate(page: Page, language: ContentLanguage) throws -> String {
func generate(page: Page, language: ContentLanguage) throws -> (page: String, results: PageGenerationResults) {
let contentGenerator = PageContentParser(
page: page,
content: content,
language: language,
results: results,
imageGenerator: imageGenerator)
language: language)
let rawPageContent = try content.storage.pageContent(for: page.id, language: language)
let pageContent = contentGenerator.generatePage(from: rawPageContent)
contentGenerator.requestImages(imageGenerator)
let localized = page.localized(in: language)
let tags: [FeedEntryData.Tag] = page.tags.map { tag in
@ -33,7 +30,7 @@ final class PageGenerator {
url: content.tagLink(tag, language: language))
}
return ContentPage(
let fullPage = ContentPage(
language: language,
dateString: page.dateText(in: language),
title: localized.title,
@ -43,5 +40,7 @@ final class PageGenerator {
navigationBarData: navigationBarData,
pageContent: pageContent)
.content
return (fullPage, contentGenerator.results)
}
}

View File

@ -66,7 +66,7 @@ final class PostListPageGenerator {
text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings
}
let tags: [FeedEntryData.Tag] = post.tags.map { tag in
let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in
.init(name: tag.localized(in: language).name,
url: content.tagLink(tag, language: language))
}
@ -102,7 +102,11 @@ final class PostListPageGenerator {
imageGenerator.generateImageSet(
for: image.id,
maxWidth: mainContentMaximumWidth,
maxHeight: mainContentMaximumWidth,
maxHeight: mainContentMaximumWidth)
return .init(
rawImagePath: content.pathToImage(image),
width: Int(mainContentMaximumWidth),
height: Int(mainContentMaximumWidth),
altText: image.getDescription(for: language))
}

View File

@ -18,7 +18,7 @@ enum ShorthandMarkdownKey: String {
case hikingStatistics = "hiking-stats"
/// A video
/// Format: `![video](<fileId>;<alt>;<option1...>]`
/// Format: `![video](<fileId>;<option1...>]`
case video
/// An SVG image

View File

@ -1,11 +1,123 @@
/// HTML video options
enum VideoOption: String {
enum VideoOption {
/// Specifies that video controls should be displayed (such as a play/pause button etc).
case controls
/// Specifies that the video will start playing as soon as it is ready
case autoplay
case muted
/// Specifies that the video will start over again, every time it is finished
case loop
/// Specifies that the audio output of the video should be muted
case muted
/// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays
case playsinline
case poster
case preload
/// Sets the height of the video player
case height(Int)
/// Sets the width of the video player
case width(Int)
/// Specifies if and how the author thinks the video should be loaded when the page loads
case preload(VideoPreloadOption)
/// Specifies an image to be shown while the video is downloading, or until the user hits the play button
case poster(image: String)
/// Specifies the URL of the video file
case src(String)
init?(rawValue: String) {
switch rawValue {
case "controls":
self = .controls
return
case "autoplay":
self = .autoplay
return
case "muted":
self = .muted
return
case "loop":
self = .loop
return
case "playsinline":
self = .playsinline
return
default: break
}
let parts = rawValue.components(separatedBy: "=")
guard parts.count == 2 else {
return nil
}
let optionName = parts[0]
let value = parts[1].removingSurroundingQuotes
switch optionName {
case "height":
guard let height = Int(value) else {
return nil
}
self = .height(height)
case "width":
guard let width = Int(value) else {
return nil
}
self = .width(width)
case "preload":
guard let preloadOption = VideoPreloadOption(rawValue: value) else {
return nil
}
self = .preload(preloadOption)
case "poster":
self = .poster(image: value)
case "src":
self = .src(value)
default:
return nil
}
return
}
var rawValue: String {
switch self {
case .controls: return "controls"
case .autoplay: return "autoplay"
case .muted: return "muted"
case .loop: return "loop"
case .playsinline: return "playsinline"
case .height(let height): return "height='\(height)'"
case .width(let width): return "width='\(width)'"
case .preload(let option): return "preload='\(option)'"
case .poster(let image): return "poster='\(image)'"
case .src(let url): return "src='\(url)'"
}
}
}
/**
The `preload` attribute specifies if and how the author thinks that the video should be loaded when the page loads.
The `preload` attribute allows the author to provide a hint to the browser about what he/she thinks will lead to the best user experience.
This attribute may be ignored in some instances.
Note: The `preload` attribute is ignored if `autoplay` is present.
*/
enum VideoPreloadOption: String {
/// The author thinks that the browser should load the entire video when the page loads
case auto
/// The author thinks that the browser should load only metadata when the page loads
case metadata
/// The author thinks that the browser should NOT load the video when the page loads
case none
}

View File

@ -1,18 +1,9 @@
import SwiftUI
import SFSafeSymbols
/*
Page: One page -> One post with overview
Post: One post -> No page
Page update: One page -> Multiple posts
*/
#warning("Consolidate images and files")
#warning("Allow selection of pages as navigation bar items")
#warning("Transfer images of posts to other language")
#warning("Show tag selection view for pages")
@main
struct MainView: App {
@ -46,7 +37,7 @@ struct MainView: App {
private var selectedFile: FileResource?
@State
private var selectedSection: SettingsSection? = .generation
private var selectedSection: SettingsSection = .folders
@State
private var showAddSheet = false
@ -63,9 +54,7 @@ struct MainView: App {
case .files:
FileListView(selectedFile: $selectedFile)
case .generation:
List(SettingsSection.allCases, selection: $selectedSection) { item in
Label(item.rawValue, systemSymbol: item.icon).tag(item)
}
SettingsListView(selectedSection: $selectedSection)
}
}
@ -81,7 +70,7 @@ struct MainView: App {
case .files:
SelectedContentView<FileContentView>(selected: $selectedFile)
case .generation:
GenerationDetailView(section: selectedSection)
GenerationContentView()
}
}
@ -97,7 +86,7 @@ struct MainView: App {
case .files:
SelectedDetailView<FileDetailView>(selected: $selectedFile)
case .generation:
Text("")
GenerationDetailView(section: selectedSection)
}
}

View File

@ -18,6 +18,10 @@ extension Content {
return prefix + page.localized(in: language).urlString
}
func page(_ pageId: String) -> Page? {
pages.first { $0.id == pageId }
}
func pageLink(pageId: String, language: ContentLanguage) -> String? {
guard let page = pages.first(where: { $0.id == pageId }) else {
// TODO: Note missing link
@ -31,18 +35,42 @@ extension Content {
guard let file = file(id: fileId) else {
return nil
}
switch file.type {
case .image: return pathToImage(file)
case .video: return pathToVideo(file)
default: return pathToFile(file)
}
}
func pathToFile(_ file: FileResource) -> String {
#warning("Add files path to settings")
return "/files/\(file.id)"
}
func image(_ imageId: String) -> FileResource? {
files.first { $0.id == imageId }
func pathToImage(_ image: FileResource) -> String {
return "/images/\(image.id)"
}
func imageLink(imageId: String) {
func image(_ imageId: String) -> FileResource? {
files.first { $0.id == imageId && $0.type.isImage }
}
func video(_ videoId: String) -> FileResource? {
files.first { $0.id == videoId && $0.type.isVideo }
}
func pathToVideo(_ videoId: String) -> String? {
guard let video = video(videoId) else {
return nil
}
return pathToVideo(video)
}
func pathToVideo(_ video: FileResource) -> String {
"/videos/\(video.id)"
}
func file(id: String) -> FileResource? {
files.first { $0.id == id }
}

View File

@ -25,6 +25,7 @@ extension Content {
private func convert(_ page: LocalizedPageFile, images: [String : FileResource]) -> LocalizedPage {
LocalizedPage(
content: self,
urlString: page.url,
title: page.title,
lastModified: page.lastModifiedDate,
@ -57,12 +58,24 @@ extension Content {
let pagesData = try storage.loadAllPages()
let postsData = try storage.loadAllPosts()
let fileList = try storage.loadAllFiles()
let externalFiles = try storage.loadExternalFileList()
let files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in
var files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in
let descriptions = imageDescriptions[fileId]
files[fileId] = FileResource(
content: self,
id: fileId,
isExternallyStored: false,
en: descriptions?.english ?? "",
de: descriptions?.german ?? "")
}
for fileId in externalFiles {
let descriptions = imageDescriptions[fileId]
files[fileId] = FileResource(
content: self,
id: fileId,
isExternallyStored: true,
en: descriptions?.english ?? "",
de: descriptions?.german ?? "")
}
@ -115,7 +128,8 @@ extension Content {
let pages = PageSettings(
pageUrlPrefix: settings.pages.pageUrlPrefix,
contentWidth: settings.pages.contentWidth)
contentWidth: settings.pages.contentWidth,
largeImageWidth: settings.pages.largeImageWidth)
return Settings(
outputDirectoryPath: settings.outputDirectoryPath,

View File

@ -29,6 +29,9 @@ extension Content {
try storage.save(fileDescriptions: fileDescriptions)
let externalFileList = files.filter { $0.isExternallyStored }.map { $0.id }
try storage.save(externalFileList: externalFileList)
do {
try storage.deletePostFiles(notIn: posts.map { $0.id })
try storage.deletePageFiles(notIn: pages.map { $0.id })
@ -116,7 +119,7 @@ private extension LocalizedTag {
name: name,
subtitle: subtitle,
description: description,
thumbnail: thumbnail?.id,
thumbnail: linkPreviewImage?.id,
originalURL: originalUrl)
}
}
@ -154,7 +157,8 @@ private extension PageSettings {
var file: PageSettingsFile {
.init(pageUrlPrefix: pageUrlPrefix,
contentWidth: contentWidth)
contentWidth: contentWidth,
largeImageWidth: largeImageWidth)
}
}

View File

@ -11,6 +11,9 @@ final class FileResource: ObservableObject {
@Published
var id: String
@Published
var isExternallyStored: Bool
@Published
var germanDescription: String
@ -20,12 +23,13 @@ final class FileResource: ObservableObject {
@Published
var size: CGSize = .zero
init(content: Content, id: String, en: String, de: String) {
init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) {
self.content = content
self.id = id
self.type = FileType(fileExtension: id.fileExtension)
self.englishDescription = en
self.germanDescription = de
self.isExternallyStored = isExternallyStored
}
/**
@ -37,6 +41,7 @@ final class FileResource: ObservableObject {
self.id = resourceImage
self.englishDescription = "A test image included in the bundle"
self.germanDescription = "Ein Testbild aus dem Bundle"
self.isExternallyStored = true
}
func getDescription(for language: ContentLanguage) -> String {

View File

@ -8,6 +8,8 @@ import SwiftUI
*/
final class LocalizedPage: ObservableObject {
unowned let content: Content
/**
The string to use when creating the url for the page.
@ -64,7 +66,8 @@ final class LocalizedPage: ObservableObject {
@Published
var linkPreviewDescription: String?
init(urlString: String,
init(content: Content,
urlString: String,
title: String,
lastModified: Date? = nil,
originalUrl: String? = nil,
@ -74,6 +77,7 @@ final class LocalizedPage: ObservableObject {
linkPreviewImage: FileResource? = nil,
linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil) {
self.content = content
self.urlString = urlString
self.title = title
self.lastModified = lastModified

View File

@ -17,7 +17,7 @@ final class LocalizedTag: ObservableObject {
/// The image id of the thumbnail
@Published
var thumbnail: FileResource?
var linkPreviewImage: FileResource?
/// The original url in the previous site layout
let originalUrl: String?
@ -32,7 +32,7 @@ final class LocalizedTag: ObservableObject {
self.name = name
self.subtitle = subtitle
self.description = description
self.thumbnail = thumbnail
self.linkPreviewImage = thumbnail
self.originalUrl = originalUrl
}
}

View File

@ -98,6 +98,13 @@ extension Page: Hashable {
}
}
extension Page: Comparable {
static func < (lhs: Page, rhs: Page) -> Bool {
lhs.id < rhs.id
}
}
extension Page: DateItem {
}

View File

@ -10,8 +10,12 @@ final class PageSettings: ObservableObject {
@Published
var contentWidth: Int
init(pageUrlPrefix: String, contentWidth: Int) {
@Published
var largeImageWidth: Int
init(pageUrlPrefix: String, contentWidth: Int, largeImageWidth: Int) {
self.pageUrlPrefix = pageUrlPrefix
self.contentWidth = contentWidth
self.largeImageWidth = largeImageWidth
}
}

View File

@ -1,13 +1,22 @@
struct DownloadButtons {
struct ContentButtons {
struct Item {
let icon: PageIcon
let filePath: String
let text: String
let downloadFileName: String?
init(icon: PageIcon, filePath: String, text: String, downloadFileName: String? = nil) {
self.icon = icon
self.filePath = filePath
self.text = text
self.downloadFileName = downloadFileName
}
}
let items: [Item]
@ -17,7 +26,7 @@ struct DownloadButtons {
}
var content: String {
var result = "<p style='display: flex'>"
var result = "<p class='tags tag-buttons'>"
for item in items {
addButton(of: item, to: &result)
}
@ -27,8 +36,8 @@ struct DownloadButtons {
private func addButton(of item: Item, to result: inout String) {
let downloadText = item.downloadFileName.map { " download='\($0)'" } ?? ""
result += "<a class='download-button' href='\(item.filePath)'\(downloadText)>"
result += "\(item.text)<span class='icon icon-download'></span>"
result += "<a class='tag' href='\(item.filePath)'\(downloadText)>"
result += "<svg><use href='#\(item.icon.name)'></use></svg>\(item.text)"
result += "</a>"
}
}

View File

@ -0,0 +1,107 @@
enum PageIcon: CaseIterable {
case time
case elevationUp
case elevationDown
case distance
case calories
case download
case externalLink
case gitLink
var icon: String {
switch self {
case .time: return PageIcon.timeIcon
case .elevationUp: return PageIcon.elevationUpIcon
case .elevationDown: return PageIcon.elevationDownIcon
case .distance: return PageIcon.distanceIcon
case .calories: return PageIcon.caloriesIcon
case .download: return PageIcon.downloadIcon
case .externalLink: return PageIcon.externalLinkIcon
case .gitLink: return PageIcon.gitLinkIcon
}
}
var name: String {
switch self {
case .time: return "icon-clock"
case .elevationUp: return "icon-arrow-up"
case .elevationDown: return "icon-arrow-down"
case .distance: return "icon-sign"
case .calories: return "icon-flame"
case .download: return "icon-download"
case .externalLink: return "icon-external"
case .gitLink: return "icon-git"
}
}
}
extension PageIcon {
private static let timeIcon =
"""
<svg id="icon-clock" width="16" height="16" viewBox="0 0 16 16">
<path fill="currentColor" d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .3.4l3.5 2a.5.5 0 0 0 .4-.8L8 8.7V3.5z"/>
<path fill="currentColor" d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
</svg>
"""
private static let elevationUpIcon =
"""
<svg id="icon-arrow-up" width="16" height="16">
<path fill="currentColor" d="m14 2.5a.5.5 0 0 0 -.5-.5h-6a.5.5 0 0 0 0 1h4.8l-10.16 10.15a.5.5 0 0 0 .7.7l10.16-10.14v4.79a.5.5 0 0 0 1 0z"/>
</svg>
"""
private static let elevationDownIcon =
"""
<svg id="icon-arrow-down" width="16" height="16">
<path fill="currentColor" fill-rule="evenodd" d="M14 13.5a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1 0-1h4.8L2.14 2.85a.5.5 0 1 1 .7-.7L13 12.29V7.5a.5.5 0 0 1 1 0v6z"/>
</svg>
"""
private static let distanceIcon =
"""
<svg id="icon-sign" width="16" height="16">
<path fill="currentColor" d="M7 1.4V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.5a1 1 0 0 0 .8-.4l2-2.3a.5.5 0 0 0 0-.6l-2-2.3a1 1 0 0 0-.8-.4H9V1.4a1 1 0 0 0-2 0zM12.5 5l1.7 2-1.7 2H2V5h10.5z"/>
</svg>
"""
private static let caloriesIcon =
"""
<svg id="icon-flame" width="16" height="16">
<path fill="currentColor" d="M8 16c3.3 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .3 1.5-1.3 2-1.3 2C11 4 9 .5 6 0c.4 2 .5 4-2 6-1.3 1-2 2.7-2 4.5C2 14 4.7 16 8 16Zm0-1c-1.7 0-3-1-3-2.8 0-.7.3-2 1.3-3-.2.8.7 1.3.7 1.3-.4-1.3.5-3.3 2-3.5-.2 1-.3 2 1 3a3 3 0 0 1 1 2.3C11 14 9.7 15 8 15Z"/>
</svg>
"""
private static let downloadIcon: String =
"""
<svg id="icon-download" viewBox="0 0 40 40">
<path fill="currentColor" fill-rule="evenodd" stroke="none" d="M20 40a20 20 0 1 1 20-20 20 20 0 0 1-20 20zm0-36.8A16.8 16.8 0 1 0 36.8 20 16.8 16.8 0 0 0 20 3.2zm.8 27a1 1 0 0 1-1.6 0L12.1 21c-.4-.4 0-1 .7-1H17v-8.7a.8.8 0 0 1 .8-.8h4.4a.8.8 0 0 1 .8.8V20h4.2c.6 0 1.1.5.7 1l-7.1 9.2z"/>
</svg>
"""
private static let externalLinkIcon: String =
"""
<svg id="icon-external" viewBox="0 0 16 16">
<path fill="currentColor" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/>
</svg>
"""
private static let gitLinkIcon: String =
"""
<svg id="icon-git" viewBox="0 0 16 16">
<path fill="currentColor" d="M15.698 7.287 8.712.302a1.03 1.03 0 0 0-1.457 0l-1.45 1.45 1.84 1.84a1.223 1.223 0 0 1 1.55 1.56l1.773 1.774a1.224 1.224 0 0 1 1.267 2.025 1.226 1.226 0 0 1-2.002-1.334L8.58 5.963v4.353a1.226 1.226 0 1 1-1.008-.036V5.887a1.226 1.226 0 0 1-.666-1.608L5.093 2.465l-4.79 4.79a1.03 1.03 0 0 0 0 1.457l6.986 6.986a1.03 1.03 0 0 0 1.457 0l6.953-6.953a1.03 1.03 0 0 0 0-1.457"/>
</svg>
"""
}

View File

@ -1,6 +1,19 @@
struct WebsiteImage {
static func imagePath(prefix: String, width: Int, height: Int) -> String {
"\(prefix)@\(width)x\(height)"
}
static func imagePath(prefix: String, extension fileExtension: String, width: Int, height: Int) -> String {
"\(prefix)@\(width)x\(height).\(fileExtension)"
}
static func imagePath(source: String, width: Int, height: Int) -> String {
let (prefix, ext) = source.fileNameAndExtension
return imagePath(prefix: prefix, extension: ext ?? ".jpg", width: width, height: height)
}
private let prefix1x: String
private let prefix2x: String
@ -18,8 +31,8 @@ struct WebsiteImage {
init(rawImagePath: String, width: Int, height: Int, altText: String) {
let (prefix, ext) = rawImagePath.fileNameAndExtension
self.prefix1x = "\(prefix)@\(width)x\(height)"
self.prefix2x = "\(prefix)@\(width*2)x\(height*2)"
self.prefix1x = WebsiteImage.imagePath(prefix: prefix, width: width, height: height)
self.prefix2x = WebsiteImage.imagePath(prefix: prefix, width: 2*width, height: 2*height)
self.altText = altText.htmlEscaped()
self.ext = ext ?? "jpg"
}

View File

@ -49,25 +49,13 @@ struct ContentPage: HtmlProducer {
result += "</body></html>" // Close content
}
private let symbols: String =
"""
<div style="display:none">
<svg id="icon-clock" width="16" height="16" viewBox="0 0 16 16">
<path fill="currentColor" d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .3.4l3.5 2a.5.5 0 0 0 .4-.8L8 8.7V3.5z"/>
<path fill="currentColor" d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
</svg>
<svg id="icon-arrow-up" width="16" height="16">
<path fill="currentColor" d="m14 2.5a.5.5 0 0 0 -.5-.5h-6a.5.5 0 0 0 0 1h4.8l-10.16 10.15a.5.5 0 0 0 .7.7l10.16-10.14v4.79a.5.5 0 0 0 1 0z"/>
</svg>
<svg id="icon-arrow-down" width="16" height="16">
<path fill="currentColor" fill-rule="evenodd" d="M14 13.5a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1 0-1h4.8L2.14 2.85a.5.5 0 1 1 .7-.7L13 12.29V7.5a.5.5 0 0 1 1 0v6z"/>
</svg>
<svg id="icon-sign" width="16" height="16">
<path fill="currentColor" d="M7 1.4V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.5a1 1 0 0 0 .8-.4l2-2.3a.5.5 0 0 0 0-.6l-2-2.3a1 1 0 0 0-.8-.4H9V1.4a1 1 0 0 0-2 0zM12.5 5l1.7 2-1.7 2H2V5h10.5z"/>
</svg>
<svg id="icon-flame" width="16" height="16">
<path fill="currentColor" d="M8 16c3.3 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .3 1.5-1.3 2-1.3 2C11 4 9 .5 6 0c.4 2 .5 4-2 6-1.3 1-2 2.7-2 4.5C2 14 4.7 16 8 16Zm0-1c-1.7 0-3-1-3-2.8 0-.7.3-2 1.3-3-.2.8.7 1.3.7 1.3-.4-1.3.5-3.3 2-3.5-.2 1-.3 2 1 3a3 3 0 0 1 1 2.3C11 14 9.7 15 8 15Z"/>
</svg>
</div>
"""
#warning("Select only required symbols")
private let symbols: String = {
var result = "<div style='display:none'>"
for icon in PageIcon.allCases {
result += icon.icon
}
result += "</div>"
return result
}()
}

View File

@ -2,6 +2,6 @@
extension FileResource {
static var mock: FileResource {
.init(content: .mock, id: "my-file.txt", en: "Some text file", de: "Eine Textdatei")
.init(content: .mock, id: "my-file.txt", isExternallyStored: true, en: "Some text file", de: "Eine Textdatei")
}
}

View File

@ -19,6 +19,7 @@ extension Page {
extension LocalizedPage {
static let english = LocalizedPage(
content: .mock,
urlString: "my-project",
title: "My First Project",
lastModified: nil,
@ -28,6 +29,7 @@ extension LocalizedPage {
requiredFiles: [])
static let german = LocalizedPage(
content: .mock,
urlString: "mein-projekt",
title: "Mein Erstes Projekt",
lastModified: nil,

View File

@ -21,7 +21,7 @@ extension PostSettings {
extension PageSettings {
static var mock: PageSettings {
.init(pageUrlPrefix: "pages", contentWidth: 600)
.init(pageUrlPrefix: "pages", contentWidth: 600, largeImageWidth: 1200)
}
}

View File

@ -4,6 +4,8 @@ struct PageSettingsFile {
let pageUrlPrefix: String
let contentWidth: Int
let largeImageWidth: Int
}
extension PageSettingsFile: Codable {
@ -14,6 +16,7 @@ extension PageSettingsFile {
static var `default`: PageSettingsFile {
.init(pageUrlPrefix: "page",
contentWidth: 600)
contentWidth: 600,
largeImageWidth: 1200)
}
}

View File

@ -379,6 +379,18 @@ final class Storage {
return try readExistingFile(at: path)
}
// MARK: External file list
private let externalFileListName = "external-files.json"
func loadExternalFileList() throws -> [String] {
try read(at: externalFileListName, defaultValue: [])
}
func save(externalFileList: [String]) throws {
try writeIfChanged(externalFileList.sorted(), to: externalFileListName)
}
// MARK: Website data
private let settingsDataFileName: String = "settings.json"

View File

@ -34,6 +34,7 @@ struct AddFileView: View {
HStack {
Button("Cancel", role: .cancel) { dismiss() }
Button("Select more files", action: openFilePanel)
Button("Add placeholder", action: addPlaceholderFile)
Button("Add selected", action: importSelectedFiles)
.disabled(filesToAdd.isEmpty)
}
@ -68,6 +69,11 @@ struct AddFileView: View {
}
}
private func addPlaceholderFile() {
let newFile = FileToAdd(content: content, externalFile: "placeholder")
filesToAdd.append(newFile)
}
private func delete(file: FileToAdd) {
guard let index = filesToAdd.firstIndex(of: file) else {
return
@ -85,16 +91,19 @@ struct AddFileView: View {
print("Skipping existing file \(file.uniqueId)")
continue
}
do {
try content.storage.copyFile(at: file.url, fileId: file.uniqueId)
} catch {
print("Failed to import file '\(file.uniqueId)' at \(file.url.path()): \(error)")
return
if let url = file.url {
do {
try content.storage.copyFile(at: url, fileId: file.uniqueId)
} catch {
print("Failed to import file '\(file.uniqueId)' at \(url.path()): \(error)")
return
}
}
let resource = FileResource(
content: content,
id: file.uniqueId,
isExternallyStored: file.url == nil,
en: "", de: "")
// TODO: Insert at correct index?
content.files.insert(resource, at: 0)

View File

@ -13,44 +13,56 @@ struct FileContentView: View {
var body: some View {
VStack {
switch file.type {
case .image:
file.imageToDisplay
.resizable()
.aspectRatio(contentMode: .fit)
case .model:
if file.isExternallyStored {
VStack {
Image(systemSymbol: .cubeTransparent)
Image(systemSymbol: .squareDashed)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: iconSize)
Text("No preview available")
Text("External file")
.font(.title)
}
.foregroundStyle(.secondary)
case .text, .code:
TextFileContentView(file: file)
.id(file.id)
case .video:
VStack {
Image(systemSymbol: .film)
} else {
switch file.type {
case .image:
file.imageToDisplay
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: iconSize)
Text("No preview available")
.font(.title)
case .model:
VStack {
Image(systemSymbol: .cubeTransparent)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: iconSize)
Text("No preview available")
.font(.title)
}
.foregroundStyle(.secondary)
case .text, .code:
TextFileContentView(file: file)
.id(file.id)
case .video:
VStack {
Image(systemSymbol: .film)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: iconSize)
Text("No preview available")
.font(.title)
}
.foregroundStyle(.secondary)
case .other:
VStack {
Image(systemSymbol: .docQuestionmark)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: iconSize)
Text("No preview available")
.font(.title)
}
.foregroundStyle(.secondary)
}
.foregroundStyle(.secondary)
case .other:
VStack {
Image(systemSymbol: .docQuestionmark)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: iconSize)
Text("No preview available")
.font(.title)
}
.foregroundStyle(.secondary)
}
}.padding()
}

View File

@ -2,9 +2,12 @@ import Foundation
final class FileToAdd: ObservableObject {
let id: Int
unowned let content: Content
let url: URL
// The external path to the file, or nil if the file is just a placeholder
let url: URL?
@Published
var uniqueId: String
@ -13,11 +16,19 @@ final class FileToAdd: ObservableObject {
var isSelected: Bool = true
init(content: Content, url: URL) {
self.id = .random()
self.content = content
self.url = url
self.uniqueId = url.lastPathComponent
}
init(content: Content, externalFile: String) {
self.id = .random()
self.content = content
self.url = nil
self.uniqueId = externalFile
}
var idAlreadyExists: Bool {
content.files.contains { $0.id == uniqueId }
}
@ -25,9 +36,6 @@ final class FileToAdd: ObservableObject {
extension FileToAdd: Identifiable {
var id: URL {
url
}
}
extension FileToAdd: Equatable {

View File

@ -30,7 +30,7 @@ struct FileToAddView: View {
.frame(maxWidth: 200)
}
Text(file.url.path())
Text(file.url?.path() ?? "Placeholder file")
.foregroundStyle(.secondary)
}

View File

@ -1,26 +0,0 @@
import SwiftUI
struct ImageContentView: View {
@ObservedObject
var image: FileResource
var body: some View {
image.imageToDisplay
.resizable()
.aspectRatio(contentMode: .fit)
}
}
extension ImageContentView: MainContentView {
init(item: FileResource) {
self.image = item
}
static let itemDescription = "an image"
}
#Preview {
ImageContentView(image: .init(resourceImage: "image1", type: .jpg))
}

View File

@ -73,9 +73,11 @@ struct AddPageView: View {
createdDate: .now,
startDate: .now,
endDate: nil,
german: .init(urlString: "seite",
german: .init(content: content,
urlString: "seite",
title: "Ein Titel"),
english: .init(urlString: "page",
english: .init(content: content,
urlString: "page",
title: "A Title"),
tags: [])
content.pages.insert(page, at: 0)

View File

@ -1,4 +1,5 @@
import SwiftUI
import SFSafeSymbols
import HighlightedTextEditor
struct LocalizedPageContentView: View {
@ -11,17 +12,21 @@ struct LocalizedPageContentView: View {
@Environment(\.language)
private var language
@EnvironmentObject
private var content: Content
@State
private var isGeneratingWebsite = false
@State
private var loadedPageContentLanguage: ContentLanguage?
@State
private var pageContent: String = ""
@State
private var didLoadContent = false
private var pageContentUsedForGeneration: String = ""
@State
private var generationResults = PageGenerationResults()
init(pageId: String, page: LocalizedPage) {
self.pageId = pageId
@ -41,8 +46,12 @@ struct LocalizedPageContentView: View {
Button(action: saveContent) {
Text("Save")
}
Button(action: checkContent) {
Text("Check")
}
Spacer()
}
PageContentResultsView(results: generationResults)
HighlightedTextEditor(
text: $pageContent,
highlightRules: .markdown)
@ -53,32 +62,50 @@ struct LocalizedPageContentView: View {
}
private func loadContent() {
let language = language
do {
let content = try content.storage.pageContent(for: pageId, language: language)
let content = try page.content.storage.pageContent(for: pageId, language: language)
guard content != "" else {
pageContent = "New file"
didLoadContent = false
loadedPageContentLanguage = nil
return
}
pageContent = content
didLoadContent = true
loadedPageContentLanguage = language
checkContent()
} catch {
print("Failed to load page content: \(error)")
pageContent = "Failed to load"
loadedPageContentLanguage = nil
}
}
private func saveContent() {
guard didLoadContent else {
guard let loadedPageContentLanguage else {
return
}
do {
try content.storage.save(pageContent: pageContent, for: pageId, language: language)
try page.content.storage.save(pageContent: pageContent, for: pageId, language: loadedPageContentLanguage)
} catch {
print("Failed to save content: \(error)")
}
}
private func checkContent() {
let content = self.pageContent
guard content != pageContentUsedForGeneration else {
return
}
isGeneratingWebsite = true
DispatchQueue.global(qos: .background).async {
let generator = PageContentParser(content: page.content, language: language)
_ = generator.generatePage(from: content)
DispatchQueue.main.async {
self.generationResults = generator.results
isGeneratingWebsite = false
}
}
}
}

View File

@ -4,22 +4,47 @@ import SFSafeSymbols
struct LocalizedPageDetailView: View {
@ObservedObject
private var item: LocalizedPage
private var page: LocalizedPage
init(page: LocalizedPage, showImagePicker: Bool = false) {
self.item = page
self.page = page
self.showImagePicker = showImagePicker
self.newUrlString = page.urlString
}
@State
private var showImagePicker = false
@State
private var newUrlString: String
private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted
private var idExists: Bool {
page.content.pages.contains {
$0.german.urlString == newUrlString
|| $0.english.urlString == newUrlString
}
}
private var containsInvalidCharacters: Bool {
newUrlString.rangeOfCharacter(from: allowedCharactersInPostId) != nil
}
var body: some View {
VStack(alignment: .leading) {
HStack {
TextField("", text: $newUrlString)
.textFieldStyle(.roundedBorder)
Button("Update", action: setNewId)
.disabled(newUrlString.isEmpty || containsInvalidCharacters || idExists)
}
.padding(.bottom)
Text("Link Preview Title")
.font(.headline)
OptionalTextField("", text: $item.linkPreviewTitle,
prompt: item.title)
OptionalTextField("", text: $page.linkPreviewTitle,
prompt: page.title)
.textFieldStyle(.roundedBorder)
.padding(.bottom)
@ -35,13 +60,13 @@ struct LocalizedPageDetailView: View {
IconButton(symbol: .trashCircleFill,
size: 22,
color: .red) {
item.linkPreviewImage = nil
}.disabled(item.linkPreviewImage == nil)
page.linkPreviewImage = nil
}.disabled(page.linkPreviewImage == nil)
Spacer()
}
.buttonStyle(.plain)
if let image = item.linkPreviewImage {
if let image = page.linkPreviewImage {
image.imageToDisplay
.resizable()
.aspectRatio(contentMode: .fit)
@ -52,16 +77,20 @@ struct LocalizedPageDetailView: View {
Text("Link Preview Description")
.font(.headline)
.padding(.top)
OptionalDescriptionField(text: $item.linkPreviewDescription)
OptionalDescriptionField(text: $page.linkPreviewDescription)
.textFieldStyle(.roundedBorder)
.padding(.bottom)
}
.sheet(isPresented: $showImagePicker) {
ImagePickerView(showImagePicker: $showImagePicker) { image in
item.linkPreviewImage = image
page.linkPreviewImage = image
}
}
}
private func setNewId() {
page.urlString = newUrlString
}
}
#Preview {

View File

@ -0,0 +1,118 @@
import SwiftUI
import SFSafeSymbols
private struct ListPopup: View {
@Environment(\.dismiss)
var dismiss
let items: [String]
var body: some View {
VStack {
List {
ForEach(items, id: \.self) { page in
Text(page)
}
}
.frame(minHeight: min(CGFloat(items.count) * 31, 500))
Button("Dismiss") { dismiss() }
}
.padding(.vertical)
.onTapGesture {
dismiss()
}
}
}
private struct TextWithPopup: View {
let symbol: SFSymbol
let text: LocalizedStringKey
let items: [String]
@State
private var isHovering = false
var body: some View {
HStack {
Image(systemSymbol: symbol)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 16, height: 16)
Text(text)
}
.contentShape(Rectangle())
.onTapGesture {
if items.count > 0 {
isHovering.toggle()
}
}
.sheet(isPresented: $isHovering) {
ListPopup(items: items)
.onTapGesture {
isHovering.toggle()
}
}
}
}
struct PageContentResultsView: View {
@Environment(\.language)
private var language
@ObservedObject
var results: PageGenerationResults
var body: some View {
HStack {
TextWithPopup(
symbol: .photoOnRectangleAngled,
text: "\(results.files.count + results.missingFiles.count) images and files",
items: results.files.sorted().map { $0.id })
.foregroundStyle(.secondary)
TextWithPopup(
symbol: .docBadgePlus,
text: "\(results.linkedPages.count + results.missingPages.count) page links",
items: results.linkedPages.sorted().map { $0.localized(in: language).title })
.foregroundStyle(.secondary)
if !results.missingPages.isEmpty {
TextWithPopup(
symbol: .exclamationmarkTriangleFill,
text: "\(results.missingPages.count) missing pages",
items: results.missingPages.sorted())
.foregroundStyle(.red)
}
if !results.missingFiles.isEmpty {
TextWithPopup(
symbol: .exclamationmarkTriangleFill,
text: "\(results.missingFiles.count) missing files",
items: results.missingFiles.sorted())
.foregroundStyle(.red)
}
if !results.warnings.isEmpty {
TextWithPopup(
symbol: .exclamationmarkTriangleFill,
text: "\(results.warnings.count) errors",
items: results.warnings.sorted())
.foregroundStyle(.red)
}
if !results.invalidCommandArguments.isEmpty {
TextWithPopup(
symbol: .exclamationmarkTriangleFill,
text: "\(results.invalidCommandArguments.count) errors",
items: results.invalidCommandArguments.map {
"\($0.command.rawValue): \($0.arguments.joined(separator: ";"))"
})
.foregroundStyle(.red)
}
}
}
}
#Preview {
PageContentResultsView(results: .init())
}

View File

@ -1,4 +1,5 @@
import SwiftUI
import SFSafeSymbols
struct PageDetailView: View {
@ -17,6 +18,9 @@ struct PageDetailView: View {
@State
private var newId: String
@State
private var didGenerateWebsite: Bool?
init(page: Page) {
self.page = page
self.newId = page.id
@ -35,10 +39,21 @@ struct PageDetailView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Button(action: generate) {
Text("Generate")
HStack {
Button(action: generate) {
Text("Generate")
}
.disabled(isGeneratingWebsite)
if let didGenerateWebsite {
if didGenerateWebsite {
Image(systemSymbol: .checkmarkCircleFill)
.foregroundStyle(.green)
} else {
Image(systemSymbol: .xmarkCircleFill)
.foregroundStyle(.red)
}
}
}
.disabled(isGeneratingWebsite)
HStack {
TextField("", text: $newId)
.textFieldStyle(.roundedBorder)
@ -86,6 +101,7 @@ struct PageDetailView: View {
}
LocalizedPageDetailView(page: page.localized(in: language))
.id(page.id + language.rawValue)
}
.padding()
@ -106,11 +122,13 @@ struct PageDetailView: View {
isGeneratingWebsite = true
print("Generating page")
DispatchQueue.global(qos: .userInitiated).async {
let generator = WebsiteGenerator(
content: content,
language: language)
if !generator.generate(page: page) {
print("Generation failed")
for language in ContentLanguage.allCases {
let generator = LocalizedWebsiteGenerator(
content: content,
language: language)
if !generator.generate(page: page) {
print("Generation failed")
}
}
DispatchQueue.main.async {
isGeneratingWebsite = false

View File

@ -1,6 +1,6 @@
import SwiftUI
struct GenerationSettingsView: View {
struct GenerationContentView: View {
@Environment(\.language)
private var language
@ -37,7 +37,7 @@ struct GenerationSettingsView: View {
Text(generatorText)
Spacer()
}
}
}.padding()
}
}
@ -54,7 +54,7 @@ struct GenerationSettingsView: View {
}
isGeneratingWebsite = true
DispatchQueue.global(qos: .userInitiated).async {
let generator = WebsiteGenerator(
let generator = LocalizedWebsiteGenerator(
content: content,
language: language)
_ = generator.generateWebsite { text in
@ -71,7 +71,7 @@ struct GenerationSettingsView: View {
}
#Preview {
GenerationSettingsView()
GenerationContentView()
.environmentObject(Content.mock)
.padding()
}

View File

@ -0,0 +1,30 @@
import SwiftUI
struct GenerationDetailView: View {
let section: SettingsSection
var body: some View {
Group {
switch section {
//case .generation:
// GenerationSettingsView()
case .folders:
FolderSettingsView()
case .navigationBar:
NavigationBarSettingsView()
case .postFeed:
PostFeedSettingsView()
case .pages:
PageSettingsView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
.navigationTitle("")
}
}
#Preview {
GenerationDetailView(section: .folders)
}

View File

@ -26,7 +26,7 @@ struct NavigationBarSettingsView: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text("Notification Bar Settings")
Text("Navigation Bar")
.font(.largeTitle)
.bold()
Text("Customize the navigation bar for all pages at the top of the website")
@ -37,7 +37,6 @@ struct NavigationBarSettingsView: View {
.font(.headline)
TextField("", text: $content.settings.navigationBar.iconPath)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
Text("Specify the path to the icon file with regard to the final website folder.")
.foregroundStyle(.secondary)
.padding(.bottom, 30)

View File

@ -21,16 +21,22 @@ struct PageSettingsView: View {
.font(.headline)
IntegerField("", number: $content.settings.pages.contentWidth)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 400)
Text("The maximum width of the content in pages (in pixels)")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("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)
Text("Page URL Prefix")
.font(.headline)
TextField("", text: $content.settings.pages.pageUrlPrefix)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 400)
Text("The URL prefix used for the links to pages")
.foregroundStyle(.secondary)
.padding(.bottom)

View File

@ -1,49 +0,0 @@
import SwiftUI
struct SectionedSettingsView: View {
@State
private var selectedSection: SettingsSection? = .generation
var body: some View {
NavigationSplitView {
SettingsSidebar(selectedSection: $selectedSection)
.frame(minWidth: 200, idealWidth: 200, maxWidth: 200)
} detail: {
GenerationDetailView(section: selectedSection)
}
}
}
struct GenerationDetailView: View {
let section: SettingsSection?
var body: some View {
Group {
switch section {
case .generation:
GenerationSettingsView()
case .folders:
FolderSettingsView()
case .navigationBar:
NavigationBarSettingsView()
case .postFeed:
PostFeedSettingsView()
case .pages:
PageSettingsView()
case .none:
Text("Select a setting from the sidebar")
.foregroundStyle(.secondary)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
.navigationTitle("")
}
}
#Preview {
SectionedSettingsView()
}

View File

@ -0,0 +1,13 @@
import SwiftUI
struct SettingsListView: View {
@Binding
var selectedSection: SettingsSection
var body: some View {
List(SettingsSection.allCases, selection: $selectedSection) { item in
Label(item.rawValue, systemSymbol: item.icon).tag(item)
}
}
}

View File

@ -2,7 +2,7 @@ import SFSafeSymbols
enum SettingsSection: String {
case generation = "Generation"
//case generation = "Generation"
case folders = "Folders"
@ -18,7 +18,7 @@ extension SettingsSection {
var icon: SFSymbol {
switch self {
case .generation: return .arrowTriangle2Circlepath
//case .generation: return .arrowTriangle2Circlepath
case .folders: return .folder
case .navigationBar: return .menubarRectangle
case .postFeed: return .rectangleGrid1x2

View File

@ -1,15 +0,0 @@
import SwiftUI
import SFSafeSymbols
struct SettingsSidebar: View {
@Binding var selectedSection: SettingsSection?
var body: some View {
List(SettingsSection.allCases, selection: $selectedSection) { item in
Label(item.rawValue, systemSymbol: item.icon)
.tag(item)
}
.navigationTitle("Settings")
}
}

View File

@ -19,50 +19,71 @@ struct LocalizedTagDetailView: View {
VStack(alignment: .leading) {
Toggle("Appears in overviews", isOn: $tagIsVisible)
.toggleStyle(.switch)
.font(.callout)
.foregroundStyle(.secondary)
.font(.headline)
.padding(.bottom)
Text("Name")
.font(.callout)
.foregroundStyle(.secondary)
.font(.headline)
TextField("", text: $tag.name)
.textFieldStyle(.roundedBorder)
.padding(.bottom)
Text("URL String")
.font(.callout)
.foregroundStyle(.secondary)
.font(.headline)
TextField("", text: $tag.urlComponent)
.textFieldStyle(.roundedBorder)
.padding(.bottom)
Text("Original url")
.font(.callout)
.foregroundStyle(.secondary)
.font(.headline)
Text(tag.originalUrl ?? "-")
.padding(.top, 1)
.padding(.bottom)
Text("Subtitle")
.font(.callout)
.foregroundStyle(.secondary)
.font(.headline)
OptionalTextField("", text: $tag.subtitle)
.textFieldStyle(.roundedBorder)
.padding(.bottom)
Text("Description")
.font(.callout)
.foregroundStyle(.secondary)
OptionalTextField("", text: $tag.description)
Text("Link Preview Description")
.font(.headline)
.padding(.top)
OptionalDescriptionField(text: $tag.description)
.textFieldStyle(.roundedBorder)
.padding(.bottom)
Text("Thumbnail")
.font(.callout)
.foregroundStyle(.secondary)
Button(action: { showImagePicker = true }) {
Text(tag.thumbnail?.id ?? "Select")
HStack {
Text("Link Preview Image")
.font(.headline)
IconButton(symbol: .squareAndPencilCircleFill,
size: 22,
color: .blue) {
showImagePicker = true
}
IconButton(symbol: .trashCircleFill,
size: 22,
color: .red) {
tag.linkPreviewImage = nil
}.disabled(tag.linkPreviewImage == nil)
Spacer()
}
.buttonStyle(.plain)
.foregroundStyle(.blue)
if let image = tag.linkPreviewImage {
image.imageToDisplay
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 400, maxHeight: 300)
.cornerRadius(8)
}
}
.padding()
}
.sheet(isPresented: $showImagePicker) {
ImagePickerView(showImagePicker: $showImagePicker) { image in
tag.thumbnail = image
tag.linkPreviewImage = image
}
}
}