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

View File

@ -9,6 +9,16 @@ extension String {
.replacingOccurrences(of: ">", with: "&gt;") .replacingOccurrences(of: ">", with: "&gt;")
} }
var removingSurroundingQuotes: String {
if hasPrefix("\"") && hasSuffix("\"") {
return dropBeforeFirst("\"").dropAfterLast("\"")
}
if hasPrefix("'") && hasSuffix("'") {
return dropBeforeFirst("'").dropAfterLast("'")
}
return self
}
var nonEmpty: String? { var nonEmpty: String? {
isEmpty ? nil : self isEmpty ? nil : self
} }
@ -30,7 +40,7 @@ extension String {
/** /**
Remove the part after the last occurence of the separator (including the separator itself). 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 { func dropAfterLast(_ separator: String) -> String {
guard contains(separator) else { guard contains(separator) else {

View File

@ -7,6 +7,8 @@ final class GenerationResultsHandler {
/// Generic warnings for pages /// Generic warnings for pages
private var pageWarnings: [(message: String, source: String)] = [] private var pageWarnings: [(message: String, source: String)] = []
private var missingPages: [String : [String]] = [:]
func warning(_ message: String, page: Page) { func warning(_ message: String, page: Page) {
pageWarnings.append((message, page.id)) pageWarnings.append((message, page.id))
print("Page: \(page.id): \(message)") print("Page: \(page.id): \(message)")
@ -15,4 +17,8 @@ final class GenerationResultsHandler {
func addRequiredVideoFile(fileId: String) { func addRequiredVideoFile(fileId: String) {
requiredVideoFiles.insert(fileId) 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 { func runJobs(callback: (String) -> Void) -> Bool {
guard !jobs.isEmpty else {
return true
}
print("Generating \(jobs.count) images...") print("Generating \(jobs.count) images...")
for job in jobs { for job in jobs {
callback("Generating image \(job.version)") callback("Generating image \(job.version)")
@ -80,7 +83,7 @@ final class ImageGenerator {
return "\(prefix).\(type.fileExtension)" 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 type = ImageFileType(fileExtension: image.fileExtension!)!
let width2x = maxWidth * 2 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: maxWidth, maximumHeight: maxHeight)
_ = generateVersion(for: image, type: type, maximumWidth: width2x, maximumHeight: height2x) _ = 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 { func generateVersion(for image: String, type: ImageFileType, maximumWidth: CGFloat, maximumHeight: CGFloat) -> String {
@ -133,6 +130,10 @@ final class ImageGenerator {
return versions.contains(version) return versions.contains(version)
} }
private func exists(imageVersion version: String) -> Bool {
inOutputImagesFolder { $0.appendingPathComponent(version).exists }
}
private func hasNowGenerated(version: String, for image: String) { private func hasNowGenerated(version: String, for image: String) {
guard var versions = generatedImages[image] else { guard var versions = generatedImages[image] else {
generatedImages[image] = [version] generatedImages[image] = [version]
@ -149,7 +150,8 @@ final class ImageGenerator {
// MARK: Image operations // MARK: Image operations
private func generate(job: ImageJob) -> Bool { 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 return true
} }
@ -201,7 +203,7 @@ final class ImageGenerator {
let url = folder.appendingPathComponent(job.version) let url = folder.appendingPathComponent(job.version)
if job.type == .avif { if job.type == .avif {
let out = url.path() 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)") print("avifenc -q 70 \(input) \(out)")
return true return true
} }

View File

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

View File

@ -8,68 +8,86 @@ final class PageContentParser {
private let pageLinkMarker = "page:" private let pageLinkMarker = "page:"
private let largeImageIndicator = "*large*"
private let swift = SyntaxHighlighter(format: HTMLOutputFormat()) private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
private let results: GenerationResultsHandler let results = PageGenerationResults()
private let content: Content private let content: Content
private let imageGenerator: ImageGenerator
private let page: Page
private let language: ContentLanguage private let language: ContentLanguage
private var largeImageCount: Int = 0 private var largeImageCount: Int = 0
init(page: Page, content: Content, language: ContentLanguage, results: GenerationResultsHandler, imageGenerator: ImageGenerator) { var largeImageWidth: Int {
self.page = page content.settings.pages.largeImageWidth
}
var thumbnailWidth: Int {
content.settings.pages.contentWidth
}
init(content: Content, language: ContentLanguage) {
self.content = content self.content = content
self.language = language 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 { func generatePage(from content: String) -> String {
reset()
let imageModifier = Modifier(target: .images) { html, markdown in let parser = MarkdownParser(modifiers: [
self.processMarkdownImage(markdown: markdown, html: html) Modifier(target: .images, closure: processMarkdownImage),
} Modifier(target: .codeBlocks, closure: handleCode),
let codeModifier = Modifier(target: .codeBlocks) { html, markdown in Modifier(target: .links, closure: handleLink),
if markdown.starts(with: "```swift") { Modifier(target: .html, closure: handleHTML),
let code = markdown.between("```swift", and: "```").trimmed Modifier(target: .headings, closure: handleHeadlines)
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])
return parser.html(from: content) 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 { private func handleLink(html: String, markdown: Substring) -> String {
let file = markdown.between("(", and: ")") let file = markdown.between("(", and: ")")
if file.hasPrefix(pageLinkMarker) { if file.hasPrefix(pageLinkMarker) {
// Retain links pointing to elements within a page // Retain links pointing to elements within a page
let textToChange = file.dropAfterFirst("#") let textToChange = file.dropAfterFirst("#")
let pageId = textToChange.replacingOccurrences(of: pageLinkMarker, with: "") 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 // Remove link since the page can't be found
return markdown.between("[", and: "]") return markdown.between("[", and: "]")
} }
// Adjust file path to get the page url results.linkedPages.insert(page)
// TODO: Calculate relative links to make pages more portable let pagePath = content.pageLink(page, language: language)
return html.replacingOccurrences(of: textToChange, with: pagePath) return html.replacingOccurrences(of: textToChange, with: pagePath)
} }
@ -92,6 +110,13 @@ final class PageContentParser {
return html 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 { private func handleHeadlines(html: String, markdown: Substring) -> String {
let id = markdown let id = markdown
.last(after: "#") .last(after: "#")
@ -101,18 +126,18 @@ final class PageContentParser {
.components(separatedBy: " ") .components(separatedBy: " ")
.filter { $0 != "" } .filter { $0 != "" }
.joined(separator: "-") .joined(separator: "-")
let parts = html.components(separatedBy: ">") let parts = html.components(separatedBy: ">")
return parts[0] + " id=\"\(id)\">" + parts.dropFirst().joined(separator: ">") 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 // First, check the content type, then parse the remaining arguments
// Notation: // Notation:
// <abc?> -> Optional argument // <abc?> -> Optional argument
// <abc...> -> Repeated argument (0 or more) // <abc...> -> Repeated argument (0 or more)
// ![url](<url>;<text>) // ![url](<url>;<text>)
// ![image](<imageId>;<caption?>] // ![image](<imageId>;<caption?>]
// ![video](<fileId>;<alt>;<option1...>] // ![video](<fileId>;<option1...>]
// ![svg](<fileId>;<<x>;<y>;<width>;<height>?>) // ![svg](<fileId>;<<x>;<y>;<width>;<height>?>)
// ![download](<<fileId>,<text>,<download-filename?>;...) // ![download](<<fileId>,<text>,<download-filename?>;...)
// ![box](<title>;<body>) // ![box](<title>;<body>)
@ -120,12 +145,11 @@ final class PageContentParser {
// ![page](<pageId>) // ![page](<pageId>)
// ![external](<<url>;<text>...> // ![external](<<url>;<text>...>
// ![html](<fileId>) // ![html](<fileId>)
guard let argumentList = markdown.between(first: "](", andLast: ")").removingPercentEncoding else {
results.warning("Invalid percent encoding for markdown image", page: page) let argumentList = markdown.between(first: "](", andLast: ")").removingPercentEncoding ?? markdown.between(first: "](", andLast: ")")
return ""
}
let arguments = argumentList.components(separatedBy: ";") let arguments = argumentList.components(separatedBy: ";")
let rawCommand = markdown.between("![", and: "]").trimmed let rawCommand = markdown.between("![", and: "]").trimmed
guard rawCommand != "" else { guard rawCommand != "" else {
return handleImage(arguments) return handleImage(arguments)
@ -134,7 +158,7 @@ final class PageContentParser {
guard let convertedCommand = rawCommand.removingPercentEncoding, guard let convertedCommand = rawCommand.removingPercentEncoding,
let command = ShorthandMarkdownKey(rawValue: convertedCommand) else { let command = ShorthandMarkdownKey(rawValue: convertedCommand) else {
// Treat unknown commands as normal links // Treat unknown commands as normal links
print("Unknown markdown command: \(rawCommand)") results.warnings.append("Unknown markdown command '\(rawCommand)'")
return html return html
} }
@ -147,12 +171,9 @@ final class PageContentParser {
return handleDownloadButtons(arguments) return handleDownloadButtons(arguments)
case .video: case .video:
return handleVideo(arguments) return handleVideo(arguments)
default:
print("Unhandled markdown command: \(command)")
return ""
/*
case .externalLink: case .externalLink:
return handleExternalButtons(content: content) return handleExternalButtons(arguments)
/*
case .includedHtml: case .includedHtml:
return handleExternalHTML(file: content) return handleExternalHTML(file: content)
case .box: case .box:
@ -162,35 +183,42 @@ final class PageContentParser {
case .model: case .model:
return handle3dModel(content: content) return handle3dModel(content: content)
*/ */
default:
results.warnings.append("Unhandled command '\(command.rawValue)'")
return ""
} }
} }
private func handleImage(_ arguments: [String]) -> String { private func handleImage(_ arguments: [String]) -> String {
// [image](<imageId>;<caption?>] // [image](<imageId>;<caption?>]
guard (1...2).contains(arguments.count) else { guard (1...2).contains(arguments.count) else {
results.warning("Invalid image arguments: \(arguments)", page: page) results.invalidCommandArguments.append((.image , arguments))
return "" return ""
} }
let imageId = arguments[0] let imageId = arguments[0]
guard let image = content.image(imageId) else { guard let image = content.image(imageId) else {
results.warning("Missing image \(imageId)", page: page) results.missingFiles.insert(imageId)
return "" return ""
} }
results.files.insert(image)
let caption = arguments.count == 2 ? arguments[1] : nil let caption = arguments.count == 2 ? arguments[1] : nil
let altText = image.getDescription(for: language) let altText = image.getDescription(for: language)
let thumbnailWidth = CGFloat(content.settings.pages.contentWidth) let path = content.pathToImage(image)
let thumbnail = imageGenerator.generateImageSet(
for: imageId, let thumbnail = FeedEntryData.Image(
maxWidth: thumbnailWidth, maxHeight: thumbnailWidth, rawImagePath: path,
width: thumbnailWidth,
height: thumbnailWidth,
altText: altText) altText: altText)
let largeImageWidth = CGFloat(1200) // TODO: Move to settings let largeImage = FeedEntryData.Image(
rawImagePath: path,
let largeImage = imageGenerator.generateImageSet( width: largeImageWidth,
for: imageId, height: largeImageWidth,
maxWidth: largeImageWidth, maxHeight: largeImageWidth,
altText: altText) altText: altText)
return PageImage( return PageImage(
@ -202,7 +230,7 @@ final class PageContentParser {
private func handleHikingStatistics(_ arguments: [String]) -> String { private func handleHikingStatistics(_ arguments: [String]) -> String {
guard (1...5).contains(arguments.count) else { guard (1...5).contains(arguments.count) else {
results.warning("Invalid hiking statistic arguments: \(arguments)", page: page) results.invalidCommandArguments.append((.hikingStatistics, arguments))
return "" return ""
} }
@ -222,10 +250,11 @@ final class PageContentParser {
} }
private func handleDownloadButtons(_ arguments: [String]) -> String { 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: ",") let parts = button.components(separatedBy: ",")
guard (2...3).contains(parts.count) else { guard (2...3).contains(parts.count) else {
results.warning("Invalid download definition with \(parts)", page: page) results.invalidCommandArguments.append((.downloadButtons, parts))
return nil return nil
} }
let file = parts[0].trimmed let file = parts[0].trimmed
@ -234,44 +263,36 @@ final class PageContentParser {
// Ensure that file is available // Ensure that file is available
guard let filePath = content.pathToFile(file) else { guard let filePath = content.pathToFile(file) else {
results.warning("Missing download file \(file)", page: page) results.missingFiles.insert(file)
return nil return nil
} }
return ContentButtons.Item(icon: .download, filePath: filePath, text: title, downloadFileName: downloadName)
return DownloadButtons.Item(filePath: filePath, text: title, downloadFileName: downloadName)
} }
return DownloadButtons(items: buttons).content return ContentButtons(items: buttons).content
} }
private func handleVideo(_ arguments: [String]) -> String { private func handleVideo(_ arguments: [String]) -> String {
// ![video](<fileId>;<option1...>]
guard arguments.count >= 1 else { guard arguments.count >= 1 else {
results.invalidCommandArguments.append((.video, arguments))
return "" return ""
} }
let fileId = arguments[0].trimmed let fileId = arguments[0].trimmed
let options: [VideoOption] = arguments.dropFirst().compactMap { optionText in let options = arguments.dropFirst().compactMap(convertVideoOption)
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
}
guard let filePath = content.pathToFile(fileId), guard let file = content.file(id: fileId) else {
let file = content.file(id: fileId) else { results.missingFiles.insert(fileId)
results.warning("Missing video file \(fileId)", page: page)
return "" return ""
} }
results.files.insert(file)
guard let videoType = file.type.videoType?.htmlType else { 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 "" return ""
} }
results.addRequiredVideoFile(fileId: fileId) let filePath = content.pathToFile(file)
return ContentPageVideo( return ContentPageVideo(
filePath: filePath, filePath: filePath,
videoType: videoType, videoType: videoType,
@ -279,6 +300,40 @@ final class PageContentParser {
.content .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 { 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) results.warning("Unhandled file \(file) with extension \(fileExtension)", source: page.path)
return "" return ""
} }
*/
private func handleExternalButtons(content: String) -> String { private func handleExternalButtons(_ arguments: [String]) -> String {
let buttons = content // ![external](<<url>;<text>...>
.components(separatedBy: ";") guard arguments.count >= 1 else {
.compactMap { button -> (url: String, text: String)? in results.invalidCommandArguments.append((.externalLink, arguments))
let parts = button.components(separatedBy: ",") return ""
guard parts.count == 2 else { }
results.warning("Invalid external link definition", page: page) let buttons: [ContentButtons.Item] = arguments.compactMap { button in
return nil let parts = button.components(separatedBy: ",")
} guard parts.count == 2 else {
guard let url = parts[0].trimmed.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { results.invalidCommandArguments.append((.externalLink, parts))
results.warning("Invalid external link \(parts[0].trimmed)", source: page.path) return nil
return nil
}
let title = parts[1].trimmed
return (url, title)
} }
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 { private func handleExternalHTML(file: String) -> String {
let path = page.pathRelativeToRootForContainedInputFile(file) let path = page.pathRelativeToRootForContainedInputFile(file)
return results.getContentOfRequiredFile(at: path, source: page.path) ?? "" 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 private let navigationBarData: NavigationBarData
let results = GenerationResultsHandler()
init(content: Content, imageGenerator: ImageGenerator, navigationBarData: NavigationBarData) { init(content: Content, imageGenerator: ImageGenerator, navigationBarData: NavigationBarData) {
self.content = content self.content = content
self.imageGenerator = imageGenerator self.imageGenerator = imageGenerator
self.navigationBarData = navigationBarData 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( let contentGenerator = PageContentParser(
page: page,
content: content, content: content,
language: language, language: language)
results: results,
imageGenerator: imageGenerator)
let rawPageContent = try content.storage.pageContent(for: page.id, language: language) let rawPageContent = try content.storage.pageContent(for: page.id, language: language)
let pageContent = contentGenerator.generatePage(from: rawPageContent) let pageContent = contentGenerator.generatePage(from: rawPageContent)
contentGenerator.requestImages(imageGenerator)
let localized = page.localized(in: language) let localized = page.localized(in: language)
let tags: [FeedEntryData.Tag] = page.tags.map { tag in let tags: [FeedEntryData.Tag] = page.tags.map { tag in
@ -33,7 +30,7 @@ final class PageGenerator {
url: content.tagLink(tag, language: language)) url: content.tagLink(tag, language: language))
} }
return ContentPage( let fullPage = ContentPage(
language: language, language: language,
dateString: page.dateText(in: language), dateString: page.dateText(in: language),
title: localized.title, title: localized.title,
@ -43,5 +40,7 @@ final class PageGenerator {
navigationBarData: navigationBarData, navigationBarData: navigationBarData,
pageContent: pageContent) pageContent: pageContent)
.content .content
return (fullPage, contentGenerator.results)
} }
} }

View File

@ -66,7 +66,7 @@ final class PostListPageGenerator {
text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings 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, .init(name: tag.localized(in: language).name,
url: content.tagLink(tag, language: language)) url: content.tagLink(tag, language: language))
} }
@ -102,7 +102,11 @@ final class PostListPageGenerator {
imageGenerator.generateImageSet( imageGenerator.generateImageSet(
for: image.id, for: image.id,
maxWidth: mainContentMaximumWidth, maxWidth: mainContentMaximumWidth,
maxHeight: mainContentMaximumWidth, maxHeight: mainContentMaximumWidth)
return .init(
rawImagePath: content.pathToImage(image),
width: Int(mainContentMaximumWidth),
height: Int(mainContentMaximumWidth),
altText: image.getDescription(for: language)) altText: image.getDescription(for: language))
} }

View File

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

View File

@ -1,11 +1,123 @@
/// HTML video options /// HTML video options
enum VideoOption: String { enum VideoOption {
/// Specifies that video controls should be displayed (such as a play/pause button etc).
case controls case controls
/// Specifies that the video will start playing as soon as it is ready
case autoplay case autoplay
case muted
/// Specifies that the video will start over again, every time it is finished
case loop 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 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 SwiftUI
import SFSafeSymbols 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("Allow selection of pages as navigation bar items")
#warning("Transfer images of posts to other language") #warning("Transfer images of posts to other language")
#warning("Show tag selection view for pages")
@main @main
struct MainView: App { struct MainView: App {
@ -46,7 +37,7 @@ struct MainView: App {
private var selectedFile: FileResource? private var selectedFile: FileResource?
@State @State
private var selectedSection: SettingsSection? = .generation private var selectedSection: SettingsSection = .folders
@State @State
private var showAddSheet = false private var showAddSheet = false
@ -63,9 +54,7 @@ struct MainView: App {
case .files: case .files:
FileListView(selectedFile: $selectedFile) FileListView(selectedFile: $selectedFile)
case .generation: case .generation:
List(SettingsSection.allCases, selection: $selectedSection) { item in SettingsListView(selectedSection: $selectedSection)
Label(item.rawValue, systemSymbol: item.icon).tag(item)
}
} }
} }
@ -81,7 +70,7 @@ struct MainView: App {
case .files: case .files:
SelectedContentView<FileContentView>(selected: $selectedFile) SelectedContentView<FileContentView>(selected: $selectedFile)
case .generation: case .generation:
GenerationDetailView(section: selectedSection) GenerationContentView()
} }
} }
@ -97,7 +86,7 @@ struct MainView: App {
case .files: case .files:
SelectedDetailView<FileDetailView>(selected: $selectedFile) SelectedDetailView<FileDetailView>(selected: $selectedFile)
case .generation: case .generation:
Text("") GenerationDetailView(section: selectedSection)
} }
} }

