External files, improve page generation
This commit is contained in:
parent
8183bc4903
commit
efc9234917
@ -19,7 +19,7 @@
|
||||
E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850262CF3B42D0090B18B /* PostDetailView.swift */; };
|
||||
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; };
|
||||
E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; };
|
||||
E218502F2CFAF69C0090B18B /* WebsiteGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */; };
|
||||
E218502F2CFAF69C0090B18B /* LocalizedWebsiteGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */; };
|
||||
E21850332CFAFA2F0090B18B /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850322CFAFA200090B18B /* Settings.swift */; };
|
||||
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850342CFAFA570090B18B /* SettingsFile.swift */; };
|
||||
E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; };
|
||||
@ -31,7 +31,6 @@
|
||||
E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5082CFD964E00AEF16D /* TagContentView.swift */; };
|
||||
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */; };
|
||||
E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */; };
|
||||
E25DA50F2CFDD76B00AEF16D /* ImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50E2CFDD76B00AEF16D /* ImageContentView.swift */; };
|
||||
E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5142CFF00B900AEF16D /* Content+Load.swift */; };
|
||||
E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5162CFF00F200AEF16D /* Content+Save.swift */; };
|
||||
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5182CFF035200AEF16D /* Array+Split.swift */; };
|
||||
@ -44,7 +43,6 @@
|
||||
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */; };
|
||||
E25DA52C2CFFC3EC00AEF16D /* SDWebImageAVIFCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52B2CFFC3EC00AEF16D /* SDWebImageAVIFCoder */; };
|
||||
E25DA52F2CFFC91B00AEF16D /* SDWebImageWebPCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52E2CFFC91B00AEF16D /* SDWebImageWebPCoder */; };
|
||||
E25DA5312D003FCB00AEF16D /* SectionedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5302D003FC000AEF16D /* SectionedSettingsView.swift */; };
|
||||
E25DA5342D0041CB00AEF16D /* NavigationBarSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5332D0041CB00AEF16D /* NavigationBarSettingsFile.swift */; };
|
||||
E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */; };
|
||||
E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */; };
|
||||
@ -52,11 +50,10 @@
|
||||
E25DA53D2D0043E600AEF16D /* LocalizedSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA53C2D0043E200AEF16D /* LocalizedSettings.swift */; };
|
||||
E25DA53F2D00441F00AEF16D /* NavigationBarSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA53E2D00441C00AEF16D /* NavigationBarSettings.swift */; };
|
||||
E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5402D00446700AEF16D /* PostSettings.swift */; };
|
||||
E25DA5432D0094A900AEF16D /* SettingsSidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5422D0094A400AEF16D /* SettingsSidebar.swift */; };
|
||||
E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5442D00952D00AEF16D /* SettingsSection.swift */; };
|
||||
E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */; };
|
||||
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */; };
|
||||
E25DA5712D01015400AEF16D /* GenerationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5702D01015400AEF16D /* GenerationSettingsView.swift */; };
|
||||
E25DA5712D01015400AEF16D /* GenerationContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5702D01015400AEF16D /* GenerationContentView.swift */; };
|
||||
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5722D018AA100AEF16D /* FileContentView.swift */; };
|
||||
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5742D018B6100AEF16D /* FileDetailView.swift */; };
|
||||
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5762D018B9500AEF16D /* File+Mock.swift */; };
|
||||
@ -77,7 +74,7 @@
|
||||
E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5982D02401A00AEF16D /* PageGenerator.swift */; };
|
||||
E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA59A2D024A2900AEF16D /* DateItem.swift */; };
|
||||
E29D31202D0320E70051B7F4 /* HikingStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D311F2D0320E20051B7F4 /* HikingStatistics.swift */; };
|
||||
E29D31222D0363FD0051B7F4 /* DownloadButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31212D0363FA0051B7F4 /* DownloadButtons.swift */; };
|
||||
E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31212D0363FA0051B7F4 /* ContentButtons.swift */; };
|
||||
E29D31242D0366860051B7F4 /* TagList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31232D0366820051B7F4 /* TagList.swift */; };
|
||||
E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31252D0370A50051B7F4 /* VideoOption.swift */; };
|
||||
E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31272D0371870051B7F4 /* ContentPageVideo.swift */; };
|
||||
@ -109,6 +106,12 @@
|
||||
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */; };
|
||||
E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31682D0702670051B7F4 /* TextFileContentView.swift */; };
|
||||
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */; };
|
||||
E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */; };
|
||||
E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D316E2D0822720051B7F4 /* SettingsListView.swift */; };
|
||||
E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */; };
|
||||
E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */; };
|
||||
E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317C2D086AAE0051B7F4 /* Int+Random.swift */; };
|
||||
E29D317F2D086F4C0051B7F4 /* Icons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D317E2D086F490051B7F4 /* Icons.swift */; };
|
||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; };
|
||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C072CB17B810060935B /* TagView.swift */; };
|
||||
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0D2CB189D70060935B /* Color+RGB.swift */; };
|
||||
@ -164,7 +167,7 @@
|
||||
E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; };
|
||||
E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = "<group>"; };
|
||||
E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = "<group>"; };
|
||||
E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteGenerator.swift; sourceTree = "<group>"; };
|
||||
E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedWebsiteGenerator.swift; sourceTree = "<group>"; };
|
||||
E21850322CFAFA200090B18B /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
E21850342CFAFA570090B18B /* SettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFile.swift; sourceTree = "<group>"; };
|
||||
E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = "<group>"; };
|
||||
@ -176,7 +179,6 @@
|
||||
E25DA5082CFD964E00AEF16D /* TagContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentView.swift; sourceTree = "<group>"; };
|
||||
E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTagAssignmentView.swift; sourceTree = "<group>"; };
|
||||
E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTagAssignmentView.swift; sourceTree = "<group>"; };
|
||||
E25DA50E2CFDD76B00AEF16D /* ImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContentView.swift; sourceTree = "<group>"; };
|
||||
E25DA5142CFF00B900AEF16D /* Content+Load.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Load.swift"; sourceTree = "<group>"; };
|
||||
E25DA5162CFF00F200AEF16D /* Content+Save.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Save.swift"; sourceTree = "<group>"; };
|
||||
E25DA5182CFF035200AEF16D /* Array+Split.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Split.swift"; sourceTree = "<group>"; };
|
||||
@ -187,7 +189,6 @@
|
||||
E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSSize+Scaling.swift"; sourceTree = "<group>"; };
|
||||
E25DA5262CFF745200AEF16D /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFileType.swift; sourceTree = "<group>"; };
|
||||
E25DA5302D003FC000AEF16D /* SectionedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionedSettingsView.swift; sourceTree = "<group>"; };
|
||||
E25DA5332D0041CB00AEF16D /* NavigationBarSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsFile.swift; sourceTree = "<group>"; };
|
||||
E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsFile.swift; sourceTree = "<group>"; };
|
||||
E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettingsFile.swift; sourceTree = "<group>"; };
|
||||
@ -195,11 +196,10 @@
|
||||
E25DA53C2D0043E200AEF16D /* LocalizedSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedSettings.swift; sourceTree = "<group>"; };
|
||||
E25DA53E2D00441C00AEF16D /* NavigationBarSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettings.swift; sourceTree = "<group>"; };
|
||||
E25DA5402D00446700AEF16D /* PostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettings.swift; sourceTree = "<group>"; };
|
||||
E25DA5422D0094A400AEF16D /* SettingsSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSidebar.swift; sourceTree = "<group>"; };
|
||||
E25DA5442D00952D00AEF16D /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
|
||||
E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsView.swift; sourceTree = "<group>"; };
|
||||
E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedSettingsView.swift; sourceTree = "<group>"; };
|
||||
E25DA5702D01015400AEF16D /* GenerationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationSettingsView.swift; sourceTree = "<group>"; };
|
||||
E25DA5702D01015400AEF16D /* GenerationContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationContentView.swift; sourceTree = "<group>"; };
|
||||
E25DA5722D018AA100AEF16D /* FileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileContentView.swift; sourceTree = "<group>"; };
|
||||
E25DA5742D018B6100AEF16D /* FileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDetailView.swift; sourceTree = "<group>"; };
|
||||
E25DA5762D018B9500AEF16D /* File+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+Mock.swift"; sourceTree = "<group>"; };
|
||||
@ -218,7 +218,7 @@
|
||||
E25DA5982D02401A00AEF16D /* PageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerator.swift; sourceTree = "<group>"; };
|
||||
E25DA59A2D024A2900AEF16D /* DateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateItem.swift; sourceTree = "<group>"; };
|
||||
E29D311F2D0320E20051B7F4 /* HikingStatistics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HikingStatistics.swift; sourceTree = "<group>"; };
|
||||
E29D31212D0363FA0051B7F4 /* DownloadButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadButtons.swift; sourceTree = "<group>"; };
|
||||
E29D31212D0363FA0051B7F4 /* ContentButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentButtons.swift; sourceTree = "<group>"; };
|
||||
E29D31232D0366820051B7F4 /* TagList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagList.swift; sourceTree = "<group>"; };
|
||||
E29D31252D0370A50051B7F4 /* VideoOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOption.swift; sourceTree = "<group>"; };
|
||||
E29D31272D0371870051B7F4 /* ContentPageVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPageVideo.swift; sourceTree = "<group>"; };
|
||||
@ -250,6 +250,12 @@
|
||||
E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationIcon.swift; sourceTree = "<group>"; };
|
||||
E29D31682D0702670051B7F4 /* TextFileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileContentView.swift; sourceTree = "<group>"; };
|
||||
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListPageGenerator.swift; sourceTree = "<group>"; };
|
||||
E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerationResults.swift; sourceTree = "<group>"; };
|
||||
E29D316E2D0822720051B7F4 /* SettingsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsListView.swift; sourceTree = "<group>"; };
|
||||
E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationDetailView.swift; sourceTree = "<group>"; };
|
||||
E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentResultsView.swift; sourceTree = "<group>"; };
|
||||
E29D317C2D086AAE0051B7F4 /* Int+Random.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Int+Random.swift"; sourceTree = "<group>"; };
|
||||
E29D317E2D086F490051B7F4 /* Icons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icons.swift; sourceTree = "<group>"; };
|
||||
E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = "<group>"; };
|
||||
E2A21C072CB17B810060935B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
||||
E2A21C0D2CB189D70060935B /* Color+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+RGB.swift"; sourceTree = "<group>"; };
|
||||
@ -352,14 +358,15 @@
|
||||
E25DA5782D01C56200AEF16D /* Generator */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */,
|
||||
E29D31252D0370A50051B7F4 /* VideoOption.swift */,
|
||||
E25DA5862D01CA8D00AEF16D /* GenerationResultsHandler.swift */,
|
||||
E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */,
|
||||
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
|
||||
E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */,
|
||||
E25DA5982D02401A00AEF16D /* PageGenerator.swift */,
|
||||
E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */,
|
||||
E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */,
|
||||
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
|
||||
E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */,
|
||||
E25DA5982D02401A00AEF16D /* PageGenerator.swift */,
|
||||
E29D316A2D07488B0051B7F4 /* PostListPageGenerator.swift */,
|
||||
E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */,
|
||||
E29D31252D0370A50051B7F4 /* VideoOption.swift */,
|
||||
);
|
||||
path = Generator;
|
||||
sourceTree = "<group>";
|
||||
@ -381,9 +388,10 @@
|
||||
E29D311E2D0320D90051B7F4 /* ContentElements */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D317E2D086F490051B7F4 /* Icons.swift */,
|
||||
E29D31272D0371870051B7F4 /* ContentPageVideo.swift */,
|
||||
E29D31232D0366820051B7F4 /* TagList.swift */,
|
||||
E29D31212D0363FA0051B7F4 /* DownloadButtons.swift */,
|
||||
E29D31212D0363FA0051B7F4 /* ContentButtons.swift */,
|
||||
E29D311F2D0320E20051B7F4 /* HikingStatistics.swift */,
|
||||
);
|
||||
path = ContentElements;
|
||||
@ -404,6 +412,7 @@
|
||||
E2A21C322CB5BCAC0060935B /* Pages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D31782D083DDA0051B7F4 /* PageContentResultsView.swift */,
|
||||
E2A37D242CEBD7A10000979F /* PageListView.swift */,
|
||||
E2A21C312CB5BCAC0060935B /* PageContentView.swift */,
|
||||
E29D312B2D039DB30051B7F4 /* PageDetailView.swift */,
|
||||
@ -417,15 +426,15 @@
|
||||
E2A21C342CB9A3CA0060935B /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D316E2D0822720051B7F4 /* SettingsListView.swift */,
|
||||
E25DA5702D01015400AEF16D /* GenerationContentView.swift */,
|
||||
E29D31702D08234D0051B7F4 /* GenerationDetailView.swift */,
|
||||
E25DA5942D023BCC00AEF16D /* PageSettingsView.swift */,
|
||||
E25DA5442D00952D00AEF16D /* SettingsSection.swift */,
|
||||
E25DA5422D0094A400AEF16D /* SettingsSidebar.swift */,
|
||||
E25DA5302D003FC000AEF16D /* SectionedSettingsView.swift */,
|
||||
E2A21C352CB9A3D70060935B /* FolderSettingsView.swift */,
|
||||
E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */,
|
||||
E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */,
|
||||
E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */,
|
||||
E25DA5702D01015400AEF16D /* GenerationSettingsView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@ -445,14 +454,6 @@
|
||||
path = Generic;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E2A21C492CBB168F0060935B /* Images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E25DA50E2CFDD76B00AEF16D /* ImageContentView.swift */,
|
||||
);
|
||||
path = Images;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E2A21C522CBBF86D0060935B /* Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -547,7 +548,6 @@
|
||||
E2A21C322CB5BCAC0060935B /* Pages */,
|
||||
E2A9CB7F2C7E686C005C89CC /* Tags */,
|
||||
E2A21C522CBBF86D0060935B /* Files */,
|
||||
E2A21C492CBB168F0060935B /* Images */,
|
||||
E2A21C342CB9A3CA0060935B /* Settings */,
|
||||
);
|
||||
path = Views;
|
||||
@ -575,6 +575,7 @@
|
||||
E2B85F552C4BD0AD0047CD0C /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E29D317C2D086AAE0051B7F4 /* Int+Random.swift */,
|
||||
E25DA5262CFF745200AEF16D /* URL+Extensions.swift */,
|
||||
E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */,
|
||||
E25DA5182CFF035200AEF16D /* Array+Split.swift */,
|
||||
@ -727,6 +728,7 @@
|
||||
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
|
||||
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
|
||||
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
|
||||
E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */,
|
||||
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */,
|
||||
E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */,
|
||||
E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */,
|
||||
@ -747,13 +749,14 @@
|
||||
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */,
|
||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */,
|
||||
E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */,
|
||||
E29D317F2D086F4C0051B7F4 /* Icons.swift in Sources */,
|
||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
||||
E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */,
|
||||
E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */,
|
||||
E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */,
|
||||
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
|
||||
E25DA53F2D00441F00AEF16D /* NavigationBarSettings.swift in Sources */,
|
||||
E218502F2CFAF69C0090B18B /* WebsiteGenerator.swift in Sources */,
|
||||
E218502F2CFAF69C0090B18B /* LocalizedWebsiteGenerator.swift in Sources */,
|
||||
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */,
|
||||
E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */,
|
||||
E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */,
|
||||
@ -799,12 +802,12 @@
|
||||
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */,
|
||||
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
|
||||
E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */,
|
||||
E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */,
|
||||
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */,
|
||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
||||
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */,
|
||||
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
|
||||
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
|
||||
E25DA5432D0094A900AEF16D /* SettingsSidebar.swift in Sources */,
|
||||
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
|
||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
||||
E29D31202D0320E70051B7F4 /* HikingStatistics.swift in Sources */,
|
||||
@ -821,9 +824,9 @@
|
||||
E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */,
|
||||
E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */,
|
||||
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
|
||||
E25DA50F2CFDD76B00AEF16D /* ImageContentView.swift in Sources */,
|
||||
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
|
||||
E29D31322D03B5680051B7F4 /* LocalizedPostDetailView.swift in Sources */,
|
||||
E29D31712D08234D0051B7F4 /* GenerationDetailView.swift in Sources */,
|
||||
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */,
|
||||
E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */,
|
||||
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */,
|
||||
@ -842,20 +845,21 @@
|
||||
E25DA5272CFF745700AEF16D /* URL+Extensions.swift in Sources */,
|
||||
E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */,
|
||||
E29D31342D03B5D50051B7F4 /* IconButton.swift in Sources */,
|
||||
E25DA5712D01015400AEF16D /* GenerationSettingsView.swift in Sources */,
|
||||
E25DA5712D01015400AEF16D /* GenerationContentView.swift in Sources */,
|
||||
E25DA5872D01CA9300AEF16D /* GenerationResultsHandler.swift in Sources */,
|
||||
E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */,
|
||||
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */,
|
||||
E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */,
|
||||
E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */,
|
||||
E25DA5312D003FCB00AEF16D /* SectionedSettingsView.swift in Sources */,
|
||||
E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */,
|
||||
E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */,
|
||||
E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */,
|
||||
E29D31222D0363FD0051B7F4 /* DownloadButtons.swift in Sources */,
|
||||
E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */,
|
||||
E2A21C362CB9A3D70060935B /* FolderSettingsView.swift in Sources */,
|
||||
E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */,
|
||||
E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */,
|
||||
E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */,
|
||||
E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */,
|
||||
E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
7
CHDataManagement/Extensions/Int+Random.swift
Normal file
7
CHDataManagement/Extensions/Int+Random.swift
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
extension Int {
|
||||
|
||||
static func random() -> Int {
|
||||
random(in: Int.min...Int.max)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
extension Sequence {
|
||||
extension Collection {
|
||||
|
||||
func sorted<T>(ascending: Bool = true, using conversion: (Element) -> T) -> [Element] where T: Comparable {
|
||||
guard ascending else {
|
||||
|
@ -9,6 +9,16 @@ extension String {
|
||||
.replacingOccurrences(of: ">", with: ">")
|
||||
}
|
||||
|
||||
var removingSurroundingQuotes: String {
|
||||
if hasPrefix("\"") && hasSuffix("\"") {
|
||||
return dropBeforeFirst("\"").dropAfterLast("\"")
|
||||
}
|
||||
if hasPrefix("'") && hasSuffix("'") {
|
||||
return dropBeforeFirst("'").dropAfterLast("'")
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
var nonEmpty: String? {
|
||||
isEmpty ? nil : self
|
||||
}
|
||||
@ -30,7 +40,7 @@ extension String {
|
||||
/**
|
||||
Remove the part after the last occurence of the separator (including the separator itself).
|
||||
|
||||
The string is left unchanges, if it does not contain the separator.
|
||||
The string is left unchanged, if it does not contain the separator.
|
||||
*/
|
||||
func dropAfterLast(_ separator: String) -> String {
|
||||
guard contains(separator) else {
|
||||
|
@ -7,6 +7,8 @@ final class GenerationResultsHandler {
|
||||
/// Generic warnings for pages
|
||||
private var pageWarnings: [(message: String, source: String)] = []
|
||||
|
||||
private var missingPages: [String : [String]] = [:]
|
||||
|
||||
func warning(_ message: String, page: Page) {
|
||||
pageWarnings.append((message, page.id))
|
||||
print("Page: \(page.id): \(message)")
|
||||
@ -15,4 +17,8 @@ final class GenerationResultsHandler {
|
||||
func addRequiredVideoFile(fileId: String) {
|
||||
requiredVideoFiles.insert(fileId)
|
||||
}
|
||||
|
||||
func missing(page: String, linkedBy source: String) {
|
||||
missingPages[page, default: []].append(source)
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,9 @@ final class ImageGenerator {
|
||||
}
|
||||
|
||||
func runJobs(callback: (String) -> Void) -> Bool {
|
||||
guard !jobs.isEmpty else {
|
||||
return true
|
||||
}
|
||||
print("Generating \(jobs.count) images...")
|
||||
for job in jobs {
|
||||
callback("Generating image \(job.version)")
|
||||
@ -80,7 +83,7 @@ final class ImageGenerator {
|
||||
return "\(prefix).\(type.fileExtension)"
|
||||
}
|
||||
|
||||
func generateImageSet(for image: String, maxWidth: CGFloat, maxHeight: CGFloat, altText: String) -> FeedEntryData.Image {
|
||||
func generateImageSet(for image: String, maxWidth: CGFloat, maxHeight: CGFloat) {
|
||||
let type = ImageFileType(fileExtension: image.fileExtension!)!
|
||||
|
||||
let width2x = maxWidth * 2
|
||||
@ -94,12 +97,6 @@ final class ImageGenerator {
|
||||
|
||||
_ = generateVersion(for: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight)
|
||||
_ = generateVersion(for: image, type: type, maximumWidth: width2x, maximumHeight: height2x)
|
||||
|
||||
let path = "/" + relativeImageOutputPath + "/" + image
|
||||
return .init(rawImagePath: path,
|
||||
width: Int(maxWidth),
|
||||
height: Int(maxHeight),
|
||||
altText: altText)
|
||||
}
|
||||
|
||||
func generateVersion(for image: String, type: ImageFileType, maximumWidth: CGFloat, maximumHeight: CGFloat) -> String {
|
||||
@ -133,6 +130,10 @@ final class ImageGenerator {
|
||||
return versions.contains(version)
|
||||
}
|
||||
|
||||
private func exists(imageVersion version: String) -> Bool {
|
||||
inOutputImagesFolder { $0.appendingPathComponent(version).exists }
|
||||
}
|
||||
|
||||
private func hasNowGenerated(version: String, for image: String) {
|
||||
guard var versions = generatedImages[image] else {
|
||||
generatedImages[image] = [version]
|
||||
@ -149,7 +150,8 @@ final class ImageGenerator {
|
||||
// MARK: Image operations
|
||||
|
||||
private func generate(job: ImageJob) -> Bool {
|
||||
if hasPreviouslyGenerated(version: job.version, for: job.image), exists(job.version) {
|
||||
if hasPreviouslyGenerated(version: job.version, for: job.image), exists(job.version),
|
||||
exists(imageVersion: job.version) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -201,7 +203,7 @@ final class ImageGenerator {
|
||||
let url = folder.appendingPathComponent(job.version)
|
||||
if job.type == .avif {
|
||||
let out = url.path()
|
||||
let input = out.replacingOccurrences(of: ".avif", with: ".jpg")
|
||||
let input = url.deletingPathExtension().appendingPathExtension(job.image.fileExtension!).path()
|
||||
print("avifenc -q 70 \(input) \(out)")
|
||||
return true
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
final class WebsiteGenerator {
|
||||
final class LocalizedWebsiteGenerator {
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
@ -38,7 +38,7 @@ final class WebsiteGenerator {
|
||||
self.localizedSettings = content.settings.localized(in: language)
|
||||
self.imageGenerator = ImageGenerator(
|
||||
storage: content.storage,
|
||||
relativeImageOutputPath: "images")
|
||||
relativeImageOutputPath: "images") // TODO: Get from settings
|
||||
}
|
||||
|
||||
func generateWebsite(callback: (String) -> Void) -> Bool {
|
||||
@ -48,6 +48,7 @@ final class WebsiteGenerator {
|
||||
guard createMainPostFeedPages() else {
|
||||
return false
|
||||
}
|
||||
#warning("Generate content pages")
|
||||
guard generateTagPages() else {
|
||||
return false
|
||||
}
|
||||
@ -127,12 +128,17 @@ final class WebsiteGenerator {
|
||||
let pageGenerator = PageGenerator(content: content, imageGenerator: imageGenerator, navigationBarData: navigationBarData)
|
||||
|
||||
let content: String
|
||||
let results: PageGenerationResults
|
||||
do {
|
||||
content = try pageGenerator.generate(page: page, language: language)
|
||||
(content, results) = try pageGenerator.generate(page: page, language: language)
|
||||
} catch {
|
||||
print("Failed to generate page \(page.id) in language \(language): \(error)")
|
||||
return false
|
||||
}
|
||||
guard !content.trimmed.isEmpty else {
|
||||
#warning("Generate page with placeholder content")
|
||||
return true
|
||||
}
|
||||
|
||||
let path = self.content.pageLink(page, language: language) + ".html"
|
||||
guard save(content, to: path) else {
|
||||
@ -142,22 +148,32 @@ final class WebsiteGenerator {
|
||||
guard imageGenerator.runJobs(callback: { _ in }) else {
|
||||
return false
|
||||
}
|
||||
guard copy(requiredVideoFiles: pageGenerator.results.requiredVideoFiles) else {
|
||||
guard copy(requiredFiles: results.files) else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func copy(requiredVideoFiles: Set<String>) -> Bool {
|
||||
print("Copying \(requiredVideoFiles.count) videos...")
|
||||
for fileId in requiredVideoFiles {
|
||||
guard let outputPath = content.pathToFile(fileId) else {
|
||||
return false
|
||||
private func copy(requiredFiles: Set<FileResource>) -> Bool {
|
||||
//print("Copying \(requiredVideoFiles.count) files...")
|
||||
for file in requiredFiles {
|
||||
guard !file.isExternallyStored else {
|
||||
continue
|
||||
}
|
||||
|
||||
let outputPath: String
|
||||
switch file.type {
|
||||
case .video:
|
||||
outputPath = content.pathToVideo(file)
|
||||
case .image:
|
||||
outputPath = content.pathToImage(file)
|
||||
default:
|
||||
outputPath = content.pathToFile(file)
|
||||
}
|
||||
do {
|
||||
try content.storage.copy(file: fileId, to: outputPath)
|
||||
try content.storage.copy(file: file.id, to: outputPath)
|
||||
} catch {
|
||||
print("Failed to copy video file: \(error)")
|
||||
print("Failed to copy file \(file.id): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
@ -8,68 +8,86 @@ final class PageContentParser {
|
||||
|
||||
private let pageLinkMarker = "page:"
|
||||
|
||||
private let largeImageIndicator = "*large*"
|
||||
|
||||
private let swift = SyntaxHighlighter(format: HTMLOutputFormat())
|
||||
|
||||
private let results: GenerationResultsHandler
|
||||
let results = PageGenerationResults()
|
||||
|
||||
private let content: Content
|
||||
|
||||
private let imageGenerator: ImageGenerator
|
||||
|
||||
private let page: Page
|
||||
|
||||
private let language: ContentLanguage
|
||||
|
||||
private var largeImageCount: Int = 0
|
||||
|
||||
init(page: Page, content: Content, language: ContentLanguage, results: GenerationResultsHandler, imageGenerator: ImageGenerator) {
|
||||
self.page = page
|
||||
var largeImageWidth: Int {
|
||||
content.settings.pages.largeImageWidth
|
||||
}
|
||||
|
||||
var thumbnailWidth: Int {
|
||||
content.settings.pages.contentWidth
|
||||
}
|
||||
|
||||
init(content: Content, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.language = language
|
||||
self.results = results
|
||||
self.imageGenerator = imageGenerator
|
||||
}
|
||||
|
||||
func requestImages(_ generator: ImageGenerator) {
|
||||
let thumbnailWidth = CGFloat(thumbnailWidth)
|
||||
let largeImageWidth = CGFloat(largeImageWidth)
|
||||
|
||||
for image in results.files {
|
||||
guard case .image = image.type else {
|
||||
continue
|
||||
}
|
||||
generator.generateImageSet(
|
||||
for: image.id,
|
||||
maxWidth: thumbnailWidth, maxHeight: thumbnailWidth)
|
||||
|
||||
generator.generateImageSet(
|
||||
for: image.id,
|
||||
maxWidth: largeImageWidth, maxHeight: largeImageWidth)
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
results.reset()
|
||||
largeImageCount = 0
|
||||
}
|
||||
|
||||
func generatePage(from content: String) -> String {
|
||||
|
||||
let imageModifier = Modifier(target: .images) { html, markdown in
|
||||
self.processMarkdownImage(markdown: markdown, html: html)
|
||||
}
|
||||
let codeModifier = Modifier(target: .codeBlocks) { html, markdown in
|
||||
if markdown.starts(with: "```swift") {
|
||||
let code = markdown.between("```swift", and: "```").trimmed
|
||||
return "<pre><code>" + self.swift.highlight(code) + "</pre></code>"
|
||||
}
|
||||
return html
|
||||
}
|
||||
let linkModifier = Modifier(target: .links) { html, markdown in
|
||||
self.handleLink(html: html, markdown: markdown)
|
||||
}
|
||||
let htmlModifier = Modifier(target: .html) { html, markdown in
|
||||
self.handleHTML(html: html, markdown: markdown)
|
||||
}
|
||||
let headlinesModifier = Modifier(target: .headings) { html, markdown in
|
||||
self.handleHeadlines(html: html, markdown: markdown)
|
||||
}
|
||||
|
||||
let parser = MarkdownParser(modifiers: [imageModifier, codeModifier, linkModifier, htmlModifier, headlinesModifier])
|
||||
reset()
|
||||
let parser = MarkdownParser(modifiers: [
|
||||
Modifier(target: .images, closure: processMarkdownImage),
|
||||
Modifier(target: .codeBlocks, closure: handleCode),
|
||||
Modifier(target: .links, closure: handleLink),
|
||||
Modifier(target: .html, closure: handleHTML),
|
||||
Modifier(target: .headings, closure: handleHeadlines)
|
||||
])
|
||||
return parser.html(from: content)
|
||||
}
|
||||
|
||||
private func handleCode(html: String, markdown: Substring) -> String {
|
||||
guard markdown.starts(with: "```swift") else {
|
||||
return html // Just use normal code highlighting
|
||||
}
|
||||
// Highlight swift code using Splash
|
||||
let code = markdown.between("```swift", and: "```").trimmed
|
||||
return "<pre><code>" + swift.highlight(code) + "</pre></code>"
|
||||
}
|
||||
|
||||
private func handleLink(html: String, markdown: Substring) -> String {
|
||||
let file = markdown.between("(", and: ")")
|
||||
if file.hasPrefix(pageLinkMarker) {
|
||||
// Retain links pointing to elements within a page
|
||||
let textToChange = file.dropAfterFirst("#")
|
||||
let pageId = textToChange.replacingOccurrences(of: pageLinkMarker, with: "")
|
||||
guard let pagePath = content.pageLink(pageId: pageId, language: language) else {
|
||||
guard let page = content.page(pageId) else {
|
||||
results.missingPages.insert(pageId)
|
||||
// Remove link since the page can't be found
|
||||
return markdown.between("[", and: "]")
|
||||
}
|
||||
// Adjust file path to get the page url
|
||||
// TODO: Calculate relative links to make pages more portable
|
||||
results.linkedPages.insert(page)
|
||||
let pagePath = content.pageLink(page, language: language)
|
||||
return html.replacingOccurrences(of: textToChange, with: pagePath)
|
||||
}
|
||||
|
||||
@ -92,6 +110,13 @@ final class PageContentParser {
|
||||
return html
|
||||
}
|
||||
|
||||
/**
|
||||
Modify headlines by extracting an id from the headline and adding it into the html element
|
||||
|
||||
Format: ##<title>#<id>
|
||||
|
||||
The id is created by lowercasing the string, removing all special characters, and replacing spaces with scores
|
||||
*/
|
||||
private func handleHeadlines(html: String, markdown: Substring) -> String {
|
||||
let id = markdown
|
||||
.last(after: "#")
|
||||
@ -101,18 +126,18 @@ final class PageContentParser {
|
||||
.components(separatedBy: " ")
|
||||
.filter { $0 != "" }
|
||||
.joined(separator: "-")
|
||||
let parts = html.components(separatedBy: ">")
|
||||
let parts = html.components(separatedBy: ">")
|
||||
return parts[0] + " id=\"\(id)\">" + parts.dropFirst().joined(separator: ">")
|
||||
}
|
||||
|
||||
private func processMarkdownImage(markdown: Substring, html: String) -> String {
|
||||
private func processMarkdownImage(html: String, markdown: Substring) -> String {
|
||||
// First, check the content type, then parse the remaining arguments
|
||||
// Notation:
|
||||
// <abc?> -> Optional argument
|
||||
// <abc...> -> Repeated argument (0 or more)
|
||||
// 
|
||||
// 
|
||||
// 
|
||||
// 
|
||||
@ -120,12 +145,11 @@ final class PageContentParser {
|
||||
// 
|
||||
// 
|
||||
guard let argumentList = markdown.between(first: "](", andLast: ")").removingPercentEncoding else {
|
||||
results.warning("Invalid percent encoding for markdown image", page: page)
|
||||
return ""
|
||||
}
|
||||
|
||||
let argumentList = markdown.between(first: "](", andLast: ")").removingPercentEncoding ?? markdown.between(first: "](", andLast: ")")
|
||||
let arguments = argumentList.components(separatedBy: ";")
|
||||
|
||||
|
||||
let rawCommand = markdown.between("![", and: "]").trimmed
|
||||
guard rawCommand != "" else {
|
||||
return handleImage(arguments)
|
||||
@ -134,7 +158,7 @@ final class PageContentParser {
|
||||
guard let convertedCommand = rawCommand.removingPercentEncoding,
|
||||
let command = ShorthandMarkdownKey(rawValue: convertedCommand) else {
|
||||
// Treat unknown commands as normal links
|
||||
print("Unknown markdown command: \(rawCommand)")
|
||||
results.warnings.append("Unknown markdown command '\(rawCommand)'")
|
||||
return html
|
||||
}
|
||||
|
||||
@ -147,12 +171,9 @@ final class PageContentParser {
|
||||
return handleDownloadButtons(arguments)
|
||||
case .video:
|
||||
return handleVideo(arguments)
|
||||
default:
|
||||
print("Unhandled markdown command: \(command)")
|
||||
return ""
|
||||
/*
|
||||
case .externalLink:
|
||||
return handleExternalButtons(content: content)
|
||||
return handleExternalButtons(arguments)
|
||||
/*
|
||||
case .includedHtml:
|
||||
return handleExternalHTML(file: content)
|
||||
case .box:
|
||||
@ -162,35 +183,42 @@ final class PageContentParser {
|
||||
case .model:
|
||||
return handle3dModel(content: content)
|
||||
*/
|
||||
default:
|
||||
results.warnings.append("Unhandled command '\(command.rawValue)'")
|
||||
return ""
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func handleImage(_ arguments: [String]) -> String {
|
||||
// [image](<imageId>;<caption?>]
|
||||
guard (1...2).contains(arguments.count) else {
|
||||
results.warning("Invalid image arguments: \(arguments)", page: page)
|
||||
results.invalidCommandArguments.append((.image , arguments))
|
||||
return ""
|
||||
}
|
||||
let imageId = arguments[0]
|
||||
|
||||
guard let image = content.image(imageId) else {
|
||||
results.warning("Missing image \(imageId)", page: page)
|
||||
results.missingFiles.insert(imageId)
|
||||
return ""
|
||||
}
|
||||
results.files.insert(image)
|
||||
|
||||
let caption = arguments.count == 2 ? arguments[1] : nil
|
||||
let altText = image.getDescription(for: language)
|
||||
|
||||
let thumbnailWidth = CGFloat(content.settings.pages.contentWidth)
|
||||
let thumbnail = imageGenerator.generateImageSet(
|
||||
for: imageId,
|
||||
maxWidth: thumbnailWidth, maxHeight: thumbnailWidth,
|
||||
let path = content.pathToImage(image)
|
||||
|
||||
let thumbnail = FeedEntryData.Image(
|
||||
rawImagePath: path,
|
||||
width: thumbnailWidth,
|
||||
height: thumbnailWidth,
|
||||
altText: altText)
|
||||
|
||||
let largeImageWidth = CGFloat(1200) // TODO: Move to settings
|
||||
|
||||
let largeImage = imageGenerator.generateImageSet(
|
||||
for: imageId,
|
||||
maxWidth: largeImageWidth, maxHeight: largeImageWidth,
|
||||
let largeImage = FeedEntryData.Image(
|
||||
rawImagePath: path,
|
||||
width: largeImageWidth,
|
||||
height: largeImageWidth,
|
||||
altText: altText)
|
||||
|
||||
return PageImage(
|
||||
@ -202,7 +230,7 @@ final class PageContentParser {
|
||||
|
||||
private func handleHikingStatistics(_ arguments: [String]) -> String {
|
||||
guard (1...5).contains(arguments.count) else {
|
||||
results.warning("Invalid hiking statistic arguments: \(arguments)", page: page)
|
||||
results.invalidCommandArguments.append((.hikingStatistics, arguments))
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -222,10 +250,11 @@ final class PageContentParser {
|
||||
}
|
||||
|
||||
private func handleDownloadButtons(_ arguments: [String]) -> String {
|
||||
let buttons: [DownloadButtons.Item] = arguments.compactMap { button in
|
||||
// 
|
||||
let buttons: [ContentButtons.Item] = arguments.compactMap { button in
|
||||
let parts = button.components(separatedBy: ",")
|
||||
guard (2...3).contains(parts.count) else {
|
||||
results.warning("Invalid download definition with \(parts)", page: page)
|
||||
results.invalidCommandArguments.append((.downloadButtons, parts))
|
||||
return nil
|
||||
}
|
||||
let file = parts[0].trimmed
|
||||
@ -234,44 +263,36 @@ final class PageContentParser {
|
||||
|
||||
// Ensure that file is available
|
||||
guard let filePath = content.pathToFile(file) else {
|
||||
results.warning("Missing download file \(file)", page: page)
|
||||
results.missingFiles.insert(file)
|
||||
return nil
|
||||
}
|
||||
|
||||
return DownloadButtons.Item(filePath: filePath, text: title, downloadFileName: downloadName)
|
||||
return ContentButtons.Item(icon: .download, filePath: filePath, text: title, downloadFileName: downloadName)
|
||||
}
|
||||
return DownloadButtons(items: buttons).content
|
||||
return ContentButtons(items: buttons).content
|
||||
}
|
||||
|
||||
private func handleVideo(_ arguments: [String]) -> String {
|
||||
// )
|
||||
return ""
|
||||
}
|
||||
let fileId = arguments[0].trimmed
|
||||
|
||||
let options: [VideoOption] = arguments.dropFirst().compactMap { optionText in
|
||||
guard let optionText = optionText.trimmed.nonEmpty else {
|
||||
return nil
|
||||
}
|
||||
guard let option = VideoOption(rawValue: optionText) else {
|
||||
results.warning("Unknown video option \(optionText)", page: page)
|
||||
return nil
|
||||
}
|
||||
return option
|
||||
}
|
||||
let options = arguments.dropFirst().compactMap(convertVideoOption)
|
||||
|
||||
guard let filePath = content.pathToFile(fileId),
|
||||
let file = content.file(id: fileId) else {
|
||||
results.warning("Missing video file \(fileId)", page: page)
|
||||
guard let file = content.file(id: fileId) else {
|
||||
results.missingFiles.insert(fileId)
|
||||
return ""
|
||||
}
|
||||
results.files.insert(file)
|
||||
|
||||
guard let videoType = file.type.videoType?.htmlType else {
|
||||
results.warning("Unknown video file type for \(fileId)", page: page)
|
||||
results.warnings.append("Unknown video file type for \(fileId)")
|
||||
return ""
|
||||
}
|
||||
|
||||
results.addRequiredVideoFile(fileId: fileId)
|
||||
|
||||
let filePath = content.pathToFile(file)
|
||||
return ContentPageVideo(
|
||||
filePath: filePath,
|
||||
videoType: videoType,
|
||||
@ -279,6 +300,40 @@ final class PageContentParser {
|
||||
.content
|
||||
}
|
||||
|
||||
private func convertVideoOption(_ videoOption: String) -> VideoOption? {
|
||||
guard let optionText = videoOption.trimmed.nonEmpty else {
|
||||
return nil
|
||||
}
|
||||
guard let option = VideoOption(rawValue: optionText) else {
|
||||
results.invalidCommandArguments.append((.video, [optionText]))
|
||||
return nil
|
||||
}
|
||||
if case let .poster(imageId) = option {
|
||||
if let image = content.image(imageId) {
|
||||
results.files.insert(image)
|
||||
let link = content.pathToImage(image)
|
||||
let width = 2*thumbnailWidth
|
||||
let fullLink = WebsiteImage.imagePath(source: link, width: width, height: width)
|
||||
return .poster(image: fullLink)
|
||||
} else {
|
||||
results.missingFiles.insert(imageId)
|
||||
return nil // Image file not present, so skip the option
|
||||
}
|
||||
}
|
||||
if case let .src(videoId) = option {
|
||||
if let video = content.video(videoId) {
|
||||
results.files.insert(video)
|
||||
let link = content.pathToVideo(video)
|
||||
// TODO: Set correct video path?
|
||||
return .src(link)
|
||||
} else {
|
||||
results.missingFiles.insert(videoId)
|
||||
return nil // Video file not present, so skip the option
|
||||
}
|
||||
}
|
||||
return option
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
private func handleGif(file: String, altText: String) -> String {
|
||||
@ -334,27 +389,34 @@ final class PageContentParser {
|
||||
results.warning("Unhandled file \(file) with extension \(fileExtension)", source: page.path)
|
||||
return ""
|
||||
}
|
||||
|
||||
private func handleExternalButtons(content: String) -> String {
|
||||
let buttons = content
|
||||
.components(separatedBy: ";")
|
||||
.compactMap { button -> (url: String, text: String)? in
|
||||
let parts = button.components(separatedBy: ",")
|
||||
guard parts.count == 2 else {
|
||||
results.warning("Invalid external link definition", page: page)
|
||||
return nil
|
||||
}
|
||||
guard let url = parts[0].trimmed.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
||||
results.warning("Invalid external link \(parts[0].trimmed)", source: page.path)
|
||||
return nil
|
||||
}
|
||||
let title = parts[1].trimmed
|
||||
|
||||
return (url, title)
|
||||
*/
|
||||
private func handleExternalButtons(_ arguments: [String]) -> String {
|
||||
// )
|
||||
return ""
|
||||
}
|
||||
let buttons: [ContentButtons.Item] = arguments.compactMap { button in
|
||||
let parts = button.components(separatedBy: ",")
|
||||
guard parts.count == 2 else {
|
||||
results.invalidCommandArguments.append((.externalLink, parts))
|
||||
return nil
|
||||
}
|
||||
return factory.html.externalButtons(buttons)
|
||||
}
|
||||
let rawUrl = parts[0].trimmed
|
||||
guard let url = rawUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
||||
results.invalidCommandArguments.append((.externalLink, parts))
|
||||
return nil
|
||||
}
|
||||
let title = parts[1].trimmed
|
||||
|
||||
return .init(
|
||||
icon: .externalLink,
|
||||
filePath: url,
|
||||
text: title)
|
||||
}
|
||||
return ContentButtons(items: buttons).content
|
||||
}
|
||||
/*
|
||||
private func handleExternalHTML(file: String) -> String {
|
||||
let path = page.pathRelativeToRootForContainedInputFile(file)
|
||||
return results.getContentOfRequiredFile(at: path, source: page.path) ?? ""
|
||||
|
32
CHDataManagement/Generator/PageGenerationResults.swift
Normal file
32
CHDataManagement/Generator/PageGenerationResults.swift
Normal 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 = []
|
||||
}
|
||||
}
|
@ -6,26 +6,23 @@ final class PageGenerator {
|
||||
|
||||
private let navigationBarData: NavigationBarData
|
||||
|
||||
let results = GenerationResultsHandler()
|
||||
|
||||
init(content: Content, imageGenerator: ImageGenerator, navigationBarData: NavigationBarData) {
|
||||
self.content = content
|
||||
self.imageGenerator = imageGenerator
|
||||
self.navigationBarData = navigationBarData
|
||||
}
|
||||
|
||||
func generate(page: Page, language: ContentLanguage) throws -> String {
|
||||
func generate(page: Page, language: ContentLanguage) throws -> (page: String, results: PageGenerationResults) {
|
||||
let contentGenerator = PageContentParser(
|
||||
page: page,
|
||||
content: content,
|
||||
language: language,
|
||||
results: results,
|
||||
imageGenerator: imageGenerator)
|
||||
language: language)
|
||||
|
||||
let rawPageContent = try content.storage.pageContent(for: page.id, language: language)
|
||||
|
||||
let pageContent = contentGenerator.generatePage(from: rawPageContent)
|
||||
|
||||
contentGenerator.requestImages(imageGenerator)
|
||||
|
||||
let localized = page.localized(in: language)
|
||||
|
||||
let tags: [FeedEntryData.Tag] = page.tags.map { tag in
|
||||
@ -33,7 +30,7 @@ final class PageGenerator {
|
||||
url: content.tagLink(tag, language: language))
|
||||
}
|
||||
|
||||
return ContentPage(
|
||||
let fullPage = ContentPage(
|
||||
language: language,
|
||||
dateString: page.dateText(in: language),
|
||||
title: localized.title,
|
||||
@ -43,5 +40,7 @@ final class PageGenerator {
|
||||
navigationBarData: navigationBarData,
|
||||
pageContent: pageContent)
|
||||
.content
|
||||
|
||||
return (fullPage, contentGenerator.results)
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ final class PostListPageGenerator {
|
||||
text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings
|
||||
}
|
||||
|
||||
let tags: [FeedEntryData.Tag] = post.tags.map { tag in
|
||||
let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in
|
||||
.init(name: tag.localized(in: language).name,
|
||||
url: content.tagLink(tag, language: language))
|
||||
}
|
||||
@ -102,7 +102,11 @@ final class PostListPageGenerator {
|
||||
imageGenerator.generateImageSet(
|
||||
for: image.id,
|
||||
maxWidth: mainContentMaximumWidth,
|
||||
maxHeight: mainContentMaximumWidth,
|
||||
maxHeight: mainContentMaximumWidth)
|
||||
return .init(
|
||||
rawImagePath: content.pathToImage(image),
|
||||
width: Int(mainContentMaximumWidth),
|
||||
height: Int(mainContentMaximumWidth),
|
||||
altText: image.getDescription(for: language))
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ enum ShorthandMarkdownKey: String {
|
||||
case hikingStatistics = "hiking-stats"
|
||||
|
||||
/// A video
|
||||
/// Format: `.
|
||||
case controls
|
||||
|
||||
/// Specifies that the video will start playing as soon as it is ready
|
||||
case autoplay
|
||||
case muted
|
||||
|
||||
/// Specifies that the video will start over again, every time it is finished
|
||||
case loop
|
||||
|
||||
/// Specifies that the audio output of the video should be muted
|
||||
case muted
|
||||
|
||||
/// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays
|
||||
case playsinline
|
||||
case poster
|
||||
case preload
|
||||
|
||||
/// Sets the height of the video player
|
||||
case height(Int)
|
||||
|
||||
/// Sets the width of the video player
|
||||
case width(Int)
|
||||
|
||||
/// Specifies if and how the author thinks the video should be loaded when the page loads
|
||||
case preload(VideoPreloadOption)
|
||||
|
||||
/// Specifies an image to be shown while the video is downloading, or until the user hits the play button
|
||||
case poster(image: String)
|
||||
|
||||
/// Specifies the URL of the video file
|
||||
case src(String)
|
||||
|
||||
init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "controls":
|
||||
self = .controls
|
||||
return
|
||||
case "autoplay":
|
||||
self = .autoplay
|
||||
return
|
||||
case "muted":
|
||||
self = .muted
|
||||
return
|
||||
case "loop":
|
||||
self = .loop
|
||||
return
|
||||
case "playsinline":
|
||||
self = .playsinline
|
||||
return
|
||||
default: break
|
||||
}
|
||||
|
||||
let parts = rawValue.components(separatedBy: "=")
|
||||
guard parts.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let optionName = parts[0]
|
||||
let value = parts[1].removingSurroundingQuotes
|
||||
|
||||
switch optionName {
|
||||
case "height":
|
||||
guard let height = Int(value) else {
|
||||
return nil
|
||||
}
|
||||
self = .height(height)
|
||||
case "width":
|
||||
guard let width = Int(value) else {
|
||||
return nil
|
||||
}
|
||||
self = .width(width)
|
||||
case "preload":
|
||||
guard let preloadOption = VideoPreloadOption(rawValue: value) else {
|
||||
return nil
|
||||
}
|
||||
self = .preload(preloadOption)
|
||||
case "poster":
|
||||
self = .poster(image: value)
|
||||
case "src":
|
||||
self = .src(value)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var rawValue: String {
|
||||
switch self {
|
||||
case .controls: return "controls"
|
||||
case .autoplay: return "autoplay"
|
||||
case .muted: return "muted"
|
||||
case .loop: return "loop"
|
||||
case .playsinline: return "playsinline"
|
||||
case .height(let height): return "height='\(height)'"
|
||||
case .width(let width): return "width='\(width)'"
|
||||
case .preload(let option): return "preload='\(option)'"
|
||||
case .poster(let image): return "poster='\(image)'"
|
||||
case .src(let url): return "src='\(url)'"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The `preload` attribute specifies if and how the author thinks that the video should be loaded when the page loads.
|
||||
|
||||
The `preload` attribute allows the author to provide a hint to the browser about what he/she thinks will lead to the best user experience.
|
||||
This attribute may be ignored in some instances.
|
||||
|
||||
Note: The `preload` attribute is ignored if `autoplay` is present.
|
||||
*/
|
||||
enum VideoPreloadOption: String {
|
||||
|
||||
/// The author thinks that the browser should load the entire video when the page loads
|
||||
case auto
|
||||
|
||||
/// The author thinks that the browser should load only metadata when the page loads
|
||||
case metadata
|
||||
|
||||
/// The author thinks that the browser should NOT load the video when the page loads
|
||||
case none
|
||||
}
|
||||
|
@ -1,18 +1,9 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
/*
|
||||
Page: One page -> One post with overview
|
||||
Post: One post -> No page
|
||||
Page update: One page -> Multiple posts
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
#warning("Consolidate images and files")
|
||||
#warning("Allow selection of pages as navigation bar items")
|
||||
#warning("Transfer images of posts to other language")
|
||||
#warning("Show tag selection view for pages")
|
||||
|
||||
@main
|
||||
struct MainView: App {
|
||||
@ -46,7 +37,7 @@ struct MainView: App {
|
||||
private var selectedFile: FileResource?
|
||||
|
||||
@State
|
||||
private var selectedSection: SettingsSection? = .generation
|
||||
private var selectedSection: SettingsSection = .folders
|
||||
|
||||
@State
|
||||
private var showAddSheet = false
|
||||
@ -63,9 +54,7 @@ struct MainView: App {
|
||||
case .files:
|
||||
FileListView(selectedFile: $selectedFile)
|
||||
case .generation:
|
||||
List(SettingsSection.allCases, selection: $selectedSection) { item in
|
||||
Label(item.rawValue, systemSymbol: item.icon).tag(item)
|
||||
}
|
||||
SettingsListView(selectedSection: $selectedSection)
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,7 +70,7 @@ struct MainView: App {
|
||||
case .files:
|
||||
SelectedContentView<FileContentView>(selected: $selectedFile)
|
||||
case .generation:
|
||||
GenerationDetailView(section: selectedSection)
|
||||
GenerationContentView()
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,7 +86,7 @@ struct MainView: App {
|
||||
case .files:
|
||||
SelectedDetailView<FileDetailView>(selected: $selectedFile)
|
||||
case .generation:
|
||||
Text("")
|
||||
GenerationDetailView(section: selectedSection)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,10 @@ extension Content {
|
||||
return prefix + page.localized(in: language).urlString
|
||||
}
|
||||
|
||||
func page(_ pageId: String) -> Page? {
|
||||
pages.first { $0.id == pageId }
|
||||
}
|
||||
|
||||
func pageLink(pageId: String, language: ContentLanguage) -> String? {
|
||||
guard let page = pages.first(where: { $0.id == pageId }) else {
|
||||
// TODO: Note missing link
|
||||
@ -31,18 +35,42 @@ extension Content {
|
||||
guard let file = file(id: fileId) else {
|
||||
return nil
|
||||
}
|
||||
switch file.type {
|
||||
case .image: return pathToImage(file)
|
||||
case .video: return pathToVideo(file)
|
||||
default: return pathToFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
func pathToFile(_ file: FileResource) -> String {
|
||||
#warning("Add files path to settings")
|
||||
return "/files/\(file.id)"
|
||||
}
|
||||
|
||||
func image(_ imageId: String) -> FileResource? {
|
||||
files.first { $0.id == imageId }
|
||||
func pathToImage(_ image: FileResource) -> String {
|
||||
return "/images/\(image.id)"
|
||||
}
|
||||
|
||||
func imageLink(imageId: String) {
|
||||
|
||||
func image(_ imageId: String) -> FileResource? {
|
||||
files.first { $0.id == imageId && $0.type.isImage }
|
||||
}
|
||||
|
||||
func video(_ videoId: String) -> FileResource? {
|
||||
files.first { $0.id == videoId && $0.type.isVideo }
|
||||
}
|
||||
|
||||
func pathToVideo(_ videoId: String) -> String? {
|
||||
guard let video = video(videoId) else {
|
||||
return nil
|
||||
}
|
||||
return pathToVideo(video)
|
||||
}
|
||||
|
||||
func pathToVideo(_ video: FileResource) -> String {
|
||||
"/videos/\(video.id)"
|
||||
}
|
||||
|
||||
|
||||
func file(id: String) -> FileResource? {
|
||||
files.first { $0.id == id }
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ extension Content {
|
||||
|
||||
private func convert(_ page: LocalizedPageFile, images: [String : FileResource]) -> LocalizedPage {
|
||||
LocalizedPage(
|
||||
content: self,
|
||||
urlString: page.url,
|
||||
title: page.title,
|
||||
lastModified: page.lastModifiedDate,
|
||||
@ -57,12 +58,24 @@ extension Content {
|
||||
let pagesData = try storage.loadAllPages()
|
||||
let postsData = try storage.loadAllPosts()
|
||||
let fileList = try storage.loadAllFiles()
|
||||
let externalFiles = try storage.loadExternalFileList()
|
||||
|
||||
let files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in
|
||||
var files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in
|
||||
let descriptions = imageDescriptions[fileId]
|
||||
files[fileId] = FileResource(
|
||||
content: self,
|
||||
id: fileId,
|
||||
isExternallyStored: false,
|
||||
en: descriptions?.english ?? "",
|
||||
de: descriptions?.german ?? "")
|
||||
}
|
||||
|
||||
for fileId in externalFiles {
|
||||
let descriptions = imageDescriptions[fileId]
|
||||
files[fileId] = FileResource(
|
||||
content: self,
|
||||
id: fileId,
|
||||
isExternallyStored: true,
|
||||
en: descriptions?.english ?? "",
|
||||
de: descriptions?.german ?? "")
|
||||
}
|
||||
@ -115,7 +128,8 @@ extension Content {
|
||||
|
||||
let pages = PageSettings(
|
||||
pageUrlPrefix: settings.pages.pageUrlPrefix,
|
||||
contentWidth: settings.pages.contentWidth)
|
||||
contentWidth: settings.pages.contentWidth,
|
||||
largeImageWidth: settings.pages.largeImageWidth)
|
||||
|
||||
return Settings(
|
||||
outputDirectoryPath: settings.outputDirectoryPath,
|
||||
|
@ -29,6 +29,9 @@ extension Content {
|
||||
|
||||
try storage.save(fileDescriptions: fileDescriptions)
|
||||
|
||||
let externalFileList = files.filter { $0.isExternallyStored }.map { $0.id }
|
||||
try storage.save(externalFileList: externalFileList)
|
||||
|
||||
do {
|
||||
try storage.deletePostFiles(notIn: posts.map { $0.id })
|
||||
try storage.deletePageFiles(notIn: pages.map { $0.id })
|
||||
@ -116,7 +119,7 @@ private extension LocalizedTag {
|
||||
name: name,
|
||||
subtitle: subtitle,
|
||||
description: description,
|
||||
thumbnail: thumbnail?.id,
|
||||
thumbnail: linkPreviewImage?.id,
|
||||
originalURL: originalUrl)
|
||||
}
|
||||
}
|
||||
@ -154,7 +157,8 @@ private extension PageSettings {
|
||||
|
||||
var file: PageSettingsFile {
|
||||
.init(pageUrlPrefix: pageUrlPrefix,
|
||||
contentWidth: contentWidth)
|
||||
contentWidth: contentWidth,
|
||||
largeImageWidth: largeImageWidth)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,9 @@ final class FileResource: ObservableObject {
|
||||
@Published
|
||||
var id: String
|
||||
|
||||
@Published
|
||||
var isExternallyStored: Bool
|
||||
|
||||
@Published
|
||||
var germanDescription: String
|
||||
|
||||
@ -20,12 +23,13 @@ final class FileResource: ObservableObject {
|
||||
@Published
|
||||
var size: CGSize = .zero
|
||||
|
||||
init(content: Content, id: String, en: String, de: String) {
|
||||
init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) {
|
||||
self.content = content
|
||||
self.id = id
|
||||
self.type = FileType(fileExtension: id.fileExtension)
|
||||
self.englishDescription = en
|
||||
self.germanDescription = de
|
||||
self.isExternallyStored = isExternallyStored
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,6 +41,7 @@ final class FileResource: ObservableObject {
|
||||
self.id = resourceImage
|
||||
self.englishDescription = "A test image included in the bundle"
|
||||
self.germanDescription = "Ein Testbild aus dem Bundle"
|
||||
self.isExternallyStored = true
|
||||
}
|
||||
|
||||
func getDescription(for language: ContentLanguage) -> String {
|
||||
|
@ -8,6 +8,8 @@ import SwiftUI
|
||||
*/
|
||||
final class LocalizedPage: ObservableObject {
|
||||
|
||||
unowned let content: Content
|
||||
|
||||
/**
|
||||
The string to use when creating the url for the page.
|
||||
|
||||
@ -64,7 +66,8 @@ final class LocalizedPage: ObservableObject {
|
||||
@Published
|
||||
var linkPreviewDescription: String?
|
||||
|
||||
init(urlString: String,
|
||||
init(content: Content,
|
||||
urlString: String,
|
||||
title: String,
|
||||
lastModified: Date? = nil,
|
||||
originalUrl: String? = nil,
|
||||
@ -74,6 +77,7 @@ final class LocalizedPage: ObservableObject {
|
||||
linkPreviewImage: FileResource? = nil,
|
||||
linkPreviewTitle: String? = nil,
|
||||
linkPreviewDescription: String? = nil) {
|
||||
self.content = content
|
||||
self.urlString = urlString
|
||||
self.title = title
|
||||
self.lastModified = lastModified
|
||||
|
@ -17,7 +17,7 @@ final class LocalizedTag: ObservableObject {
|
||||
|
||||
/// The image id of the thumbnail
|
||||
@Published
|
||||
var thumbnail: FileResource?
|
||||
var linkPreviewImage: FileResource?
|
||||
|
||||
/// The original url in the previous site layout
|
||||
let originalUrl: String?
|
||||
@ -32,7 +32,7 @@ final class LocalizedTag: ObservableObject {
|
||||
self.name = name
|
||||
self.subtitle = subtitle
|
||||
self.description = description
|
||||
self.thumbnail = thumbnail
|
||||
self.linkPreviewImage = thumbnail
|
||||
self.originalUrl = originalUrl
|
||||
}
|
||||
}
|
||||
|
@ -98,6 +98,13 @@ extension Page: Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: Comparable {
|
||||
|
||||
static func < (lhs: Page, rhs: Page) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: DateItem {
|
||||
|
||||
}
|
||||
|
@ -10,8 +10,12 @@ final class PageSettings: ObservableObject {
|
||||
@Published
|
||||
var contentWidth: Int
|
||||
|
||||
init(pageUrlPrefix: String, contentWidth: Int) {
|
||||
@Published
|
||||
var largeImageWidth: Int
|
||||
|
||||
init(pageUrlPrefix: String, contentWidth: Int, largeImageWidth: Int) {
|
||||
self.pageUrlPrefix = pageUrlPrefix
|
||||
self.contentWidth = contentWidth
|
||||
self.largeImageWidth = largeImageWidth
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,22 @@
|
||||
|
||||
struct DownloadButtons {
|
||||
struct ContentButtons {
|
||||
|
||||
struct Item {
|
||||
|
||||
let icon: PageIcon
|
||||
|
||||
let filePath: String
|
||||
|
||||
let text: String
|
||||
|
||||
let downloadFileName: String?
|
||||
|
||||
init(icon: PageIcon, filePath: String, text: String, downloadFileName: String? = nil) {
|
||||
self.icon = icon
|
||||
self.filePath = filePath
|
||||
self.text = text
|
||||
self.downloadFileName = downloadFileName
|
||||
}
|
||||
}
|
||||
|
||||
let items: [Item]
|
||||
@ -17,7 +26,7 @@ struct DownloadButtons {
|
||||
}
|
||||
|
||||
var content: String {
|
||||
var result = "<p style='display: flex'>"
|
||||
var result = "<p class='tags tag-buttons'>"
|
||||
for item in items {
|
||||
addButton(of: item, to: &result)
|
||||
}
|
||||
@ -27,8 +36,8 @@ struct DownloadButtons {
|
||||
|
||||
private func addButton(of item: Item, to result: inout String) {
|
||||
let downloadText = item.downloadFileName.map { " download='\($0)'" } ?? ""
|
||||
result += "<a class='download-button' href='\(item.filePath)'\(downloadText)>"
|
||||
result += "\(item.text)<span class='icon icon-download'></span>"
|
||||
result += "<a class='tag' href='\(item.filePath)'\(downloadText)>"
|
||||
result += "<svg><use href='#\(item.icon.name)'></use></svg>\(item.text)"
|
||||
result += "</a>"
|
||||
}
|
||||
}
|
107
CHDataManagement/Page Elements/ContentElements/Icons.swift
Normal file
107
CHDataManagement/Page Elements/ContentElements/Icons.swift
Normal 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>
|
||||
"""
|
||||
}
|
@ -1,6 +1,19 @@
|
||||
|
||||
struct WebsiteImage {
|
||||
|
||||
static func imagePath(prefix: String, width: Int, height: Int) -> String {
|
||||
"\(prefix)@\(width)x\(height)"
|
||||
}
|
||||
|
||||
static func imagePath(prefix: String, extension fileExtension: String, width: Int, height: Int) -> String {
|
||||
"\(prefix)@\(width)x\(height).\(fileExtension)"
|
||||
}
|
||||
|
||||
static func imagePath(source: String, width: Int, height: Int) -> String {
|
||||
let (prefix, ext) = source.fileNameAndExtension
|
||||
return imagePath(prefix: prefix, extension: ext ?? ".jpg", width: width, height: height)
|
||||
}
|
||||
|
||||
private let prefix1x: String
|
||||
|
||||
private let prefix2x: String
|
||||
@ -18,8 +31,8 @@ struct WebsiteImage {
|
||||
|
||||
init(rawImagePath: String, width: Int, height: Int, altText: String) {
|
||||
let (prefix, ext) = rawImagePath.fileNameAndExtension
|
||||
self.prefix1x = "\(prefix)@\(width)x\(height)"
|
||||
self.prefix2x = "\(prefix)@\(width*2)x\(height*2)"
|
||||
self.prefix1x = WebsiteImage.imagePath(prefix: prefix, width: width, height: height)
|
||||
self.prefix2x = WebsiteImage.imagePath(prefix: prefix, width: 2*width, height: 2*height)
|
||||
self.altText = altText.htmlEscaped()
|
||||
self.ext = ext ?? "jpg"
|
||||
}
|
||||
|
@ -49,25 +49,13 @@ struct ContentPage: HtmlProducer {
|
||||
result += "</body></html>" // Close content
|
||||
}
|
||||
|
||||
private let symbols: String =
|
||||
"""
|
||||
<div style="display:none">
|
||||
<svg id="icon-clock" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .3.4l3.5 2a.5.5 0 0 0 .4-.8L8 8.7V3.5z"/>
|
||||
<path fill="currentColor" d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"/>
|
||||
</svg>
|
||||
<svg id="icon-arrow-up" width="16" height="16">
|
||||
<path fill="currentColor" d="m14 2.5a.5.5 0 0 0 -.5-.5h-6a.5.5 0 0 0 0 1h4.8l-10.16 10.15a.5.5 0 0 0 .7.7l10.16-10.14v4.79a.5.5 0 0 0 1 0z"/>
|
||||
</svg>
|
||||
<svg id="icon-arrow-down" width="16" height="16">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M14 13.5a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1 0-1h4.8L2.14 2.85a.5.5 0 1 1 .7-.7L13 12.29V7.5a.5.5 0 0 1 1 0v6z"/>
|
||||
</svg>
|
||||
<svg id="icon-sign" width="16" height="16">
|
||||
<path fill="currentColor" d="M7 1.4V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.5a1 1 0 0 0 .8-.4l2-2.3a.5.5 0 0 0 0-.6l-2-2.3a1 1 0 0 0-.8-.4H9V1.4a1 1 0 0 0-2 0zM12.5 5l1.7 2-1.7 2H2V5h10.5z"/>
|
||||
</svg>
|
||||
<svg id="icon-flame" width="16" height="16">
|
||||
<path fill="currentColor" d="M8 16c3.3 0 6-2 6-5.5 0-1.5-.5-4-2.5-6 .3 1.5-1.3 2-1.3 2C11 4 9 .5 6 0c.4 2 .5 4-2 6-1.3 1-2 2.7-2 4.5C2 14 4.7 16 8 16Zm0-1c-1.7 0-3-1-3-2.8 0-.7.3-2 1.3-3-.2.8.7 1.3.7 1.3-.4-1.3.5-3.3 2-3.5-.2 1-.3 2 1 3a3 3 0 0 1 1 2.3C11 14 9.7 15 8 15Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
"""
|
||||
#warning("Select only required symbols")
|
||||
private let symbols: String = {
|
||||
var result = "<div style='display:none'>"
|
||||
for icon in PageIcon.allCases {
|
||||
result += icon.icon
|
||||
}
|
||||
result += "</div>"
|
||||
return result
|
||||
}()
|
||||
}
|
||||
|
@ -2,6 +2,6 @@
|
||||
extension FileResource {
|
||||
|
||||
static var mock: FileResource {
|
||||
.init(content: .mock, id: "my-file.txt", en: "Some text file", de: "Eine Textdatei")
|
||||
.init(content: .mock, id: "my-file.txt", isExternallyStored: true, en: "Some text file", de: "Eine Textdatei")
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ extension Page {
|
||||
extension LocalizedPage {
|
||||
|
||||
static let english = LocalizedPage(
|
||||
content: .mock,
|
||||
urlString: "my-project",
|
||||
title: "My First Project",
|
||||
lastModified: nil,
|
||||
@ -28,6 +29,7 @@ extension LocalizedPage {
|
||||
requiredFiles: [])
|
||||
|
||||
static let german = LocalizedPage(
|
||||
content: .mock,
|
||||
urlString: "mein-projekt",
|
||||
title: "Mein Erstes Projekt",
|
||||
lastModified: nil,
|
||||
|
@ -21,7 +21,7 @@ extension PostSettings {
|
||||
extension PageSettings {
|
||||
|
||||
static var mock: PageSettings {
|
||||
.init(pageUrlPrefix: "pages", contentWidth: 600)
|
||||
.init(pageUrlPrefix: "pages", contentWidth: 600, largeImageWidth: 1200)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,8 @@ struct PageSettingsFile {
|
||||
let pageUrlPrefix: String
|
||||
|
||||
let contentWidth: Int
|
||||
|
||||
let largeImageWidth: Int
|
||||
}
|
||||
|
||||
extension PageSettingsFile: Codable {
|
||||
@ -14,6 +16,7 @@ extension PageSettingsFile {
|
||||
|
||||
static var `default`: PageSettingsFile {
|
||||
.init(pageUrlPrefix: "page",
|
||||
contentWidth: 600)
|
||||
contentWidth: 600,
|
||||
largeImageWidth: 1200)
|
||||
}
|
||||
}
|
||||
|
@ -379,6 +379,18 @@ final class Storage {
|
||||
return try readExistingFile(at: path)
|
||||
}
|
||||
|
||||
// MARK: External file list
|
||||
|
||||
private let externalFileListName = "external-files.json"
|
||||
|
||||
func loadExternalFileList() throws -> [String] {
|
||||
try read(at: externalFileListName, defaultValue: [])
|
||||
}
|
||||
|
||||
func save(externalFileList: [String]) throws {
|
||||
try writeIfChanged(externalFileList.sorted(), to: externalFileListName)
|
||||
}
|
||||
|
||||
// MARK: Website data
|
||||
|
||||
private let settingsDataFileName: String = "settings.json"
|
||||
|
@ -34,6 +34,7 @@ struct AddFileView: View {
|
||||
HStack {
|
||||
Button("Cancel", role: .cancel) { dismiss() }
|
||||
Button("Select more files", action: openFilePanel)
|
||||
Button("Add placeholder", action: addPlaceholderFile)
|
||||
Button("Add selected", action: importSelectedFiles)
|
||||
.disabled(filesToAdd.isEmpty)
|
||||
}
|
||||
@ -68,6 +69,11 @@ struct AddFileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func addPlaceholderFile() {
|
||||
let newFile = FileToAdd(content: content, externalFile: "placeholder")
|
||||
filesToAdd.append(newFile)
|
||||
}
|
||||
|
||||
private func delete(file: FileToAdd) {
|
||||
guard let index = filesToAdd.firstIndex(of: file) else {
|
||||
return
|
||||
@ -85,16 +91,19 @@ struct AddFileView: View {
|
||||
print("Skipping existing file \(file.uniqueId)")
|
||||
continue
|
||||
}
|
||||
do {
|
||||
try content.storage.copyFile(at: file.url, fileId: file.uniqueId)
|
||||
} catch {
|
||||
print("Failed to import file '\(file.uniqueId)' at \(file.url.path()): \(error)")
|
||||
return
|
||||
if let url = file.url {
|
||||
do {
|
||||
try content.storage.copyFile(at: url, fileId: file.uniqueId)
|
||||
} catch {
|
||||
print("Failed to import file '\(file.uniqueId)' at \(url.path()): \(error)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let resource = FileResource(
|
||||
content: content,
|
||||
id: file.uniqueId,
|
||||
isExternallyStored: file.url == nil,
|
||||
en: "", de: "")
|
||||
// TODO: Insert at correct index?
|
||||
content.files.insert(resource, at: 0)
|
||||
|
@ -13,44 +13,56 @@ struct FileContentView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
switch file.type {
|
||||
case .image:
|
||||
file.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
case .model:
|
||||
if file.isExternallyStored {
|
||||
VStack {
|
||||
Image(systemSymbol: .cubeTransparent)
|
||||
Image(systemSymbol: .squareDashed)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: iconSize)
|
||||
Text("No preview available")
|
||||
Text("External file")
|
||||
.font(.title)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
case .text, .code:
|
||||
TextFileContentView(file: file)
|
||||
.id(file.id)
|
||||
case .video:
|
||||
VStack {
|
||||
Image(systemSymbol: .film)
|
||||
} else {
|
||||
switch file.type {
|
||||
case .image:
|
||||
file.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: iconSize)
|
||||
Text("No preview available")
|
||||
.font(.title)
|
||||
case .model:
|
||||
VStack {
|
||||
Image(systemSymbol: .cubeTransparent)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: iconSize)
|
||||
Text("No preview available")
|
||||
.font(.title)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
case .text, .code:
|
||||
TextFileContentView(file: file)
|
||||
.id(file.id)
|
||||
case .video:
|
||||
VStack {
|
||||
Image(systemSymbol: .film)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: iconSize)
|
||||
Text("No preview available")
|
||||
.font(.title)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
case .other:
|
||||
VStack {
|
||||
Image(systemSymbol: .docQuestionmark)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: iconSize)
|
||||
Text("No preview available")
|
||||
.font(.title)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
case .other:
|
||||
VStack {
|
||||
Image(systemSymbol: .docQuestionmark)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: iconSize)
|
||||
Text("No preview available")
|
||||
.font(.title)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
|
@ -2,9 +2,12 @@ import Foundation
|
||||
|
||||
final class FileToAdd: ObservableObject {
|
||||
|
||||
let id: Int
|
||||
|
||||
unowned let content: Content
|
||||
|
||||
let url: URL
|
||||
// The external path to the file, or nil if the file is just a placeholder
|
||||
let url: URL?
|
||||
|
||||
@Published
|
||||
var uniqueId: String
|
||||
@ -13,11 +16,19 @@ final class FileToAdd: ObservableObject {
|
||||
var isSelected: Bool = true
|
||||
|
||||
init(content: Content, url: URL) {
|
||||
self.id = .random()
|
||||
self.content = content
|
||||
self.url = url
|
||||
self.uniqueId = url.lastPathComponent
|
||||
}
|
||||
|
||||
init(content: Content, externalFile: String) {
|
||||
self.id = .random()
|
||||
self.content = content
|
||||
self.url = nil
|
||||
self.uniqueId = externalFile
|
||||
}
|
||||
|
||||
var idAlreadyExists: Bool {
|
||||
content.files.contains { $0.id == uniqueId }
|
||||
}
|
||||
@ -25,9 +36,6 @@ final class FileToAdd: ObservableObject {
|
||||
|
||||
extension FileToAdd: Identifiable {
|
||||
|
||||
var id: URL {
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
extension FileToAdd: Equatable {
|
||||
|
@ -30,7 +30,7 @@ struct FileToAddView: View {
|
||||
.frame(maxWidth: 200)
|
||||
|
||||
}
|
||||
Text(file.url.path())
|
||||
Text(file.url?.path() ?? "Placeholder file")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
@ -73,9 +73,11 @@ struct AddPageView: View {
|
||||
createdDate: .now,
|
||||
startDate: .now,
|
||||
endDate: nil,
|
||||
german: .init(urlString: "seite",
|
||||
german: .init(content: content,
|
||||
urlString: "seite",
|
||||
title: "Ein Titel"),
|
||||
english: .init(urlString: "page",
|
||||
english: .init(content: content,
|
||||
urlString: "page",
|
||||
title: "A Title"),
|
||||
tags: [])
|
||||
content.pages.insert(page, at: 0)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
import HighlightedTextEditor
|
||||
|
||||
struct LocalizedPageContentView: View {
|
||||
@ -11,17 +12,21 @@ struct LocalizedPageContentView: View {
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var isGeneratingWebsite = false
|
||||
|
||||
@State
|
||||
private var loadedPageContentLanguage: ContentLanguage?
|
||||
|
||||
@State
|
||||
private var pageContent: String = ""
|
||||
|
||||
@State
|
||||
private var didLoadContent = false
|
||||
private var pageContentUsedForGeneration: String = ""
|
||||
|
||||
@State
|
||||
private var generationResults = PageGenerationResults()
|
||||
|
||||
|
||||
init(pageId: String, page: LocalizedPage) {
|
||||
self.pageId = pageId
|
||||
@ -41,8 +46,12 @@ struct LocalizedPageContentView: View {
|
||||
Button(action: saveContent) {
|
||||
Text("Save")
|
||||
}
|
||||
Button(action: checkContent) {
|
||||
Text("Check")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
PageContentResultsView(results: generationResults)
|
||||
HighlightedTextEditor(
|
||||
text: $pageContent,
|
||||
highlightRules: .markdown)
|
||||
@ -53,32 +62,50 @@ struct LocalizedPageContentView: View {
|
||||
}
|
||||
|
||||
private func loadContent() {
|
||||
let language = language
|
||||
do {
|
||||
let content = try content.storage.pageContent(for: pageId, language: language)
|
||||
let content = try page.content.storage.pageContent(for: pageId, language: language)
|
||||
|
||||
guard content != "" else {
|
||||
pageContent = "New file"
|
||||
didLoadContent = false
|
||||
loadedPageContentLanguage = nil
|
||||
return
|
||||
}
|
||||
pageContent = content
|
||||
didLoadContent = true
|
||||
|
||||
loadedPageContentLanguage = language
|
||||
checkContent()
|
||||
} catch {
|
||||
print("Failed to load page content: \(error)")
|
||||
pageContent = "Failed to load"
|
||||
loadedPageContentLanguage = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func saveContent() {
|
||||
guard didLoadContent else {
|
||||
guard let loadedPageContentLanguage else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try content.storage.save(pageContent: pageContent, for: pageId, language: language)
|
||||
try page.content.storage.save(pageContent: pageContent, for: pageId, language: loadedPageContentLanguage)
|
||||
} catch {
|
||||
print("Failed to save content: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func checkContent() {
|
||||
let content = self.pageContent
|
||||
guard content != pageContentUsedForGeneration else {
|
||||
return
|
||||
}
|
||||
isGeneratingWebsite = true
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
let generator = PageContentParser(content: page.content, language: language)
|
||||
_ = generator.generatePage(from: content)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.generationResults = generator.results
|
||||
isGeneratingWebsite = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,22 +4,47 @@ import SFSafeSymbols
|
||||
struct LocalizedPageDetailView: View {
|
||||
|
||||
@ObservedObject
|
||||
private var item: LocalizedPage
|
||||
private var page: LocalizedPage
|
||||
|
||||
init(page: LocalizedPage, showImagePicker: Bool = false) {
|
||||
self.item = page
|
||||
self.page = page
|
||||
self.showImagePicker = showImagePicker
|
||||
self.newUrlString = page.urlString
|
||||
}
|
||||
|
||||
@State
|
||||
private var showImagePicker = false
|
||||
|
||||
@State
|
||||
private var newUrlString: String
|
||||
|
||||
private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted
|
||||
|
||||
private var idExists: Bool {
|
||||
page.content.pages.contains {
|
||||
$0.german.urlString == newUrlString
|
||||
|| $0.english.urlString == newUrlString
|
||||
}
|
||||
}
|
||||
|
||||
private var containsInvalidCharacters: Bool {
|
||||
newUrlString.rangeOfCharacter(from: allowedCharactersInPostId) != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
TextField("", text: $newUrlString)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button("Update", action: setNewId)
|
||||
.disabled(newUrlString.isEmpty || containsInvalidCharacters || idExists)
|
||||
}
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Link Preview Title")
|
||||
.font(.headline)
|
||||
OptionalTextField("", text: $item.linkPreviewTitle,
|
||||
prompt: item.title)
|
||||
OptionalTextField("", text: $page.linkPreviewTitle,
|
||||
prompt: page.title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
@ -35,13 +60,13 @@ struct LocalizedPageDetailView: View {
|
||||
IconButton(symbol: .trashCircleFill,
|
||||
size: 22,
|
||||
color: .red) {
|
||||
item.linkPreviewImage = nil
|
||||
}.disabled(item.linkPreviewImage == nil)
|
||||
page.linkPreviewImage = nil
|
||||
}.disabled(page.linkPreviewImage == nil)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
.buttonStyle(.plain)
|
||||
if let image = item.linkPreviewImage {
|
||||
if let image = page.linkPreviewImage {
|
||||
image.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
@ -52,16 +77,20 @@ struct LocalizedPageDetailView: View {
|
||||
Text("Link Preview Description")
|
||||
.font(.headline)
|
||||
.padding(.top)
|
||||
OptionalDescriptionField(text: $item.linkPreviewDescription)
|
||||
OptionalDescriptionField(text: $page.linkPreviewDescription)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(showImagePicker: $showImagePicker) { image in
|
||||
item.linkPreviewImage = image
|
||||
page.linkPreviewImage = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setNewId() {
|
||||
page.urlString = newUrlString
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
118
CHDataManagement/Views/Pages/PageContentResultsView.swift
Normal file
118
CHDataManagement/Views/Pages/PageContentResultsView.swift
Normal 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())
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
struct PageDetailView: View {
|
||||
|
||||
@ -17,6 +18,9 @@ struct PageDetailView: View {
|
||||
@State
|
||||
private var newId: String
|
||||
|
||||
@State
|
||||
private var didGenerateWebsite: Bool?
|
||||
|
||||
init(page: Page) {
|
||||
self.page = page
|
||||
self.newId = page.id
|
||||
@ -35,10 +39,21 @@ struct PageDetailView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Button(action: generate) {
|
||||
Text("Generate")
|
||||
HStack {
|
||||
Button(action: generate) {
|
||||
Text("Generate")
|
||||
}
|
||||
.disabled(isGeneratingWebsite)
|
||||
if let didGenerateWebsite {
|
||||
if didGenerateWebsite {
|
||||
Image(systemSymbol: .checkmarkCircleFill)
|
||||
.foregroundStyle(.green)
|
||||
} else {
|
||||
Image(systemSymbol: .xmarkCircleFill)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(isGeneratingWebsite)
|
||||
HStack {
|
||||
TextField("", text: $newId)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
@ -86,6 +101,7 @@ struct PageDetailView: View {
|
||||
}
|
||||
|
||||
LocalizedPageDetailView(page: page.localized(in: language))
|
||||
.id(page.id + language.rawValue)
|
||||
|
||||
}
|
||||
.padding()
|
||||
@ -106,11 +122,13 @@ struct PageDetailView: View {
|
||||
isGeneratingWebsite = true
|
||||
print("Generating page")
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let generator = WebsiteGenerator(
|
||||
content: content,
|
||||
language: language)
|
||||
if !generator.generate(page: page) {
|
||||
print("Generation failed")
|
||||
for language in ContentLanguage.allCases {
|
||||
let generator = LocalizedWebsiteGenerator(
|
||||
content: content,
|
||||
language: language)
|
||||
if !generator.generate(page: page) {
|
||||
print("Generation failed")
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
isGeneratingWebsite = false
|
||||
|
@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GenerationSettingsView: View {
|
||||
struct GenerationContentView: View {
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
@ -37,7 +37,7 @@ struct GenerationSettingsView: View {
|
||||
Text(generatorText)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ struct GenerationSettingsView: View {
|
||||
}
|
||||
isGeneratingWebsite = true
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let generator = WebsiteGenerator(
|
||||
let generator = LocalizedWebsiteGenerator(
|
||||
content: content,
|
||||
language: language)
|
||||
_ = generator.generateWebsite { text in
|
||||
@ -71,7 +71,7 @@ struct GenerationSettingsView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
GenerationSettingsView()
|
||||
GenerationContentView()
|
||||
.environmentObject(Content.mock)
|
||||
.padding()
|
||||
}
|
30
CHDataManagement/Views/Settings/GenerationDetailView.swift
Normal file
30
CHDataManagement/Views/Settings/GenerationDetailView.swift
Normal 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)
|
||||
}
|
@ -26,7 +26,7 @@ struct NavigationBarSettingsView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Notification Bar Settings")
|
||||
Text("Navigation Bar")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Customize the navigation bar for all pages at the top of the website")
|
||||
@ -37,7 +37,6 @@ struct NavigationBarSettingsView: View {
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.navigationBar.iconPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 300)
|
||||
Text("Specify the path to the icon file with regard to the final website folder.")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 30)
|
||||
|
@ -21,16 +21,22 @@ struct PageSettingsView: View {
|
||||
.font(.headline)
|
||||
IntegerField("", number: $content.settings.pages.contentWidth)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 400)
|
||||
Text("The maximum width of the content in pages (in pixels)")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Image Width")
|
||||
.font(.headline)
|
||||
IntegerField("", number: $content.settings.pages.largeImageWidth)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The maximum width of images that are diplayed fullscreen")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Page URL Prefix")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.pages.pageUrlPrefix)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(maxWidth: 400)
|
||||
Text("The URL prefix used for the links to pages")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
|
@ -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()
|
||||
}
|
13
CHDataManagement/Views/Settings/SettingsListView.swift
Normal file
13
CHDataManagement/Views/Settings/SettingsListView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ import SFSafeSymbols
|
||||
|
||||
enum SettingsSection: String {
|
||||
|
||||
case generation = "Generation"
|
||||
//case generation = "Generation"
|
||||
|
||||
case folders = "Folders"
|
||||
|
||||
@ -18,7 +18,7 @@ extension SettingsSection {
|
||||
|
||||
var icon: SFSymbol {
|
||||
switch self {
|
||||
case .generation: return .arrowTriangle2Circlepath
|
||||
//case .generation: return .arrowTriangle2Circlepath
|
||||
case .folders: return .folder
|
||||
case .navigationBar: return .menubarRectangle
|
||||
case .postFeed: return .rectangleGrid1x2
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -19,50 +19,71 @@ struct LocalizedTagDetailView: View {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle("Appears in overviews", isOn: $tagIsVisible)
|
||||
.toggleStyle(.switch)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.headline)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Name")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.headline)
|
||||
TextField("", text: $tag.name)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("URL String")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.headline)
|
||||
TextField("", text: $tag.urlComponent)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Original url")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.headline)
|
||||
Text(tag.originalUrl ?? "-")
|
||||
.padding(.top, 1)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Subtitle")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.headline)
|
||||
OptionalTextField("", text: $tag.subtitle)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Description")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
OptionalTextField("", text: $tag.description)
|
||||
Text("Link Preview Description")
|
||||
.font(.headline)
|
||||
.padding(.top)
|
||||
OptionalDescriptionField(text: $tag.description)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Thumbnail")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
Button(action: { showImagePicker = true }) {
|
||||
Text(tag.thumbnail?.id ?? "Select")
|
||||
HStack {
|
||||
Text("Link Preview Image")
|
||||
.font(.headline)
|
||||
IconButton(symbol: .squareAndPencilCircleFill,
|
||||
size: 22,
|
||||
color: .blue) {
|
||||
showImagePicker = true
|
||||
}
|
||||
|
||||
IconButton(symbol: .trashCircleFill,
|
||||
size: 22,
|
||||
color: .red) {
|
||||
tag.linkPreviewImage = nil
|
||||
}.disabled(tag.linkPreviewImage == nil)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.blue)
|
||||
if let image = tag.linkPreviewImage {
|
||||
image.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400, maxHeight: 300)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(showImagePicker: $showImagePicker) { image in
|
||||
tag.thumbnail = image
|
||||
tag.linkPreviewImage = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user