diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index ef58a8b..0ecc801 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -22,11 +22,11 @@ E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; }; E218502F2CFAF69C0090B18B /* WebsiteGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */; }; E21850312CFAF8880090B18B /* Content+Import.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850302CFAF8840090B18B /* Content+Import.swift */; }; - E21850332CFAFA2F0090B18B /* WebsiteData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850322CFAFA200090B18B /* WebsiteData.swift */; }; - E21850352CFAFA5A0090B18B /* WebsiteDataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850342CFAFA570090B18B /* WebsiteDataFile.swift */; }; - E21850372CFCA55F0090B18B /* LocalizedWebsiteData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedWebsiteData.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 */; }; E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */; }; - E218503D2CFCFD910090B18B /* LocalizedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503C2CFCFD8C0090B18B /* LocalizedSettingsView.swift */; }; + E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */; }; E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; }; E24252032C5163CF0029FF16 /* Importer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252022C5163CF0029FF16 /* Importer.swift */; }; E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252052C51684E0029FF16 /* GenericMetadata.swift */; }; @@ -44,13 +44,25 @@ E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */; }; E25DA51D2CFF135E00AEF16D /* GenericPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA51C2CFF135B00AEF16D /* GenericPage.swift */; }; E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */; }; - E25DA5212CFF1B9300AEF16D /* WebsiteGenerator+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5202CFF1B8900AEF16D /* WebsiteGenerator+Mock.swift */; }; E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */; }; E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */; }; E25DA5272CFF745700AEF16D /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5262CFF745200AEF16D /* URL+Extensions.swift */; }; E25DA5292CFFBFBB00AEF16D /* ImageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5282CFFBFB800AEF16D /* ImageType.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 */; }; + E25DA53A2D00424000AEF16D /* LocalizedSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5392D00423F00AEF16D /* LocalizedSettingsFile.swift */; }; + 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 */; }; E2A21C012CB16A820060935B /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C002CB16A820060935B /* PostView.swift */; }; E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; }; E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C042CB176670060935B /* LocalizedText.swift */; }; @@ -65,7 +77,7 @@ E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C2B2CB2BB210060935B /* PostList.swift */; }; E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */; }; E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C312CB5BCAC0060935B /* PageDetailView.swift */; }; - E2A21C362CB9A3D70060935B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C352CB9A3D70060935B /* SettingsView.swift */; }; + E2A21C362CB9A3D70060935B /* FolderSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C352CB9A3D70060935B /* FolderSettingsView.swift */; }; E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C3A2CB9D9A50060935B /* ImageResource.swift */; }; E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */; }; E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C472CBAF8830060935B /* String+Extensions.swift */; }; @@ -119,11 +131,11 @@ E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = ""; }; E218502E2CFAF6990090B18B /* WebsiteGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteGenerator.swift; sourceTree = ""; }; E21850302CFAF8840090B18B /* Content+Import.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Import.swift"; sourceTree = ""; }; - E21850322CFAFA200090B18B /* WebsiteData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteData.swift; sourceTree = ""; }; - E21850342CFAFA570090B18B /* WebsiteDataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteDataFile.swift; sourceTree = ""; }; - E21850362CFCA5580090B18B /* LocalizedWebsiteData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedWebsiteData.swift; sourceTree = ""; }; + E21850322CFAFA200090B18B /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; + E21850342CFAFA570090B18B /* SettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFile.swift; sourceTree = ""; }; + E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = ""; }; E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Mock.swift"; sourceTree = ""; }; - E218503C2CFCFD8C0090B18B /* LocalizedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedSettingsView.swift; sourceTree = ""; }; + E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostFeedSettingsView.swift; sourceTree = ""; }; E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = ""; }; E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = ""; }; E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.swift"; sourceTree = ""; }; @@ -141,11 +153,23 @@ E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedPageNavigation.swift; sourceTree = ""; }; E25DA51C2CFF135B00AEF16D /* GenericPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPage.swift; sourceTree = ""; }; E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; - E25DA5202CFF1B8900AEF16D /* WebsiteGenerator+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteGenerator+Mock.swift"; sourceTree = ""; }; E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGenerator.swift; sourceTree = ""; }; E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSSize+Scaling.swift"; sourceTree = ""; }; E25DA5262CFF745200AEF16D /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; E25DA5282CFFBFB800AEF16D /* ImageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageType.swift; sourceTree = ""; }; + E25DA5302D003FC000AEF16D /* SectionedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionedSettingsView.swift; sourceTree = ""; }; + E25DA5332D0041CB00AEF16D /* NavigationBarSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsFile.swift; sourceTree = ""; }; + E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsFile.swift; sourceTree = ""; }; + E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettingsFile.swift; sourceTree = ""; }; + E25DA5392D00423F00AEF16D /* LocalizedSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedSettingsFile.swift; sourceTree = ""; }; + E25DA53C2D0043E200AEF16D /* LocalizedSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedSettings.swift; sourceTree = ""; }; + E25DA53E2D00441C00AEF16D /* NavigationBarSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettings.swift; sourceTree = ""; }; + E25DA5402D00446700AEF16D /* PostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettings.swift; sourceTree = ""; }; + E25DA5422D0094A400AEF16D /* SettingsSidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSidebar.swift; sourceTree = ""; }; + E25DA5442D00952D00AEF16D /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; + E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsView.swift; sourceTree = ""; }; + E25DA56E2D00F99900AEF16D /* PostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedSettingsView.swift; sourceTree = ""; }; + E25DA5702D01015400AEF16D /* GenerationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationSettingsView.swift; sourceTree = ""; }; E2A21C002CB16A820060935B /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = ""; }; E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = ""; }; E2A21C042CB176670060935B /* LocalizedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedText.swift; sourceTree = ""; }; @@ -160,7 +184,7 @@ E2A21C2B2CB2BB210060935B /* PostList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostList.swift; sourceTree = ""; }; E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCenter.swift; sourceTree = ""; }; E2A21C312CB5BCAC0060935B /* PageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageDetailView.swift; sourceTree = ""; }; - E2A21C352CB9A3D70060935B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + E2A21C352CB9A3D70060935B /* FolderSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderSettingsView.swift; sourceTree = ""; }; E2A21C3A2CB9D9A50060935B /* ImageResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageResource.swift; sourceTree = ""; }; E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntryContent.swift; sourceTree = ""; }; E2A21C472CBAF8830060935B /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; @@ -227,7 +251,7 @@ E25DA5112CFF001900AEF16D /* Model */ = { isa = PBXGroup; children = ( - E21850342CFAFA570090B18B /* WebsiteDataFile.swift */, + E25DA5322D0041C400AEF16D /* Settings */, E21850142CEE55D40090B18B /* FileOnDisk.swift */, E21850162CEE55FB0090B18B /* FileType.swift */, E2A37D102CE537670000979F /* PageFile.swift */, @@ -238,6 +262,30 @@ path = Model; sourceTree = ""; }; + E25DA5322D0041C400AEF16D /* Settings */ = { + isa = PBXGroup; + children = ( + E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */, + E25DA5392D00423F00AEF16D /* LocalizedSettingsFile.swift */, + E25DA5332D0041CB00AEF16D /* NavigationBarSettingsFile.swift */, + E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */, + E21850342CFAFA570090B18B /* SettingsFile.swift */, + ); + path = Settings; + sourceTree = ""; + }; + E25DA53B2D0042EA00AEF16D /* Settings */ = { + isa = PBXGroup; + children = ( + E25DA5402D00446700AEF16D /* PostSettings.swift */, + E25DA53E2D00441C00AEF16D /* NavigationBarSettings.swift */, + E25DA53C2D0043E200AEF16D /* LocalizedSettings.swift */, + E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */, + E21850322CFAFA200090B18B /* Settings.swift */, + ); + path = Settings; + sourceTree = ""; + }; E2A21C322CB5BCAC0060935B /* Pages */ = { isa = PBXGroup; children = ( @@ -250,8 +298,14 @@ E2A21C342CB9A3CA0060935B /* Settings */ = { isa = PBXGroup; children = ( - E218503C2CFCFD8C0090B18B /* LocalizedSettingsView.swift */, - E2A21C352CB9A3D70060935B /* SettingsView.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 = ""; @@ -312,9 +366,8 @@ E2B85F392C428F020047CD0C /* Model */ = { isa = PBXGroup; children = ( + E25DA53B2D0042EA00AEF16D /* Settings */, E25DA5282CFFBFB800AEF16D /* ImageType.swift */, - E21850322CFAFA200090B18B /* WebsiteData.swift */, - E21850362CFCA5580090B18B /* LocalizedWebsiteData.swift */, E2E06DFA2CA4A6570019C2AF /* Content.swift */, E25DA5162CFF00F200AEF16D /* Content+Save.swift */, E25DA5142CFF00B900AEF16D /* Content+Load.swift */, @@ -425,8 +478,8 @@ E2DD04722C276F31003BFF1F /* CHDataManagement */ = { isa = PBXGroup; children = ( - E2A37D0F2CE5375E0000979F /* Storage */, E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */, + E2A37D0F2CE5375E0000979F /* Storage */, E2B85F392C428F020047CD0C /* Model */, E2B85F462C42C7CA0047CD0C /* Views */, E2B85F3F2C42946E0047CD0C /* Page Elements */, @@ -443,7 +496,6 @@ E2DD047C2C276F32003BFF1F /* Preview Content */ = { isa = PBXGroup; children = ( - E25DA5202CFF1B8900AEF16D /* WebsiteGenerator+Mock.swift */, E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */, E218500A2CEE02FA0090B18B /* Content+Mock.swift */, E2A37D1A2CEA45530000979F /* Tag+Mock.swift */, @@ -538,16 +590,19 @@ buildActionMask = 2147483647; files = ( E218502B2CF790B30090B18B /* PostContentView.swift in Sources */, + E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */, E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */, E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */, E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */, + E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */, E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, - E25DA5212CFF1B9300AEF16D /* WebsiteGenerator+Mock.swift in Sources */, E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */, E21850312CFAF8880090B18B /* Content+Import.swift in Sources */, E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, + E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */, E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */, E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */, + E25DA53A2D00424000AEF16D /* LocalizedSettingsFile.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, E21850172CEE55FC0090B18B /* FileType.swift in Sources */, E2A37D112CE537800000979F /* PageFile.swift in Sources */, @@ -559,16 +614,19 @@ 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 */, E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */, E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */, E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */, + E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */, E2B85F3D2C4293F80047CD0C /* PageInFeed.swift in Sources */, E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */, E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */, E24252082C5168750029FF16 /* GenericMetadata+Localized.swift in Sources */, E2581DED2C75202400F1F079 /* Tag.swift in Sources */, E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */, + E25DA53D2D0043E600AEF16D /* LocalizedSettings.swift in Sources */, E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, @@ -581,27 +639,28 @@ E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */, E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */, - E21850332CFAFA2F0090B18B /* WebsiteData.swift in Sources */, + E21850332CFAFA2F0090B18B /* Settings.swift in Sources */, E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */, E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */, E2A21C202CB28ED20060935B /* MockImage.swift in Sources */, E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */, - E21850352CFAFA5A0090B18B /* WebsiteDataFile.swift in Sources */, + E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, + E25DA5432D0094A900AEF16D /* SettingsSidebar.swift in Sources */, E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */, E25DA51D2CFF135E00AEF16D /* GenericPage.swift in Sources */, E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */, E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */, - E218503D2CFCFD910090B18B /* LocalizedSettingsView.swift in Sources */, + E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */, - E21850372CFCA55F0090B18B /* LocalizedWebsiteData.swift in Sources */, + E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */, E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */, E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */, E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */, @@ -611,20 +670,25 @@ E25DA5292CFFBFBB00AEF16D /* ImageType.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, + E25DA5342D0041CB00AEF16D /* NavigationBarSettingsFile.swift in Sources */, E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */, E2A37D0E2CE527070000979F /* Storage.swift in Sources */, E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */, E218502D2CF791440090B18B /* PostImagesView.swift in Sources */, E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */, E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */, + E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */, E25DA5272CFF745700AEF16D /* URL+Extensions.swift in Sources */, E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */, + E25DA5712D01015400AEF16D /* GenerationSettingsView.swift in Sources */, E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */, E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */, + E25DA5312D003FCB00AEF16D /* SectionedSettingsView.swift in Sources */, E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */, - E2A21C362CB9A3D70060935B /* SettingsView.swift in Sources */, + E2A21C362CB9A3D70060935B /* FolderSettingsView.swift in Sources */, E2A21C012CB16A820060935B /* PostView.swift in Sources */, E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */, + E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/CHDataManagement/CHDataManagementApp.swift b/CHDataManagement/CHDataManagementApp.swift index eabca7a..988b5e1 100644 --- a/CHDataManagement/CHDataManagementApp.swift +++ b/CHDataManagement/CHDataManagementApp.swift @@ -40,7 +40,7 @@ struct CHDataManagementApp: App { FilesView() } Tab("Settings", systemImage: SFSymbol.gear.rawValue) { - SettingsView() + SectionedSettingsView() } } .environment(\.language, selectedLanguage) @@ -54,18 +54,16 @@ struct CHDataManagementApp: App { .tag(ContentLanguage.german) }.pickerStyle(.segmented) } - } - .onAppear(perform: importOldContent) - .onReceive(Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()) { _ in - save() - } - .toolbar { - ToolbarItem(placement: .navigation) { + ToolbarItem(placement: .primaryAction) { Button(action: save) { Text("Save") } } } + .onAppear(perform: importOldContent) + .onReceive(Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()) { _ in + save() + } } } diff --git a/CHDataManagement/Model/Content+Load.swift b/CHDataManagement/Model/Content+Load.swift index 7314035..f7b8319 100644 --- a/CHDataManagement/Model/Content+Load.swift +++ b/CHDataManagement/Model/Content+Load.swift @@ -37,16 +37,18 @@ extension Content { linkPreviewDescription: page.linkPreviewDescription) } - private func convert(_ websiteData: LocalizedWebsiteDataFile) -> LocalizedWebsiteData { - .init(title: websiteData.title, - description: websiteData.description, - iconDescription: websiteData.iconDescription) + private func convert(_ settings: LocalizedSettingsFile) -> LocalizedSettings { + .init(navigationBarIconDescription: settings.navigationBarIconDescription, + posts: .init( + title: settings.posts.feedTitle, + description: settings.posts.feedDescription, + feedUrlPrefix: settings.posts.feedUrlPrefix)) } func loadFromDisk() throws { let storage = Storage(baseFolder: URL(filePath: contentPath)) - let websiteData = try storage.loadWebsiteData() + let settings = try storage.loadSettings() let tagData = try storage.loadAllTags() let pagesData = try storage.loadAllPages() @@ -104,10 +106,25 @@ extension Content { self.images = images.values.sorted { $0.id } self.videos = videos self.posts = posts.sorted(ascending: false) { $0.startDate } - self.websiteData = WebsiteData( - navigationTags: websiteData.navigationTags.map { tags[$0]! }, - german: convert(websiteData.german), - english: convert(websiteData.english)) + self.settings = makeSettings(settings, tags: tags) + } + + private func makeSettings(_ settings: SettingsFile, tags: [String : Tag]) -> Settings { + + let navigationBar = NavigationBarSettings( + iconPath: settings.navigationBar.navigationIconPath, + tags: settings.navigationBar.navigationTags.map { tags[$0]! }) + + let posts = PostSettings( + postsPerPage: settings.posts.postsPerPage, + contentWidth: settings.posts.contentWidth) + + return Settings( + outputDirectoryPath: settings.outputDirectoryPath, + navigationBar: navigationBar, + posts: posts, + german: convert(settings.german), + english: convert(settings.english)) } private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag]) -> [String : Page] { diff --git a/CHDataManagement/Model/Content+Save.swift b/CHDataManagement/Model/Content+Save.swift index ebd93c3..1bc5ac1 100644 --- a/CHDataManagement/Model/Content+Save.swift +++ b/CHDataManagement/Model/Content+Save.swift @@ -15,7 +15,7 @@ extension Content { for tag in tags { storage.save(tagMetadata: tag.tagFile, for: tag.id) } - storage.save(websiteData: websiteData.dataFile) + storage.save(settings: settings.file) do { try storage.deletePostFiles(notIn: posts.map { $0.id }) @@ -113,21 +113,48 @@ private extension LocalizedTag { } } -private extension WebsiteData { +private extension NavigationBarSettings { - var dataFile: WebsiteDataFile { + var file: NavigationBarSettingsFile { + .init(navigationIconPath: iconPath, + navigationTags: tags.map { $0.id }) + } +} + +extension Settings { + + var file: SettingsFile { .init( - navigationTags: navigationTags.map { $0.id }, - german: german.dataFile, - english: english.dataFile) + outputDirectoryPath: outputDirectoryPath, + navigationBar: navigationBar.file, + posts: posts.file, + german: german.file, + english: english.file) } } -private extension LocalizedWebsiteData { +private extension PostSettings { - var dataFile: LocalizedWebsiteDataFile { - .init(title: title, - description: description, - iconDescription: iconDescription) + var file: PostSettingsFile { + .init(postsPerPage: postsPerPage, + contentWidth: contentWidth) + } +} + +private extension LocalizedSettings { + + var file: LocalizedSettingsFile { + .init(navigationBarIconDescription: navigationBarIconDescription, + posts: posts.file) + } +} + +private extension LocalizedPostSettings { + + var file: LocalizedPostSettingsFile { + .init( + feedTitle: title, + feedDescription: description, + feedUrlPrefix: feedUrlPrefix) } } diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index 1cb5168..5ec7a9a 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -5,7 +5,7 @@ import Combine final class Content: ObservableObject { @Published - var websiteData: WebsiteData + var settings: Settings @Published var posts: [Post] @@ -39,7 +39,7 @@ final class Content: ObservableObject { private var cancellables = Set() - init(websiteData: WebsiteData, + init(settings: Settings, posts: [Post], pages: [Page], tags: [Tag], @@ -47,7 +47,7 @@ final class Content: ObservableObject { files: [FileResource], videos: [String], storedContentPath: String) { - self.websiteData = websiteData + self.settings = settings self.posts = posts self.pages = pages self.tags = tags @@ -69,7 +69,7 @@ final class Content: ObservableObject { init() { self.storage = Storage(baseFolder: URL(filePath: "")) - self.websiteData = .mock + self.settings = .mock self.posts = [] self.pages = [] self.tags = [] diff --git a/CHDataManagement/Model/LocalizedWebsiteData.swift b/CHDataManagement/Model/LocalizedWebsiteData.swift deleted file mode 100644 index d772bcc..0000000 --- a/CHDataManagement/Model/LocalizedWebsiteData.swift +++ /dev/null @@ -1,19 +0,0 @@ -import Foundation - -final class LocalizedWebsiteData: ObservableObject { - - @Published - var title: String - - @Published - var description: String - - @Published - var iconDescription: String - - init(title: String, description: String, iconDescription: String) { - self.title = title - self.description = description - self.iconDescription = iconDescription - } -} diff --git a/CHDataManagement/Model/Settings/LocalizedPostSettings.swift b/CHDataManagement/Model/Settings/LocalizedPostSettings.swift new file mode 100644 index 0000000..7104928 --- /dev/null +++ b/CHDataManagement/Model/Settings/LocalizedPostSettings.swift @@ -0,0 +1,19 @@ +import Foundation + +final class LocalizedPostSettings: ObservableObject { + + @Published + var title: String + + @Published + var description: String + + @Published + var feedUrlPrefix: String + + init(title: String, description: String, feedUrlPrefix: String) { + self.title = title + self.description = description + self.feedUrlPrefix = feedUrlPrefix + } +} diff --git a/CHDataManagement/Model/Settings/LocalizedSettings.swift b/CHDataManagement/Model/Settings/LocalizedSettings.swift new file mode 100644 index 0000000..c551fe9 --- /dev/null +++ b/CHDataManagement/Model/Settings/LocalizedSettings.swift @@ -0,0 +1,15 @@ +import Foundation + +final class LocalizedSettings: ObservableObject { + + @Published + var navigationBarIconDescription: String + + @Published + var posts: LocalizedPostSettings + + init(navigationBarIconDescription: String, posts: LocalizedPostSettings) { + self.navigationBarIconDescription = navigationBarIconDescription + self.posts = posts + } +} diff --git a/CHDataManagement/Model/Settings/NavigationBarSettings.swift b/CHDataManagement/Model/Settings/NavigationBarSettings.swift new file mode 100644 index 0000000..308a2fb --- /dev/null +++ b/CHDataManagement/Model/Settings/NavigationBarSettings.swift @@ -0,0 +1,17 @@ +import Foundation + +final class NavigationBarSettings: ObservableObject { + + /// The path to the main icon in the navigation bar + @Published + var iconPath: String + + /// The tags to show in the navigation bar + @Published + var tags: [Tag] + + init(iconPath: String, tags: [Tag]) { + self.iconPath = iconPath + self.tags = tags + } +} diff --git a/CHDataManagement/Model/Settings/PostSettings.swift b/CHDataManagement/Model/Settings/PostSettings.swift new file mode 100644 index 0000000..c0ddc23 --- /dev/null +++ b/CHDataManagement/Model/Settings/PostSettings.swift @@ -0,0 +1,17 @@ +import Foundation + +final class PostSettings: ObservableObject { + + /// The number of posts to show in a single page of the news feed + @Published + var postsPerPage: Int + + /// The maximum width of the main content + @Published + var contentWidth: CGFloat + + init(postsPerPage: Int, contentWidth: CGFloat) { + self.postsPerPage = postsPerPage + self.contentWidth = contentWidth + } +} diff --git a/CHDataManagement/Model/Settings/Settings.swift b/CHDataManagement/Model/Settings/Settings.swift new file mode 100644 index 0000000..a327f77 --- /dev/null +++ b/CHDataManagement/Model/Settings/Settings.swift @@ -0,0 +1,34 @@ +import Foundation + +final class Settings: ObservableObject { + + @Published + var outputDirectoryPath: String + + @Published + var navigationBar: NavigationBarSettings + + @Published + var posts: PostSettings + + @Published + var german: LocalizedSettings + + @Published + var english: LocalizedSettings + + init(outputDirectoryPath: String, navigationBar: NavigationBarSettings, posts: PostSettings, german: LocalizedSettings, english: LocalizedSettings) { + self.outputDirectoryPath = outputDirectoryPath + self.navigationBar = navigationBar + self.posts = posts + self.german = german + self.english = english + } + + func localized(in language: ContentLanguage) -> LocalizedSettings { + switch language { + case .english: return english + case .german: return german + } + } +} diff --git a/CHDataManagement/Model/WebsiteData.swift b/CHDataManagement/Model/WebsiteData.swift deleted file mode 100644 index b11659f..0000000 --- a/CHDataManagement/Model/WebsiteData.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation - -final class WebsiteData: ObservableObject { - - @Published - var navigationTags: [Tag] - - @Published - var german: LocalizedWebsiteData - - @Published - var english: LocalizedWebsiteData - - init(navigationTags: [Tag] = [], german: LocalizedWebsiteData, english: LocalizedWebsiteData) { - self.navigationTags = navigationTags - self.german = german - self.english = english - } - - func localized(in language: ContentLanguage) -> LocalizedWebsiteData { - switch language { - case .english: return english - case .german: return german - } - } -} diff --git a/CHDataManagement/Preview Content/Content+Mock.swift b/CHDataManagement/Preview Content/Content+Mock.swift index 1458e69..78c0934 100644 --- a/CHDataManagement/Preview Content/Content+Mock.swift +++ b/CHDataManagement/Preview Content/Content+Mock.swift @@ -15,7 +15,7 @@ extension Content { private static let dbPath = FileManager.default.documentDirectory.appendingPathComponent("db").path() static let mock: Content = Content( - websiteData: .mock, + settings: .mock, posts: [.empty, .mock, .fullMock], pages: [.empty], tags: [.hiking, .mountains, .nature, .sports], diff --git a/CHDataManagement/Preview Content/WebsiteData+Mock.swift b/CHDataManagement/Preview Content/WebsiteData+Mock.swift index a421fce..d8ab964 100644 --- a/CHDataManagement/Preview Content/WebsiteData+Mock.swift +++ b/CHDataManagement/Preview Content/WebsiteData+Mock.swift @@ -1,25 +1,48 @@ import Foundation -extension WebsiteData { +extension Settings { - static let mock: WebsiteData = .init( + static let mock: Settings = .init( + outputDirectoryPath: "/some/path", + navigationBar: .init(iconPath: "/some/other/path", tags: []), + posts: .mock, german: .german, english: .english) } -extension LocalizedWebsiteData { +extension PostSettings { - static var german: LocalizedWebsiteData { + static var mock: PostSettings { + .init(postsPerPage: 20, contentWidth: 600) + } +} + +extension LocalizedSettings { + + static var german: LocalizedSettings { + .init(navigationBarIconDescription: "Ein Symbol", + posts: .german) + } + + static var english: LocalizedSettings { + .init(navigationBarIconDescription: "An icon", + posts: .english) + } +} + +extension LocalizedPostSettings { + + static var german: LocalizedPostSettings { .init( title: "Titel", description: "Beschreibung", - iconDescription: "Icon") + feedUrlPrefix: "blog") } - static var english: LocalizedWebsiteData { + static var english: LocalizedPostSettings { .init( title: "A Title", description: "Description", - iconDescription: "Icon") + feedUrlPrefix: "feed") } } diff --git a/CHDataManagement/Preview Content/WebsiteGenerator+Mock.swift b/CHDataManagement/Preview Content/WebsiteGenerator+Mock.swift deleted file mode 100644 index 2820b72..0000000 --- a/CHDataManagement/Preview Content/WebsiteGenerator+Mock.swift +++ /dev/null @@ -1,24 +0,0 @@ -import Foundation - -extension WebsiteGeneratorConfiguration { - - static let english = WebsiteGeneratorConfiguration( - language: .english, - outputDirectory: URL(fileURLWithPath: ""), - postsPerPage: 20, - postFeedTitle: "Posts", - postFeedDescription: "The most recent posts on christophhagen.de", - postFeedUrlPrefix: "feed", - navigationIconPath: "/assets/icons/ch.svg", - mainContentMaximumWidth: 600) - - static let german = WebsiteGeneratorConfiguration( - language: .german, - outputDirectory: URL(fileURLWithPath: ""), - postsPerPage: 20, - postFeedTitle: "Beiträge", - postFeedDescription: "Die neusten Beiträge auf christophhagen.de", - postFeedUrlPrefix: "beiträge", - navigationIconPath: "/assets/icons/ch.svg", - mainContentMaximumWidth: 600) -} diff --git a/CHDataManagement/Storage/ImageGenerator.swift b/CHDataManagement/Storage/ImageGenerator.swift index b7a452e..02a1b01 100644 --- a/CHDataManagement/Storage/ImageGenerator.swift +++ b/CHDataManagement/Storage/ImageGenerator.swift @@ -49,9 +49,9 @@ final class ImageGenerator { } } - func runJobs() -> Bool { + func runJobs(callback: (String) -> Void) -> Bool { for job in jobs { - print("Generating image \(job.version)") + callback("Generating image \(job.version)") guard generate(job: job) else { return false } @@ -72,6 +72,10 @@ final class ImageGenerator { func generateVersion(for image: String, type: ImageType, maximumWidth: CGFloat, maximumHeight: CGFloat) -> String { let version = versionFileName(image: image, type: type, width: maximumWidth, height: maximumHeight) let fullPath = "/" + relativeImageOutputPath + "/" + version + if exists(version) { + hasNowGenerated(version: version, for: image) + return fullPath + } if hasPreviouslyGenerated(version: version, for: image), exists(version) { // Don't add job again return fullPath @@ -121,6 +125,7 @@ final class ImageGenerator { print("Missing image \(inputPath.path())") return false } + let data: Data do { data = try Data(contentsOf: inputPath) @@ -163,8 +168,16 @@ final class ImageGenerator { return false } + + let result = inOutputImagesFolder { folder in let url = folder.appendingPathComponent(job.version) + if job.type == .avif { + let out = url.path() + let input = out.replacingOccurrences(of: ".avif", with: ".jpg") + print("avifenc -q 70 \(input) \(out)") + return true + } do { try data.write(to: url) return true @@ -198,19 +211,20 @@ final class ImageGenerator { private func create(image: NSBitmapImageRep, type: ImageType, quality: CGFloat) -> Data? { switch type { case .jpg: - return image.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: quality)]) + return image.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 0.6)]) case .png: - return image.representation(using: .png, properties: [.compressionFactor: NSNumber(value: quality)]) + return image.representation(using: .png, properties: [.compressionFactor: NSNumber(value: 0.6)]) case .avif: - return createAvif(image: image, quality: quality) + return createAvif(image: image, quality: 0.7) case .webp: - return createWebp(image: image, quality: quality) + return createWebp(image: image, quality: 0.8) case .gif: return image.representation(using: .gif, properties: [.compressionFactor: NSNumber(value: quality)]) } } private func createAvif(image: NSBitmapImageRep, quality: CGFloat) -> Data? { + return Data() let newImage = NSImage(size: image.size) newImage.addRepresentation(image) return SDImageAVIFCoder.shared.encodedData(with: newImage, format: .AVIF, options: [.encodeCompressionQuality: quality]) diff --git a/CHDataManagement/Storage/Model/Settings/LocalizedPostSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/LocalizedPostSettingsFile.swift new file mode 100644 index 0000000..902c51e --- /dev/null +++ b/CHDataManagement/Storage/Model/Settings/LocalizedPostSettingsFile.swift @@ -0,0 +1,14 @@ + +struct LocalizedPostSettingsFile { + + /// The page title for the post feed + let feedTitle: String + + /// The page description for the post feed + let feedDescription: String + + /// The path to the feed in the final website, appended with the page number + let feedUrlPrefix: String +} + +extension LocalizedPostSettingsFile: Codable { } diff --git a/CHDataManagement/Storage/Model/Settings/LocalizedSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/LocalizedSettingsFile.swift new file mode 100644 index 0000000..39c9e0c --- /dev/null +++ b/CHDataManagement/Storage/Model/Settings/LocalizedSettingsFile.swift @@ -0,0 +1,12 @@ + +struct LocalizedSettingsFile { + + let navigationBarIconDescription: String + + let posts: LocalizedPostSettingsFile + +} + +extension LocalizedSettingsFile: Codable { + +} diff --git a/CHDataManagement/Storage/Model/Settings/NavigationBarSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/NavigationBarSettingsFile.swift new file mode 100644 index 0000000..b63c2e7 --- /dev/null +++ b/CHDataManagement/Storage/Model/Settings/NavigationBarSettingsFile.swift @@ -0,0 +1,12 @@ + +struct NavigationBarSettingsFile { + + /// The path to the main icon in the navigation bar + let navigationIconPath: String + + /// The tags to show in the navigation bar + let navigationTags: [String] +} + +extension NavigationBarSettingsFile: Codable { } + diff --git a/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift new file mode 100644 index 0000000..5ba5203 --- /dev/null +++ b/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift @@ -0,0 +1,12 @@ +import Foundation + +struct PostSettingsFile { + + /// The number of posts to show in a single page of the news feed + let postsPerPage: Int + + /// The maximum width of the main content + let contentWidth: CGFloat +} + +extension PostSettingsFile: Codable { } diff --git a/CHDataManagement/Storage/Model/Settings/SettingsFile.swift b/CHDataManagement/Storage/Model/Settings/SettingsFile.swift new file mode 100644 index 0000000..8afae56 --- /dev/null +++ b/CHDataManagement/Storage/Model/Settings/SettingsFile.swift @@ -0,0 +1,17 @@ +import Foundation + +struct SettingsFile { + + /// The file path to the output directory + let outputDirectoryPath: String + + let navigationBar: NavigationBarSettingsFile + + let posts: PostSettingsFile + + let german: LocalizedSettingsFile + + let english: LocalizedSettingsFile +} + +extension SettingsFile: Codable { } diff --git a/CHDataManagement/Storage/Model/WebsiteDataFile.swift b/CHDataManagement/Storage/Model/WebsiteDataFile.swift deleted file mode 100644 index 5c6b6aa..0000000 --- a/CHDataManagement/Storage/Model/WebsiteDataFile.swift +++ /dev/null @@ -1,28 +0,0 @@ -import Foundation - -struct WebsiteDataFile { - - let navigationTags: [String] - - let german: LocalizedWebsiteDataFile - - let english: LocalizedWebsiteDataFile -} - -extension WebsiteDataFile: Codable { - -} - -struct LocalizedWebsiteDataFile { - - let title: String - - let description: String - - let iconDescription: String - -} - -extension LocalizedWebsiteDataFile: Codable { - -} diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index b94f770..73337c1 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -236,17 +236,17 @@ final class Storage { // MARK: Website data - private var websiteDataUrl: URL { - baseFolder.appending(path: "website-data.json", directoryHint: .notDirectory) + private var settingsDataUrl: URL { + baseFolder.appending(path: "settings.json", directoryHint: .notDirectory) } - func loadWebsiteData() throws -> WebsiteDataFile { - try read(at: websiteDataUrl) + func loadSettings() throws -> SettingsFile { + try read(at: settingsDataUrl) } @discardableResult - func save(websiteData: WebsiteDataFile) -> Bool { - write(websiteData, type: "Website Data", id: "-", to: websiteDataUrl) + func save(settings: SettingsFile) -> Bool { + write(settings, type: "Settings", id: "-", to: settingsDataUrl) } // MARK: Image generation data diff --git a/CHDataManagement/Storage/WebsiteGenerator.swift b/CHDataManagement/Storage/WebsiteGenerator.swift index 1e46f85..f2cfe35 100644 --- a/CHDataManagement/Storage/WebsiteGenerator.swift +++ b/CHDataManagement/Storage/WebsiteGenerator.swift @@ -1,71 +1,61 @@ import Foundation -struct WebsiteGeneratorConfiguration { - - let language: ContentLanguage - - let outputDirectory: URL - - let postsPerPage: Int - - let postFeedTitle: String - - let postFeedDescription: String - - let postFeedUrlPrefix: String - - let navigationIconPath: String - - let mainContentMaximumWidth: CGFloat -} - final class WebsiteGenerator { let language: ContentLanguage - let outputDirectory: URL + let localizedSettings: LocalizedSettings - let postsPerPage: Int + private var outputDirectory: URL { + URL(filePath: content.settings.outputDirectoryPath) + } - let postFeedTitle: String + private var postsPerPage: Int { + content.settings.posts.postsPerPage + } - let postFeedDescription: String + private var postFeedTitle: String { + localizedSettings.posts.title + } - let postFeedUrlPrefix: String + private var postFeedDescription: String { + localizedSettings.posts.description + } - let navigationIconPath: String + private var postFeedUrlPrefix: String { + localizedSettings.posts.feedUrlPrefix + } - let mainContentMaximumWidth: CGFloat + private var navigationIconPath: String { + content.settings.navigationBar.iconPath + } + + private var mainContentMaximumWidth: CGFloat { + content.settings.posts.contentWidth + } private let content: Content private let imageGenerator: ImageGenerator - init(content: Content, configuration: WebsiteGeneratorConfiguration) { - self.language = configuration.language - self.outputDirectory = configuration.outputDirectory - self.postsPerPage = configuration.postsPerPage - self.postFeedTitle = configuration.postFeedTitle - self.postFeedDescription = configuration.postFeedDescription - self.postFeedUrlPrefix = configuration.postFeedUrlPrefix - self.navigationIconPath = configuration.navigationIconPath - self.mainContentMaximumWidth = configuration.mainContentMaximumWidth - + init(content: Content, language: ContentLanguage) { + self.language = language self.content = content + self.localizedSettings = content.settings.localized(in: language) self.imageGenerator = ImageGenerator( storage: content.storage, inputImageFolder: content.storage.filesFolder, relativeImageOutputPath: "images") } - func generateWebsite() -> Bool { + func generateWebsite(callback: (String) -> Void) -> Bool { guard imageGenerator.prepareForGeneration() else { return false } guard createPostFeedPages() else { return false } - guard imageGenerator.runJobs() else { + guard imageGenerator.runJobs(callback: callback) else { return false } return imageGenerator.save() @@ -77,7 +67,9 @@ final class WebsiteGenerator { return true } - let navBarData = createNavigationBarData() + let navBarData = createNavigationBarData( + settings: content.settings.navigationBar, + iconDescription: localizedSettings.navigationBarIconDescription) let numberOfPages = (totalCount + postsPerPage - 1) / postsPerPage // Round up for pageIndex in 1...numberOfPages { @@ -91,15 +83,14 @@ final class WebsiteGenerator { return true } - private func createNavigationBarData() -> NavigationBarData { - let data = content.websiteData.localized(in: language) - let navigationItems: [NavigationBarLink] = content.websiteData.navigationTags.map { + private func createNavigationBarData(settings: NavigationBarSettings, iconDescription: String) -> NavigationBarData { + let navigationItems: [NavigationBarLink] = settings.tags.map { let localized = $0.localized(in: language) return .init(text: localized.name, url: localized.urlComponent) } return NavigationBarData( navigationIconPath: navigationIconPath, - iconDescription: data.iconDescription, + iconDescription: iconDescription, navigationItems: navigationItems) } diff --git a/CHDataManagement/Views/Pages/PageListView.swift b/CHDataManagement/Views/Pages/PageListView.swift index 4e8bea1..2c21de2 100644 --- a/CHDataManagement/Views/Pages/PageListView.swift +++ b/CHDataManagement/Views/Pages/PageListView.swift @@ -3,32 +3,108 @@ import SwiftUI struct PageListView: View { @Environment(\.language) - var language + private var language @EnvironmentObject - var content: Content + private var content: Content @State - var selectedPage: Page? + private var selected: Page? + + @State + private var showNewPageView = false + + @State + private var newPageId = "" + + @State + private var newPageIdIsValid = false + + private let allowedCharactersInPageId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted + + private var cleanPageId: String { + newPageId.trimmingCharacters(in: .whitespacesAndNewlines) + } var body: some View { NavigationSplitView { - List(content.pages, selection: $selectedPage) { page in + List(content.pages, selection: $selected) { page in Text(page.localized(in: language).title) .tag(page) - } - } detail: { - // Detail view when an item is selected - if let selectedPage { - PageDetailView(page: selectedPage) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(action: { showNewPageView = true }) { + Label("New post", systemSymbol: .plus) + } + } + } + .navigationSplitViewColumnWidth(min: 300, ideal: 300, max: 300) + } content: { + if let selected { + PageDetailView(page: selected) + .layoutPriority(1) } else { // Fallback if no item is selected - Text("Select a page to show the content.") + Text("Select a page from the list") .font(.largeTitle) .foregroundColor(.secondary) } + } detail: { + if let selected { + EmptyView() + .frame(maxWidth: 350) + } else { + EmptyView() + .frame(maxWidth: 350) + } } + .onAppear { + if selected == nil { + selected = content.pages.first + } + } + .sheet(isPresented: $showNewPageView, + onDismiss: addNewPage) { + TextEntrySheet( + title: "Enter the id for the new page", + text: $newPageId, + isValid: $newPageIdIsValid) + } + } + + private func isValid(id: String) -> Bool { + let id = cleanPageId + guard id != "" else { + return false + } + + guard !content.pages.contains(where: { $0.id == id }) else { + return false + } + // Only allow alphanumeric characters and hyphens + return id.rangeOfCharacter(from: allowedCharactersInPageId) == nil + } + + private func addNewPage() { + let id = cleanPageId + guard isValid(id: id) else { + return + } + + let page = Page( + id: id, + isDraft: true, + createdDate: .now, + startDate: .now, + endDate: nil, + german: .init(urlString: "seite", + title: "Ein Titel"), + english: .init(urlString: "page", + title: "A Title"), + tags: []) + content.pages.insert(page, at: 0) + selected = page } } diff --git a/CHDataManagement/Views/Posts/PostList.swift b/CHDataManagement/Views/Posts/PostList.swift index 462d330..187473e 100644 --- a/CHDataManagement/Views/Posts/PostList.swift +++ b/CHDataManagement/Views/Posts/PostList.swift @@ -8,15 +8,15 @@ struct PostList: View { @Environment(\.language) private var language: ContentLanguage - @State - private var newPostId = "" - @State private var selected: Post? = nil @State private var showNewPostView = false + @State + private var newPostId = "" + @State private var newPostIdIsValid = false @@ -32,7 +32,6 @@ struct PostList: View { Text(post.localized(in: language).title) .tag(post) } - .frame(minWidth: 200) .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: { showNewPostView = true }) { @@ -40,6 +39,7 @@ struct PostList: View { } } } + .navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 250) } content: { if let selected { PostContentView(post: selected) diff --git a/CHDataManagement/Views/Settings/FolderSettingsView.swift b/CHDataManagement/Views/Settings/FolderSettingsView.swift new file mode 100644 index 0000000..7d5f409 --- /dev/null +++ b/CHDataManagement/Views/Settings/FolderSettingsView.swift @@ -0,0 +1,91 @@ +import SwiftUI + +struct FolderSettingsView: View { + + @Environment(\.language) + private var language + + @AppStorage("contentPath") + private var contentPath: String = "" + + @EnvironmentObject + private var content: Content + + @State + private var folderSelection: SecurityScopeBookmark = .contentPath + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + Text("Folder Settings") + .font(.largeTitle) + .bold() + Text("Select the folders for the app to work.") + .padding(.bottom, 30) + Text("Content Folder") + .font(.headline) + .padding(.bottom, 1) + Text(contentPath) + Button(action: selectContentFolder) { + Text("Select folder") + } + .padding(.bottom) + Text("Output Folder") + .font(.headline) + .padding(.bottom, 1) + Text(content.settings.outputDirectoryPath) + Button(action: selectOutputFolder) { + Text("Select folder") + } + .padding(.bottom) + + } + } + } + + // MARK: Folder selection + + private func selectContentFolder() { + folderSelection = .contentPath + guard let url = savePanelUsingOpenPanel() else { + return + } + self.contentPath = url.path() + } + + private func selectOutputFolder() { + folderSelection = .outputPath + guard let url = savePanelUsingOpenPanel() else { + return + } + content.settings.outputDirectoryPath = url.path() + } + + private func savePanelUsingOpenPanel() -> URL? { + let panel = NSOpenPanel() + // Sets up so user can only select a single directory + panel.canChooseFiles = false + panel.canChooseDirectories = true + panel.allowsMultipleSelection = false + panel.showsHiddenFiles = false + panel.title = "Select Save Directory" + panel.prompt = "Select Save Directory" + + let response = panel.runModal() + guard response == .OK else { + + return nil + } + guard let url = panel.url else { + return nil + } + content.storage.save(folderUrl: url, in: folderSelection) + return url + } +} + +#Preview { + FolderSettingsView() + .environmentObject(Content.mock) + .padding() +} diff --git a/CHDataManagement/Views/Settings/GenerationSettingsView.swift b/CHDataManagement/Views/Settings/GenerationSettingsView.swift new file mode 100644 index 0000000..3a9175c --- /dev/null +++ b/CHDataManagement/Views/Settings/GenerationSettingsView.swift @@ -0,0 +1,76 @@ +import SwiftUI + +struct GenerationSettingsView: View { + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @State + private var isGeneratingWebsite = false + + @State + private var generatorText: String = "" + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + Text("Website Generation") + .font(.largeTitle) + .bold() + Text("Regenerate the website and monitor the output") + .padding(.bottom, 30) + + HStack { + Button(action: generateFeed) { + Text("Generate") + } + .disabled(isGeneratingWebsite) + if isGeneratingWebsite { + ProgressView() + .progressViewStyle(.circular) + .frame(height: 25) + } + Text(generatorText) + Spacer() + } + } + } + } + + private func generateFeed() { + guard content.settings.outputDirectoryPath != "" else { + print("Invalid output path") + return + } + let url = URL(fileURLWithPath: content.settings.outputDirectoryPath) + + guard FileManager.default.fileExists(atPath: url.path) else { + print("Missing output folder") + return + } + isGeneratingWebsite = true + DispatchQueue.global(qos: .userInitiated).async { + let generator = WebsiteGenerator( + content: content, + language: language) + _ = generator.generateWebsite { text in + DispatchQueue.main.async { + self.generatorText = text + } + } + DispatchQueue.main.async { + isGeneratingWebsite = false + self.generatorText = "Generation complete" + } + } + } +} + +#Preview { + GenerationSettingsView() + .environmentObject(Content.mock) + .padding() +} diff --git a/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift b/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift new file mode 100644 index 0000000..dc9fc61 --- /dev/null +++ b/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift @@ -0,0 +1,44 @@ +import SwiftUI + +struct LocalizedPostFeedSettingsView: View { + + @ObservedObject + var settings: LocalizedPostSettings + + var body: some View { + VStack(alignment: .leading) { + Text("Title") + .font(.headline) + TextField("", text: $settings.title) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: 400) + Text("The title of all post feed pages.") + .padding(.bottom) + Text("URL prefix") + .font(.headline) + TextField("", text: $settings.feedUrlPrefix) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: 400) + Text("The prefix to generate the urls for all post feed pages.") + .padding(.bottom) + Text("Description") + .font(.headline) + TextEditor(text: $settings.description) + .font(.body) + .lineLimit(5, reservesSpace: true) + .frame(maxWidth: 400, minHeight: 50, maxHeight: 500) + .textEditorStyle(.plain) + .padding(.vertical, 8) + .padding(.leading, 3) + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) + Text("The description of all post feed pages.") + .padding(.bottom) + } + } +} + +#Preview { + LocalizedPostFeedSettingsView(settings: .english) + .padding() +} diff --git a/CHDataManagement/Views/Settings/LocalizedSettingsView.swift b/CHDataManagement/Views/Settings/LocalizedSettingsView.swift deleted file mode 100644 index 78bb568..0000000 --- a/CHDataManagement/Views/Settings/LocalizedSettingsView.swift +++ /dev/null @@ -1,25 +0,0 @@ -import SwiftUI - -struct LocalizedSettingsView: View { - - @ObservedObject - var settings: LocalizedWebsiteData - - var body: some View { - VStack(alignment: .leading) { - Text("Title") - .font(.headline) - TextField("", text: $settings.title) - Text("Description") - .font(.headline) - TextField("", text: $settings.description) - Text("Icon description") - .font(.headline) - TextField("", text: $settings.iconDescription) - } - } -} - -#Preview { - LocalizedSettingsView(settings: .english) -} diff --git a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift new file mode 100644 index 0000000..0dc2991 --- /dev/null +++ b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift @@ -0,0 +1,84 @@ +import SwiftUI + +private struct IconDescriptionView: View { + + @ObservedObject + var settings: LocalizedSettings + + var body: some View { + TextField("", text: $settings.navigationBarIconDescription) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: 300) + } +} + +struct NavigationBarSettingsView: View { + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @State + private var showTagPicker = false + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + Text("Notification Bar Settings") + .font(.largeTitle) + .bold() + Text("Customize the navigation bar for all pages at the top of the website") + .padding(.bottom, 30) + Text("Icon Path") + .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.") + .padding(.bottom, 30) + Text("Icon Description") + .font(.headline) + IconDescriptionView(settings: content.settings.localized(in: language)) + Text("Provide a description of the icon for screen readers.") + .padding(.bottom, 30) + Text("Visible Tags") + .font(.headline) + FlowHStack { + ForEach(content.settings.navigationBar.tags, id: \.id) { tag in + TagView(tag: .init( + en: tag.english.name, + de: tag.german.name) + ) + .foregroundStyle(.white) + } + Button(action: { showTagPicker = true }) { + Image(systemSymbol: .squareAndPencilCircleFill) + .resizable() + .aspectRatio(1, contentMode: .fit) + .frame(height: 22) + .foregroundColor(Color.gray) + .background(Circle() + .fill(Color.white) + .padding(1)) + } + .buttonStyle(.plain) + } + Text("Select the tags to show in the navigation bar. The number should be even.") + } + } + .sheet(isPresented: $showTagPicker) { + TagSelectionView( + presented: $showTagPicker, + selected: $content.settings.navigationBar.tags, + tags: $content.tags) + } + } +} + +#Preview { + NavigationBarSettingsView() + .environmentObject(Content.mock) + .padding() +} diff --git a/CHDataManagement/Views/Settings/PostFeedSettingsView.swift b/CHDataManagement/Views/Settings/PostFeedSettingsView.swift new file mode 100644 index 0000000..b1abf3b --- /dev/null +++ b/CHDataManagement/Views/Settings/PostFeedSettingsView.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct PostFeedSettingsView: View { + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + Text("Post Feed Settings") + .font(.largeTitle) + .bold() + Text("Change the way the posts are displayed") + .padding(.bottom, 30) + LocalizedPostFeedSettingsView(settings: content.settings.localized(in: language).posts) + } + } + } +} + + +#Preview { + PostFeedSettingsView() + .environmentObject(Content.mock) + .padding() +} diff --git a/CHDataManagement/Views/Settings/SectionedSettingsView.swift b/CHDataManagement/Views/Settings/SectionedSettingsView.swift new file mode 100644 index 0000000..6e6fcc3 --- /dev/null +++ b/CHDataManagement/Views/Settings/SectionedSettingsView.swift @@ -0,0 +1,81 @@ +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: { + DetailView(section: selectedSection) + } + } +} + + +struct DetailView: View { + + let section: SettingsSection? + + var body: some View { + Group { + switch section { + case .generation: + GenerationSettingsView() + case .folders: + FolderSettingsView() + case .navigationBar: + NavigationBarSettingsView() + case .postFeed: + PostFeedSettingsView() + case .none: + Text("Select a setting from the sidebar") + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .padding() + .navigationTitle(section?.rawValue ?? "") + } +} + + +struct AppearanceView: View { + var body: some View { + VStack(alignment: .leading) { + Text("Appearance Settings") + .font(.largeTitle) + .bold() + Text("Customize the look and feel of the app.") + } + } +} + +struct NotificationsView: View { + var body: some View { + VStack(alignment: .leading) { + Text("Notifications Settings") + .font(.largeTitle) + .bold() + Text("Manage your notification preferences.") + } + } +} + +struct PrivacyView: View { + var body: some View { + VStack(alignment: .leading) { + Text("Privacy Settings") + .font(.largeTitle) + .bold() + Text("Configure your privacy and security settings.") + } + } +} + +#Preview { + SectionedSettingsView() +} diff --git a/CHDataManagement/Views/Settings/SettingsSection.swift b/CHDataManagement/Views/Settings/SettingsSection.swift new file mode 100644 index 0000000..d0b0626 --- /dev/null +++ b/CHDataManagement/Views/Settings/SettingsSection.swift @@ -0,0 +1,34 @@ +import SFSafeSymbols + +enum SettingsSection: String { + + case generation = "Generation" + + case folders = "Folders" + + case navigationBar = "Navigation Bar" + + case postFeed = "Post Feed" + +} + +extension SettingsSection { + + var icon: SFSymbol { + switch self { + case .generation: return .arrowTriangle2Circlepath + case .folders: return .folder + case .navigationBar: return .menubarRectangle + case .postFeed: return .rectangleGrid1x2 + } + } +} + +extension SettingsSection: CaseIterable { + +} + +extension SettingsSection: Identifiable { + + var id: String { rawValue } +} diff --git a/CHDataManagement/Views/Settings/SettingsSidebar.swift b/CHDataManagement/Views/Settings/SettingsSidebar.swift new file mode 100644 index 0000000..5296fba --- /dev/null +++ b/CHDataManagement/Views/Settings/SettingsSidebar.swift @@ -0,0 +1,15 @@ +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") + } +} diff --git a/CHDataManagement/Views/Settings/SettingsView.swift b/CHDataManagement/Views/Settings/SettingsView.swift deleted file mode 100644 index e08de64..0000000 --- a/CHDataManagement/Views/Settings/SettingsView.swift +++ /dev/null @@ -1,170 +0,0 @@ -import SwiftUI - -struct SettingsView: View { - - @Environment(\.language) - var language - - @AppStorage("contentPath") - var contentPath: String = "" - - @AppStorage("outputPath") - var outputPath: String = "" - - @EnvironmentObject - var content: Content - - @State - private var folderSelection: SecurityScopeBookmark = .contentPath - - @State - private var showTagPicker = false - - @State - private var isGeneratingWebsite = false - - var body: some View { - ScrollView { - VStack(alignment: .leading) { - Text("Content Folder") - .font(.headline) - TextField("Content Folder", text: $contentPath) - Button(action: selectContentFolder) { - Text("Select folder") - } - Text("Output Folder") - .font(.headline) - TextField("Output Folder", text: $outputPath) - Button(action: selectOutputFolder) { - Text("Select folder") - } - Text("Navigation Bar Items") - .font(.headline) - FlowHStack { - ForEach(content.websiteData.navigationTags, id: \.id) { tag in - TagView(tag: .init( - en: tag.english.name, - de: tag.german.name) - ) - .foregroundStyle(.white) - } - Button(action: { showTagPicker = true }) { - Image(systemSymbol: .squareAndPencilCircleFill) - .resizable() - .aspectRatio(1, contentMode: .fit) - .frame(height: 22) - .foregroundColor(Color.gray) - .background(Circle() - .fill(Color.white) - .padding(1)) - } - .buttonStyle(.plain) - } - LocalizedSettingsView(settings: content.websiteData.localized(in: language)) - Text("Feed") - .font(.headline) - HStack { - Button(action: generateFeed) { - Text("Generate") - } - .disabled(isGeneratingWebsite) - if isGeneratingWebsite { - ProgressView() - .progressViewStyle(.circular) - .frame(height: 25) - } - Spacer() - } - } - .padding() - } - .sheet(isPresented: $showTagPicker) { - TagSelectionView( - presented: $showTagPicker, - selected: $content.websiteData.navigationTags, - tags: $content.tags) - } - } - - // MARK: Folder selection - - private func selectContentFolder() { - folderSelection = .contentPath - guard let url = savePanelUsingOpenPanel() else { - return - } - self.contentPath = url.path() - } - - private func selectOutputFolder() { - folderSelection = .outputPath - guard let url = savePanelUsingOpenPanel() else { - return - } - self.outputPath = url.path() - } - - // MARK: Feed - - private func generateFeed() { - guard outputPath != "" else { - print("Invalid output path") - return - } - let url = URL(fileURLWithPath: outputPath) - - guard FileManager.default.fileExists(atPath: url.path) else { - print("Missing output folder") - return - } - isGeneratingWebsite = true - DispatchQueue.global(qos: .userInitiated).async { - let generator = WebsiteGenerator( - content: content, - configuration: configuration) - _ = generator.generateWebsite() - DispatchQueue.main.async { - isGeneratingWebsite = false - } - } - } - - private var configuration: WebsiteGeneratorConfiguration { - return .init( - language: language, - outputDirectory: URL(filePath: outputPath, directoryHint: .isDirectory), - postsPerPage: 20, - postFeedTitle: "Posts", - postFeedDescription: "The most recent posts on christophhagen.de", - postFeedUrlPrefix: "feed", - navigationIconPath: "/assets/icons/ch.svg", - mainContentMaximumWidth: 600) - } - - func savePanelUsingOpenPanel() -> URL? { - let panel = NSOpenPanel() - // Sets up so user can only select a single directory - panel.canChooseFiles = false - panel.canChooseDirectories = true - panel.allowsMultipleSelection = false - panel.showsHiddenFiles = false - panel.title = "Select Save Directory" - panel.prompt = "Select Save Directory" - - let response = panel.runModal() - guard response == .OK else { - - return nil - } - guard let url = panel.url else { - return nil - } - content.storage.save(folderUrl: url, in: folderSelection) - return url - } -} - -#Preview { - SettingsView() - .environmentObject(Content.mock) -}