diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index f8a7a01..17b0045 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850082CEE01BF0090B18B /* PagePickerView.swift */; }; E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500A2CEE02FA0090B18B /* Content+Mock.swift */; }; - E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218500C2CEE07140090B18B /* ColorPalette.swift */; }; E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850142CEE55D40090B18B /* FileOnDisk.swift */; }; E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; }; E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850182CEE561B0090B18B /* PageOnDisk.swift */; }; @@ -37,7 +36,7 @@ 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 /* ImagesContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50E2CFDD76B00AEF16D /* ImagesContentView.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 */; }; @@ -47,7 +46,7 @@ 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 */; }; + 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 */; }; @@ -69,7 +68,7 @@ E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */; }; E25DA57D2D01C67900AEF16D /* Ink in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA57C2D01C67900AEF16D /* Ink */; }; E25DA5802D01C6AC00AEF16D /* Splash in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA57F2D01C6AC00AEF16D /* Splash */; }; - E25DA5832D01C7A400AEF16D /* VideoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5822D01C7A100AEF16D /* VideoType.swift */; }; + E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5822D01C7A100AEF16D /* VideoFileType.swift */; }; E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */; }; E25DA5872D01CA9300AEF16D /* GenerationResultsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5862D01CA8D00AEF16D /* GenerationResultsHandler.swift */; }; E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */; }; @@ -87,35 +86,47 @@ 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 */; }; - E29D312A2D039B090051B7F4 /* ImageDescriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31292D039B050051B7F4 /* ImageDescriptions.swift */; }; + E29D312A2D039B090051B7F4 /* FileDescriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31292D039B050051B7F4 /* FileDescriptions.swift */; }; E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D312B2D039DB30051B7F4 /* PageDetailView.swift */; }; E29D312E2D03A0D70051B7F4 /* LocalizedPageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D312D2D03A0CF0051B7F4 /* LocalizedPageDetailView.swift */; }; E29D31302D03A2C50051B7F4 /* DescriptionField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */; }; E29D31322D03B5680051B7F4 /* LocalizedPostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31312D03B5610051B7F4 /* LocalizedPostDetailView.swift */; }; E29D31342D03B5D50051B7F4 /* IconButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31332D03B5D30051B7F4 /* IconButton.swift */; }; - E2A21C012CB16A820060935B /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C002CB16A820060935B /* PostView.swift */; }; + E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31352D04353F0051B7F4 /* TabSelection.swift */; }; + E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D313A2D0446490051B7F4 /* LocalizedTagDetailView.swift */; }; + E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D313C2D047C150051B7F4 /* LocalizedPageContentView.swift */; }; + E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D313E2D04822C0051B7F4 /* AddPostView.swift */; }; + E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31402D04887D0051B7F4 /* SelectedDetailView.swift */; }; + E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31422D0488950051B7F4 /* MainContentView.swift */; }; + E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31442D0488CB0051B7F4 /* SelectedContentView.swift */; }; + E29D31472D04892E0051B7F4 /* FileListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31462D04892B0051B7F4 /* FileListView.swift */; }; + E29D31492D0489BB0051B7F4 /* AddFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31482D0489B80051B7F4 /* AddFileView.swift */; }; + E29D314B2D04FC950051B7F4 /* FileToAdd.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D314A2D04FC940051B7F4 /* FileToAdd.swift */; }; + E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D314C2D04FCBF0051B7F4 /* FileToAddView.swift */; }; + E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31502D0616890051B7F4 /* PostListView.swift */; }; + E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31522D0618700051B7F4 /* AddPageView.swift */; }; + E29D31552D06D2CE0051B7F4 /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31542D06D2CB0051B7F4 /* TagListView.swift */; }; + E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31562D06D3880051B7F4 /* AddTagView.swift */; }; + E29D315B2D06D63E0051B7F4 /* ModelFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D315A2D06D63C0051B7F4 /* ModelFileType.swift */; }; + E29D315D2D06D6EA0051B7F4 /* TextFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D315C2D06D6EA0051B7F4 /* TextFileType.swift */; }; + E29D315F2D06D6F30051B7F4 /* CodeFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D315E2D06D6F30051B7F4 /* CodeFileType.swift */; }; + E29D31612D06D95C0051B7F4 /* ResourceFileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31602D06D9570051B7F4 /* ResourceFileType.swift */; }; + E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */; }; + E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31682D0702670051B7F4 /* TextFileContentView.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 */; }; E2A21C082CB17B870060935B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C072CB17B810060935B /* TagView.swift */; }; E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0D2CB189D70060935B /* Color+RGB.swift */; }; E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0F2CB18B390060935B /* FlowHStack.swift */; }; E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C112CB18D520060935B /* DatePickerView.swift */; }; - E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C152CB1A3C60060935B /* PostImageGalleryView.swift */; }; E2A21C202CB28ED20060935B /* MockImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C1F2CB28ED20060935B /* MockImage.swift */; }; E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C272CB29B290060935B /* FeedEntryData.swift */; }; E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C292CB2AA4C0060935B /* Post+Mock.swift */; }; - E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C2B2CB2BB210060935B /* PostList.swift */; }; E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */; }; E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C312CB5BCAC0060935B /* PageContentView.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 */; }; - E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C4C2CBB16B50060935B /* ImagesView.swift */; }; - E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */; }; E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C502CBBD53C0060935B /* FileResource.swift */; }; - E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C532CBBF87A0060935B /* FilesView.swift */; }; - E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */; }; E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25A0B882CE4021400F33674 /* LocalizedPage.swift */; }; E2A37D0E2CE527070000979F /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D0D2CE527040000979F /* Storage.swift */; }; E2A37D112CE537800000979F /* PageFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D102CE537670000979F /* PageFile.swift */; }; @@ -127,7 +138,6 @@ E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */; }; E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */; }; E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D242CEBD7A10000979F /* PageListView.swift */; }; - E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D282CED2C6A0000979F /* TagsListView.swift */; }; E2A37D2B2CED2CC30000979F /* TagDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D2A2CED2CC30000979F /* TagDetailView.swift */; }; E2A37D2D2CED2EF10000979F /* OptionalTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */; }; E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; }; @@ -138,7 +148,7 @@ E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F422C4294F60047CD0C /* FeedEntry.swift */; }; E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F442C429ED60047CD0C /* ImageGallery.swift */; }; E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */; }; - E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */; }; + E2DD04742C276F31003BFF1F /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DD04732C276F31003BFF1F /* MainView.swift */; }; E2DD047A2C276F32003BFF1F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2DD04792C276F32003BFF1F /* Assets.xcassets */; }; E2DD047E2C276F32003BFF1F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */; }; E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E06DFA2CA4A6570019C2AF /* Content.swift */; }; @@ -148,7 +158,6 @@ /* Begin PBXFileReference section */ E21850082CEE01BF0090B18B /* PagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePickerView.swift; sourceTree = ""; }; E218500A2CEE02FA0090B18B /* Content+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Mock.swift"; sourceTree = ""; }; - E218500C2CEE07140090B18B /* ColorPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPalette.swift; sourceTree = ""; }; E21850142CEE55D40090B18B /* FileOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOnDisk.swift; sourceTree = ""; }; E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = ""; }; E21850182CEE561B0090B18B /* PageOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageOnDisk.swift; sourceTree = ""; }; @@ -176,7 +185,7 @@ E25DA5082CFD964E00AEF16D /* TagContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentView.swift; sourceTree = ""; }; E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTagAssignmentView.swift; sourceTree = ""; }; E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTagAssignmentView.swift; sourceTree = ""; }; - E25DA50E2CFDD76B00AEF16D /* ImagesContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesContentView.swift; sourceTree = ""; }; + E25DA50E2CFDD76B00AEF16D /* ImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContentView.swift; sourceTree = ""; }; E25DA5142CFF00B900AEF16D /* Content+Load.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Load.swift"; sourceTree = ""; }; E25DA5162CFF00F200AEF16D /* Content+Save.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Save.swift"; sourceTree = ""; }; E25DA5182CFF035200AEF16D /* Array+Split.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Split.swift"; sourceTree = ""; }; @@ -186,7 +195,7 @@ 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 = ""; }; + E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFileType.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 = ""; }; @@ -204,7 +213,7 @@ E25DA5742D018B6100AEF16D /* FileDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDetailView.swift; sourceTree = ""; }; E25DA5762D018B9500AEF16D /* File+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "File+Mock.swift"; sourceTree = ""; }; E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentGenerator.swift; sourceTree = ""; }; - E25DA5822D01C7A100AEF16D /* VideoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoType.swift; sourceTree = ""; }; + E25DA5822D01C7A100AEF16D /* VideoFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoFileType.swift; sourceTree = ""; }; E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShorthandMarkdownKey.swift; sourceTree = ""; }; E25DA5862D01CA8D00AEF16D /* GenerationResultsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResultsHandler.swift; sourceTree = ""; }; E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Generation.swift"; sourceTree = ""; }; @@ -222,35 +231,47 @@ E29D31232D0366820051B7F4 /* TagList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagList.swift; sourceTree = ""; }; E29D31252D0370A50051B7F4 /* VideoOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOption.swift; sourceTree = ""; }; E29D31272D0371870051B7F4 /* ContentPageVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPageVideo.swift; sourceTree = ""; }; - E29D31292D039B050051B7F4 /* ImageDescriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDescriptions.swift; sourceTree = ""; }; + E29D31292D039B050051B7F4 /* FileDescriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDescriptions.swift; sourceTree = ""; }; E29D312B2D039DB30051B7F4 /* PageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageDetailView.swift; sourceTree = ""; }; E29D312D2D03A0CF0051B7F4 /* LocalizedPageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageDetailView.swift; sourceTree = ""; }; E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionField.swift; sourceTree = ""; }; E29D31312D03B5610051B7F4 /* LocalizedPostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostDetailView.swift; sourceTree = ""; }; E29D31332D03B5D30051B7F4 /* IconButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconButton.swift; sourceTree = ""; }; - E2A21C002CB16A820060935B /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = ""; }; + E29D31352D04353F0051B7F4 /* TabSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSelection.swift; sourceTree = ""; }; + E29D313A2D0446490051B7F4 /* LocalizedTagDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedTagDetailView.swift; sourceTree = ""; }; + E29D313C2D047C150051B7F4 /* LocalizedPageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageContentView.swift; sourceTree = ""; }; + E29D313E2D04822C0051B7F4 /* AddPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPostView.swift; sourceTree = ""; }; + E29D31402D04887D0051B7F4 /* SelectedDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedDetailView.swift; sourceTree = ""; }; + E29D31422D0488950051B7F4 /* MainContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainContentView.swift; sourceTree = ""; }; + E29D31442D0488CB0051B7F4 /* SelectedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedContentView.swift; sourceTree = ""; }; + E29D31462D04892B0051B7F4 /* FileListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListView.swift; sourceTree = ""; }; + E29D31482D0489B80051B7F4 /* AddFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFileView.swift; sourceTree = ""; }; + E29D314A2D04FC940051B7F4 /* FileToAdd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileToAdd.swift; sourceTree = ""; }; + E29D314C2D04FCBF0051B7F4 /* FileToAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileToAddView.swift; sourceTree = ""; }; + E29D31502D0616890051B7F4 /* PostListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostListView.swift; sourceTree = ""; }; + E29D31522D0618700051B7F4 /* AddPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPageView.swift; sourceTree = ""; }; + E29D31542D06D2CB0051B7F4 /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.swift; sourceTree = ""; }; + E29D31562D06D3880051B7F4 /* AddTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddTagView.swift; sourceTree = ""; }; + E29D315A2D06D63C0051B7F4 /* ModelFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelFileType.swift; sourceTree = ""; }; + E29D315C2D06D6EA0051B7F4 /* TextFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileType.swift; sourceTree = ""; }; + E29D315E2D06D6F30051B7F4 /* CodeFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeFileType.swift; sourceTree = ""; }; + E29D31602D06D9570051B7F4 /* ResourceFileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceFileType.swift; sourceTree = ""; }; + E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationIcon.swift; sourceTree = ""; }; + E29D31682D0702670051B7F4 /* TextFileContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFileContentView.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 = ""; }; E2A21C072CB17B810060935B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; E2A21C0D2CB189D70060935B /* Color+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+RGB.swift"; sourceTree = ""; }; E2A21C0F2CB18B390060935B /* FlowHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowHStack.swift; sourceTree = ""; }; E2A21C112CB18D520060935B /* DatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerView.swift; sourceTree = ""; }; - E2A21C152CB1A3C60060935B /* PostImageGalleryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImageGalleryView.swift; sourceTree = ""; }; E2A21C1F2CB28ED20060935B /* MockImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImage.swift; sourceTree = ""; }; E2A21C272CB29B290060935B /* FeedEntryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntryData.swift; sourceTree = ""; }; E2A21C292CB2AA4C0060935B /* Post+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+Mock.swift"; sourceTree = ""; }; - 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 /* PageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageContentView.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 = ""; }; - E2A21C4C2CBB16B50060935B /* ImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesView.swift; sourceTree = ""; }; - E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDetailsView.swift; sourceTree = ""; }; E2A21C502CBBD53C0060935B /* FileResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResource.swift; sourceTree = ""; }; - E2A21C532CBBF87A0060935B /* FilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesView.swift; sourceTree = ""; }; - E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleColumnView.swift; sourceTree = ""; }; E2A37D0D2CE527040000979F /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; E2A37D102CE537670000979F /* PageFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageFile.swift; sourceTree = ""; }; E2A37D142CE68BEA0000979F /* PostFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFile.swift; sourceTree = ""; }; @@ -261,7 +282,6 @@ E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = ""; }; E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Sorted.swift"; sourceTree = ""; }; E2A37D242CEBD7A10000979F /* PageListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListView.swift; sourceTree = ""; }; - E2A37D282CED2C6A0000979F /* TagsListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsListView.swift; sourceTree = ""; }; E2A37D2A2CED2CC30000979F /* TagDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDetailView.swift; sourceTree = ""; }; E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextField.swift; sourceTree = ""; }; E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; }; @@ -272,7 +292,7 @@ E2B85F442C429ED60047CD0C /* ImageGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGallery.swift; sourceTree = ""; }; E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Extension.swift"; sourceTree = ""; }; E2DD04702C276F31003BFF1F /* CHDataManagement.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CHDataManagement.app; sourceTree = BUILT_PRODUCTS_DIR; }; - E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CHDataManagementApp.swift; sourceTree = ""; }; + E2DD04732C276F31003BFF1F /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; E2DD04792C276F32003BFF1F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; E2DD047B2C276F32003BFF1F /* CHDataManagement.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CHDataManagement.entitlements; sourceTree = ""; }; E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -311,7 +331,7 @@ E25DA5112CFF001900AEF16D /* Model */ = { isa = PBXGroup; children = ( - E29D31292D039B050051B7F4 /* ImageDescriptions.swift */, + E29D31292D039B050051B7F4 /* FileDescriptions.swift */, E25DA5322D0041C400AEF16D /* Settings */, E21850142CEE55D40090B18B /* FileOnDisk.swift */, E2A37D102CE537670000979F /* PageFile.swift */, @@ -365,9 +385,13 @@ E25DA5812D01C79800AEF16D /* Types */ = { isa = PBXGroup; children = ( + E29D31602D06D9570051B7F4 /* ResourceFileType.swift */, + E29D315E2D06D6F30051B7F4 /* CodeFileType.swift */, + E29D315C2D06D6EA0051B7F4 /* TextFileType.swift */, + E29D315A2D06D63C0051B7F4 /* ModelFileType.swift */, E21850162CEE55FB0090B18B /* FileType.swift */, - E25DA5282CFFBFB800AEF16D /* ImageType.swift */, - E25DA5822D01C7A100AEF16D /* VideoType.swift */, + E25DA5282CFFBFB800AEF16D /* ImageFileType.swift */, + E25DA5822D01C7A100AEF16D /* VideoFileType.swift */, ); path = Types; sourceTree = ""; @@ -383,13 +407,27 @@ path = ContentElements; sourceTree = ""; }; + E29D31372D043EB80051B7F4 /* Main */ = { + isa = PBXGroup; + children = ( + E29D31422D0488950051B7F4 /* MainContentView.swift */, + E2DD04732C276F31003BFF1F /* MainView.swift */, + E29D31442D0488CB0051B7F4 /* SelectedContentView.swift */, + E29D31402D04887D0051B7F4 /* SelectedDetailView.swift */, + E29D31352D04353F0051B7F4 /* TabSelection.swift */, + ); + path = Main; + sourceTree = ""; + }; E2A21C322CB5BCAC0060935B /* Pages */ = { isa = PBXGroup; children = ( - E29D312D2D03A0CF0051B7F4 /* LocalizedPageDetailView.swift */, - E29D312B2D039DB30051B7F4 /* PageDetailView.swift */, - E2A21C312CB5BCAC0060935B /* PageContentView.swift */, E2A37D242CEBD7A10000979F /* PageListView.swift */, + E2A21C312CB5BCAC0060935B /* PageContentView.swift */, + E29D312B2D039DB30051B7F4 /* PageDetailView.swift */, + E29D31522D0618700051B7F4 /* AddPageView.swift */, + E29D312D2D03A0CF0051B7F4 /* LocalizedPageDetailView.swift */, + E29D313C2D047C150051B7F4 /* LocalizedPageContentView.swift */, ); path = Pages; sourceTree = ""; @@ -413,6 +451,7 @@ E2A21C372CB9A4F10060935B /* Generic */ = { isa = PBXGroup; children = ( + E29D31622D06E95A0051B7F4 /* NavigationIcon.swift */, E29D31332D03B5D30051B7F4 /* IconButton.swift */, E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */, E25DA5902D023A7E00AEF16D /* IntegerField.swift */, @@ -427,10 +466,7 @@ E2A21C492CBB168F0060935B /* Images */ = { isa = PBXGroup; children = ( - E2A21C4C2CBB16B50060935B /* ImagesView.swift */, - E25DA50E2CFDD76B00AEF16D /* ImagesContentView.swift */, - E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */, - E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */, + E25DA50E2CFDD76B00AEF16D /* ImageContentView.swift */, ); path = Images; sourceTree = ""; @@ -438,9 +474,13 @@ E2A21C522CBBF86D0060935B /* Files */ = { isa = PBXGroup; children = ( - E2A21C532CBBF87A0060935B /* FilesView.swift */, + E29D31462D04892B0051B7F4 /* FileListView.swift */, E25DA5722D018AA100AEF16D /* FileContentView.swift */, E25DA5742D018B6100AEF16D /* FileDetailView.swift */, + E29D31482D0489B80051B7F4 /* AddFileView.swift */, + E29D31682D0702670051B7F4 /* TextFileContentView.swift */, + E29D314A2D04FC940051B7F4 /* FileToAdd.swift */, + E29D314C2D04FCBF0051B7F4 /* FileToAddView.swift */, ); path = Files; sourceTree = ""; @@ -457,10 +497,12 @@ E2A9CB7F2C7E686C005C89CC /* Tags */ = { isa = PBXGroup; children = ( - E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */, - E2A37D282CED2C6A0000979F /* TagsListView.swift */, - E2A37D2A2CED2CC30000979F /* TagDetailView.swift */, + E29D31562D06D3880051B7F4 /* AddTagView.swift */, + E29D31542D06D2CB0051B7F4 /* TagListView.swift */, E25DA5082CFD964E00AEF16D /* TagContentView.swift */, + E2A37D2A2CED2CC30000979F /* TagDetailView.swift */, + E29D313A2D0446490051B7F4 /* LocalizedTagDetailView.swift */, + E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */, E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */, ); path = Tags; @@ -469,7 +511,6 @@ E2B85F392C428F020047CD0C /* Model */ = { isa = PBXGroup; children = ( - E25DA59A2D024A2900AEF16D /* DateItem.swift */, E25DA5812D01C79800AEF16D /* Types */, E25DA53B2D0042EA00AEF16D /* Settings */, E2E06DFA2CA4A6570019C2AF /* Content.swift */, @@ -478,9 +519,8 @@ E25DA5142CFF00B900AEF16D /* Content+Load.swift */, E21850302CFAF8840090B18B /* Content+Import.swift */, E24252092C52C9260029FF16 /* ContentLanguage.swift */, + E25DA59A2D024A2900AEF16D /* DateItem.swift */, E2A21C502CBBD53C0060935B /* FileResource.swift */, - E2A21C3A2CB9D9A50060935B /* ImageResource.swift */, - E2A21C042CB176670060935B /* LocalizedText.swift */, E2B85F3A2C428F0D0047CD0C /* Post.swift */, E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */, E2581DEC2C75202400F1F079 /* Tag.swift */, @@ -521,14 +561,13 @@ E2B85F462C42C7CA0047CD0C /* Views */ = { isa = PBXGroup; children = ( - E218500C2CEE07140090B18B /* ColorPalette.swift */, - E2A21C522CBBF86D0060935B /* Files */, - E2A21C492CBB168F0060935B /* Images */, E2A21C372CB9A4F10060935B /* Generic */, - E2A21C342CB9A3CA0060935B /* Settings */, + E2B85F4B2C4B8B7F0047CD0C /* Posts */, E2A21C322CB5BCAC0060935B /* Pages */, E2A9CB7F2C7E686C005C89CC /* Tags */, - E2B85F4B2C4B8B7F0047CD0C /* Posts */, + E2A21C522CBBF86D0060935B /* Files */, + E2A21C492CBB168F0060935B /* Images */, + E2A21C342CB9A3CA0060935B /* Settings */, ); path = Views; sourceTree = ""; @@ -536,17 +575,16 @@ E2B85F4B2C4B8B7F0047CD0C /* Posts */ = { isa = PBXGroup; children = ( + E29D31502D0616890051B7F4 /* PostListView.swift */, E218502A2CF790AC0090B18B /* PostContentView.swift */, + E21850262CF3B42D0090B18B /* PostDetailView.swift */, + E29D313E2D04822C0051B7F4 /* AddPostView.swift */, E21850222CF10C840090B18B /* TagSelectionView.swift */, E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */, E21850082CEE01BF0090B18B /* PagePickerView.swift */, E2A21C112CB18D520060935B /* DatePickerView.swift */, - E2A21C152CB1A3C60060935B /* PostImageGalleryView.swift */, - E2A21C2B2CB2BB210060935B /* PostList.swift */, - E2A21C002CB16A820060935B /* PostView.swift */, E2A21C072CB17B810060935B /* TagView.swift */, E21850242CF38BCE0090B18B /* TextEntrySheet.swift */, - E21850262CF3B42D0090B18B /* PostDetailView.swift */, E29D31312D03B5610051B7F4 /* LocalizedPostDetailView.swift */, E218502C2CF791440090B18B /* PostImagesView.swift */, ); @@ -588,7 +626,7 @@ E2DD04722C276F31003BFF1F /* CHDataManagement */ = { isa = PBXGroup; children = ( - E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */, + E29D31372D043EB80051B7F4 /* Main */, E25DA5782D01C56200AEF16D /* Generator */, E2A37D0F2CE5375E0000979F /* Storage */, E2B85F392C428F020047CD0C /* Model */, @@ -710,9 +748,9 @@ E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */, E218502B2CF790B30090B18B /* PostContentView.swift in Sources */, E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */, - E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */, + E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */, E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */, - E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */, + E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */, E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */, E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */, E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */, @@ -725,18 +763,22 @@ E25DA53A2D00424000AEF16D /* LocalizedSettingsFile.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, E21850172CEE55FC0090B18B /* FileType.swift in Sources */, + E29D31612D06D95C0051B7F4 /* ResourceFileType.swift in Sources */, E2A37D112CE537800000979F /* PageFile.swift in Sources */, E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */, E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */, E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */, E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.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 */, - E25DA5832D01C7A400AEF16D /* VideoType.swift in Sources */, + E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */, + E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */, + E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */, E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */, E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */, E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */, @@ -747,38 +789,40 @@ E2B85F3D2C4293F80047CD0C /* PageInFeed.swift in Sources */, E25DA5952D023BD100AEF16D /* PageSettingsView.swift in Sources */, E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */, + E29D31492D0489BB0051B7F4 /* AddFileView.swift in Sources */, E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */, E29D312E2D03A0D70051B7F4 /* LocalizedPageDetailView.swift in Sources */, E24252082C5168750029FF16 /* GenericMetadata+Localized.swift in Sources */, E2581DED2C75202400F1F079 /* Tag.swift in Sources */, - E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */, + E29D315B2D06D63E0051B7F4 /* ModelFileType.swift in Sources */, E29D31302D03A2C50051B7F4 /* DescriptionField.swift in Sources */, E25DA53D2D0043E600AEF16D /* LocalizedSettings.swift in Sources */, + E29D31552D06D2CE0051B7F4 /* TagListView.swift in Sources */, E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */, E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */, E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */, - E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */, E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */, + E29D31432D0488960051B7F4 /* MainContentView.swift in Sources */, E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */, E24252032C5163CF0029FF16 /* Importer.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */, E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */, + E29D315D2D06D6EA0051B7F4 /* TextFileType.swift in Sources */, E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */, E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */, E21850332CFAFA2F0090B18B /* Settings.swift in Sources */, + E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */, E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */, E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */, - E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */, E2A21C202CB28ED20060935B /* MockImage.swift in Sources */, - E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */, + E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */, E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, - E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */, E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, @@ -788,30 +832,34 @@ E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E29D31202D0320E70051B7F4 /* HikingStatistics.swift in Sources */, + E29D314B2D04FC950051B7F4 /* FileToAdd.swift in Sources */, E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */, E25DA51D2CFF135E00AEF16D /* GenericPage.swift in Sources */, E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */, - E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */, + E29D31472D04892E0051B7F4 /* FileListView.swift in Sources */, E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */, E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */, - E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */, + E2DD04742C276F31003BFF1F /* MainView.swift in Sources */, E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */, + E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */, E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */, - E25DA50F2CFDD76B00AEF16D /* ImagesContentView.swift in Sources */, + E25DA50F2CFDD76B00AEF16D /* ImageContentView.swift in Sources */, E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */, E29D31322D03B5680051B7F4 /* LocalizedPostDetailView.swift in Sources */, E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */, - E25DA5292CFFBFBB00AEF16D /* ImageType.swift in Sources */, + E29D31532D0618740051B7F4 /* AddPageView.swift in Sources */, + E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */, + E29D315F2D06D6F30051B7F4 /* CodeFileType.swift in Sources */, E25DA5342D0041CB00AEF16D /* NavigationBarSettingsFile.swift in Sources */, - E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */, E2A37D0E2CE527070000979F /* Storage.swift in Sources */, E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */, + E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */, E218502D2CF791440090B18B /* PostImagesView.swift in Sources */, - E29D312A2D039B090051B7F4 /* ImageDescriptions.swift in Sources */, + E29D312A2D039B090051B7F4 /* FileDescriptions.swift in Sources */, E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */, E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */, E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */, @@ -829,8 +877,8 @@ E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */, E29D31222D0363FD0051B7F4 /* DownloadButtons.swift in Sources */, E2A21C362CB9A3D70060935B /* FolderSettingsView.swift in Sources */, - E2A21C012CB16A820060935B /* PostView.swift in Sources */, - E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */, + E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */, + E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */, E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */, E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */, ); diff --git a/CHDataManagement/CHDataManagementApp.swift b/CHDataManagement/CHDataManagementApp.swift deleted file mode 100644 index 988b5e1..0000000 --- a/CHDataManagement/CHDataManagementApp.swift +++ /dev/null @@ -1,83 +0,0 @@ -import SwiftUI -import SFSafeSymbols - - -enum ContentDisplayType { - case markdown - case html - case rendered -} - -@main -struct CHDataManagementApp: App { - - private var navigationTitle: String { - "" - } - - @StateObject - private var content: Content = .init() - - @State - private var selectedLanguage: ContentLanguage = .english - - var body: some Scene { - WindowGroup { - TabView { - Tab("Posts", systemImage: SFSymbol.rectangleAndPencilAndEllipsis.rawValue) { - PostList() - } - Tab("Pages", systemImage: SFSymbol.textBelowPhoto.rawValue) { - PageListView() - } - Tab("Tags", systemImage: SFSymbol.tag.rawValue) { - TagsListView() - } - Tab("Images", systemImage: SFSymbol.photo.rawValue) { - ImagesView() - } - Tab("Files", systemImage: SFSymbol.doc.rawValue) { - FilesView() - } - Tab("Settings", systemImage: SFSymbol.gear.rawValue) { - SectionedSettingsView() - } - } - .environment(\.language, selectedLanguage) - .environmentObject(content) - .toolbar { - ToolbarItem(placement: .primaryAction) { - Picker("", selection: $selectedLanguage) { - Text("English") - .tag(ContentLanguage.english) - Text("German") - .tag(ContentLanguage.german) - }.pickerStyle(.segmented) - } - ToolbarItem(placement: .primaryAction) { - Button(action: save) { - Text("Save") - } - } - } - .onAppear(perform: importOldContent) - .onReceive(Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()) { _ in - save() - } - } - } - - private func save() { - // Save all changed files - content.saveToDisk() - } - - private func importOldContent() { - do { - try content.loadFromDisk() - //content.importOldContent() - } catch { - print("Failed to load content: \(error.localizedDescription)") - } - } -} diff --git a/CHDataManagement/Generator/ImageGenerator.swift b/CHDataManagement/Generator/ImageGenerator.swift index d35b456..a58f06d 100644 --- a/CHDataManagement/Generator/ImageGenerator.swift +++ b/CHDataManagement/Generator/ImageGenerator.swift @@ -15,14 +15,14 @@ private struct ImageJob { let quality: CGFloat - let type: ImageType + let type: ImageFileType } final class ImageGenerator { private let storage: Storage - private let inputImageFolder: URL + //private let inputImageFolder: URL private let relativeImageOutputPath: String @@ -30,9 +30,8 @@ final class ImageGenerator { private var jobs: [ImageJob] = [] - init(storage: Storage, inputImageFolder: URL, relativeImageOutputPath: String) { + init(storage: Storage, relativeImageOutputPath: String) { self.storage = storage - self.inputImageFolder = inputImageFolder self.relativeImageOutputPath = relativeImageOutputPath self.generatedImages = storage.loadListOfGeneratedImages() } @@ -64,14 +63,14 @@ final class ImageGenerator { storage.save(listOfGeneratedImages: generatedImages) } - private func versionFileName(image: String, type: ImageType, width: CGFloat, height: CGFloat) -> String { + private func versionFileName(image: String, type: ImageFileType, width: CGFloat, height: CGFloat) -> String { let fileName = image.fileNameAndExtension.fileName let prefix = "\(fileName)@\(Int(width))x\(Int(height))" return "\(prefix).\(type.fileExtension)" } func generateImageSet(for image: String, maxWidth: CGFloat, maxHeight: CGFloat, altText: String) -> FeedEntryData.Image { - let type = ImageType(fileExtension: image.fileExtension!)! + let type = ImageFileType(fileExtension: image.fileExtension!)! let width2x = maxWidth * 2 let height2x = maxHeight * 2 @@ -92,7 +91,7 @@ final class ImageGenerator { altText: altText) } - func generateVersion(for image: String, type: ImageType, maximumWidth: CGFloat, maximumHeight: CGFloat) -> String { + func generateVersion(for image: String, type: ImageFileType, maximumWidth: CGFloat, maximumHeight: CGFloat) -> String { let version = versionFileName(image: image, type: type, width: maximumWidth, height: maximumHeight) let fullPath = "/" + relativeImageOutputPath + "/" + version if exists(version) { @@ -142,18 +141,12 @@ final class ImageGenerator { if hasPreviouslyGenerated(version: job.version, for: job.image), exists(job.version) { return true } - let inputPath = inputImageFolder.appendingPathComponent(job.image) - #warning("TODO: Read through security scope") - guard inputPath.exists else { - print("Missing image \(inputPath.path())") - return false - } let data: Data do { - data = try Data(contentsOf: inputPath) + data = try storage.fileData(for: job.image) } catch { - print("Failed to load image \(inputPath.path()): \(error)") + print("Failed to load image \(job.image): \(error)") return false } @@ -231,7 +224,7 @@ final class ImageGenerator { // MARK: Avif images - private func create(image: NSBitmapImageRep, type: ImageType, quality: CGFloat) -> Data? { + private func create(image: NSBitmapImageRep, type: ImageFileType, quality: CGFloat) -> Data? { switch type { case .jpg: return image.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 0.6)]) @@ -243,6 +236,10 @@ final class ImageGenerator { return createWebp(image: image, quality: 0.8) case .gif: return image.representation(using: .gif, properties: [.compressionFactor: NSNumber(value: quality)]) + case .svg: + return nil + case .tiff: + return nil } } diff --git a/CHDataManagement/Generator/PageContentGenerator.swift b/CHDataManagement/Generator/PageContentGenerator.swift index 9f1fdc0..5a5a674 100644 --- a/CHDataManagement/Generator/PageContentGenerator.swift +++ b/CHDataManagement/Generator/PageContentGenerator.swift @@ -2,7 +2,7 @@ import Foundation import Ink import Splash -typealias VideoSource = (url: String, type: VideoType) +typealias VideoSource = (url: String, type: VideoFileType) final class PageContentParser { diff --git a/CHDataManagement/Generator/WebsiteGenerator.swift b/CHDataManagement/Generator/WebsiteGenerator.swift index 1d68e93..70aa825 100644 --- a/CHDataManagement/Generator/WebsiteGenerator.swift +++ b/CHDataManagement/Generator/WebsiteGenerator.swift @@ -50,7 +50,6 @@ final class WebsiteGenerator { self.localizedSettings = content.settings.localized(in: language) self.imageGenerator = ImageGenerator( storage: content.storage, - inputImageFolder: content.storage.filesFolder, relativeImageOutputPath: "images") } @@ -96,7 +95,7 @@ final class WebsiteGenerator { navigationItems: navigationItems) } - private func createImageSet(for image: ImageResource) -> FeedEntryData.Image { + private func createImageSet(for image: FileResource) -> FeedEntryData.Image { imageGenerator.generateImageSet( for: image.id, maxWidth: mainContentMaximumWidth, diff --git a/CHDataManagement/Import/Importer.swift b/CHDataManagement/Import/Importer.swift index 66c19ca..c32f34a 100644 --- a/CHDataManagement/Import/Importer.swift +++ b/CHDataManagement/Import/Importer.swift @@ -104,7 +104,7 @@ final class Importer { } let type = FileType(fileExtension: fileExtension) - guard case .resource = type else { + guard case .other = type else { self.ignoredFiles.append(url) return nil } diff --git a/CHDataManagement/Main/MainContentView.swift b/CHDataManagement/Main/MainContentView.swift new file mode 100644 index 0000000..d11826e --- /dev/null +++ b/CHDataManagement/Main/MainContentView.swift @@ -0,0 +1,10 @@ +import SwiftUI + +protocol MainContentView: View { + + associatedtype Item: Identifiable + + init(item: Item) + + static var itemDescription: String { get } +} diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift new file mode 100644 index 0000000..be2b1aa --- /dev/null +++ b/CHDataManagement/Main/MainView.swift @@ -0,0 +1,185 @@ +import SwiftUI +import SFSafeSymbols + + +#warning("Consolidate images and files") +#warning("Allow selection of pages as navigation bar items") +#warning("Transfer images of posts to other language") + +@main +struct MainView: App { + + private let sidebarWidth: CGFloat = 250 + + private let detailWidth: CGFloat = 300 + + @StateObject + private var content: Content = .init() + + @State + private var language: ContentLanguage = .english + + @State + private var selectedTab: MainViewTab = .posts + + @State + private var selectedPost: Post? + + @State + private var selectedPage: Page? + + @State + private var selectedTag: Tag? + + @State + private var selectedImage: ImageResource? + + @State + private var selectedFile: FileResource? + + @State + private var selectedSection: SettingsSection? = .generation + + @State + private var showAddSheet = false + + @ViewBuilder + var sidebar: some View { + switch selectedTab { + case .posts: + PostListView(selectedPost: $selectedPost) + case .pages: + PageListView(selectedPage: $selectedPage) + case .tags: + TagListView(selectedTag: $selectedTag) + case .files: + FileListView(selectedFile: $selectedFile) + case .generation: + List(SettingsSection.allCases, selection: $selectedSection) { item in + Label(item.rawValue, systemSymbol: item.icon).tag(item) + } + } + } + + @ViewBuilder + var viewContent: some View { + switch selectedTab { + case .posts: + SelectedContentView(selected: $selectedPost) + case .pages: + SelectedContentView(selected: $selectedPage) + case .tags: + SelectedContentView(selected: $selectedTag) + case .files: + SelectedContentView(selected: $selectedFile) + case .generation: + GenerationDetailView(section: selectedSection) + } + } + + @ViewBuilder + var detail: some View { + switch selectedTab { + case .posts: + SelectedDetailView(selected: $selectedPost) + case .pages: + SelectedDetailView(selected: $selectedPage) + case .tags: + SelectedDetailView(selected: $selectedTag) + case .files: + SelectedDetailView(selected: $selectedFile) + case .generation: + Text("") + } + } + + @ViewBuilder + var addItemSheet: some View { + switch selectedTab { + case .posts: + AddPostView(selected: $selectedPost) + case .pages: + AddPageView(selected: $selectedPage) + case .tags: + AddTagView(selected: $selectedTag) + case .files: + AddFileView(selectedImage: $selectedImage, selectedFile: $selectedFile) + case .generation: + Text("Not implemented") + } + } + + var body: some Scene { + WindowGroup { + NavigationSplitView { + sidebar + .toolbar { + ToolbarItem(placement: .navigation) { + Picker("", selection: $selectedTab) { + Text("Posts").tag(MainViewTab.posts) + Text("Pages").tag(MainViewTab.pages) + Text("Tags").tag(MainViewTab.tags) + Text("Files").tag(MainViewTab.files) + Text("Generation").tag(MainViewTab.generation) + }.pickerStyle(.segmented) + } + } + .navigationSplitViewColumnWidth(min: sidebarWidth, ideal: sidebarWidth, max: sidebarWidth) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button(action: { showAddSheet = true }) { + Label("Add", systemSymbol: .plus) + } + .disabled(!selectedTab.canAddItems) + } + } + } content: { + viewContent + } detail: { + detail + .navigationSplitViewColumnWidth(min: detailWidth, ideal: detailWidth, max: detailWidth) + } + .toolbar { + ToolbarItem(placement: .primaryAction) { + Picker("", selection: $language) { + Text("English") + .tag(ContentLanguage.english) + Text("German") + .tag(ContentLanguage.german) + }.pickerStyle(.segmented) + } + ToolbarItem(placement: .primaryAction) { + Button(action: save) { + Text("Save") + } + } + } + .navigationTitle("") + .environment(\.language, language) + .environmentObject(content) + .onAppear(perform: loadContent) + .onReceive(Timer.publish(every: 60.0, on: .main, in: .common).autoconnect()) { _ in + save() + } + .sheet(isPresented: $showAddSheet) { + addItemSheet + .environment(\.language, language) + .environmentObject(content) + } + } + } + + private func save() { + // Save all changed files + content.saveToDisk() + } + + private func loadContent() { + do { + try content.loadFromDisk() + } catch { + print("Failed to load content: \(error.localizedDescription)") + } + } +} + diff --git a/CHDataManagement/Main/SelectedContentView.swift b/CHDataManagement/Main/SelectedContentView.swift new file mode 100644 index 0000000..02a5be0 --- /dev/null +++ b/CHDataManagement/Main/SelectedContentView.swift @@ -0,0 +1,25 @@ +import SwiftUI + +struct SelectedContentView: View where Contained: MainContentView { + + @Binding + var selected: Contained.Item? + + init(selected: Binding) { + self._selected = selected + } + + var body: some View { + if let item = selected { + Contained(item: item) + } else { + HStack { + Spacer() + Text("Select \(Contained.itemDescription) from the sidebar") + .font(.largeTitle) + .foregroundColor(.secondary) + Spacer() + } + } + } +} diff --git a/CHDataManagement/Main/SelectedDetailView.swift b/CHDataManagement/Main/SelectedDetailView.swift new file mode 100644 index 0000000..e20dd25 --- /dev/null +++ b/CHDataManagement/Main/SelectedDetailView.swift @@ -0,0 +1,20 @@ +import SwiftUI + +struct SelectedDetailView: View where Contained: MainContentView { + + @Binding + var selected: Contained.Item? + + init(selected: Binding) { + self._selected = selected + } + + var body: some View { + if let item = selected { + Contained(item: item) + .id(item.id) + } else { + EmptyView() + } + } +} diff --git a/CHDataManagement/Main/TabSelection.swift b/CHDataManagement/Main/TabSelection.swift new file mode 100644 index 0000000..a7a7db5 --- /dev/null +++ b/CHDataManagement/Main/TabSelection.swift @@ -0,0 +1,18 @@ +import SwiftUI +import SFSafeSymbols + +enum MainViewTab { + case posts + case pages + case tags + case files + case generation + + var canAddItems: Bool { + if case .generation = self { + return false + } + return true + } +} + diff --git a/CHDataManagement/Model/Content+Generation.swift b/CHDataManagement/Model/Content+Generation.swift index bd5d796..740777d 100644 --- a/CHDataManagement/Model/Content+Generation.swift +++ b/CHDataManagement/Model/Content+Generation.swift @@ -27,11 +27,11 @@ extension Content { return nil } #warning("Add files path to settings") - return "/files/\(file.uniqueId)" + return "/files/\(file.id)" } - func image(_ imageId: String) -> ImageResource? { - images.first { $0.id == imageId } + func image(_ imageId: String) -> FileResource? { + files.first { $0.id == imageId } } func imageLink(imageId: String) { diff --git a/CHDataManagement/Model/Content+Load.swift b/CHDataManagement/Model/Content+Load.swift index 8a97cd4..afd109d 100644 --- a/CHDataManagement/Model/Content+Load.swift +++ b/CHDataManagement/Model/Content+Load.swift @@ -2,7 +2,7 @@ import Foundation extension Content { - private func convert(_ tag: LocalizedTagFile, images: [String : ImageResource]) -> LocalizedTag { + private func convert(_ tag: LocalizedTagFile, images: [String : FileResource]) -> LocalizedTag { LocalizedTag( urlComponent: tag.urlComponent, name: tag.name, @@ -12,7 +12,7 @@ extension Content { originalUrl: tag.originalURL) } - private func convert(_ post: LocalizedPostFile, images: [String : ImageResource]) -> LocalizedPost { + private func convert(_ post: LocalizedPostFile, images: [String : FileResource]) -> LocalizedPost { LocalizedPost( title: post.title, content: post.content, @@ -23,7 +23,7 @@ extension Content { linkPreviewDescription: post.linkPreviewDescription) } - private func convert(_ page: LocalizedPageFile, images: [String : ImageResource]) -> LocalizedPage { + private func convert(_ page: LocalizedPageFile, images: [String : FileResource]) -> LocalizedPage { LocalizedPage( urlString: page.url, title: page.title, @@ -49,34 +49,26 @@ extension Content { let storage = Storage(baseFolder: URL(filePath: contentPath)) let settings = try storage.loadSettings() - let imageDescriptions = storage.loadImageDescriptions().reduce(into: [:]) { descriptions, description in - descriptions[description.imageId] = description + let imageDescriptions = storage.loadFileDescriptions().reduce(into: [:]) { descriptions, description in + descriptions[description.fileId] = description } let tagData = try storage.loadAllTags() let pagesData = try storage.loadAllPages() let postsData = try storage.loadAllPosts() - let filesData = try storage.loadAllFiles() + let fileList = try storage.loadAllFiles() - var images: [String : ImageResource] = [:] - var files: [FileResource] = [] - - for (file, url) in filesData { - let ext = file.components(separatedBy: ".").last!.lowercased() - let type = FileType(fileExtension: ext) - if case .image(let type) = type { - let descriptions = imageDescriptions[file] - images[file] = ImageResource( - type: type, - uniqueId: file, - en: descriptions?.english ?? "", - de: descriptions?.german ?? "", - fileUrl: url) - } else { - files.append(FileResource(type: type, uniqueId: file, description: "")) - } + let files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in + let descriptions = imageDescriptions[fileId] + files[fileId] = FileResource( + content: self, + id: fileId, + en: descriptions?.english ?? "", + de: descriptions?.german ?? "") } + let images = files.filter { $0.value.type.isImage } + let tags = tagData.reduce(into: [:]) { (tags, data) in tags[data.key] = Tag( isVisible: data.value.isVisible, @@ -105,8 +97,7 @@ extension Content { self.tags = tags.values.sorted() self.pages = pages.values.sorted(ascending: false) { $0.startDate } - self.files = files.sorted { $0.uniqueId } - self.images = images.values.sorted { $0.id } + self.files = files.values.sorted { $0.id } self.posts = posts.sorted(ascending: false) { $0.startDate } self.settings = makeSettings(settings, tags: tags) } @@ -134,7 +125,7 @@ extension Content { english: convert(settings.english)) } - private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], images: [String : ImageResource]) -> [String : Page] { + private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], images: [String : FileResource]) -> [String : Page] { pagesData.reduce(into: [:]) { pages, data in let (pageId, page) = data pages[pageId] = Page( diff --git a/CHDataManagement/Model/Content+Save.swift b/CHDataManagement/Model/Content+Save.swift index 58071ad..8fee01c 100644 --- a/CHDataManagement/Model/Content+Save.swift +++ b/CHDataManagement/Model/Content+Save.swift @@ -17,24 +17,23 @@ extension Content { } storage.save(settings: settings.file) - let imageDescriptions: [ImageDescriptions] = images.sorted().compactMap { image in - guard !image.englishDescription.isEmpty || !image.germanDescription.isEmpty else { + let fileDescriptions: [FileDescriptions] = files.sorted().compactMap { file in + guard !file.englishDescription.isEmpty || !file.germanDescription.isEmpty else { return nil } - return ImageDescriptions( - imageId: image.id, - german: image.germanDescription.nonEmpty, - english: image.englishDescription.nonEmpty) + return FileDescriptions( + fileId: file.id, + german: file.germanDescription.nonEmpty, + english: file.englishDescription.nonEmpty) } - storage.save(imageDescriptions: imageDescriptions) + storage.save(fileDescriptions: fileDescriptions) do { try storage.deletePostFiles(notIn: posts.map { $0.id }) try storage.deletePageFiles(notIn: pages.map { $0.id }) try storage.deleteTagFiles(notIn: tags.map { $0.id }) - let allFiles = files.map { $0.uniqueId } + images.map { $0.id } - try storage.deleteFiles(notIn: allFiles) + try storage.deleteFiles(notIn: files.map { $0.id }) } catch { print("Failed to remove unused files: \(error)") } diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index f5d7025..40aa7a5 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -16,9 +16,6 @@ final class Content: ObservableObject { @Published var tags: [Tag] - @Published - var images: [ImageResource] - @Published var files: [FileResource] @@ -40,14 +37,12 @@ final class Content: ObservableObject { posts: [Post], pages: [Page], tags: [Tag], - images: [ImageResource], files: [FileResource], storedContentPath: String) { self.settings = settings self.posts = posts self.pages = pages self.tags = tags - self.images = images self.files = files self.storedContentPath = storedContentPath self.contentPath = storedContentPath @@ -68,7 +63,6 @@ final class Content: ObservableObject { self.posts = [] self.pages = [] self.tags = [] - self.images = [] self.files = [] contentPath = storedContentPath @@ -95,4 +89,8 @@ final class Content: ObservableObject { } .store(in: &cancellables) } + + var images: [FileResource] { + files.filter { $0.type.isImage } + } } diff --git a/CHDataManagement/Model/FileResource.swift b/CHDataManagement/Model/FileResource.swift index 374ba6d..50cddc0 100644 --- a/CHDataManagement/Model/FileResource.swift +++ b/CHDataManagement/Model/FileResource.swift @@ -1,44 +1,117 @@ import Foundation +import SwiftUI final class FileResource: ObservableObject { + unowned let content: Content + let type: FileType /// Globally unique id @Published - var uniqueId: String + var id: String @Published - var description: String + var germanDescription: String - init(uniqueId: String, description: String) { - self.type = FileType(fileExtension: uniqueId.fileExtension) - self.uniqueId = uniqueId - self.description = description + @Published + var englishDescription: String + + @Published + var size: CGSize = .zero + + init(content: Content, id: String, en: String, de: String) { + self.content = content + self.id = id + self.type = FileType(fileExtension: id.fileExtension) + self.englishDescription = en + self.germanDescription = de } - init(type: FileType, uniqueId: String, description: String) { - self.type = type - self.uniqueId = uniqueId - self.description = description + /** + Only for bundle images + */ + init(resourceImage: String, type: ImageFileType) { + self.content = .mock // TODO: Add images to mock + self.type = .image(type) + self.id = resourceImage + self.englishDescription = "A test image included in the bundle" + self.germanDescription = "Ein Testbild aus dem Bundle" + } + + func getDescription(for language: ContentLanguage) -> String { + switch language { + case .english: return englishDescription + case .german: return germanDescription + } + } + + // MARK: Text + + func textContent() -> String { + do { + return try content.storage.fileContent(for: id) + } catch { + print("Failed to load text of file \(id): \(error)") + return "" + } + } + + // MARK: Images + + var aspectRatio: CGFloat { + guard size.height > 0 else { + return 0 + } + return size.width / size.height + } + + var imageToDisplay: Image { + let imageData: Data + do { + imageData = try content.storage.fileData(for: id) + } catch { + print("Failed to load data for image \(id): \(error)") + return failureImage + } + guard let loadedImage = NSImage(data: imageData) else { + print("Failed to create image \(id)") + return failureImage + } + if self.size == .zero && loadedImage.size != .zero { + DispatchQueue.main.async { + self.size = loadedImage.size + } + } + return .init(nsImage: loadedImage) + } + + private var failureImage: Image { + Image(systemSymbol: .exclamationmarkTriangle) } } extension FileResource: Identifiable { - var id: String { uniqueId } } extension FileResource: Equatable { static func == (lhs: FileResource, rhs: FileResource) -> Bool { - lhs.uniqueId == rhs.uniqueId + lhs.id == rhs.id } } extension FileResource: Hashable { func hash(into hasher: inout Hasher) { - hasher.combine(uniqueId) + hasher.combine(id) + } +} + +extension FileResource: Comparable { + + static func < (lhs: FileResource, rhs: FileResource) -> Bool { + lhs.id < rhs.id } } diff --git a/CHDataManagement/Model/ImageResource.swift b/CHDataManagement/Model/ImageResource.swift deleted file mode 100644 index 31285e4..0000000 --- a/CHDataManagement/Model/ImageResource.swift +++ /dev/null @@ -1,119 +0,0 @@ -import SwiftUI - -final class ImageResource: ObservableObject { - - @Published - var type: ImageType - - /// Globally unique id - @Published - var id: String - - @Published - var germanDescription: String - - @Published - var englishDescription: String - - @Published - var size: CGSize = .zero - - var aspectRatio: CGFloat { - guard size.height > 0 else { - return 0 - } - return size.width / size.height - } - - private let source: ImageSource - - init(type: ImageType, uniqueId: String, en: String, de: String, fileUrl: URL) { - self.type = type - self.id = uniqueId - self.source = .file(fileUrl) - self.englishDescription = en - self.germanDescription = de - } - - init(resourceName: String, type: ImageType) { - self.type = type - self.id = resourceName - self.source = .resource(resourceName) - self.englishDescription = "A test image included in the bundle" - self.germanDescription = "Ein Test-Image aus dem Bundle" - } - - private enum ImageSource { - case file(URL) - case resource(String) - } - - func getDescription(for language: ContentLanguage) -> String { - switch language { - case .english: return englishDescription - case .german: return germanDescription - } - } -} - -extension ImageResource: Identifiable { - -} - -extension ImageResource: Comparable { - - static func < (lhs: ImageResource, rhs: ImageResource) -> Bool { - lhs.id < rhs.id - } -} - -extension ImageResource: Equatable { - - static func == (lhs: ImageResource, rhs: ImageResource) -> Bool { - lhs.id == rhs.id - } -} - -extension ImageResource: Hashable { - - func hash(into hasher: inout Hasher) { - hasher.combine(id) - } -} - -extension ImageResource { - - var imageToDisplay: Image { - switch source { - case .file(let url): - return image(at: url) - case .resource(let name): - return .init(name) - } - } - - private func image(at url: URL) -> Image { - let imageData: Data - do { - imageData = try Data(contentsOf: url) - } catch { - print("Failed to load image data from \(url.path): \(error)") - return failureImage - } - guard let loadedImage = NSImage(data: imageData) else { - print("Failed to create image from \(url.path)") - return failureImage - } - if self.size == .zero && loadedImage.size != .zero { - DispatchQueue.main.async { - self.size = loadedImage.size - } - } - return .init(nsImage: loadedImage) - } - - private var failureImage: SwiftUI.Image { - Image(systemSymbol: .exclamationmarkTriangle) - } - -} diff --git a/CHDataManagement/Model/LocalizedPage.swift b/CHDataManagement/Model/LocalizedPage.swift index d30b3fe..68565a7 100644 --- a/CHDataManagement/Model/LocalizedPage.swift +++ b/CHDataManagement/Model/LocalizedPage.swift @@ -56,7 +56,7 @@ final class LocalizedPage: ObservableObject { var requiredFiles: Set = [] @Published - var linkPreviewImage: ImageResource? + var linkPreviewImage: FileResource? @Published var linkPreviewTitle: String? @@ -71,7 +71,7 @@ final class LocalizedPage: ObservableObject { files: Set = [], externalFiles: Set = [], requiredFiles: Set = [], - linkPreviewImage: ImageResource? = nil, + linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) { self.urlString = urlString diff --git a/CHDataManagement/Model/LocalizedPost.swift b/CHDataManagement/Model/LocalizedPost.swift index 5e5bdf4..48a34e0 100644 --- a/CHDataManagement/Model/LocalizedPost.swift +++ b/CHDataManagement/Model/LocalizedPost.swift @@ -13,10 +13,10 @@ final class LocalizedPost: ObservableObject { var lastModified: Date? @Published - var images: [ImageResource] + var images: [FileResource] @Published - var linkPreviewImage: ImageResource? + var linkPreviewImage: FileResource? @Published var linkPreviewTitle: String? @@ -27,8 +27,8 @@ final class LocalizedPost: ObservableObject { init(title: String? = nil, content: String, lastModified: Date? = nil, - images: [ImageResource] = [], - linkPreviewImage: ImageResource? = nil, + images: [FileResource] = [], + linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) { self.title = title ?? "" diff --git a/CHDataManagement/Model/LocalizedTag.swift b/CHDataManagement/Model/LocalizedTag.swift index f5aa06c..0d42bb2 100644 --- a/CHDataManagement/Model/LocalizedTag.swift +++ b/CHDataManagement/Model/LocalizedTag.swift @@ -17,7 +17,7 @@ final class LocalizedTag: ObservableObject { /// The image id of the thumbnail @Published - var thumbnail: ImageResource? + var thumbnail: FileResource? /// The original url in the previous site layout let originalUrl: String? @@ -26,7 +26,7 @@ final class LocalizedTag: ObservableObject { name: String, subtitle: String? = nil, description: String? = nil, - thumbnail: ImageResource? = nil, + thumbnail: FileResource? = nil, originalUrl: String? = nil) { self.urlComponent = urlComponent self.name = name diff --git a/CHDataManagement/Model/LocalizedText.swift b/CHDataManagement/Model/LocalizedText.swift deleted file mode 100644 index d2863df..0000000 --- a/CHDataManagement/Model/LocalizedText.swift +++ /dev/null @@ -1,48 +0,0 @@ -import Foundation -import SwiftUI - -/// A simple container for localized text - -final class LocalizedText: ObservableObject { - - @Published - var en: String - - @Published - var de: String - - init(en: String, de: String) { - self.en = en - self.de = de - } - - var id: String { - en - } - - func set(text: String, for language: ContentLanguage) { - switch language { - case .english: self.en = text - case .german: self.de = text - } - } - - func getText(for language: ContentLanguage) -> String { - switch language { - case .english: return en - case .german: return de - } - } - - @MainActor - func text(for language: ContentLanguage) -> Binding { - Binding( - get: { - self.getText(for: language) - }, - set: { newValue in - self.set(text: newValue, for: language) - } - ) - } -} diff --git a/CHDataManagement/Model/Types/CodeFileType.swift b/CHDataManagement/Model/Types/CodeFileType.swift new file mode 100644 index 0000000..4c5e27e --- /dev/null +++ b/CHDataManagement/Model/Types/CodeFileType.swift @@ -0,0 +1,21 @@ + +enum CodeFileType: String { + + case html + + case css + + case js + + case cpp + + case swift + + init?(fileExtension: String) { + self.init(rawValue: fileExtension) + } + + var fileExtension: String { + rawValue + } +} diff --git a/CHDataManagement/Model/Types/FileType.swift b/CHDataManagement/Model/Types/FileType.swift index f73499b..4ff9346 100644 --- a/CHDataManagement/Model/Types/FileType.swift +++ b/CHDataManagement/Model/Types/FileType.swift @@ -1,51 +1,71 @@ import Foundation +enum FileTypeCategory: String, CaseIterable { + case image + case code + case model + case text + case video + case resource + + var text: String { + switch self { + case .image: return "Images" + case .code: return "Code" + case .model: return "Models" + case .text: return "Text" + case .video: return "Videos" + case .resource: return "Other" + } + } +} + +extension FileTypeCategory: Hashable { + +} + +extension FileTypeCategory: Identifiable { + + var id: String { + rawValue + } +} + enum FileType { - case image(ImageType) - - case file(String) - case video(VideoType) - case resource(String) - + case image(ImageFileType) + case code(CodeFileType) + case model(ModelFileType) + case text(TextFileType) + case video(VideoFileType) + case other(ResourceFileType) init(fileExtension: String?) { - guard let ext = fileExtension?.lowercased() else { - self = .file("") - return - } - switch ext { - case "jpg", "jpeg": - self = .image(.jpg) - case "png": - self = .image(.png) - case "avif": - self = .image(.avif) - case "webp": - self = .image(.webp) - case "gif": - self = .image(.gif) - case "html", "stl", "f3d", "step", "f3z", "zip", "json", "conf", "css", "js", "cpp", "cddx", "svg", "glb", "mp3", "pdf", "swift": - self = .file(ext) - case "mp4": - self = .video(.mp4) - case "m4v": - self = .video(.m4v) - case "webm": - self = .video(.webm) - case "key", "psd": - self = .resource(ext) - default: - print("Unhandled file type: \(ext)") - self = .resource(ext) + let ext = fileExtension?.lowercased() ?? "" + + if let image = ImageFileType(fileExtension: ext) { + self = .image(image) + } else if let code = CodeFileType(fileExtension: ext) { + self = .code(code) + } else if let model = ModelFileType(fileExtension: ext) { + self = .model(model) + } else if let text = TextFileType(fileExtension: ext) { + self = .text(text) + } else if let video = VideoFileType(fileExtension: ext) { + self = .video(video) + } else { + let resource = ResourceFileType(fileExtension: ext) + self = .other(resource) } } var fileExtension: String { switch self { - case .image(let imageType): return imageType.fileExtension - case .video(let videoType): return videoType.fileExtension - default: - return "" // TODO: Fix + case .image(let type): return type.fileExtension + case .code(let type): return type.fileExtension + case .model(let type): return type.fileExtension + case .text(let type): return type.fileExtension + case .video(let type): return type.fileExtension + case .other(let type): return type.fileExtension } } @@ -63,7 +83,21 @@ enum FileType { return false } - var videoType: VideoType? { + var isTextFile: Bool { + switch self { + case .code, .text: return true + default: return false + } + } + + var isOtherFile: Bool { + switch self { + case .model, .other: return true + default: return false + } + } + + var videoType: VideoFileType? { if case .video(let videoType) = self { return videoType } diff --git a/CHDataManagement/Model/Types/ImageFileType.swift b/CHDataManagement/Model/Types/ImageFileType.swift new file mode 100644 index 0000000..0634151 --- /dev/null +++ b/CHDataManagement/Model/Types/ImageFileType.swift @@ -0,0 +1,41 @@ +import Foundation +import AppKit + +enum ImageFileType: String { + + case jpg + case png + case avif + case webp + case gif + case svg + case tiff + + init?(fileExtension: String) { + if fileExtension == "jpeg" { + self = .jpg + return + } + self.init(rawValue: fileExtension) + } + + var fileExtension: String { + rawValue + } + + var fileType: NSBitmapImageRep.FileType? { + switch self { + case .jpg: + return .jpeg + case .png, .avif, .webp: + return .png + case .gif: return .gif + case .tiff: return .tiff + case .svg: return nil + } + } +} + +extension ImageFileType: CaseIterable { + +} diff --git a/CHDataManagement/Model/Types/ImageType.swift b/CHDataManagement/Model/Types/ImageType.swift deleted file mode 100644 index 23c4952..0000000 --- a/CHDataManagement/Model/Types/ImageType.swift +++ /dev/null @@ -1,53 +0,0 @@ -import Foundation -import AppKit - -enum ImageType { - case jpg - case png - case avif - case webp - case gif - - var fileExtension: String { - switch self { - case .jpg: return "jpg" - case .png: return "png" - case .avif: return "avif" - case .webp: return "webp" - case .gif: return "gif" - } - } - - var fileType: NSBitmapImageRep.FileType { - switch self { - case .jpg: - return .jpeg - case .png, .avif, .webp: - return .png - case .gif: - return .gif - } - } -} - -extension ImageType: CaseIterable { - -} - -extension ImageType { - - init?(fileExtension: String) { - switch fileExtension { - case "jpg", "jpeg": - self = .jpg - case "png": - self = .png - case "avif": - self = .avif - case "webp": - self = .webp - default: - return nil - } - } -} diff --git a/CHDataManagement/Model/Types/ModelFileType.swift b/CHDataManagement/Model/Types/ModelFileType.swift new file mode 100644 index 0000000..5b42a2a --- /dev/null +++ b/CHDataManagement/Model/Types/ModelFileType.swift @@ -0,0 +1,21 @@ + +enum ModelFileType: String { + + case stl + + case f3d + + case step + + case glb + + case f3z + + init?(fileExtension: String) { + self.init(rawValue: fileExtension) + } + + var fileExtension: String { + rawValue + } +} diff --git a/CHDataManagement/Model/Types/ResourceFileType.swift b/CHDataManagement/Model/Types/ResourceFileType.swift new file mode 100644 index 0000000..3e48239 --- /dev/null +++ b/CHDataManagement/Model/Types/ResourceFileType.swift @@ -0,0 +1,54 @@ + +enum ResourceFileType { + + case noExtension + + case zip + + case cddx + + case mp3 + + case pdf + + case key + + case psd + + case other(String) + + init(fileExtension: String) { + switch fileExtension { + case "": self = .noExtension + case "zip": self = .zip + case "cddx": self = .cddx + case "mp3": self = .mp3 + case "pdf": self = .pdf + case "key": self = .key + case "psd": self = .psd + default: + self = .other(fileExtension) + } + } + + var fileExtension: String { + switch self { + case .noExtension: + return "" + case .zip: + return "zip" + case .cddx: + return "cddx" + case .mp3: + return "mp3" + case .pdf: + return "pdf" + case .key: + return "key" + case .psd: + return "psd" + case .other(let ext): + return ext + } + } +} diff --git a/CHDataManagement/Model/Types/TextFileType.swift b/CHDataManagement/Model/Types/TextFileType.swift new file mode 100644 index 0000000..3e919c8 --- /dev/null +++ b/CHDataManagement/Model/Types/TextFileType.swift @@ -0,0 +1,18 @@ + +enum TextFileType: String { + + case json + + case conf + + case yaml + + init?(fileExtension: String) { + self.init(rawValue: fileExtension) + } + + var fileExtension: String { + rawValue + } +} + diff --git a/CHDataManagement/Model/Types/VideoType.swift b/CHDataManagement/Model/Types/VideoFileType.swift similarity index 51% rename from CHDataManagement/Model/Types/VideoType.swift rename to CHDataManagement/Model/Types/VideoFileType.swift index 3230f36..2fe4d44 100644 --- a/CHDataManagement/Model/Types/VideoType.swift +++ b/CHDataManagement/Model/Types/VideoFileType.swift @@ -1,24 +1,18 @@ -enum VideoType: String { +enum VideoFileType: String { case mp4 case m4v case webm -} -extension VideoType { + init?(fileExtension: String) { + self.init(rawValue: fileExtension) + } var fileExtension: String { - switch self { - case .mp4: - return "mp4" - case .m4v: - return "m4v" - case .webm: - return "webm" - } + rawValue } var htmlType: String { @@ -31,6 +25,6 @@ extension VideoType { } } -extension VideoType: CaseIterable { +extension VideoFileType: CaseIterable { } diff --git a/CHDataManagement/Preview Content/Content+Mock.swift b/CHDataManagement/Preview Content/Content+Mock.swift index 809d7b5..609c46e 100644 --- a/CHDataManagement/Preview Content/Content+Mock.swift +++ b/CHDataManagement/Preview Content/Content+Mock.swift @@ -19,7 +19,6 @@ extension Content { posts: [.empty, .mock, .fullMock], pages: [.empty], tags: [.hiking, .mountains, .nature, .sports], - images: [], files: [], storedContentPath: dbPath) } diff --git a/CHDataManagement/Preview Content/File+Mock.swift b/CHDataManagement/Preview Content/File+Mock.swift index 4aba13a..1b3a4da 100644 --- a/CHDataManagement/Preview Content/File+Mock.swift +++ b/CHDataManagement/Preview Content/File+Mock.swift @@ -2,6 +2,6 @@ extension FileResource { static var mock: FileResource { - .init(uniqueId: "my-file.txt", description: "Some text file") + .init(content: .mock, id: "my-file.txt", en: "Some text file", de: "Eine Textdatei") } } diff --git a/CHDataManagement/Preview Content/MockImage.swift b/CHDataManagement/Preview Content/MockImage.swift index a4af9b1..79a7ca6 100644 --- a/CHDataManagement/Preview Content/MockImage.swift +++ b/CHDataManagement/Preview Content/MockImage.swift @@ -5,9 +5,9 @@ struct MockImage { let name: String - static var images: [ImageResource] { + static var images: [FileResource] { ["image1", "image2", "image3", "image4"] - .map { ImageResource(resourceName: $0, type: .jpg) } + .map { FileResource(resourceImage: $0, type: .jpg) } } } diff --git a/CHDataManagement/Preview Content/Tag+Mock.swift b/CHDataManagement/Preview Content/Tag+Mock.swift index a57c2c0..c4eeb87 100644 --- a/CHDataManagement/Preview Content/Tag+Mock.swift +++ b/CHDataManagement/Preview Content/Tag+Mock.swift @@ -34,7 +34,7 @@ extension LocalizedTag { name: "Electronics", subtitle: "Projects with electronics", description: "Some description of the tag", - thumbnail: ImageResource(resourceName: "image1", type: .jpg), + thumbnail: FileResource(resourceImage: "image1", type: .jpg), originalUrl: "projects/electronics") static let german = LocalizedTag( @@ -42,6 +42,6 @@ extension LocalizedTag { name: "Elektronik", subtitle: "Projekte mit Elektronik", description: "Eine Beschreibung des Tags", - thumbnail: ImageResource(resourceName: "image2", type: .jpg), + thumbnail: FileResource(resourceImage: "image2", type: .jpg), originalUrl: "projects/electronics") } diff --git a/CHDataManagement/Storage/Model/FileDescriptions.swift b/CHDataManagement/Storage/Model/FileDescriptions.swift new file mode 100644 index 0000000..d98acef --- /dev/null +++ b/CHDataManagement/Storage/Model/FileDescriptions.swift @@ -0,0 +1,13 @@ + +struct FileDescriptions { + + let fileId: String + + let german: String? + + let english: String? +} + +extension FileDescriptions: Codable { + +} diff --git a/CHDataManagement/Storage/Model/FileOnDisk.swift b/CHDataManagement/Storage/Model/FileOnDisk.swift index 7b44887..c83dff8 100644 --- a/CHDataManagement/Storage/Model/FileOnDisk.swift +++ b/CHDataManagement/Storage/Model/FileOnDisk.swift @@ -10,7 +10,7 @@ struct FileOnDisk { init(image: String, url: URL) { let ext = image.fileExtension! - let type = ImageType(fileExtension: ext)! + let type = ImageFileType(fileExtension: ext)! self.type = .image(type) self.url = url self.name = image diff --git a/CHDataManagement/Storage/Model/ImageDescriptions.swift b/CHDataManagement/Storage/Model/ImageDescriptions.swift deleted file mode 100644 index f2b3035..0000000 --- a/CHDataManagement/Storage/Model/ImageDescriptions.swift +++ /dev/null @@ -1,13 +0,0 @@ - -struct ImageDescriptions { - - let imageId: String - - let german: String? - - let english: String? -} - -extension ImageDescriptions: Codable { - -} diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index f83127c..140385d 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -101,16 +101,20 @@ final class Storage { } func createFolderStructure() throws { - try create(folder: pagesFolder) - try create(folder: filesFolder) - try create(folder: postsFolder) - try create(folder: tagsFolder) + try operate(in: .contentPath) { contentPath in + try create(folder: pagesFolder) + try create(folder: filesFolder(in: contentPath)) + try create(folder: postsFolder) + try create(folder: tagsFolder) + } } // MARK: Pages + private let pagesFolderName = "pages" + /// The folder path where the markdown and metadata files of the pages are stored (by their id/url component) - private var pagesFolder: URL { subFolder("pages") } + private var pagesFolder: URL { subFolder(pagesFolderName) } private func pageContentFileName(_ id: String, _ language: ContentLanguage) -> String { "\(id)-\(language.rawValue).md" @@ -169,13 +173,15 @@ final class Storage { for language in ContentLanguage.allCases { files.formUnion(pages.map { pageContentFileName($0, language) }) } - try deleteFiles(in: pagesFolder, notIn: files) + try deleteFiles(in: pagesFolderName, notIn: files) } // MARK: Posts + private let postsFolderName = "posts" + /// The folder path where the markdown files of the posts are stored (by their unique id/url component) - private var postsFolder: URL { subFolder("posts") } + private var postsFolder: URL { subFolder(postsFolderName) } private func postFileUrl(postId: String) -> URL { postsFolder.appending(path: postId, directoryHint: .notDirectory).appendingPathExtension("json") @@ -202,13 +208,15 @@ final class Storage { func deletePostFiles(notIn posts: [String]) throws { let files = Set(posts.map { $0 + ".json" }) - try deleteFiles(in: postsFolder, notIn: files) + try deleteFiles(in: postsFolderName, notIn: files) } // MARK: Tags + private let tagsFolderName = "tags" + /// The folder path where the source images are stored (by their unique name) - private var tagsFolder: URL { subFolder("tags") } + private var tagsFolder: URL { subFolder(tagsFolderName) } private func tagFileUrl(tagId: String) -> URL { tagsFolder.appending(path: tagId, directoryHint: .notDirectory) @@ -230,44 +238,44 @@ final class Storage { func deleteTagFiles(notIn tags: [String]) throws { let files = Set(tags.map { $0 + ".json" }) - try deleteFiles(in: tagsFolder, notIn: files) + try deleteFiles(in: tagsFolderName, notIn: files) } - // MARK: Files + // MARK: File descriptions - private var imageDescriptionFilename: String { - "image-descriptions.json" - } + private let fileDescriptionFilename = "file-descriptions.json" - private var imageDescriptionUrl: URL { - baseFolder.appending(path: "image-descriptions.json") - } - - func loadImageDescriptions() -> [ImageDescriptions] { + func loadFileDescriptions() -> [FileDescriptions] { do { - return try read(relativePath: imageDescriptionFilename) + return try read(relativePath: fileDescriptionFilename) } catch { - print("Failed to read image descriptions: \(error)") + print("Failed to read file descriptions: \(error)") return [] } } @discardableResult - func save(imageDescriptions: [ImageDescriptions]) -> Bool { + func save(fileDescriptions: [FileDescriptions]) -> Bool { do { - try writeIfChanged(imageDescriptions, to: imageDescriptionFilename) + try writeIfChanged(fileDescriptions, to: fileDescriptionFilename) return true } catch { - print("Failed to write image descriptions: \(error)") + print("Failed to write file descriptions: \(error)") return false } } - /// The folder path where other files are stored (by their unique name) - var filesFolder: URL { subFolder("files") } + // MARK: Files - private func fileUrl(file: String) -> URL { - filesFolder.appending(path: file, directoryHint: .notDirectory) + private let filesFolderName = "files" + + /// The folder path where other files are stored (by their unique name) + func filesFolder(in folder: URL) -> URL { + folder.appending(path: filesFolderName, directoryHint: .isDirectory) + } + + private func fileUrl(file: String, in folder: URL) -> URL { + filesFolder(in: folder).appending(path: file, directoryHint: .notDirectory) } /** @@ -275,8 +283,30 @@ final class Storage { */ @discardableResult func copyFile(at url: URL, fileId: String) -> Bool { - let contentUrl = fileUrl(file: fileId) - return copy(file: url, to: contentUrl, type: "file", id: fileId) + do { + try operate(in: .contentPath) { contentPath in + let destination = fileUrl(file: fileId, in: contentPath) + try fm.copyItem(at: url, to: destination) + } + return true + } catch { + print("Failed to copy external file \(url.path()) to \(fileId): \(error)") + return false + } + } + + func move(file fileId: String, to newFile: String) -> Bool { + do { + try operate(in: .contentPath) { contentPath in + let source = fileUrl(file: fileId, in: contentPath) + let destination = fileUrl(file: newFile, in: contentPath) + try fm.moveItem(at: source, to: destination) + } + return true + } catch { + print("Failed to move file \(fileId) to \(newFile): \(error)") + return false + } } func copy(file fileId: String, to relativeOutputPath: String) -> Bool { @@ -287,8 +317,9 @@ final class Storage { if output.exists { return } - let input = contentPath.appending(path: "files/\(fileId)", directoryHint: .notDirectory) try output.ensureParentFolderExistence() + + let input = fileUrl(file: fileId, in: contentPath) try FileManager.default.copyItem(at: input, to: output) } } @@ -299,14 +330,15 @@ final class Storage { } } - func loadAllFiles() throws -> [String : URL] { - try files(in: filesFolder).reduce(into: [:]) { files, url in - files[url.lastPathComponent] = url + func loadAllFiles() throws -> [String] { + try operate(in: .contentPath) { contentPath in + let folder = filesFolder(in: contentPath) + return try files(in: folder).map { $0.lastPathComponent } } } func deleteFiles(notIn fileSet: [String]) throws { - try deleteFiles(in: filesFolder, notIn: Set(fileSet)) + try deleteFiles(in: filesFolderName, notIn: Set(fileSet)) } func fileContent(for file: String) throws -> String { @@ -318,6 +350,15 @@ final class Storage { } } + func fileData(for file: String) throws -> Data { + try operate(in: .contentPath) { folder in + let fileUrl = folder + .appending(path: "files", directoryHint: .isDirectory) + .appending(path: file, directoryHint: .notDirectory) + return try Data(contentsOf: fileUrl) + } + } + // MARK: Website data private var settingsDataUrl: URL { @@ -403,13 +444,16 @@ final class Storage { // MARK: Writing files - private func deleteFiles(in folder: URL, notIn fileSet: Set) throws { - let filesToDelete = try files(in: folder) - .filter { !fileSet.contains($0.lastPathComponent) } + private func deleteFiles(in folder: String, notIn fileSet: Set) throws { + try operate(in: .contentPath) { contentPath in + let subFolder = contentPath.appending(path: folder, directoryHint: .isDirectory) + let filesToDelete = try files(in: subFolder) + .filter { !fileSet.contains($0.lastPathComponent) } - for file in filesToDelete { - try fm.removeItem(at: file) - print("Deleted \(file.path())") + for file in filesToDelete { + try fm.removeItem(at: file) + print("Deleted \(file.path())") + } } } diff --git a/CHDataManagement/Views/ColorPalette.swift b/CHDataManagement/Views/ColorPalette.swift deleted file mode 100644 index 3a105e8..0000000 --- a/CHDataManagement/Views/ColorPalette.swift +++ /dev/null @@ -1,20 +0,0 @@ -import SwiftUI - -enum ColorPalette { - - static let tagBackground = Color(r: 188, g: 188, b: 188) // Color(r: 9, g: 62, b: 103) - - static let tagForeground = Color.primary // Color(r: 96, g: 186, b: 255) - - static let listBackground = Color(r: 2, g: 15, b: 26) - - static let postBackground = Color(r: 222, g: 222, b: 222) // Color(r: 4, g: 31, b: 52) - - static let postText = Color(r: 221, g: 221, b: 221) - - static let postDate = tagForeground - - static let link = Color.blue - -} - diff --git a/CHDataManagement/Views/Files/AddFileView.swift b/CHDataManagement/Views/Files/AddFileView.swift new file mode 100644 index 0000000..36b6f1e --- /dev/null +++ b/CHDataManagement/Views/Files/AddFileView.swift @@ -0,0 +1,109 @@ +import SwiftUI + +struct AddFileView: View { + + @Environment(\.dismiss) + private var dismiss: DismissAction + + @EnvironmentObject + private var content: Content + + @Binding + var selectedFile: FileResource? + + @Binding + var selectedImage: ImageResource? + + @State + private var filesToAdd: [FileToAdd] = [] + + init(selectedImage: Binding, selectedFile: Binding) { + _selectedFile = selectedFile + _selectedImage = selectedImage + } + + var body: some View { + ScrollView { + Text("Select files to add") + .foregroundStyle(.secondary) + List { + ForEach(filesToAdd) { file in + FileToAddView(file: file, delete: delete) + } + }.frame(minHeight: 300) + HStack { + Button("Cancel", role: .cancel) { dismiss() } + Button("Select more files", action: openFilePanel) + Button("Add selected", action: importSelectedFiles) + .disabled(filesToAdd.isEmpty) + } + } + .padding() + } + + private func openFilePanel() { + let panel = NSOpenPanel() + + panel.canChooseFiles = true + panel.canChooseDirectories = false + panel.allowsMultipleSelection = true + panel.showsHiddenFiles = false + panel.title = "Select files to add" + panel.prompt = "" + + let response = panel.runModal() + guard response == .OK else { + print("Failed to select files to import") + return + } + + for url in panel.urls { + guard !filesToAdd.contains(where: { $0.url == url }) else { + print("Skipping already selected file \(url.path())") + continue + } + print("Selected file \(url.path())") + let newFile = FileToAdd(content: content, url: url) + filesToAdd.append(newFile) + } + } + + private func delete(file: FileToAdd) { + guard let index = filesToAdd.firstIndex(of: file) else { + return + } + filesToAdd.remove(at: index) + } + + private func importSelectedFiles() { + for file in filesToAdd { + guard file.isSelected else { + print("Skipping unselected file \(file.uniqueId)") + continue + } + guard !file.idAlreadyExists else { + print("Skipping existing file \(file.uniqueId)") + continue + } + + guard content.storage.copyFile(at: file.url, fileId: file.uniqueId) else { + print("Failed to import file '\(file.uniqueId)' at \(file.url.path())") + return + } + + let resource = FileResource( + content: content, + id: file.uniqueId, + en: "", de: "") + // TODO: Insert at correct index? + content.files.insert(resource, at: 0) + selectedFile = resource + } + dismiss() + } +} + +#Preview { + AddFileView(selectedImage: .constant(nil), + selectedFile: .constant(nil)) +} diff --git a/CHDataManagement/Views/Files/FileContentView.swift b/CHDataManagement/Views/Files/FileContentView.swift index 8fd9bcd..e748694 100644 --- a/CHDataManagement/Views/Files/FileContentView.swift +++ b/CHDataManagement/Views/Files/FileContentView.swift @@ -1,37 +1,68 @@ import SwiftUI +import SFSafeSymbols struct FileContentView: View { + private let iconSize: CGFloat = 150 + @ObservedObject var file: FileResource - @EnvironmentObject - private var content: Content - @State private var fileContent: String = "" var body: some View { VStack { - if fileContent != "" { - TextEditor(text: $fileContent) - .font(.body.monospaced()) - .textEditorStyle(.plain) - } else { - Text("The file is not a text file") - .onAppear(perform: loadFileContent) + switch file.type { + case .image: + file.imageToDisplay + .resizable() + .aspectRatio(contentMode: .fit) + 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) } }.padding() } +} - private func loadFileContent() { - do { - fileContent = try content.storage.fileContent(for: file.uniqueId) - } catch { - print(error) - fileContent = "" - } +extension FileContentView: MainContentView { + + init(item: FileResource) { + self.file = item } + + static let itemDescription = "a file" } #Preview { diff --git a/CHDataManagement/Views/Files/FileDetailView.swift b/CHDataManagement/Views/Files/FileDetailView.swift index ced9884..68a70f2 100644 --- a/CHDataManagement/Views/Files/FileDetailView.swift +++ b/CHDataManagement/Views/Files/FileDetailView.swift @@ -5,22 +5,75 @@ struct FileDetailView: View { @ObservedObject var file: FileResource + @State + private var newId: String + + init(file: FileResource) { + self.file = file + self.newId = file.id + } + + private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-.")).inverted + + private var idExists: Bool { + file.content.files.contains { $0.id == newId } + } + + private var containsInvalidCharacters: Bool { + newId.rangeOfCharacter(from: allowedCharactersInPostId) != nil + } + var body: some View { VStack(alignment: .leading) { Text("File Name") .font(.headline) - TextField("", text: $file.uniqueId) - .textFieldStyle(.roundedBorder) - .padding(.bottom) - .disabled(true) - Text("Description") + HStack { + TextField("", text: $newId) + .textFieldStyle(.roundedBorder) + Button(action: setNewId) { + Text("Update") + } + .disabled(newId.isEmpty || containsInvalidCharacters || idExists) + } + Text("German Description") .font(.headline) - TextField("", text: $file.description) + TextField("", text: $file.germanDescription) .textFieldStyle(.roundedBorder) + Text("English Description") + .font(.headline) + TextField("", text: $file.englishDescription) + .textFieldStyle(.roundedBorder) + if file.type.isImage { + Text("Image size") + .font(.headline) + Text("\(Int(file.size.width)) x \(Int(file.size.height)) (\(file.aspectRatio))") + .foregroundStyle(.secondary) + #warning("Add button to show image versions") + } + Spacer() }.padding() } + + private func setNewId() { + guard file.content.storage.move(file: file.id, to: newId) else { + print("Failed to move file \(file.id)") + newId = file.id + return + } + file.id = newId + } } +extension FileDetailView: MainContentView { + + init(item: FileResource) { + self.init(file: item) + } + + static let itemDescription = "a file" +} + + #Preview { FileDetailView(file: .mock) } diff --git a/CHDataManagement/Views/Files/FileListView.swift b/CHDataManagement/Views/Files/FileListView.swift new file mode 100644 index 0000000..6737541 --- /dev/null +++ b/CHDataManagement/Views/Files/FileListView.swift @@ -0,0 +1,100 @@ +import SwiftUI + +private enum FileFilterType: String, Hashable, CaseIterable, Identifiable { + case images + case text + case videos + case other + + var text: String { + switch self { + case .images: return "Image" + case .text: return "Text" + case .videos: return "Video" + case .other: return "Other" + } + } + + var id: String { + rawValue + } + + func matches(_ type: FileType) -> Bool { + switch self { + case .images: return type.isImage + case .text: return type.isTextFile + case .videos: return type.isVideo + case .other: return type.isOtherFile + } + } +} + +struct FileListView: View { + + @EnvironmentObject + private var content: Content + + @Binding + var selectedFile: FileResource? + + @State + private var selectedFileType: FileFilterType = .images + + @State + private var searchString = "" + + var filesBySelectedType: [FileResource] { + content.files.filter { selectedFileType.matches($0.type) } + } + + var filteredFiles: [FileResource] { + guard !searchString.isEmpty else { + return filesBySelectedType + } + return filesBySelectedType.filter { $0.id.contains(searchString) } + } + + var body: some View { + VStack(alignment: .center) { + Picker("", selection: $selectedFileType) { + ForEach(FileFilterType.allCases) { type in + Text(type.text).tag(type) + } + } + .pickerStyle(.segmented) + .padding(.trailing, 7) + TextField("", text: $searchString, prompt: Text("Search")) + .textFieldStyle(.roundedBorder) + .padding(.horizontal, 8) + List(filteredFiles, selection: $selectedFile) { file in + Text(file.id).tag(file) + } + .onChange(of: selectedFileType) { oldValue, newValue in + guard oldValue != newValue else { + return + } + if let selectedFile, + newValue.matches(selectedFile.type) { + return + } + selectedFile = filteredFiles.first + } + } + .onAppear { + if selectedFile == nil { + selectedFile = content.files.first + } + } + } +} + +#Preview { + NavigationSplitView { + FileListView(selectedFile: .constant(nil)) + .environmentObject(Content.mock) + .navigationSplitViewColumnWidth(250) + } detail: { + Text("") + .frame(width: 50) + } +} diff --git a/CHDataManagement/Views/Files/FileToAdd.swift b/CHDataManagement/Views/Files/FileToAdd.swift new file mode 100644 index 0000000..ebb4ba6 --- /dev/null +++ b/CHDataManagement/Views/Files/FileToAdd.swift @@ -0,0 +1,46 @@ +import Foundation + +final class FileToAdd: ObservableObject { + + unowned let content: Content + + let url: URL + + @Published + var uniqueId: String + + @Published + var isSelected: Bool = true + + init(content: Content, url: URL) { + self.content = content + self.url = url + self.uniqueId = url.lastPathComponent + } + + var idAlreadyExists: Bool { + content.files.contains { $0.id == uniqueId } + } +} + +extension FileToAdd: Identifiable { + + var id: URL { + url + } +} + +extension FileToAdd: Equatable { + + static func == (lhs: FileToAdd, rhs: FileToAdd) -> Bool { + lhs.url == rhs.url + } +} + +extension FileToAdd: Hashable { + + func hash(into hasher: inout Hasher) { + hasher.combine(url) + } +} + diff --git a/CHDataManagement/Views/Files/FileToAddView.swift b/CHDataManagement/Views/Files/FileToAddView.swift new file mode 100644 index 0000000..d24c582 --- /dev/null +++ b/CHDataManagement/Views/Files/FileToAddView.swift @@ -0,0 +1,45 @@ +import SwiftUI +import SFSafeSymbols + +struct FileToAddView: View { + + @ObservedObject + var file: FileToAdd + + let delete: (FileToAdd) -> Void + + var body: some View { + VStack(alignment: .leading) { + HStack { + Image(systemSymbol: file.isSelected ? .checkmarkCircleFill : .circle) + .resizable() + .frame(width: 20, height: 20) + .foregroundStyle(.blue) + .onTapGesture { + file.isSelected.toggle() + } + Image(systemSymbol: .trashCircleFill) + .resizable() + .frame(width: 20, height: 20) + .foregroundStyle(.red) + .onTapGesture { + delete(file) + } + TextField("", text: $file.uniqueId) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: 200) + + } + Text(file.url.path()) + .foregroundStyle(.secondary) + } + + } +} + +#Preview { + List { + FileToAddView(file: .init(content: .mock, url: URL(fileURLWithPath: "/path/to/file.swift")), delete: { _ in }) + FileToAddView(file: .init(content: .mock, url: URL(fileURLWithPath: "/path/to/file2.swift")), delete: { _ in }) + } +} diff --git a/CHDataManagement/Views/Files/FilesView.swift b/CHDataManagement/Views/Files/FilesView.swift deleted file mode 100644 index cf9d6b7..0000000 --- a/CHDataManagement/Views/Files/FilesView.swift +++ /dev/null @@ -1,79 +0,0 @@ -import SwiftUI - -struct FilesView: View { - - @EnvironmentObject - private var content: Content - - @State - private var selected: FileResource? = nil - - var body: some View { - NavigationSplitView { - List(content.files, selection: $selected) { file in - Text(file.uniqueId) - .tag(file) - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button(action: openFilePanel) { - Label("Add file", systemSymbol: .plus) - } - } - } - .navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 250) - } content: { - if let selected { - FileContentView(file: selected) - .id(selected.uniqueId) - } else { - Text("Select a file") - } - } detail: { - if let selected { - FileDetailView(file: selected) - } else { - EmptyView() - } - } - } - - private func openFilePanel() { - let panel = NSOpenPanel() - // Sets up so user can only select a single directory - panel.canChooseFiles = true - panel.canChooseDirectories = false - panel.allowsMultipleSelection = true - panel.showsHiddenFiles = false - panel.title = "Select files to add" - panel.prompt = "" - - let response = panel.runModal() - guard response == .OK else { - print("Failed to select files to import") - return - } - - for url in panel.urls { - let fileId = url.lastPathComponent - guard !content.files.contains(where: { $0.uniqueId == fileId }) else { - print("A file '\(fileId)' already exists") - continue - } - let type = FileType(fileExtension: fileId.fileExtension) - let file = FileResource(type: type, uniqueId: fileId, description: "") - guard content.storage.copyFile(at: url, fileId: fileId) else { - print("Failed to import file '\(fileId)'") - continue - } - content.files.insert(file, at: 0) - } - } - - -} - -#Preview { - FilesView() - .environmentObject(Content.mock) -} diff --git a/CHDataManagement/Views/Files/TextFileContentView.swift b/CHDataManagement/Views/Files/TextFileContentView.swift new file mode 100644 index 0000000..ba2915d --- /dev/null +++ b/CHDataManagement/Views/Files/TextFileContentView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct TextFileContentView: View { + + @ObservedObject + var file: FileResource + + @State + private var fileContent: String = "" + + var body: some View { + if fileContent != "" { + TextEditor(text: $fileContent) + .font(.body.monospaced()) + .textEditorStyle(.plain) + //.background(.clear) + } else { + VStack { + Image(systemSymbol: .docText) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 150) + Text("No preview available") + .font(.title) + } + .foregroundStyle(.secondary) + .onAppear(perform: loadFileContent) + } + } + + private func loadFileContent() { + guard fileContent == "" else { + return + } + fileContent = file.textContent() + print("Loaded content of file \(file.id)") + } +} diff --git a/CHDataManagement/Views/Generic/NavigationIcon.swift b/CHDataManagement/Views/Generic/NavigationIcon.swift new file mode 100644 index 0000000..3e1736a --- /dev/null +++ b/CHDataManagement/Views/Generic/NavigationIcon.swift @@ -0,0 +1,21 @@ +import SwiftUI +import SFSafeSymbols + +struct NavigationIcon: View { + + let symbol: SFSymbol + + let edge: Edge.Set + + var body: some View { + SwiftUI.Image(systemSymbol: symbol) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(5) + .padding(edge, 2) + .fontWeight(.light) + .foregroundStyle(Color.white.opacity(0.8)) + .frame(width: 30, height: 30) + .background(Color.black.opacity(0.7).clipShape(Circle())) + } +} diff --git a/CHDataManagement/Views/Images/FlexibleColumnView.swift b/CHDataManagement/Views/Images/FlexibleColumnView.swift deleted file mode 100644 index 6560f87..0000000 --- a/CHDataManagement/Views/Images/FlexibleColumnView.swift +++ /dev/null @@ -1,49 +0,0 @@ -import SwiftUI - -struct FlexibleColumnView: View where Content: Identifiable, Inner: View { - - @Binding - var items: [Content] - - let maximumItemWidth: CGFloat - - let spacing: CGFloat - - private let content: (_ item: Content, _ width: CGFloat) -> Inner - - init(items: Binding<[Content]>, maximumItemWidth: CGFloat = 300, spacing: CGFloat = 20, content: @escaping (_ item: Content, _ width: CGFloat) -> Inner) { - self._items = items - self.maximumItemWidth = maximumItemWidth - self.spacing = spacing - self.content = content - } - - var body: some View { - GeometryReader { geometry in - let totalWidth = geometry.size.width - let columnCount = max(Int((totalWidth + spacing) / (maximumItemWidth + spacing)), 1) - let totalSpacing = spacing * CGFloat(columnCount + 1) - let trueItemWidth = (totalWidth - totalSpacing) / CGFloat(columnCount) - - let columns = Array(repeating: GridItem(.flexible(), spacing: spacing), count: columnCount) - - ScrollView { - LazyVGrid(columns: columns, spacing: spacing) { - ForEach(items) { item in - content(item, trueItemWidth) - } - } - .padding(spacing) - } - } - } -} - -#Preview { - FlexibleColumnView(items: .constant(MockImage.images), maximumItemWidth: 150) { image, width in - image.imageToDisplay - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: width) - } -} diff --git a/CHDataManagement/Views/Images/ImageContentView.swift b/CHDataManagement/Views/Images/ImageContentView.swift new file mode 100644 index 0000000..d77e09e --- /dev/null +++ b/CHDataManagement/Views/Images/ImageContentView.swift @@ -0,0 +1,26 @@ +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)) +} diff --git a/CHDataManagement/Views/Images/ImageDetailsView.swift b/CHDataManagement/Views/Images/ImageDetailsView.swift deleted file mode 100644 index dd5ad23..0000000 --- a/CHDataManagement/Views/Images/ImageDetailsView.swift +++ /dev/null @@ -1,63 +0,0 @@ -import SwiftUI - -struct ImageDetailsView: View { - - @Environment(\.language) - var language - - @ObservedObject - var image: ImageResource - - @State - private var newId: String - - init(image: ImageResource) { - self.image = image - self.newId = image.id - } - - var body: some View { - VStack(alignment: .leading) { - Text("Unique identifier") - .font(.headline) - HStack { - TextField("", text: $newId) - Button(action: setNewId) { - Text("Update") - } - } - Text("German Description") - .font(.headline) - TextField("", text: $image.germanDescription) - Text("English Description") - .font(.headline) - TextField("", text: $image.englishDescription) - Text("Info") - .font(.headline) - HStack(alignment: .top) { - VStack(alignment: .leading) { - Text("Original Size") - Text("Aspect ratio") - } - VStack(alignment: .trailing) { - Text("\(Int(image.size.width)) x \(Int(image.size.height))") - Text("\(image.aspectRatio)") - } - }.padding(.vertical) - Text("Versions") - .font(.headline) - Spacer() - } - .padding() - } - - private func setNewId() { - #warning("Check if ID is unique") - // TODO: Clean id - image.id = newId - } -} - -#Preview { - ImageDetailsView(image: MockImage.images.first!) -} diff --git a/CHDataManagement/Views/Images/ImagesContentView.swift b/CHDataManagement/Views/Images/ImagesContentView.swift deleted file mode 100644 index 997512d..0000000 --- a/CHDataManagement/Views/Images/ImagesContentView.swift +++ /dev/null @@ -1,17 +0,0 @@ -import SwiftUI - -struct ImagesContentView: View { - - @ObservedObject - var image: ImageResource - - var body: some View { - image.imageToDisplay - .resizable() - .aspectRatio(contentMode: .fit) - } -} - -#Preview { - ImagesContentView(image: .init(resourceName: "image1", type: .jpg)) -} diff --git a/CHDataManagement/Views/Images/ImagesView.swift b/CHDataManagement/Views/Images/ImagesView.swift deleted file mode 100644 index 7fa79f3..0000000 --- a/CHDataManagement/Views/Images/ImagesView.swift +++ /dev/null @@ -1,50 +0,0 @@ -import SwiftUI -import SFSafeSymbols - -struct ImagesView: View { - - @EnvironmentObject - var content: Content - - let maximumItemWidth: CGFloat = 300 - - let aspectRatio: CGFloat = 1.5 - - let spacing: CGFloat = 20 - - @State - private var selectedImage: ImageResource? - - @State - private var showImageDetails = false - - var body: some View { - NavigationSplitView { - List(content.images, selection: $selectedImage) { image in - Text(image.id) - .tag(image) - } - } content: { - if let selectedImage { - ImagesContentView(image: selectedImage) - .layoutPriority(1) - } else { - Text("Select an image in the sidebar") - } - } detail: { - if let selectedImage { - ImageDetailsView(image: selectedImage) - .frame(maxWidth: 350) - } else { - EmptyView() - } - } - } -} - -#Preview { - let content = Content() - content.images = MockImage.images - return ImagesView() - .environmentObject(content) -} diff --git a/CHDataManagement/Views/Pages/AddPageView.swift b/CHDataManagement/Views/Pages/AddPageView.swift new file mode 100644 index 0000000..ea13b32 --- /dev/null +++ b/CHDataManagement/Views/Pages/AddPageView.swift @@ -0,0 +1,93 @@ +import SwiftUI + +struct AddPageView: View { + + @Environment(\.dismiss) + private var dismiss: DismissAction + + @Environment(\.language) + private var language: ContentLanguage + + @EnvironmentObject + private var content: Content + + @Binding + var selectedPage: Page? + + @State + private var newPageId = "" + + private let allowedCharactersInPageId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted + + init(selected: Binding) { + self._selectedPage = selected + } + + private var idExists: Bool { + content.pages.contains { $0.id == newPageId } + } + + private var containsInvalidCharacters: Bool { + newPageId.rangeOfCharacter(from: allowedCharactersInPageId) != nil + } + + var body: some View { + VStack { + Text("New page") + .font(.headline) + + TextField("", text: $newPageId) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: 350) + if newPageId.isEmpty { + Text("Enter the id of the new page to create") + .foregroundStyle(.secondary) + } else if idExists { + Text("A page with the same id already exists") + .foregroundStyle(Color.red) + } else if containsInvalidCharacters { + Text("The id contains invalid characters") + .foregroundStyle(Color.red) + } else { + Text("Create a new page with the id") + .foregroundStyle(.secondary) + } + HStack { + Button(role: .cancel, action: dismissSheet) { + Text("Cancel") + } + Button(action: addNewPage) { + Text("Create") + } + .disabled(newPageId.isEmpty || containsInvalidCharacters || idExists) + } + } + .padding() + } + + private func addNewPage() { + let page = Page( + id: newPageId, + 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) + selectedPage = page + dismissSheet() + } + + private func dismissSheet() { + dismiss() + } +} + +#Preview { + AddPageView(selected: .constant(nil)) + .environmentObject(Content.mock) +} diff --git a/CHDataManagement/Views/Pages/LocalizedPageContentView.swift b/CHDataManagement/Views/Pages/LocalizedPageContentView.swift new file mode 100644 index 0000000..1a149d9 --- /dev/null +++ b/CHDataManagement/Views/Pages/LocalizedPageContentView.swift @@ -0,0 +1,67 @@ +import SwiftUI +import HighlightedTextEditor + +struct LocalizedPageContentView: View { + + let pageId: String + + @ObservedObject + var page: LocalizedPage + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @State + private var isGeneratingWebsite = false + + @State + private var pageContent: String = "" + + init(pageId: String, page: LocalizedPage) { + self.pageId = pageId + self.page = page + } + + var body: some View { + VStack(alignment: .leading) { + TextField("", text: $page.title) + .font(.title) + .textFieldStyle(.plain) + + HStack(alignment: .firstTextBaseline) { + Button(action: loadContent) { + Text("Load") + } + Button(action: saveContent) { + Text("Save") + } + Spacer() + } + HighlightedTextEditor( + text: $pageContent, + highlightRules: .markdown) + } + .padding() + .onAppear(perform: loadContent) + .onDisappear(perform: saveContent) + } + + private func loadContent() { + let content = content.storage.pageContent(for: pageId, language: language) + guard content != "" else { + pageContent = "New file" + return + } + pageContent = content + } + + private func saveContent() { + guard pageContent != "", pageContent != "New file" else { + return + } + content.storage.save(pageContent: pageContent, for: pageId, language: language) + } +} diff --git a/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift b/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift index 2a83335..1c171fe 100644 --- a/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift +++ b/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift @@ -36,7 +36,7 @@ struct LocalizedPageDetailView: View { size: 22, color: .red) { item.linkPreviewImage = nil - } + }.disabled(item.linkPreviewImage == nil) Spacer() } diff --git a/CHDataManagement/Views/Pages/PageContentView.swift b/CHDataManagement/Views/Pages/PageContentView.swift index 6ae16e8..da4ce69 100644 --- a/CHDataManagement/Views/Pages/PageContentView.swift +++ b/CHDataManagement/Views/Pages/PageContentView.swift @@ -1,15 +1,25 @@ import SwiftUI import HighlightedTextEditor +struct PageTitleView: View { + + @ObservedObject + var page: LocalizedPage + + var body: some View { + TextField("", text: $page.title) + .font(.title) + .textFieldStyle(.plain) + } +} + struct PageContentView: View { @ObservedObject var page: Page - @ObservedObject - private var localized: LocalizedPage - - let language: ContentLanguage + @Environment(\.language) + private var language @EnvironmentObject private var content: Content @@ -17,87 +27,26 @@ struct PageContentView: View { @State private var isGeneratingWebsite = false - @State - private var pageContent: String = "" - - init(page: Page, language: ContentLanguage) { + init(page: Page) { self.page = page - self.localized = page.localized(in: language) - self.language = language } var body: some View { - VStack(alignment: .leading) { - TextField("", text: $localized.title) - .font(.title) - .textFieldStyle(.plain) - - HStack(alignment: .firstTextBaseline) { - Button(action: loadContent) { - Text("Load") - } - Button(action: saveContent) { - Text("Save") - } - Button(action: generate) { - Text("Generate") - } - .disabled(isGeneratingWebsite) - Spacer() - } - HighlightedTextEditor( - text: $pageContent, - highlightRules: .markdown) - } - .padding() - .onAppear(perform: loadContent) - .onDisappear(perform: saveContent) + LocalizedPageContentView(pageId: page.id, page: page.localized(in: language)) + .id(page.id + language.rawValue) } - private func loadContent() { - let content = content.storage.pageContent(for: page.id, language: language) - guard content != "" else { - pageContent = "New file" - return - } - pageContent = content +} + +extension PageContentView: MainContentView { + + init(item: Page) { + self.page = item } - private func saveContent() { - guard pageContent != "", pageContent != "New file" else { - return - } - content.storage.save(pageContent: pageContent, for: page.id, language: language) - } - - private func generate() { - 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 - print("Generating page") - DispatchQueue.global(qos: .userInitiated).async { - let generator = WebsiteGenerator( - content: content, - language: language) - if !generator.generate(page: page) { - print("Generation failed") - } - DispatchQueue.main.async { - isGeneratingWebsite = false - print("Done") - } - } - } + static let itemDescription = "a page" } #Preview { - PageContentView(page: .empty, language: .english) + PageContentView(page: .empty) } diff --git a/CHDataManagement/Views/Pages/PageDetailView.swift b/CHDataManagement/Views/Pages/PageDetailView.swift index 72da1bd..5409e03 100644 --- a/CHDataManagement/Views/Pages/PageDetailView.swift +++ b/CHDataManagement/Views/Pages/PageDetailView.swift @@ -5,19 +5,29 @@ struct PageDetailView: View { @Environment(\.language) private var language + @EnvironmentObject + private var content: Content + @ObservedObject - private var item: Page + private var page: Page + + @State + private var isGeneratingWebsite = false init(page: Page) { - self.item = page + self.page = page } var body: some View { ScrollView { VStack(alignment: .leading) { + Button(action: generate) { + Text("Generate") + } + .disabled(isGeneratingWebsite) Text("ID") .font(.headline) - TextField("", text: $item.id) + TextField("", text: $page.id) .textFieldStyle(.roundedBorder) .padding(.bottom) @@ -25,7 +35,7 @@ struct PageDetailView: View { Text("Draft") .font(.headline) Spacer() - Toggle("", isOn: $item.isDraft) + Toggle("", isOn: $page.isDraft) .toggleStyle(.switch) } .padding(.bottom) @@ -34,7 +44,7 @@ struct PageDetailView: View { Text("Start") .font(.headline) Spacer() - DatePicker("", selection: $item.startDate, displayedComponents: .date) + DatePicker("", selection: $page.startDate, displayedComponents: .date) .datePickerStyle(.compact) .padding(.bottom) } @@ -43,30 +53,67 @@ struct PageDetailView: View { Text("Has end date") .font(.headline) Spacer() - Toggle("", isOn: $item.hasEndDate) + Toggle("", isOn: $page.hasEndDate) .toggleStyle(.switch) .padding(.bottom) } - if item.hasEndDate { + if page.hasEndDate { HStack(alignment: .firstTextBaseline) { Text("End date") .font(.headline) Spacer() - DatePicker("", selection: $item.endDate, displayedComponents: .date) + DatePicker("", selection: $page.endDate, displayedComponents: .date) .datePickerStyle(.compact) .padding(.bottom) } } - LocalizedPageDetailView(page: item.localized(in: language)) + LocalizedPageDetailView(page: page.localized(in: language)) } .padding() } } + + private func generate() { + 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 + print("Generating page") + DispatchQueue.global(qos: .userInitiated).async { + let generator = WebsiteGenerator( + content: content, + language: language) + if !generator.generate(page: page) { + print("Generation failed") + } + DispatchQueue.main.async { + isGeneratingWebsite = false + print("Done") + } + } + } } +extension PageDetailView: MainContentView { + + init(item: Page) { + self.page = item + } + + static let itemDescription = "a page" +} + + #Preview { PageDetailView(page: .empty) } diff --git a/CHDataManagement/Views/Pages/PageListView.swift b/CHDataManagement/Views/Pages/PageListView.swift index 159b69e..dfc9a39 100644 --- a/CHDataManagement/Views/Pages/PageListView.swift +++ b/CHDataManagement/Views/Pages/PageListView.swift @@ -8,108 +8,41 @@ struct PageListView: View { @EnvironmentObject private var content: Content - @State - private var selected: Page? + @Binding + private var selectedPage: Page? @State - private var showNewPageView = false + private var searchString = "" - @State - private var newPageId = "" + init(selectedPage: Binding) { + self._selectedPage = selectedPage + } - @State - private var newPageIdIsValid = false - - private let allowedCharactersInPageId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted - - private var cleanPageId: String { - newPageId.trimmingCharacters(in: .whitespacesAndNewlines) + private var filteredPages: [Page] { + guard !searchString.isEmpty else { + return content.pages + } + return content.pages.filter { $0.localized(in: language).title.contains(searchString) } } var body: some View { - NavigationSplitView { - List(content.pages, selection: $selected) { page in - Text(page.localized(in: language).title) - .tag(page) - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button(action: { showNewPageView = true }) { - Label("New post", systemSymbol: .plus) - } - } - } - .navigationSplitViewColumnWidth(min: 300, ideal: 300, max: 300) - } content: { - if let selected { - PageContentView(page: selected, language: language) - .id(selected.id + language.rawValue) - .layoutPriority(1) - } else { - // Fallback if no item is selected - Text("Select a page from the list") - .font(.largeTitle) - .foregroundColor(.secondary) - } - } detail: { - if let selected { - PageDetailView(page: selected) - .frame(maxWidth: 350) - } else { - EmptyView() - .frame(maxWidth: 350) + VStack { + TextField("", text: $searchString, prompt: Text("Search")) + .textFieldStyle(.roundedBorder) + .padding(.horizontal, 8) + List(filteredPages, selection: $selectedPage) { page in + Text(page.localized(in: language).title).tag(page) } } .onAppear { - if selected == nil { - selected = content.pages.first + if selectedPage == nil { + selectedPage = 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 } } #Preview { - PageListView() + PageListView(selectedPage: .constant(nil)) .environmentObject(Content.mock) } diff --git a/CHDataManagement/Views/Posts/AddPostView.swift b/CHDataManagement/Views/Posts/AddPostView.swift new file mode 100644 index 0000000..11769dd --- /dev/null +++ b/CHDataManagement/Views/Posts/AddPostView.swift @@ -0,0 +1,91 @@ +import SwiftUI + +struct AddPostView: View { + + @Environment(\.dismiss) + private var dismiss: DismissAction + + @Environment(\.language) + private var language: ContentLanguage + + @EnvironmentObject + private var content: Content + + @Binding + var selectedPost: Post? + + @State + private var newPostId = "" + + private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted + + init(selected: Binding) { + self._selectedPost = selected + } + + private var idExists: Bool { + content.posts.contains { $0.id == newPostId } + } + + private var containsInvalidCharacters: Bool { + newPostId.rangeOfCharacter(from: allowedCharactersInPostId) != nil + } + + var body: some View { + VStack { + Text("New post") + .font(.headline) + + TextField("", text: $newPostId) + .textFieldStyle(.roundedBorder) + .frame(maxWidth: 350) + if newPostId.isEmpty { + Text("Enter the id of the new post to create") + .foregroundStyle(.secondary) + } else if idExists { + Text("A post with the same id already exists") + .foregroundStyle(Color.red) + } else if containsInvalidCharacters { + Text("The id contains invalid characters") + .foregroundStyle(Color.red) + } else { + Text("Create a new post with the id") + .foregroundStyle(.secondary) + } + HStack { + Button(role: .cancel, action: dismissSheet) { + Text("Cancel") + } + Button(action: addNewPost) { + Text("Create") + } + .disabled(newPostId.isEmpty || containsInvalidCharacters || idExists) + } + } + .padding() + } + + private func addNewPost() { + let post = Post( + id: newPostId, + isDraft: true, + createdDate: .now, + startDate: .now, + endDate: nil, + tags: [], + german: .init(title: "Titel", content: "Text"), + english: .init(title: "Title", content: "Text")) + content.posts.insert(post, at: 0) + selectedPost = post + dismissSheet() + } + + private func dismissSheet() { + dismiss() + } +} + +#Preview { + AddPostView(selected: .constant(nil)) + .environmentObject(Content.mock) +} diff --git a/CHDataManagement/Views/Posts/ImagePickerView.swift b/CHDataManagement/Views/Posts/ImagePickerView.swift index 9938096..60f661b 100644 --- a/CHDataManagement/Views/Posts/ImagePickerView.swift +++ b/CHDataManagement/Views/Posts/ImagePickerView.swift @@ -5,7 +5,7 @@ struct ImagePickerView: View { @Binding var showImagePicker: Bool - private let selected: (ImageResource) -> Void + private let selected: (FileResource) -> Void @EnvironmentObject private var content: Content @@ -13,13 +13,13 @@ struct ImagePickerView: View { @Environment(\.language) private var language - init(showImagePicker: Binding, selected: @escaping (ImageResource) -> Void) { + init(showImagePicker: Binding, selected: @escaping (FileResource) -> Void) { self._showImagePicker = showImagePicker self.selected = selected } @State - private var selectedImage: ImageResource? + private var selectedImage: FileResource? var body: some View { VStack { diff --git a/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift b/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift index ce92121..d4df67e 100644 --- a/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift +++ b/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift @@ -35,7 +35,7 @@ struct LocalizedPostDetailView: View { size: 22, color: .red) { item.linkPreviewImage = nil - } + }.disabled(item.linkPreviewImage == nil) Spacer() } diff --git a/CHDataManagement/Views/Posts/PostContentView.swift b/CHDataManagement/Views/Posts/PostContentView.swift index 26ac8e5..276289f 100644 --- a/CHDataManagement/Views/Posts/PostContentView.swift +++ b/CHDataManagement/Views/Posts/PostContentView.swift @@ -10,11 +10,24 @@ struct PostContentView: View { @Environment(\.language) private var language + init(post: Post) { + self.post = post + } + var body: some View { LocalizedPostContentView(post: post) } } +extension PostContentView: MainContentView { + + init(item: Post) { + self.post = item + } + + static let itemDescription = "a post" +} + private struct LocalizedTitle: View { @ObservedObject @@ -44,9 +57,13 @@ private struct LocalizedContentEditor: View { var body: some View { TextEditor(text: $post.content) -// HighlightedTextEditor( -// text: $post.content, -// highlightRules: .markdown) + .font(.body) + .frame(minHeight: 150) + .textEditorStyle(.plain) + .padding(.vertical, 8) + .padding(.leading, 3) + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) } } @@ -76,10 +93,7 @@ struct LocalizedPostContentView: View { LocalizedTitle(post: post.localized(in: language)) FlowHStack { ForEach(post.tags, id: \.id) { tag in - TagView(tag: .init( - en: tag.english.name, - de: tag.german.name) - ) + TagView(text: tag.localized(in: language).name) .foregroundStyle(.white) } Button(action: { showTagPicker = true }) { diff --git a/CHDataManagement/Views/Posts/PostDetailView.swift b/CHDataManagement/Views/Posts/PostDetailView.swift index bef536e..d27a413 100644 --- a/CHDataManagement/Views/Posts/PostDetailView.swift +++ b/CHDataManagement/Views/Posts/PostDetailView.swift @@ -95,6 +95,16 @@ struct PostDetailView: View { } } +extension PostDetailView: MainContentView { + + init(item: Post) { + self.item = item + } + + static let itemDescription = "a post" +} + + #Preview(traits: .fixedLayout(width: 270, height: 500)) { PostDetailView(post: .fullMock) } diff --git a/CHDataManagement/Views/Posts/PostImageGalleryView.swift b/CHDataManagement/Views/Posts/PostImageGalleryView.swift deleted file mode 100644 index bcb65fd..0000000 --- a/CHDataManagement/Views/Posts/PostImageGalleryView.swift +++ /dev/null @@ -1,147 +0,0 @@ -import SwiftUI -import SFSafeSymbols - -struct NavigationIcon: View { - - let symbol: SFSymbol - - let edge: Edge.Set - - var body: some View { - SwiftUI.Image(systemSymbol: symbol) - .resizable() - .aspectRatio(contentMode: .fit) - .padding(5) - .padding(edge, 2) - .fontWeight(.light) - .foregroundStyle(Color.white.opacity(0.8)) - .frame(width: 30, height: 30) - .background(Color.black.opacity(0.7).clipShape(Circle())) - } -} - -struct PostImageGalleryView: View { - - @ObservedObject - var post: LocalizedPost - - @State private var currentIndex = 0 - - @State - private var showImagePicker = false - - private var imageAtCurrentIndex: Image? { - guard !post.images.isEmpty else { - return nil - } - guard currentIndex < post.images.count else { - return post.images.last?.imageToDisplay - } - return post.images[currentIndex].imageToDisplay - } - - var body: some View { - ZStack(alignment: .center) { - ZStack(alignment: .bottomTrailing) { - ZStack(alignment: .bottom) { - if let imageAtCurrentIndex { - imageAtCurrentIndex - .resizable() - .scaledToFit() - } - if post.images.count > 1 { - HStack(spacing: 8) { - ForEach(0.. 1 { - HStack { - Button(action: previous) { - NavigationIcon(symbol: .chevronLeft, edge: .trailing) - } - .buttonStyle(.plain) - .padding() - - Spacer() - Button(action: next) { - NavigationIcon(symbol: .chevronRight, edge: .leading) - } - .buttonStyle(.plain) - .padding() - } - } - } - .sheet(isPresented: $showImagePicker) { - ImagePickerView(showImagePicker: $showImagePicker) { image in - post.images.append(image) - } - } - } - - private func previous() { - if currentIndex > 0 { - currentIndex -= 1 - } else { - currentIndex = post.images.count - 1 - } - } - - private func next() { - if currentIndex < post.images.count - 1 { - currentIndex += 1 - } else { - currentIndex = 0 - } - } - - private func shiftBack() { - guard currentIndex > 0 else { - return - } - post.images.swapAt(currentIndex, currentIndex-1) - currentIndex -= 1 - } - - private func shiftForward() { - guard currentIndex < post.images.count - 1 else { - return - } - post.images.swapAt(currentIndex, currentIndex+1) - currentIndex += 1 - } - - private func removeImage() { - post.images.remove(at: currentIndex) - if currentIndex >= post.images.count { - currentIndex = post.images.count - 1 - } - } -} - -#Preview(traits: .fixedLayout(width: 300, height: 250)) { - PostImageGalleryView(post: .german) -} diff --git a/CHDataManagement/Views/Posts/PostImagesView.swift b/CHDataManagement/Views/Posts/PostImagesView.swift index 9c075e6..0d1a41b 100644 --- a/CHDataManagement/Views/Posts/PostImagesView.swift +++ b/CHDataManagement/Views/Posts/PostImagesView.swift @@ -57,7 +57,7 @@ struct PostImagesView: View { } } - private func shiftLeft(_ image: ImageResource) { + private func shiftLeft(_ image: FileResource) { guard let index = post.images.firstIndex(of: image) else { return } @@ -67,7 +67,7 @@ struct PostImagesView: View { post.images.swapAt(index, index - 1) } - private func shiftRight(_ image: ImageResource) { + private func shiftRight(_ image: FileResource) { guard let index = post.images.firstIndex(of: image) else { return } @@ -77,7 +77,7 @@ struct PostImagesView: View { post.images.swapAt(index, index + 1) } - private func remove(_ image: ImageResource) { + private func remove(_ image: FileResource) { guard let index = post.images.firstIndex(of: image) else { return } diff --git a/CHDataManagement/Views/Posts/PostList.swift b/CHDataManagement/Views/Posts/PostList.swift deleted file mode 100644 index 187473e..0000000 --- a/CHDataManagement/Views/Posts/PostList.swift +++ /dev/null @@ -1,118 +0,0 @@ -import SwiftUI - -struct PostList: View { - - @EnvironmentObject - private var content: Content - - @Environment(\.language) - private var language: ContentLanguage - - @State - private var selected: Post? = nil - - @State - private var showNewPostView = false - - @State - private var newPostId = "" - - @State - private var newPostIdIsValid = false - - private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted - - private var cleanPostId: String { - newPostId.trimmingCharacters(in: .whitespacesAndNewlines) - } - - var body: some View { - NavigationSplitView { - List(content.posts, selection: $selected) { post in - Text(post.localized(in: language).title) - .tag(post) - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button(action: { showNewPostView = true }) { - Label("New post", systemSymbol: .plus) - } - } - } - .navigationSplitViewColumnWidth(min: 250, ideal: 250, max: 250) - } content: { - if let selected { - PostContentView(post: selected) - .layoutPriority(1) - } else { - HStack { - Spacer() - Text("Select a post to show the content") - .font(.largeTitle) - .foregroundColor(.secondary) - Spacer() - }.layoutPriority(1) - } - } detail: { - if let selected { - PostDetailView(post: selected) - .frame(minWidth: 280) - } else { - Text("No post selected") - .frame(minWidth: 280) - } - } - .sheet(isPresented: $showNewPostView, - onDismiss: addNewPost) { - TextEntrySheet( - title: "Enter the id for the new post", - text: $newPostId, - isValid: $newPostIdIsValid) - } - .onChange(of: newPostId) { _, newValue in - newPostIdIsValid = isValid(id: newValue) - } - .onAppear { - if selected == nil { - selected = content.posts.first - } - } - } - - private func isValid(id: String) -> Bool { - let id = cleanPostId - guard id != "" else { - return false - } - - guard !content.posts.contains(where: { $0.id == id }) else { - return false - } - // Only allow alphanumeric characters and hyphens - return id.rangeOfCharacter(from: allowedCharactersInPostId) == nil - } - - private func addNewPost() { - let id = cleanPostId - guard isValid(id: id) else { - return - } - - let post = Post( - id: id, - isDraft: true, - createdDate: .now, - startDate: .now, - endDate: nil, - tags: [], - german: .init(title: "Titel", content: "Text"), - english: .init(title: "Title", content: "Text")) - content.posts.insert(post, at: 0) - selected = post - } -} - -#Preview { - PostList() - .environmentObject(Content()) -} diff --git a/CHDataManagement/Views/Posts/PostListView.swift b/CHDataManagement/Views/Posts/PostListView.swift new file mode 100644 index 0000000..7b4e774 --- /dev/null +++ b/CHDataManagement/Views/Posts/PostListView.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct PostListView: View { + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @Binding + private var selectedPost: Post? + + @State + private var searchString = "" + + init(selectedPost: Binding) { + self._selectedPost = selectedPost + } + + private var filteredPosts: [Post] { + guard !searchString.isEmpty else { + return content.posts + } + return content.posts.filter { $0.localized(in: language).title.contains(searchString) } + } + + var body: some View { + VStack { + TextField("", text: $searchString, prompt: Text("Search")) + .textFieldStyle(.roundedBorder) + .padding(.horizontal, 8) + List(filteredPosts, selection: $selectedPost) { post in + Text(post.localized(in: language).title).tag(post) + } + }.onAppear { + if selectedPost == nil { + selectedPost = content.posts.first + } + } + } +} diff --git a/CHDataManagement/Views/Posts/PostView.swift b/CHDataManagement/Views/Posts/PostView.swift deleted file mode 100644 index dfd7249..0000000 --- a/CHDataManagement/Views/Posts/PostView.swift +++ /dev/null @@ -1,127 +0,0 @@ -import SwiftUI -import SFSafeSymbols - -struct PostView: View { - - @ObservedObject - var post: Post - - @Environment(\.language) - private var language - - var body: some View { - LocalizedPostView(post: post, localized: post.localized(in: language)) - } -} - - -struct LocalizedPostView: View { - - @ObservedObject - var post: Post - - @ObservedObject - var localized: LocalizedPost - - @State - private var showDatePicker = false - - @State - private var showImagePicker = false - - @State - private var showTagPicker = false - - @Environment(\.language) - private var language - - @EnvironmentObject - private var content: Content - - var body: some View { - VStack(alignment: .center) { - if localized.images.isEmpty { - Button(action: { showImagePicker = true }) { - Text("Add image") - } - .buttonStyle(.plain) - .foregroundStyle(.blue) - .padding(.top) - } else { - PostImageGalleryView(post: localized) - .aspectRatio(1.33, contentMode: .fill) - } - VStack(alignment: .leading) { - Text(post.dateText(in: language)) - .font(.system(size: 19, weight: .semibold)) - .foregroundStyle(.secondary) - TextField("", text: $localized.title) - .font(.system(size: 24, weight: .bold)) - .foregroundStyle(Color.primary) - .textFieldStyle(.plain) - .lineLimit(2) - FlowHStack { - ForEach(post.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.blue) - .background(Circle() - .fill(Color.white) - .padding(1)) - } - .buttonStyle(.plain) - } - TextEditor(text: $localized.content) - .font(.body) - .foregroundStyle(.secondary) - .textEditorStyle(.plain) - .padding(.leading, -5) - .scrollDisabled(true) - } - .padding() - } - .background(Color(NSColor.windowBackgroundColor)) - .cornerRadius(8) - .sheet(isPresented: $showDatePicker) { - DatePickerView( - post: post, - showDatePicker: $showDatePicker) - } - .sheet(isPresented: $showImagePicker) { - ImagePickerView(showImagePicker: $showImagePicker) { image in - localized.images.append(image) - } - } - .sheet(isPresented: $showTagPicker) { - TagSelectionView( - presented: $showTagPicker, - selected: $post.tags, - tags: $content.tags) - } - } - - private func remove(tag: Tag) { - post.tags = post.tags.filter {$0.id != tag.id } - } -} - -#Preview(traits: .fixedLayout(width: 450, height: 600)) { - List { - PostView(post: .fullMock) - .listRowSeparator(.hidden) - .environment(\.language, ContentLanguage.german) - PostView(post: .mock) - .listRowSeparator(.hidden) - } - .environmentObject(Content.mock) - //.listStyle(.plain) -} diff --git a/CHDataManagement/Views/Posts/TagView.swift b/CHDataManagement/Views/Posts/TagView.swift index 7c88008..f114745 100644 --- a/CHDataManagement/Views/Posts/TagView.swift +++ b/CHDataManagement/Views/Posts/TagView.swift @@ -4,22 +4,15 @@ import SFSafeSymbols struct TagView: View { - @Environment(\.language) - var language: ContentLanguage + let text: String - let tag: LocalizedText - - init(tag: LocalizedText) { - self.tag = tag - } - - static var add: TagView { - .init(tag: LocalizedText(en: "Add", de: "Mehr")) + init(text: String) { + self.text = text } var body: some View { HStack { - Text(tag.getText(for: language)) + Text(text) .font(.subheadline) .padding(.leading, 2) } @@ -33,10 +26,7 @@ struct TagView: View { #Preview { HStack { - TagView(tag: LocalizedText(en: "Some", de: "Etwas")) - .environment(\.language, ContentLanguage.german) - TagView(tag: LocalizedText(en: "Some", de: "Etwas")) - .environment(\.language, ContentLanguage.english) - TagView.add + TagView(text: "Some") + TagView(text: "Etwas") }.background(Color.secondary) } diff --git a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift index 17a098f..9eb7b33 100644 --- a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift +++ b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift @@ -53,10 +53,7 @@ struct NavigationBarSettingsView: View { .font(.headline) FlowHStack { ForEach(content.settings.navigationBar.tags, id: \.id) { tag in - TagView(tag: .init( - en: tag.english.name, - de: tag.german.name) - ) + TagView(text: tag.localized(in: language).name) .foregroundStyle(.white) } Button(action: { showTagPicker = true }) { diff --git a/CHDataManagement/Views/Settings/SectionedSettingsView.swift b/CHDataManagement/Views/Settings/SectionedSettingsView.swift index 8c9265b..e9f4197 100644 --- a/CHDataManagement/Views/Settings/SectionedSettingsView.swift +++ b/CHDataManagement/Views/Settings/SectionedSettingsView.swift @@ -10,13 +10,13 @@ struct SectionedSettingsView: View { SettingsSidebar(selectedSection: $selectedSection) .frame(minWidth: 200, idealWidth: 200, maxWidth: 200) } detail: { - DetailView(section: selectedSection) + GenerationDetailView(section: selectedSection) } } } -struct DetailView: View { +struct GenerationDetailView: View { let section: SettingsSection? @@ -40,7 +40,7 @@ struct DetailView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding() - .navigationTitle(section?.rawValue ?? "") + .navigationTitle("") } } diff --git a/CHDataManagement/Views/Tags/AddTagView.swift b/CHDataManagement/Views/Tags/AddTagView.swift new file mode 100644 index 0000000..c297797 --- /dev/null +++ b/CHDataManagement/Views/Tags/AddTagView.swift @@ -0,0 +1,34 @@ +import SwiftUI + +struct AddTagView: View { + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @Environment(\.dismiss) + private var dismiss + + @Binding + var selectedTag: Tag? + + init(selected: Binding) { + self._selectedTag = selected + } + + var body: some View { + Text("Creating tag...") + .onAppear(perform: addNewTag) + } + + private func addNewTag() { + let newTag = Tag(isVisible: true, + german: .init(urlComponent: "tag", name: "Neuer Tag"), + english: .init(urlComponent: "tag-en", name: "New Tag")) + // Add to top of the list, and resort when changing the name + content.tags.insert(newTag, at: 0) + dismiss() + } +} diff --git a/CHDataManagement/Views/Tags/LocalizedTagDetailView.swift b/CHDataManagement/Views/Tags/LocalizedTagDetailView.swift new file mode 100644 index 0000000..98e8e77 --- /dev/null +++ b/CHDataManagement/Views/Tags/LocalizedTagDetailView.swift @@ -0,0 +1,75 @@ +import SwiftUI + +struct LocalizedTagDetailView: View { + + @Binding + var tagIsVisible: Bool + + @ObservedObject + var tag: LocalizedTag + + @EnvironmentObject + private var content: Content + + @State + private var showImagePicker = false + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + Toggle("Appears in overviews", isOn: $tagIsVisible) + .toggleStyle(.switch) + .font(.callout) + .foregroundStyle(.secondary) + + Text("Name") + .font(.callout) + .foregroundStyle(.secondary) + TextField("", text: $tag.name) + + Text("URL String") + .font(.callout) + .foregroundStyle(.secondary) + TextField("", text: $tag.urlComponent) + + Text("Original url") + .font(.callout) + .foregroundStyle(.secondary) + Text(tag.originalUrl ?? "-") + .padding(.top, 1) + .padding(.bottom) + + Text("Subtitle") + .font(.callout) + .foregroundStyle(.secondary) + OptionalTextField("", text: $tag.subtitle) + + Text("Description") + .font(.callout) + .foregroundStyle(.secondary) + OptionalTextField("", text: $tag.description) + + Text("Thumbnail") + .font(.callout) + .foregroundStyle(.secondary) + Button(action: { showImagePicker = true }) { + Text(tag.thumbnail?.id ?? "Select") + } + .buttonStyle(.plain) + .foregroundStyle(.blue) + } + .padding() + } + .sheet(isPresented: $showImagePicker) { + ImagePickerView(showImagePicker: $showImagePicker) { image in + tag.thumbnail = image + } + } + } +} + +#Preview { + LocalizedTagDetailView( + tagIsVisible: .constant(true), + tag: Tag.mock.english) +} diff --git a/CHDataManagement/Views/Tags/TagContentView.swift b/CHDataManagement/Views/Tags/TagContentView.swift index c6777fa..41f87ab 100644 --- a/CHDataManagement/Views/Tags/TagContentView.swift +++ b/CHDataManagement/Views/Tags/TagContentView.swift @@ -57,6 +57,15 @@ struct TagContentView: View { } } +extension TagContentView: MainContentView { + + init(item: Tag) { + self.tag = item + } + + static let itemDescription = "a tag" +} + #Preview { TagContentView(tag: .hiking) .environmentObject(Content.mock) diff --git a/CHDataManagement/Views/Tags/TagDetailView.swift b/CHDataManagement/Views/Tags/TagDetailView.swift index 50a231f..dd233a2 100644 --- a/CHDataManagement/Views/Tags/TagDetailView.swift +++ b/CHDataManagement/Views/Tags/TagDetailView.swift @@ -1,75 +1,26 @@ import SwiftUI + struct TagDetailView: View { - @Binding - var tagIsVisible: Bool + @Environment(\.language) + private var language @ObservedObject - var tag: LocalizedTag - - @EnvironmentObject - private var content: Content - - @State - private var showImagePicker = false + var tag: Tag var body: some View { - ScrollView { - VStack(alignment: .leading) { - Toggle("Appears in overviews", isOn: $tagIsVisible) - .toggleStyle(.switch) - .font(.callout) - .foregroundStyle(.secondary) - - Text("Name") - .font(.callout) - .foregroundStyle(.secondary) - TextField("", text: $tag.name) - - Text("URL String") - .font(.callout) - .foregroundStyle(.secondary) - TextField("", text: $tag.urlComponent) - - Text("Original url") - .font(.callout) - .foregroundStyle(.secondary) - Text(tag.originalUrl ?? "-") - .padding(.top, 1) - .padding(.bottom) - - Text("Subtitle") - .font(.callout) - .foregroundStyle(.secondary) - OptionalTextField("", text: $tag.subtitle) - - Text("Description") - .font(.callout) - .foregroundStyle(.secondary) - OptionalTextField("", text: $tag.description) - - Text("Thumbnail") - .font(.callout) - .foregroundStyle(.secondary) - Button(action: { showImagePicker = true }) { - Text(tag.thumbnail?.id ?? "Select") - } - .buttonStyle(.plain) - .foregroundStyle(.blue) - } - .padding() - } - .sheet(isPresented: $showImagePicker) { - ImagePickerView(showImagePicker: $showImagePicker) { image in - tag.thumbnail = image - } - } + LocalizedTagDetailView( + tagIsVisible: $tag.isVisible, + tag: tag.localized(in: language)) } } -#Preview { - TagDetailView( - tagIsVisible: .constant(true), - tag: Tag.mock.english) +extension TagDetailView: MainContentView { + + init(item: Tag) { + self.tag = item + } + + static let itemDescription = "a tag" } diff --git a/CHDataManagement/Views/Tags/TagListView.swift b/CHDataManagement/Views/Tags/TagListView.swift new file mode 100644 index 0000000..7fa7df1 --- /dev/null +++ b/CHDataManagement/Views/Tags/TagListView.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct TagListView: View { + + @Environment(\.language) + private var language + + @EnvironmentObject + private var content: Content + + @Binding + var selectedTag: Tag? + + @State + private var searchString = "" + + init(selectedTag: Binding) { + _selectedTag = selectedTag + } + + private var filteredTags: [Tag] { + guard !searchString.isEmpty else { + return content.tags + } + return content.tags.filter { $0.localized(in: language).name.contains(searchString) } + } + + var body: some View { + VStack { + TextField("", text: $searchString, prompt: Text("Search")) + .textFieldStyle(.roundedBorder) + .padding(.horizontal, 8) + List(filteredTags, selection: $selectedTag) { tag in + Text(tag.localized(in: language).name).tag(tag) + } + }.onAppear { + if selectedTag == nil { + selectedTag = content.tags.first + } + } + } +} diff --git a/CHDataManagement/Views/Tags/TagsListView.swift b/CHDataManagement/Views/Tags/TagsListView.swift deleted file mode 100644 index 6fb7dcf..0000000 --- a/CHDataManagement/Views/Tags/TagsListView.swift +++ /dev/null @@ -1,78 +0,0 @@ -import SwiftUI - -private struct SelectedTagView: View { - - @Environment(\.language) - private var language - - @ObservedObject - var tag: Tag - - var body: some View { - TagDetailView( - tagIsVisible: $tag.isVisible, - tag: tag.localized(in: language)) - } -} - -struct TagsListView: View { - - @Environment(\.language) - var language - - @EnvironmentObject - var content: Content - - @State - var selectedTag: Tag? - - var body: some View { - NavigationSplitView { - List(content.tags.sorted(), selection: $selectedTag) { tag in - Text(tag.localized(in: language).name) - .tag(tag) - } - } content: { - if let selectedTag { - TagContentView(tag: selectedTag) - .layoutPriority(1) - } else { - Text("Select a tag to show the details") - .font(.largeTitle) - .foregroundColor(.secondary) - } - } detail: { - if let selectedTag { - SelectedTagView(tag: selectedTag) - .frame(maxWidth: 350) - } else { - EmptyView() - } - } - .onAppear { - if selectedTag == nil { - selectedTag = content.tags.first - } - } - .toolbar { - ToolbarItem(placement: .primaryAction) { - Button(action: addNewTag) { - Label("New tag", systemSymbol: .plus) - } - } - } - } - - private func addNewTag() { - let newTag = Tag(isVisible: true, - german: .init(urlComponent: "tag", name: "Neuer Tag"), - english: .init(urlComponent: "tag-en", name: "New Tag")) - // Add to top of the list, and resort when changing the name - content.tags.insert(newTag, at: 0) - } -} - -#Preview { - PageListView() - .environmentObject(Content()) -}