View File

@ -18,6 +18,10 @@ extension Content {
return prefix + page.localized(in: language).urlString return prefix + page.localized(in: language).urlString
} }
func page(_ pageId: String) -> Page? {
pages.first { $0.id == pageId }
}
func pageLink(pageId: String, language: ContentLanguage) -> String? { func pageLink(pageId: String, language: ContentLanguage) -> String? {
guard let page = pages.first(where: { $0.id == pageId }) else { guard let page = pages.first(where: { $0.id == pageId }) else {
// TODO: Note missing link // TODO: Note missing link
@ -31,18 +35,42 @@ extension Content {
guard let file = file(id: fileId) else { guard let file = file(id: fileId) else {
return nil 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") #warning("Add files path to settings")
return "/files/\(file.id)" return "/files/\(file.id)"
} }
func image(_ imageId: String) -> FileResource? { func pathToImage(_ image: FileResource) -> String {
files.first { $0.id == imageId } 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? { func file(id: String) -> FileResource? {
files.first { $0.id == id } files.first { $0.id == id }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +1,22 @@
struct DownloadButtons { struct ContentButtons {
struct Item { struct Item {
let icon: PageIcon
let filePath: String let filePath: String
let text: String let text: String
let downloadFileName: 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] let items: [Item]
@ -17,7 +26,7 @@ struct DownloadButtons {
} }
var content: String { var content: String {
var result = "<p style='display: flex'>" var result = "<p class='tags tag-buttons'>"
for item in items { for item in items {
addButton(of: item, to: &result) addButton(of: item, to: &result)
} }
@ -27,8 +36,8 @@ struct DownloadButtons {
private func addButton(of item: Item, to result: inout String) { private func addButton(of item: Item, to result: inout String) {
let downloadText = item.downloadFileName.map { " download='\($0)'" } ?? "" let downloadText = item.downloadFileName.map { " download='\($0)'" } ?? ""
result += "<a class='download-button' href='\(item.filePath)'\(downloadText)>" result += "<a class='tag' href='\(item.filePath)'\(downloadText)>"
result += "\(item.text)<span class='icon icon-download'></span>" result += "<svg><use href='#\(item.icon.name)'></use></svg>\(item.text)"
result += "</a>" 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 { 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 prefix1x: String
private let prefix2x: String private let prefix2x: String
@ -18,8 +31,8 @@ struct WebsiteImage {
init(rawImagePath: String, width: Int, height: Int, altText: String) { init(rawImagePath: String, width: Int, height: Int, altText: String) {
let (prefix, ext) = rawImagePath.fileNameAndExtension let (prefix, ext) = rawImagePath.fileNameAndExtension
self.prefix1x = "\(prefix)@\(width)x\(height)" self.prefix1x = WebsiteImage.imagePath(prefix: prefix, width: width, height: height)
self.prefix2x = "\(prefix)@\(width*2)x\(height*2)" self.prefix2x = WebsiteImage.imagePath(prefix: prefix, width: 2*width, height: 2*height)
self.altText = altText.htmlEscaped() self.altText = altText.htmlEscaped()
self.ext = ext ?? "jpg" self.ext = ext ?? "jpg"
} }

View File

@ -49,25 +49,13 @@ struct ContentPage: HtmlProducer {
result += "</body></html>" // Close content result += "</body></html>" // Close content
} }
private let symbols: String = #warning("Select only required symbols")
""" private let symbols: String = {
<div style="display:none"> var result = "<div style='display:none'>"
<svg id="icon-clock" width="16" height="16" viewBox="0 0 16 16"> for icon in PageIcon.allCases {
<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"/> result += icon.icon
<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> result += "</div>"
<svg id="icon-arrow-up" width="16" height="16"> return result
<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>
"""
} }

View File

@ -2,6 +2,6 @@
extension FileResource { extension FileResource {
static var mock: 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 { extension LocalizedPage {
static let english = LocalizedPage( static let english = LocalizedPage(
content: .mock,
urlString: "my-project", urlString: "my-project",
title: "My First Project", title: "My First Project",
lastModified: nil, lastModified: nil,
@ -28,6 +29,7 @@ extension LocalizedPage {
requiredFiles: []) requiredFiles: [])
static let german = LocalizedPage( static let german = LocalizedPage(
content: .mock,
urlString: "mein-projekt", urlString: "mein-projekt",
title: "Mein Erstes Projekt", title: "Mein Erstes Projekt",
lastModified: nil, lastModified: nil,

View File

@ -21,7 +21,7 @@ extension PostSettings {
extension PageSettings { extension PageSettings {
static var mock: 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 pageUrlPrefix: String
let contentWidth: Int let contentWidth: Int
let largeImageWidth: Int
} }
extension PageSettingsFile: Codable { extension PageSettingsFile: Codable {
@ -14,6 +16,7 @@ extension PageSettingsFile {
static var `default`: PageSettingsFile { static var `default`: PageSettingsFile {
.init(pageUrlPrefix: "page", .init(pageUrlPrefix: "page",
contentWidth: 600) contentWidth: 600,
largeImageWidth: 1200)
} }
} }

View File

@ -379,6 +379,18 @@ final class Storage {
return try readExistingFile(at: path) 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 // MARK: Website data
private let settingsDataFileName: String = "settings.json" private let settingsDataFileName: String = "settings.json"

View File

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

View File

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

View File

@ -2,9 +2,12 @@ import Foundation
final class FileToAdd: ObservableObject { final class FileToAdd: ObservableObject {
let id: Int
unowned let content: Content 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 @Published
var uniqueId: String var uniqueId: String
@ -13,11 +16,19 @@ final class FileToAdd: ObservableObject {
var isSelected: Bool = true var isSelected: Bool = true
init(content: Content, url: URL) { init(content: Content, url: URL) {
self.id = .random()
self.content = content self.content = content
self.url = url self.url = url
self.uniqueId = url.lastPathComponent self.uniqueId = url.lastPathComponent
} }
init(content: Content, externalFile: String) {
self.id = .random()
self.content = content
self.url = nil
self.uniqueId = externalFile
}
var idAlreadyExists: Bool { var idAlreadyExists: Bool {
content.files.contains { $0.id == uniqueId } content.files.contains { $0.id == uniqueId }
} }
@ -25,9 +36,6 @@ final class FileToAdd: ObservableObject {
extension FileToAdd: Identifiable { extension FileToAdd: Identifiable {
var id: URL {
url
}
} }
extension FileToAdd: Equatable { extension FileToAdd: Equatable {

View File

@ -30,7 +30,7 @@ struct FileToAddView: View {
.frame(maxWidth: 200) .frame(maxWidth: 200)
} }
Text(file.url.path()) Text(file.url?.path() ?? "Placeholder file")
.foregroundStyle(.secondary) .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, createdDate: .now,
startDate: .now, startDate: .now,
endDate: nil, endDate: nil,
german: .init(urlString: "seite", german: .init(content: content,
urlString: "seite",
title: "Ein Titel"), title: "Ein Titel"),
english: .init(urlString: "page", english: .init(content: content,
urlString: "page",
title: "A Title"), title: "A Title"),
tags: []) tags: [])
content.pages.insert(page, at: 0) content.pages.insert(page, at: 0)

View File

@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import SFSafeSymbols
import HighlightedTextEditor import HighlightedTextEditor
struct LocalizedPageContentView: View { struct LocalizedPageContentView: View {
@ -11,17 +12,21 @@ struct LocalizedPageContentView: View {
@Environment(\.language) @Environment(\.language)
private var language private var language
@EnvironmentObject
private var content: Content
@State @State
private var isGeneratingWebsite = false private var isGeneratingWebsite = false
@State
private var loadedPageContentLanguage: ContentLanguage?
@State @State
private var pageContent: String = "" private var pageContent: String = ""
@State @State
private var didLoadContent = false private var pageContentUsedForGeneration: String = ""
@State
private var generationResults = PageGenerationResults()
init(pageId: String, page: LocalizedPage) { init(pageId: String, page: LocalizedPage) {
self.pageId = pageId self.pageId = pageId
@ -41,8 +46,12 @@ struct LocalizedPageContentView: View {
Button(action: saveContent) { Button(action: saveContent) {
Text("Save") Text("Save")
} }
Button(action: checkContent) {
Text("Check")
}
Spacer() Spacer()
} }
PageContentResultsView(results: generationResults)
HighlightedTextEditor( HighlightedTextEditor(
text: $pageContent, text: $pageContent,
highlightRules: .markdown) highlightRules: .markdown)
@ -53,32 +62,50 @@ struct LocalizedPageContentView: View {
} }
private func loadContent() { private func loadContent() {
let language = language
do { 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 { guard content != "" else {
pageContent = "New file" pageContent = "New file"
didLoadContent = false loadedPageContentLanguage = nil
return return
} }
pageContent = content pageContent = content
didLoadContent = true loadedPageContentLanguage = language
checkContent()
} catch { } catch {
print("Failed to load page content: \(error)") print("Failed to load page content: \(error)")
pageContent = "Failed to load" pageContent = "Failed to load"
loadedPageContentLanguage = nil
} }
} }
private func saveContent() { private func saveContent() {
guard didLoadContent else { guard let loadedPageContentLanguage else {
return return
} }
do { do {
try content.storage.save(pageContent: pageContent, for: pageId, language: language) try page.content.storage.save(pageContent: pageContent, for: pageId, language: loadedPageContentLanguage)
} catch { } catch {
print("Failed to save content: \(error)") 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 { struct LocalizedPageDetailView: View {
@ObservedObject @ObservedObject
private var item: LocalizedPage private var page: LocalizedPage
init(page: LocalizedPage, showImagePicker: Bool = false) { init(page: LocalizedPage, showImagePicker: Bool = false) {
self.item = page self.page = page
self.showImagePicker = showImagePicker self.showImagePicker = showImagePicker
self.newUrlString = page.urlString
} }
@State @State
private var showImagePicker = false 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 { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack {
TextField("", text: $newUrlString)
.textFieldStyle(.roundedBorder)
Button("Update", action: setNewId)
.disabled(newUrlString.isEmpty || containsInvalidCharacters || idExists)
}
.padding(.bottom)
Text("Link Preview Title") Text("Link Preview Title")
.font(.headline) .font(.headline)
OptionalTextField("", text: $item.linkPreviewTitle, OptionalTextField("", text: $page.linkPreviewTitle,
prompt: item.title) prompt: page.title)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.padding(.bottom) .padding(.bottom)
@ -35,13 +60,13 @@ struct LocalizedPageDetailView: View {
IconButton(symbol: .trashCircleFill, IconButton(symbol: .trashCircleFill,
size: 22, size: 22,
color: .red) { color: .red) {
item.linkPreviewImage = nil page.linkPreviewImage = nil
}.disabled(item.linkPreviewImage == nil) }.disabled(page.linkPreviewImage == nil)
Spacer() Spacer()
} }
.buttonStyle(.plain) .buttonStyle(.plain)
if let image = item.linkPreviewImage { if let image = page.linkPreviewImage {
image.imageToDisplay image.imageToDisplay
.resizable() .resizable()
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
@ -52,16 +77,20 @@ struct LocalizedPageDetailView: View {
Text("Link Preview Description") Text("Link Preview Description")
.font(.headline) .font(.headline)
.padding(.top) .padding(.top)
OptionalDescriptionField(text: $item.linkPreviewDescription) OptionalDescriptionField(text: $page.linkPreviewDescription)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.padding(.bottom) .padding(.bottom)
} }
.sheet(isPresented: $showImagePicker) { .sheet(isPresented: $showImagePicker) {
ImagePickerView(showImagePicker: $showImagePicker) { image in ImagePickerView(showImagePicker: $showImagePicker) { image in
item.linkPreviewImage = image page.linkPreviewImage = image
} }
} }
} }
private func setNewId() {
page.urlString = newUrlString
}
} }
#Preview { #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 SwiftUI
import SFSafeSymbols
struct PageDetailView: View { struct PageDetailView: View {
@ -17,6 +18,9 @@ struct PageDetailView: View {
@State @State
private var newId: String private var newId: String
@State
private var didGenerateWebsite: Bool?
init(page: Page) { init(page: Page) {
self.page = page self.page = page
self.newId = page.id self.newId = page.id
@ -35,10 +39,21 @@ struct PageDetailView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Button(action: generate) { HStack {
Text("Generate") 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 { HStack {
TextField("", text: $newId) TextField("", text: $newId)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
@ -86,6 +101,7 @@ struct PageDetailView: View {
} }
LocalizedPageDetailView(page: page.localized(in: language)) LocalizedPageDetailView(page: page.localized(in: language))
.id(page.id + language.rawValue)
} }
.padding() .padding()
@ -106,11 +122,13 @@ struct PageDetailView: View {
isGeneratingWebsite = true isGeneratingWebsite = true
print("Generating page") print("Generating page")
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let generator = WebsiteGenerator( for language in ContentLanguage.allCases {
content: content, let generator = LocalizedWebsiteGenerator(
language: language) content: content,
if !generator.generate(page: page) { language: language)
print("Generation failed") if !generator.generate(page: page) {
print("Generation failed")
}
} }
DispatchQueue.main.async { DispatchQueue.main.async {
isGeneratingWebsite = false isGeneratingWebsite = false

View File

@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
struct GenerationSettingsView: View { struct GenerationContentView: View {
@Environment(\.language) @Environment(\.language)
private var language private var language
@ -37,7 +37,7 @@ struct GenerationSettingsView: View {
Text(generatorText) Text(generatorText)
Spacer() Spacer()
} }
} }.padding()
} }
} }
@ -54,7 +54,7 @@ struct GenerationSettingsView: View {
} }
isGeneratingWebsite = true isGeneratingWebsite = true
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let generator = WebsiteGenerator( let generator = LocalizedWebsiteGenerator(
content: content, content: content,
language: language) language: language)
_ = generator.generateWebsite { text in _ = generator.generateWebsite { text in
@ -71,7 +71,7 @@ struct GenerationSettingsView: View {
} }
#Preview { #Preview {
GenerationSettingsView() GenerationContentView()
.environmentObject(Content.mock) .environmentObject(Content.mock)
.padding() .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 { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Notification Bar Settings") Text("Navigation Bar")
.font(.largeTitle) .font(.largeTitle)
.bold() .bold()
Text("Customize the navigation bar for all pages at the top of the website") Text("Customize the navigation bar for all pages at the top of the website")
@ -37,7 +37,6 @@ struct NavigationBarSettingsView: View {
.font(.headline) .font(.headline)
TextField("", text: $content.settings.navigationBar.iconPath) TextField("", text: $content.settings.navigationBar.iconPath)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(maxWidth: 300)
Text("Specify the path to the icon file with regard to the final website folder.") Text("Specify the path to the icon file with regard to the final website folder.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.padding(.bottom, 30) .padding(.bottom, 30)

View File

@ -21,16 +21,22 @@ struct PageSettingsView: View {
.font(.headline) .font(.headline)
IntegerField("", number: $content.settings.pages.contentWidth) IntegerField("", number: $content.settings.pages.contentWidth)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(maxWidth: 400)
Text("The maximum width of the content in pages (in pixels)") Text("The maximum width of the content in pages (in pixels)")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.padding(.bottom) .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") Text("Page URL Prefix")
.font(.headline) .font(.headline)
TextField("", text: $content.settings.pages.pageUrlPrefix) TextField("", text: $content.settings.pages.pageUrlPrefix)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.frame(maxWidth: 400)
Text("The URL prefix used for the links to pages") Text("The URL prefix used for the links to pages")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.padding(.bottom) .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 { enum SettingsSection: String {
case generation = "Generation" //case generation = "Generation"
case folders = "Folders" case folders = "Folders"
@ -18,7 +18,7 @@ extension SettingsSection {
var icon: SFSymbol { var icon: SFSymbol {
switch self { switch self {
case .generation: return .arrowTriangle2Circlepath //case .generation: return .arrowTriangle2Circlepath
case .folders: return .folder case .folders: return .folder
case .navigationBar: return .menubarRectangle case .navigationBar: return .menubarRectangle
case .postFeed: return .rectangleGrid1x2 case .postFeed: return .rectangleGrid1x2

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