From 31d1ecb8bde559ea4a40ce44f3df8fad0500affa Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Mon, 16 Dec 2024 09:54:21 +0100 Subject: [PATCH] Unified detail views, model --- CHDataManagement.xcodeproj/project.pbxproj | 54 ++++-- .../Generator/FeedPageGenerator.swift | 9 +- .../Generator/LocalizedWebsiteGenerator.swift | 12 +- .../Generator/PageGenerator.swift | 7 +- .../Generator/PostListPageGenerator.swift | 12 +- CHDataManagement/Main/MainView.swift | 6 +- .../Model/Content+Generation.swift | 7 + CHDataManagement/Model/Content+Load.swift | 8 +- CHDataManagement/Model/Content+Save.swift | 2 +- .../Model/Content+Validation.swift | 11 ++ CHDataManagement/Model/FileResource.swift | 7 + .../Model/Item/TagOverviewPage.swift | 28 ++-- CHDataManagement/Model/LocalizedPage.swift | 5 + CHDataManagement/Model/LocalizedPost.swift | 12 +- CHDataManagement/Model/LocalizedTag.swift | 11 +- CHDataManagement/Model/Page.swift | 9 +- CHDataManagement/Model/Post.swift | 7 + CHDataManagement/Model/Tag.swift | 4 +- .../Preview Content/Post+Mock.swift | 22 ++- .../Preview Content/Tag+Mock.swift | 18 +- CHDataManagement/Storage/Storage.swift | 5 +- .../Views/Files/FileDetailView.swift | 63 +++---- .../Views/Files/FileListView.swift | 13 +- .../Views/Files/FileSelectionView.swift | 13 +- .../Views/Generic/BoolPropertyView.swift | 26 +++ .../Views/Generic/DatePropertyView.swift | 50 ++++++ .../Views/Generic/FilePropertyView.swift | 11 +- .../Generic/FolderOnDiskPropertyView.swift | 57 +++++++ .../Views/Generic/GenericPropertyView.swift | 27 +++ .../Views/Generic/IdPropertyView.swift | 56 +++++++ .../Views/Generic/IntegerPropertyView.swift | 13 +- .../Generic/OptionalImagePropertyView.swift | 36 ++++ .../Generic/OptionalStringPropertyView.swift | 27 +++ .../OptionalTextFieldPropertyView.swift | 27 +++ .../Views/Generic/PagePropertyView.swift | 30 ++++ .../Views/Generic/StringPropertyView.swift | 27 +++ .../Views/Pages/LocalizedPageDetailView.swift | 108 +++--------- .../Views/Pages/PageDetailView.swift | 105 ++++-------- .../Views/Posts/AddPostView.swift | 4 +- .../Views/Posts/DatePickerView.swift | 49 ------ .../Views/Posts/ImagePickerView.swift | 59 ------- .../Views/Posts/LocalizedPostDetailView.swift | 67 ++------ .../Views/Posts/PagePickerView.swift | 23 ++- .../Views/Posts/PostContentView.swift | 2 +- .../Views/Posts/PostDetailView.swift | 108 +++--------- .../Views/Posts/PostImagesView.swift | 5 - .../Views/Posts/TextEntrySheet.swift | 60 ------- .../Content/Pages/PageIssueView.swift | 4 +- .../Views/Settings/GenerationDetailView.swift | 29 ++-- .../Settings/NavigationBarSettingsView.swift | 9 +- .../Settings/PageSettingsDetailView.swift | 16 +- .../Views/Settings/PathSettingsView.swift | 157 +++++------------- .../Views/Settings/PostFeedSettingsView.swift | 10 +- .../Settings/TagOverviewDetailView.swift | 118 ++++--------- CHDataManagement/Views/Tags/AddTagView.swift | 4 +- .../Views/Tags/LocalizedTagDetailView.swift | 119 +++++-------- .../Views/Tags/TagDetailView.swift | 19 ++- 57 files changed, 853 insertions(+), 954 deletions(-) create mode 100644 CHDataManagement/Views/Generic/BoolPropertyView.swift create mode 100644 CHDataManagement/Views/Generic/DatePropertyView.swift create mode 100644 CHDataManagement/Views/Generic/FolderOnDiskPropertyView.swift create mode 100644 CHDataManagement/Views/Generic/GenericPropertyView.swift create mode 100644 CHDataManagement/Views/Generic/IdPropertyView.swift create mode 100644 CHDataManagement/Views/Generic/OptionalImagePropertyView.swift create mode 100644 CHDataManagement/Views/Generic/OptionalStringPropertyView.swift create mode 100644 CHDataManagement/Views/Generic/OptionalTextFieldPropertyView.swift create mode 100644 CHDataManagement/Views/Generic/PagePropertyView.swift create mode 100644 CHDataManagement/Views/Generic/StringPropertyView.swift delete mode 100644 CHDataManagement/Views/Posts/DatePickerView.swift delete mode 100644 CHDataManagement/Views/Posts/ImagePickerView.swift delete mode 100644 CHDataManagement/Views/Posts/TextEntrySheet.swift diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index cc193d3..0560a93 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -13,9 +13,7 @@ E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; }; E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850182CEE561B0090B18B /* PageOnDisk.swift */; }; E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501C2CEE6CB30090B18B /* VerticalCenter.swift */; }; - E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */; }; E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850222CF10C840090B18B /* TagSelectionView.swift */; }; - E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850242CF38BCE0090B18B /* TextEntrySheet.swift */; }; E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850262CF3B42D0090B18B /* PostDetailView.swift */; }; E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; }; E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; }; @@ -36,6 +34,16 @@ E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */; }; E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990292D0F5A10009F8D77 /* DetailTitle.swift */; }; E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902B2D0F6FC0009F8D77 /* ItemId.swift */; }; + E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902D2D0F7278009F8D77 /* IdPropertyView.swift */; }; + E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */; }; + E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990312D0F7678009F8D77 /* DatePropertyView.swift */; }; + E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990332D0F77E4009F8D77 /* PagePropertyView.swift */; }; + E22990362D0F79D2009F8D77 /* OptionalStringPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */; }; + E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */; }; + E229903A2D0F7E48009F8D77 /* GenericPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */; }; + E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */; }; + E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */; }; + E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */; }; E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; }; E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; }; E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.swift */; }; @@ -149,7 +157,6 @@ 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 */; }; 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 */; }; @@ -194,9 +201,7 @@ E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = ""; }; E21850182CEE561B0090B18B /* PageOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageOnDisk.swift; sourceTree = ""; }; E218501C2CEE6CB30090B18B /* VerticalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCenter.swift; sourceTree = ""; }; - E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = ""; }; E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = ""; }; - E21850242CF38BCE0090B18B /* TextEntrySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntrySheet.swift; sourceTree = ""; }; E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = ""; }; E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = ""; }; E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = ""; }; @@ -217,6 +222,16 @@ E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerPropertyView.swift; sourceTree = ""; }; E22990292D0F5A10009F8D77 /* DetailTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTitle.swift; sourceTree = ""; }; E229902B2D0F6FC0009F8D77 /* ItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemId.swift; sourceTree = ""; }; + E229902D2D0F7278009F8D77 /* IdPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdPropertyView.swift; sourceTree = ""; }; + E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolPropertyView.swift; sourceTree = ""; }; + E22990312D0F7678009F8D77 /* DatePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePropertyView.swift; sourceTree = ""; }; + E22990332D0F77E4009F8D77 /* PagePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePropertyView.swift; sourceTree = ""; }; + E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalStringPropertyView.swift; sourceTree = ""; }; + E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalImagePropertyView.swift; sourceTree = ""; }; + E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPropertyView.swift; sourceTree = ""; }; + E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextFieldPropertyView.swift; sourceTree = ""; }; + E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringPropertyView.swift; sourceTree = ""; }; + E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderOnDiskPropertyView.swift; sourceTree = ""; }; E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = ""; }; E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = ""; }; @@ -325,7 +340,6 @@ 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 = ""; }; 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 = ""; }; @@ -589,9 +603,19 @@ E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */, E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */, E2A21C0F2CB18B390060935B /* FlowHStack.swift */, + E22990292D0F5A10009F8D77 /* DetailTitle.swift */, + E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */, E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */, E22990252D0F5822009F8D77 /* FilePropertyView.swift */, - E22990292D0F5A10009F8D77 /* DetailTitle.swift */, + E229902D2D0F7278009F8D77 /* IdPropertyView.swift */, + E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */, + E22990312D0F7678009F8D77 /* DatePropertyView.swift */, + E22990332D0F77E4009F8D77 /* PagePropertyView.swift */, + E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */, + E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */, + E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */, + E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */, + E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */, ); path = Generic; sourceTree = ""; @@ -707,11 +731,8 @@ E21850262CF3B42D0090B18B /* PostDetailView.swift */, E29D313E2D04822C0051B7F4 /* AddPostView.swift */, E21850222CF10C840090B18B /* TagSelectionView.swift */, - E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */, E21850082CEE01BF0090B18B /* PagePickerView.swift */, - E2A21C112CB18D520060935B /* DatePickerView.swift */, E2A21C072CB17B810060935B /* TagView.swift */, - E21850242CF38BCE0090B18B /* TextEntrySheet.swift */, E29D31312D03B5610051B7F4 /* LocalizedPostDetailView.swift */, E218502C2CF791440090B18B /* PostImagesView.swift */, ); @@ -888,10 +909,10 @@ E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */, E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */, E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, + E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */, E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */, E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */, E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */, - E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, E21850172CEE55FC0090B18B /* FileType.swift in Sources */, E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */, @@ -913,6 +934,7 @@ E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */, E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */, E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */, + E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */, E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */, E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */, E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */, @@ -934,6 +956,7 @@ E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */, E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, + E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */, E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */, @@ -952,7 +975,7 @@ E29D315D2D06D6EA0051B7F4 /* TextFileType.swift in Sources */, E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */, - E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */, + E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */, E21850332CFAFA2F0090B18B /* Settings.swift in Sources */, E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */, E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */, @@ -967,12 +990,14 @@ E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */, E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, + E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, + E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */, E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */, E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */, E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, @@ -989,11 +1014,11 @@ E29D31472D04892E0051B7F4 /* FileListView.swift in Sources */, E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */, E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */, + E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */, E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */, E2DD04742C276F31003BFF1F /* MainView.swift in Sources */, - E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */, E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */, E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */, E29D319F2D0C46310051B7F4 /* PageIssueChecker.swift in Sources */, @@ -1028,10 +1053,13 @@ E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */, E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */, E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */, + E229903A2D0F7E48009F8D77 /* GenericPropertyView.swift in Sources */, E29D31AA2D0CEE3F0051B7F4 /* AudioPlayer.swift in Sources */, E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */, E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */, E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */, + E22990362D0F79D2009F8D77 /* OptionalStringPropertyView.swift in Sources */, + E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */, E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */, E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */, E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */, diff --git a/CHDataManagement/Generator/FeedPageGenerator.swift b/CHDataManagement/Generator/FeedPageGenerator.swift index 7b68daf..c04130e 100644 --- a/CHDataManagement/Generator/FeedPageGenerator.swift +++ b/CHDataManagement/Generator/FeedPageGenerator.swift @@ -8,13 +8,6 @@ final class FeedPageGenerator { self.content = content } - func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] { - content.settings.navigationItems.map { - .init(text: $0.title(in: language), - url: $0.absoluteUrl(in: language)) - } - } - var swiperIncludes: [HeaderElement] { var result = [HeaderElement]() if let swiperCss = content.settings.posts.swiperCssFile { @@ -57,7 +50,7 @@ final class FeedPageGenerator { language: language, title: title, description: description, - links: navigationBar(in: language), + links: content.navigationBar(in: language), headers: headers, additionalFooter: footer) { content in if showTitle { diff --git a/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift b/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift index 2def2d1..89ffef1 100644 --- a/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift +++ b/CHDataManagement/Generator/LocalizedWebsiteGenerator.swift @@ -22,13 +22,6 @@ final class LocalizedWebsiteGenerator { private let imageGenerator: ImageGenerator - private var navigationBarLinks: [NavigationBar.Link] { - content.settings.navigationItems.map { - .init(text: $0.title(in: language), - url: $0.absoluteUrl(in: language)) - } - } - init(content: Content, language: ContentLanguage) { self.language = language self.content = content @@ -61,7 +54,6 @@ final class LocalizedWebsiteGenerator { language: language, content: content, imageGenerator: imageGenerator, - navigationBarLinks: navigationBarLinks, showTitle: false, pageTitle: localizedPostSettings.title, pageDescription: localizedPostSettings.description, @@ -82,7 +74,6 @@ final class LocalizedWebsiteGenerator { language: language, content: content, imageGenerator: imageGenerator, - navigationBarLinks: navigationBarLinks, showTitle: true, pageTitle: localized.name, pageDescription: localized.description ?? "", @@ -115,8 +106,7 @@ final class LocalizedWebsiteGenerator { } let pageGenerator = PageGenerator( content: content, - imageGenerator: imageGenerator, - navigationBarLinks: navigationBarLinks) + imageGenerator: imageGenerator) let content: String let results: PageGenerationResults diff --git a/CHDataManagement/Generator/PageGenerator.swift b/CHDataManagement/Generator/PageGenerator.swift index 05b6880..a08fc47 100644 --- a/CHDataManagement/Generator/PageGenerator.swift +++ b/CHDataManagement/Generator/PageGenerator.swift @@ -4,12 +4,9 @@ final class PageGenerator { private let imageGenerator: ImageGenerator - private let navigationBarLinks: [NavigationBar.Link] - - init(content: Content, imageGenerator: ImageGenerator, navigationBarLinks: [NavigationBar.Link]) { + init(content: Content, imageGenerator: ImageGenerator) { self.content = content self.imageGenerator = imageGenerator - self.navigationBarLinks = navigationBarLinks } func makeHeaders(requiredItems: [HeaderFile]) -> [HeaderElement] { @@ -51,7 +48,7 @@ final class PageGenerator { tags: tags, linkTitle: localized.linkPreviewTitle ?? localized.title, description: localized.linkPreviewDescription ?? "", - navigationBarLinks: navigationBarLinks, + navigationBarLinks: content.navigationBar(in: language), pageContent: pageContent, headers: headers, footers: contentGenerator.results.requiredFooters.sorted(), diff --git a/CHDataManagement/Generator/PostListPageGenerator.swift b/CHDataManagement/Generator/PostListPageGenerator.swift index 61d6993..e327b2e 100644 --- a/CHDataManagement/Generator/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/PostListPageGenerator.swift @@ -8,9 +8,6 @@ final class PostListPageGenerator { private let imageGenerator: ImageGenerator - #warning("Get from settings") - private let navigationBarLinks: [NavigationBar.Link] - private let showTitle: Bool private let pageTitle: String @@ -20,11 +17,10 @@ final class PostListPageGenerator { /// The url of the page, excluding the extension private let pageUrlPrefix: String - init(language: ContentLanguage, content: Content, imageGenerator: ImageGenerator, navigationBarLinks: [NavigationBar.Link], showTitle: Bool, pageTitle: String, pageDescription: String, pageUrlPrefix: String) { + init(language: ContentLanguage, content: Content, imageGenerator: ImageGenerator, showTitle: Bool, pageTitle: String, pageDescription: String, pageUrlPrefix: String) { self.language = language self.content = content self.imageGenerator = imageGenerator - self.navigationBarLinks = navigationBarLinks self.showTitle = showTitle self.pageTitle = pageTitle self.pageDescription = pageDescription @@ -50,14 +46,14 @@ final class PostListPageGenerator { let startIndex = (pageIndex - 1) * postsPerPage let endIndex = min(pageIndex * postsPerPage, totalCount) let postsOnPage = posts[startIndex.., bar: [NavigationBar.Link]) -> Bool { + private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice) -> Bool { let posts: [FeedEntryData] = posts.map { post in let localized: LocalizedPost = post.localized(in: language) @@ -78,7 +74,7 @@ final class PostListPageGenerator { textAboveTitle: post.dateText(in: language), link: linkUrl, tags: tags, - text: [localized.content], // TODO: Convert from markdown to html + text: localized.text.components(separatedBy: "\n"), images: localized.images.map(createImageSet)) } diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index 0da3cee..6d7156d 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -1,8 +1,6 @@ import SwiftUI import SFSafeSymbols - -#warning("Allow selection of pages as navigation bar items") #warning("Show all warnings on page content") #warning("Button to delete file") #warning("Fix podcast") @@ -14,6 +12,10 @@ import SFSafeSymbols #warning("Calculate file sizes") #warning("Specify image aspect ratio to prevent page jumps") #warning("Add version and source url properties to file resources") +#warning("Consolidate all errors in Content") +#warning("Generate pages for posts") +#warning("Clean up mock content") +#warning("Show posts linking to a page") @main struct MainView: App { diff --git a/CHDataManagement/Model/Content+Generation.swift b/CHDataManagement/Model/Content+Generation.swift index 685ea14..3e1a12b 100644 --- a/CHDataManagement/Model/Content+Generation.swift +++ b/CHDataManagement/Model/Content+Generation.swift @@ -35,4 +35,11 @@ extension Content { func tag(_ tagId: String) -> Tag? { tags.first { $0.id == tagId } } + + func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] { + settings.navigationItems.map { + .init(text: $0.title(in: language), + url: $0.absoluteUrl(in: language)) + } + } } diff --git a/CHDataManagement/Model/Content+Load.swift b/CHDataManagement/Model/Content+Load.swift index 1096238..d26b048 100644 --- a/CHDataManagement/Model/Content+Load.swift +++ b/CHDataManagement/Model/Content+Load.swift @@ -4,6 +4,7 @@ extension Content { private func convert(_ tag: LocalizedTagFile, images: [String : FileResource]) -> LocalizedTag { LocalizedTag( + content: self, urlComponent: tag.urlComponent, name: tag.name, subtitle: tag.subtitle, @@ -14,8 +15,9 @@ extension Content { private func convert(_ post: LocalizedPostFile, images: [String : FileResource]) -> LocalizedPost { LocalizedPost( + content: self, title: post.title, - content: post.content, + text: post.content, lastModified: post.lastModifiedDate, images: post.images.compactMap { images[$0] }, linkPreviewImage: post.linkPreviewImage.map { images[$0] }, @@ -107,8 +109,8 @@ extension Content { let tagOverview = tagOverviewData.map { file in TagOverviewPage( content: self, - german: .init(file: file.german, image: file.german.linkPreviewImage.map { files[$0] }), - english: .init(file: file.english, image: file.english.linkPreviewImage.map { files[$0] })) + german: .init(content: self, file: file.german, image: file.german.linkPreviewImage.map { files[$0] }), + english: .init(content: self, file: file.english, image: file.english.linkPreviewImage.map { files[$0] })) } self.tags = tags.values.sorted() diff --git a/CHDataManagement/Model/Content+Save.swift b/CHDataManagement/Model/Content+Save.swift index 07ee177..283ce9e 100644 --- a/CHDataManagement/Model/Content+Save.swift +++ b/CHDataManagement/Model/Content+Save.swift @@ -95,7 +95,7 @@ private extension LocalizedPost { var postFile: LocalizedPostFile { .init(images: images.map { $0.id }, title: title.nonEmpty, - content: content, + content: text, lastModifiedDate: lastModified, linkPreviewImage: linkPreviewImage?.id, linkPreviewTitle: linkPreviewTitle, diff --git a/CHDataManagement/Model/Content+Validation.swift b/CHDataManagement/Model/Content+Validation.swift index 0afb0f8..e157716 100644 --- a/CHDataManagement/Model/Content+Validation.swift +++ b/CHDataManagement/Model/Content+Validation.swift @@ -18,6 +18,10 @@ extension Content { !posts.contains { $0.id == id } } + func isNewIdForFile(_ id: String) -> Bool { + !files.contains { $0.id == id } + } + func isValidIdForTagOrPageOrPost(_ id: String) -> Bool { id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil } @@ -26,6 +30,13 @@ extension Content { id.rangeOfCharacter(from: Content.disallowedCharactersInFileIds) == nil } + func containsPage(withUrlComponent urlComponent: String) -> Bool { + pages.contains { + $0.german.urlString == urlComponent || + $0.english.urlString == urlComponent + } + } + func containsTag(withUrlComponent urlComponent: String) -> Bool { (tagOverview?.contains(urlComponent: urlComponent) ?? false) || tags.contains { $0.contains(urlComponent: urlComponent) } diff --git a/CHDataManagement/Model/FileResource.swift b/CHDataManagement/Model/FileResource.swift index 4c9c5a5..3605e2a 100644 --- a/CHDataManagement/Model/FileResource.swift +++ b/CHDataManagement/Model/FileResource.swift @@ -111,6 +111,13 @@ final class FileResource: Item { // MARK: File + func isValid(id: String) -> Bool { + !id.isEmpty && + content.isValidIdForFile(id) && + content.isNewIdForFile(id) + } + + @discardableResult func update(id newId: String) -> Bool { guard !isExternallyStored else { id = newId diff --git a/CHDataManagement/Model/Item/TagOverviewPage.swift b/CHDataManagement/Model/Item/TagOverviewPage.swift index cc6ca45..38753de 100644 --- a/CHDataManagement/Model/Item/TagOverviewPage.swift +++ b/CHDataManagement/Model/Item/TagOverviewPage.swift @@ -34,11 +34,11 @@ final class TagOverviewPage: Item { } private func internalPath(for language: ContentLanguage) -> String { - content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlString + content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlComponent } func contains(urlComponent: String) -> Bool { - english.urlString == urlComponent || german.urlString == urlComponent + english.urlComponent == urlComponent || german.urlComponent == urlComponent } var file: TagOverviewFile { @@ -53,16 +53,16 @@ extension TagOverviewPage: LocalizedItem { final class LocalizedTagOverviewPage: ObservableObject { + unowned let content: Content + @Published var title: String /** The string to use when creating the url for the page. - - Defaults to ``id`` if unset. */ @Published - var urlString: String + var urlComponent: String @Published var linkPreviewImage: FileResource? @@ -73,27 +73,35 @@ final class LocalizedTagOverviewPage: ObservableObject { @Published var linkPreviewDescription: String? - init(title: String, urlString: String, linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) { + init(content: Content, title: String, urlString: String, linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) { + self.content = content self.title = title - self.urlString = urlString + self.urlComponent = urlString self.linkPreviewImage = linkPreviewImage self.linkPreviewTitle = linkPreviewTitle self.linkPreviewDescription = linkPreviewDescription } - init(file: LocalizedTagOverviewFile, image: FileResource?) { + init(content: Content, file: LocalizedTagOverviewFile, image: FileResource?) { + self.content = content self.title = file.title - self.urlString = file.url + self.urlComponent = file.url self.linkPreviewImage = image self.linkPreviewTitle = file.linkPreviewTitle self.linkPreviewDescription = file.linkPreviewDescription } var file: LocalizedTagOverviewFile { - .init(url: urlString, + .init(url: urlComponent, title: title, linkPreviewImage: linkPreviewImage?.id, linkPreviewTitle: linkPreviewTitle, linkPreviewDescription: linkPreviewDescription) } + + func isValid(urlComponent: String) -> Bool { + !urlComponent.isEmpty && + content.isValidIdForTagOrPageOrPost(urlComponent) && + !content.containsTag(withUrlComponent: urlComponent) + } } diff --git a/CHDataManagement/Model/LocalizedPage.swift b/CHDataManagement/Model/LocalizedPage.swift index c727aa0..469772c 100644 --- a/CHDataManagement/Model/LocalizedPage.swift +++ b/CHDataManagement/Model/LocalizedPage.swift @@ -89,4 +89,9 @@ final class LocalizedPage: ObservableObject { self.linkPreviewTitle = linkPreviewTitle self.linkPreviewDescription = linkPreviewDescription } + + func isValid(urlComponent: String) -> Bool { + content.isValidIdForTagOrPageOrPost(urlComponent) && + !content.containsPage(withUrlComponent: urlComponent) + } } diff --git a/CHDataManagement/Model/LocalizedPost.swift b/CHDataManagement/Model/LocalizedPost.swift index 48a34e0..e48d965 100644 --- a/CHDataManagement/Model/LocalizedPost.swift +++ b/CHDataManagement/Model/LocalizedPost.swift @@ -3,11 +3,13 @@ import SwiftUI final class LocalizedPost: ObservableObject { + unowned let content: Content + @Published var title: String @Published - var content: String + var text: String @Published var lastModified: Date? @@ -24,15 +26,17 @@ final class LocalizedPost: ObservableObject { @Published var linkPreviewDescription: String? - init(title: String? = nil, - content: String, + init(content: Content, + title: String? = nil, + text: String, lastModified: Date? = nil, images: [FileResource] = [], linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) { - self.title = title ?? "" self.content = content + self.title = title ?? "" + self.text = text self.lastModified = lastModified self.images = images self.linkPreviewImage = linkPreviewImage diff --git a/CHDataManagement/Model/LocalizedTag.swift b/CHDataManagement/Model/LocalizedTag.swift index 397c8c3..2e595af 100644 --- a/CHDataManagement/Model/LocalizedTag.swift +++ b/CHDataManagement/Model/LocalizedTag.swift @@ -2,6 +2,8 @@ import Foundation final class LocalizedTag: ObservableObject { + unowned let content: Content + @Published var urlComponent: String @@ -22,12 +24,14 @@ final class LocalizedTag: ObservableObject { /// The original url in the previous site layout let originalUrl: String? - init(urlComponent: String, + init(content: Content, + urlComponent: String, name: String, subtitle: String? = nil, description: String? = nil, thumbnail: FileResource? = nil, originalUrl: String? = nil) { + self.content = content self.urlComponent = urlComponent self.name = name self.subtitle = subtitle @@ -35,4 +39,9 @@ final class LocalizedTag: ObservableObject { self.linkPreviewImage = thumbnail self.originalUrl = originalUrl } + + func isValid(urlComponent: String) -> Bool { + content.isValidIdForTagOrPageOrPost(urlComponent) && + !content.containsTag(withUrlComponent: urlComponent) + } } diff --git a/CHDataManagement/Model/Page.swift b/CHDataManagement/Model/Page.swift index 779fe27..05ab46e 100644 --- a/CHDataManagement/Model/Page.swift +++ b/CHDataManagement/Model/Page.swift @@ -66,9 +66,16 @@ final class Page: Item { super.init(content: content, id: id) } + func isValid(id: String) -> Bool { + !id.isEmpty && + content.isValidIdForTagOrPageOrPost(id) && + content.isNewIdForPage(id) + } + + @discardableResult func update(id newId: String) -> Bool { guard content.storage.move(page: id, to: newId) else { - print("Failed to move file of page \(id)") + print("Failed to move files of page \(id)") return false } id = newId diff --git a/CHDataManagement/Model/Post.swift b/CHDataManagement/Model/Post.swift index 2c2bde4..337a360 100644 --- a/CHDataManagement/Model/Post.swift +++ b/CHDataManagement/Model/Post.swift @@ -65,6 +65,13 @@ final class Post: ObservableObject { } } + func isValid(id: String) -> Bool { + !id.isEmpty && + content.isValidIdForTagOrPageOrPost(id) && + content.isNewIdForPost(id) + } + + @discardableResult func update(id newId: String) -> Bool { do { try content.storage.move(post: id, to: newId) diff --git a/CHDataManagement/Model/Tag.swift b/CHDataManagement/Model/Tag.swift index 70d8b4a..c6d4618 100644 --- a/CHDataManagement/Model/Tag.swift +++ b/CHDataManagement/Model/Tag.swift @@ -13,9 +13,9 @@ final class Tag: Item { override init(content: Content, id: String) { self.isVisible = true - self.english = .init(urlComponent: id, name: id) + self.english = .init(content: content, urlComponent: id, name: id) let deId = id + "-" + ContentLanguage.german.rawValue - self.german = .init(urlComponent: deId, name: deId) + self.german = .init(content: content, urlComponent: deId, name: deId) super.init(content: content, id: id) } diff --git a/CHDataManagement/Preview Content/Post+Mock.swift b/CHDataManagement/Preview Content/Post+Mock.swift index 84f58da..5b372ee 100644 --- a/CHDataManagement/Preview Content/Post+Mock.swift +++ b/CHDataManagement/Preview Content/Post+Mock.swift @@ -9,8 +9,10 @@ extension Post { startDate: .now, endDate: nil, tags: [], - german: .init(content: "Text"), - english: .init(content: "Text"), + german: .init(content: .mock, + text: "Text"), + english: .init(content: .mock, + text: "Text"), linkedPage: nil) } @@ -23,8 +25,14 @@ extension Post { startDate: .now, endDate: nil, tags: [.nature, .sports, .hiking], - german: .init(title: "Der Titel", content: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend."), - english: .init(title: "The title", content: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height") + german: .init( + content: .mock, + title: "Der Titel", + text: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend."), + english: .init( + content: .mock, + title: "The title", + text: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height") ) } @@ -44,12 +52,14 @@ extension Post { extension LocalizedPost { static let german = LocalizedPost( + content: .mock, title: "Ein langer Titel", - content: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend.", + text: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend.", images: MockImage.images) static let english = LocalizedPost( + content: .mock, title: "A longer title", - content: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.", + text: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.", images: MockImage.images) } diff --git a/CHDataManagement/Preview Content/Tag+Mock.swift b/CHDataManagement/Preview Content/Tag+Mock.swift index 797b940..414f82c 100644 --- a/CHDataManagement/Preview Content/Tag+Mock.swift +++ b/CHDataManagement/Preview Content/Tag+Mock.swift @@ -11,35 +11,36 @@ extension Tag { static let nature = Tag( content: .mock, id: "nature", - german: .init(urlComponent: "natur", name: "Natur"), - english: .init(urlComponent: "nature", name: "Nature") + german: .init(content: .mock, urlComponent: "natur", name: "Natur"), + english: .init(content: .mock, urlComponent: "nature", name: "Nature") ) static let sports = Tag( content: .mock, id: "sports", - german: .init(urlComponent: "sport", name: "Sport"), - english: .init(urlComponent: "sports", name: "Sports") + german: .init(content: .mock, urlComponent: "sport", name: "Sport"), + english: .init(content: .mock, urlComponent: "sports", name: "Sports") ) static let hiking = Tag( content: .mock, id: "hiking", - german: .init(urlComponent: "wandern", name: "Wandern"), - english: .init(urlComponent: "hiking", name: "Hiking") + german: .init(content: .mock, urlComponent: "wandern", name: "Wandern"), + english: .init(content: .mock, urlComponent: "hiking", name: "Hiking") ) static let mountains = Tag( content: .mock, id: "mountains", - german: .init(urlComponent: "berge", name: "Berge"), - english: .init(urlComponent: "mountains", name: "Mountains") + german: .init(content: .mock, urlComponent: "berge", name: "Berge"), + english: .init(content: .mock, urlComponent: "mountains", name: "Mountains") ) } extension LocalizedTag { static let english = LocalizedTag( + content: .mock, urlComponent: "electronics", name: "Electronics", subtitle: "Projects with electronics", @@ -48,6 +49,7 @@ extension LocalizedTag { originalUrl: "projects/electronics") static let german = LocalizedTag( + content: .mock, urlComponent: "elektronik", name: "Elektronik", subtitle: "Projekte mit Elektronik", diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index d6675c6..cb048a5 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -435,12 +435,15 @@ final class Storage { // MARK: Folder access - func save(folderUrl url: URL, in bookmark: SecurityScopeBookmark) { + @discardableResult + func save(folderUrl url: URL, in bookmark: SecurityScopeBookmark) -> Bool { do { let bookmarkData = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil) UserDefaults.standard.set(bookmarkData, forKey: bookmark.rawValue) + return true } catch { print("Failed to create security-scoped bookmark: \(error)") + return false } } diff --git a/CHDataManagement/Views/Files/FileDetailView.swift b/CHDataManagement/Views/Files/FileDetailView.swift index db4754a..69f07d8 100644 --- a/CHDataManagement/Views/Files/FileDetailView.swift +++ b/CHDataManagement/Views/Files/FileDetailView.swift @@ -5,44 +5,29 @@ 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) - HStack { - TextField("", text: $newId) - .textFieldStyle(.roundedBorder) - Button(action: setNewId) { - Text("Update") - } - .disabled(newId.isEmpty || containsInvalidCharacters || idExists) - } - Text("German Description") - .font(.headline) - TextField("", text: $file.german) - .textFieldStyle(.roundedBorder) - Text("English Description") - .font(.headline) - TextField("", text: $file.english) - .textFieldStyle(.roundedBorder) + DetailTitle( + title: "File", + text: "A file that can be used in a post or page") + + IdPropertyView( + id: $file.id, + title: "Name", + footer: "The unique name of the file, which is also used to reference it in posts and pages.", + validation: file.isValid, + update: { file.update(id: $0) }) + + StringPropertyView( + title: "German Description", + text: $file.german, + footer: "The description for the file in German. Descriptions are used for images and to explain the content of a file.") + + StringPropertyView( + title: "English Description", + text: $file.english, + footer: "The description for the file in English. Descriptions are used for images and to explain the content of a file.") + if file.type.isImage { Text("Image size") .font(.headline) @@ -53,12 +38,6 @@ struct FileDetailView: View { Spacer() }.padding() } - - private func setNewId() { - if !file.update(id: newId) { - newId = file.id - } - } } extension FileDetailView: MainContentView { diff --git a/CHDataManagement/Views/Files/FileListView.swift b/CHDataManagement/Views/Files/FileListView.swift index 84054a0..787be58 100644 --- a/CHDataManagement/Views/Files/FileListView.swift +++ b/CHDataManagement/Views/Files/FileListView.swift @@ -1,6 +1,6 @@ import SwiftUI -private enum FileFilterType: String, Hashable, CaseIterable, Identifiable { +enum FileFilterType: String, Hashable, CaseIterable, Identifiable { case images case text case videos @@ -38,11 +38,19 @@ struct FileListView: View { var selectedFile: FileResource? @State - private var selectedFileType: FileFilterType = .images + private var selectedFileType: FileFilterType @State private var searchString = "" + let allowedType: FileFilterType? + + init(selectedFile: Binding, allowedType: FileFilterType? = nil) { + self._selectedFile = selectedFile + self.allowedType = allowedType + self.selectedFileType = allowedType ?? .images + } + var filesBySelectedType: [FileResource] { content.files.filter { selectedFileType.matches($0.type) } } @@ -63,6 +71,7 @@ struct FileListView: View { } .pickerStyle(.segmented) .padding(.trailing, 7) + .disabled(allowedType != nil) TextField("", text: $searchString, prompt: Text("Search")) .textFieldStyle(.roundedBorder) .padding(.horizontal, 8) diff --git a/CHDataManagement/Views/Files/FileSelectionView.swift b/CHDataManagement/Views/Files/FileSelectionView.swift index 4944732..b6acefc 100644 --- a/CHDataManagement/Views/Files/FileSelectionView.swift +++ b/CHDataManagement/Views/Files/FileSelectionView.swift @@ -2,15 +2,18 @@ import SwiftUI struct FileSelectionView: View { - @Binding - private var selectedFile: FileResource? - @Environment(\.dismiss) private var dismiss - init(selectedFile: Binding) { + @Binding + private var selectedFile: FileResource? + + let allowedType: FileFilterType? + + init(selectedFile: Binding, allowedType: FileFilterType? = nil) { self._selectedFile = selectedFile self.newSelection = selectedFile.wrappedValue + self.allowedType = allowedType } @State @@ -18,7 +21,7 @@ struct FileSelectionView: View { var body: some View { VStack { - FileListView(selectedFile: $newSelection) + FileListView(selectedFile: $newSelection, allowedType: allowedType) .frame(minHeight: 500, idealHeight: 600) HStack { Button("Cancel") { diff --git a/CHDataManagement/Views/Generic/BoolPropertyView.swift b/CHDataManagement/Views/Generic/BoolPropertyView.swift new file mode 100644 index 0000000..8f111e2 --- /dev/null +++ b/CHDataManagement/Views/Generic/BoolPropertyView.swift @@ -0,0 +1,26 @@ +import SwiftUI + +struct BoolPropertyView: View { + + let title: LocalizedStringKey + + @Binding + var value: Bool + + let footer: LocalizedStringKey + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(title) + .font(.headline) + Spacer() + Toggle("", isOn: $value) + .toggleStyle(.switch) + } + Text(footer) + .foregroundStyle(.secondary) + .padding(.bottom) + } + } +} diff --git a/CHDataManagement/Views/Generic/DatePropertyView.swift b/CHDataManagement/Views/Generic/DatePropertyView.swift new file mode 100644 index 0000000..8e1c66e --- /dev/null +++ b/CHDataManagement/Views/Generic/DatePropertyView.swift @@ -0,0 +1,50 @@ +import SwiftUI + +struct DatePropertyView: View { + + let title: String + + @Binding + var value: Date + + let footer: String + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.headline) + DatePicker("", selection: $value, displayedComponents: .date) + .datePickerStyle(.compact) + Text(footer) + .foregroundStyle(.secondary) + .padding(.bottom) + } + } +} + +struct OptionalDatePropertyView: View { + + let title: LocalizedStringKey + + @Binding + var isEnabled: Bool + + @Binding + var date: Date + + let footer: LocalizedStringKey + + var body: some View { + GenericPropertyView(title: title, footer: footer) { + HStack(alignment: .firstTextBaseline) { + Toggle("", isOn: $isEnabled) + .toggleStyle(.switch) + DatePicker("", selection: $date, displayedComponents: .date) + .datePickerStyle(.compact) + .padding(.bottom) + .disabled(!isEnabled) + Spacer() + } + } + } +} diff --git a/CHDataManagement/Views/Generic/FilePropertyView.swift b/CHDataManagement/Views/Generic/FilePropertyView.swift index b254669..f463f8e 100644 --- a/CHDataManagement/Views/Generic/FilePropertyView.swift +++ b/CHDataManagement/Views/Generic/FilePropertyView.swift @@ -2,9 +2,9 @@ import SwiftUI struct FilePropertyView: View { - let title: String + let title: LocalizedStringKey - let description: String + let footer: LocalizedStringKey @Binding var selectedFile: FileResource? @@ -13,9 +13,7 @@ struct FilePropertyView: View { private var showFileSelectionSheet = false var body: some View { - VStack(alignment: .leading) { - Text(title) - .font(.headline) + GenericPropertyView(title: title, footer: footer) { HStack { Text(selectedFile?.id ?? "No file selected") Spacer() @@ -23,9 +21,6 @@ struct FilePropertyView: View { showFileSelectionSheet = true } } - Text(description) - .foregroundStyle(.secondary) - .padding(.bottom) } .sheet(isPresented: $showFileSelectionSheet) { FileSelectionView(selectedFile: $selectedFile) diff --git a/CHDataManagement/Views/Generic/FolderOnDiskPropertyView.swift b/CHDataManagement/Views/Generic/FolderOnDiskPropertyView.swift new file mode 100644 index 0000000..d798965 --- /dev/null +++ b/CHDataManagement/Views/Generic/FolderOnDiskPropertyView.swift @@ -0,0 +1,57 @@ +import SwiftUI + +struct FolderOnDiskPropertyView: View { + + let title: LocalizedStringKey + + @Binding + var folder: String + + let footer: LocalizedStringKey + + let update: (URL) -> Void + + init(title: LocalizedStringKey, folder: Binding, footer: LocalizedStringKey, update: @escaping (URL) -> Void) { + self.title = title + self._folder = folder + self.footer = footer + self.update = update + } + + var body: some View { + GenericPropertyView(title: title, footer: footer) { + HStack(alignment: .firstTextBaseline) { + Text(folder) + Spacer() + Button("Select") { + guard let url = openFolderSelectionPanel() else { + return + } + DispatchQueue.main.async { + update(url) + } + } + } + } + } + + private func openFolderSelectionPanel() -> URL? { + let panel = NSOpenPanel() + // Sets up so user can only select a single directory + panel.canChooseFiles = false + panel.canChooseDirectories = true + panel.allowsMultipleSelection = false + panel.showsHiddenFiles = false + panel.title = "Select directory" + //panel.prompt = "Select Directory" + + let response = panel.runModal() + guard response == .OK else { + return nil + } + guard let url = panel.url else { + return nil + } + return url + } +} diff --git a/CHDataManagement/Views/Generic/GenericPropertyView.swift b/CHDataManagement/Views/Generic/GenericPropertyView.swift new file mode 100644 index 0000000..0487de6 --- /dev/null +++ b/CHDataManagement/Views/Generic/GenericPropertyView.swift @@ -0,0 +1,27 @@ +import SwiftUI + +struct GenericPropertyView: View where Content: View { + + let title: LocalizedStringKey + + let footer: LocalizedStringKey + + let content: Content + + public init(title: LocalizedStringKey, footer: LocalizedStringKey, @ViewBuilder content: () -> Content) { + self.title = title + self.footer = footer + self.content = content() + } + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.headline) + content + Text(footer) + .foregroundStyle(.secondary) + .padding(.bottom) + } + } +} diff --git a/CHDataManagement/Views/Generic/IdPropertyView.swift b/CHDataManagement/Views/Generic/IdPropertyView.swift new file mode 100644 index 0000000..4f394b3 --- /dev/null +++ b/CHDataManagement/Views/Generic/IdPropertyView.swift @@ -0,0 +1,56 @@ +import SwiftUI + +struct IdPropertyView: View { + + @Binding + var id: String + + let title: LocalizedStringKey + + let footer: LocalizedStringKey + + let validation: (String) -> Bool + + let update: (String) -> Void + + @State + private var newId: String + + init(id: Binding, + title: LocalizedStringKey = "ID", + footer: LocalizedStringKey, + validation: @escaping (String) -> Bool = { _ in true }, + update: @escaping (String) -> Void) { + self._id = id + self.title = title + self.footer = footer + self.validation = validation + self.update = update + self.newId = id.wrappedValue + } + + private var isValid: Bool { + validation(id) + } + + var body: some View { + GenericPropertyView(title: title, footer: footer) { + HStack { + TextField("", text: $newId) + .textFieldStyle(.roundedBorder) + Spacer() + Button("Update", action: setNewId) + .disabled(!isValid) + } + } + } + + private func setNewId() { + update(newId) + // In case of failure, resets the id + // In case of update, sets to potentially modified id + DispatchQueue.main.async { + newId = id + } + } +} diff --git a/CHDataManagement/Views/Generic/IntegerPropertyView.swift b/CHDataManagement/Views/Generic/IntegerPropertyView.swift index 222face..36d2d43 100644 --- a/CHDataManagement/Views/Generic/IntegerPropertyView.swift +++ b/CHDataManagement/Views/Generic/IntegerPropertyView.swift @@ -2,22 +2,17 @@ import SwiftUI struct IntegerPropertyView: View { + let title: LocalizedStringKey + @Binding var value: Int - let title: String - - let footer: String + let footer: LocalizedStringKey var body: some View { - VStack(alignment: .leading) { - Text(title) - .font(.headline) + GenericPropertyView(title: title, footer: footer) { IntegerField("", number: $value) .textFieldStyle(.roundedBorder) - Text(footer) - .foregroundStyle(.secondary) - .padding(.bottom) } } } diff --git a/CHDataManagement/Views/Generic/OptionalImagePropertyView.swift b/CHDataManagement/Views/Generic/OptionalImagePropertyView.swift new file mode 100644 index 0000000..7a3bb78 --- /dev/null +++ b/CHDataManagement/Views/Generic/OptionalImagePropertyView.swift @@ -0,0 +1,36 @@ +import SwiftUI + +struct OptionalImagePropertyView: View { + + let title: LocalizedStringKey + + @Binding + var selectedImage: FileResource? + + let footer: LocalizedStringKey + + @State + private var showSelectionSheet = false + + var body: some View { + GenericPropertyView(title: title, footer: footer) { + if let image = selectedImage { + image.imageToDisplay + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 300) + .cornerRadius(8) + } + HStack { + Text(selectedImage?.id ?? "No file selected") + Spacer() + Button("Select") { + showSelectionSheet = true + } + } + } + .sheet(isPresented: $showSelectionSheet) { + FileSelectionView(selectedFile: $selectedImage, allowedType: .images) + } + } +} diff --git a/CHDataManagement/Views/Generic/OptionalStringPropertyView.swift b/CHDataManagement/Views/Generic/OptionalStringPropertyView.swift new file mode 100644 index 0000000..bee3602 --- /dev/null +++ b/CHDataManagement/Views/Generic/OptionalStringPropertyView.swift @@ -0,0 +1,27 @@ +import SwiftUI + +struct OptionalStringPropertyView: View { + + let title: LocalizedStringKey + + @Binding + var text: String? + + let prompt: String? + + let footer: LocalizedStringKey + + init(title: LocalizedStringKey, text: Binding, prompt: String? = nil, footer: LocalizedStringKey) { + self.title = title + self._text = text + self.prompt = prompt + self.footer = footer + } + + var body: some View { + GenericPropertyView(title: title, footer: footer) { + OptionalTextField(title, text: $text, prompt: prompt) + .textFieldStyle(.roundedBorder) + } + } +} diff --git a/CHDataManagement/Views/Generic/OptionalTextFieldPropertyView.swift b/CHDataManagement/Views/Generic/OptionalTextFieldPropertyView.swift new file mode 100644 index 0000000..58bd06e --- /dev/null +++ b/CHDataManagement/Views/Generic/OptionalTextFieldPropertyView.swift @@ -0,0 +1,27 @@ +import SwiftUI + +struct OptionalTextFieldPropertyView: View { + + let title: LocalizedStringKey + + @Binding + var text: String? + + let prompt: String? + + let footer: LocalizedStringKey + + init(title: LocalizedStringKey, text: Binding, prompt: String? = nil, footer: LocalizedStringKey) { + self.title = title + self._text = text + self.prompt = prompt + self.footer = footer + } + + var body: some View { + GenericPropertyView(title: title, footer: footer) { + OptionalDescriptionField(text: $text) + .textFieldStyle(.roundedBorder) + } + } +} diff --git a/CHDataManagement/Views/Generic/PagePropertyView.swift b/CHDataManagement/Views/Generic/PagePropertyView.swift new file mode 100644 index 0000000..e62c89c --- /dev/null +++ b/CHDataManagement/Views/Generic/PagePropertyView.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct PagePropertyView: View { + + let title: LocalizedStringKey + + @Binding + var selectedPage: Page? + + let footer: LocalizedStringKey + + @State + private var showPageSelectionSheet = false + + var body: some View { + GenericPropertyView(title: title, footer: footer) { + HStack { + Text(selectedPage?.id ?? "No page selected") + Spacer() + Button("Select") { + showPageSelectionSheet = true + } + } + } + .sheet(isPresented: $showPageSelectionSheet) { + PagePickerView(selectedPage: $selectedPage) + } + } +} + diff --git a/CHDataManagement/Views/Generic/StringPropertyView.swift b/CHDataManagement/Views/Generic/StringPropertyView.swift new file mode 100644 index 0000000..86a3f5a --- /dev/null +++ b/CHDataManagement/Views/Generic/StringPropertyView.swift @@ -0,0 +1,27 @@ +import SwiftUI + +struct StringPropertyView: View { + + let title: LocalizedStringKey + + @Binding + var text: String + + let prompt: String? + + let footer: LocalizedStringKey + + init(title: LocalizedStringKey, text: Binding, prompt: String? = nil, footer: LocalizedStringKey) { + self.title = title + self._text = text + self.prompt = prompt + self.footer = footer + } + + var body: some View { + GenericPropertyView(title: title, footer: footer) { + TextField(title, text: $text, prompt: prompt.map(Text.init)) + .textFieldStyle(.roundedBorder) + } + } +} diff --git a/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift b/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift index b1de0cf..b8e9655 100644 --- a/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift +++ b/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift @@ -3,101 +3,41 @@ import SFSafeSymbols struct LocalizedPageDetailView: View { + let isExternalPage: Bool + @ObservedObject - private var page: LocalizedPage - - init(page: LocalizedPage, showImagePicker: Bool = false) { - self.page = page - self.showImagePicker = showImagePicker - self.newUrlString = page.urlString - } - - @State - private var showImagePicker = false - - @State - private var newUrlString: String - - private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted - - private var idExists: Bool { - page.content.pages.contains { - $0.german.urlString == newUrlString - || $0.english.urlString == newUrlString - } - } - - private var containsInvalidCharacters: Bool { - newUrlString.rangeOfCharacter(from: allowedCharactersInPostId) != nil - } + var page: LocalizedPage var body: some View { VStack(alignment: .leading) { - HStack { - Text("Page URL String") - .font(.headline) - TextField("", text: $newUrlString) - .textFieldStyle(.roundedBorder) - Button("Update", action: setNewId) - .disabled(newUrlString.isEmpty || containsInvalidCharacters || idExists) - } - .padding(.bottom) + IdPropertyView( + id: $page.urlString, + title: "Page URL String", + footer: "The url component to use for the link to the page", + validation: page.isValid, + update: { page.urlString = $0 }) + .disabled(isExternalPage) - Text("Link Preview Title") - .font(.headline) - OptionalTextField("", text: $page.linkPreviewTitle, - prompt: page.title) - .textFieldStyle(.roundedBorder) - .padding(.bottom) + OptionalStringPropertyView( + title: "Preview Title", + text: $page.linkPreviewTitle, + prompt: page.title, + footer: "The title to use for the page when linking to it") - HStack { - Text("Link Preview Image") - .font(.headline) - IconButton(symbol: .squareAndPencilCircleFill, - size: 22, - color: .blue) { - showImagePicker = true - } + OptionalImagePropertyView( + title: "Preview Image", + selectedImage: $page.linkPreviewImage, + footer: "The image to show for previews of this page") - IconButton(symbol: .trashCircleFill, - size: 22, - color: .red) { - page.linkPreviewImage = nil - }.disabled(page.linkPreviewImage == nil) - Spacer() - } - - .buttonStyle(.plain) - if let image = page.linkPreviewImage { - image.imageToDisplay - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: 400, maxHeight: 300) - .cornerRadius(8) - Text(image.id) - .font(.headline) - } - - Text("Link Preview Description") - .font(.headline) - .padding(.top) - OptionalDescriptionField(text: $page.linkPreviewDescription) - .textFieldStyle(.roundedBorder) - .padding(.bottom) + OptionalTextFieldPropertyView( + title: "Preview Description", + text: $page.linkPreviewDescription, + footer: "The description to show in previews of the page") } - .sheet(isPresented: $showImagePicker) { - ImagePickerView(showImagePicker: $showImagePicker) { image in - page.linkPreviewImage = image - } - } - } - - private func setNewId() { - page.urlString = newUrlString } } #Preview { - LocalizedPageDetailView(page: .english) + LocalizedPageDetailView(isExternalPage: false, page: .english) .environmentObject(Content.mock) } diff --git a/CHDataManagement/Views/Pages/PageDetailView.swift b/CHDataManagement/Views/Pages/PageDetailView.swift index 642bc43..0e6da23 100644 --- a/CHDataManagement/Views/Pages/PageDetailView.swift +++ b/CHDataManagement/Views/Pages/PageDetailView.swift @@ -15,30 +15,19 @@ struct PageDetailView: View { @State private var isGeneratingWebsite = false - @State - private var newId: String - @State private var didGenerateWebsite: Bool? init(page: Page) { self.page = page - self.newId = page.id - } - - private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted - - private var idExists: Bool { - page.content.pages.contains { $0.id == newId } - } - - private var containsInvalidCharacters: Bool { - newId.rangeOfCharacter(from: allowedCharactersInPostId) != nil } var body: some View { ScrollView { VStack(alignment: .leading) { + DetailTitle( + title: "Page", + text: "A page contains longer content") HStack { Button(action: generate) { Text("Generate") @@ -54,62 +43,40 @@ struct PageDetailView: View { } } } - HStack { - TextField("", text: $newId) - .textFieldStyle(.roundedBorder) - Button("Update", action: setNewId) - .disabled(newId.isEmpty || containsInvalidCharacters || idExists) - } - .padding(.bottom) + IdPropertyView( + id: $page.id, + footer: "The page id is used to link to it internally.", + validation: page.isValid, + update: { page.update(id: $0) }) - Text("External url") - .font(.headline) - OptionalTextField("", text: $page.externalLink, - prompt: "External url") - .textFieldStyle(.roundedBorder) - .padding(.bottom) + OptionalStringPropertyView( + title: "External url", + text: $page.externalLink, + footer: "Set an external url to mark this page as external. It will not be generated, and links will be created using the provided url") - HStack { - Text("Draft") - .font(.headline) - Spacer() - Toggle("", isOn: $page.isDraft) - .toggleStyle(.switch) - } - .padding(.bottom) + BoolPropertyView( + title: "Draft", + value: $page.isDraft, + footer: "Indicate a page as a draft to hide it from the website") + .disabled(page.isExternalUrl) - HStack { - Text("Start") - .font(.headline) - Spacer() - DatePicker("", selection: $page.startDate, displayedComponents: .date) - .datePickerStyle(.compact) - .padding(.bottom) - } + DatePropertyView( + title: "Start date", + value: $page.startDate, + footer: "The date when the page content started") + .disabled(page.isExternalUrl) - HStack(alignment: .firstTextBaseline) { - Text("Has end date") - .font(.headline) - Spacer() - Toggle("", isOn: $page.hasEndDate) - .toggleStyle(.switch) - .padding(.bottom) - } - - if page.hasEndDate { - HStack(alignment: .firstTextBaseline) { - Text("End date") - .font(.headline) - Spacer() - DatePicker("", selection: $page.endDate, displayedComponents: .date) - .datePickerStyle(.compact) - .padding(.bottom) - } - } - - LocalizedPageDetailView(page: page.localized(in: language)) - .id(page.id + language.rawValue) + OptionalDatePropertyView( + title: "End date", + isEnabled: $page.hasEndDate, + date: $page.endDate, + footer: "The date when the page content ended") + .disabled(page.isExternalUrl) + LocalizedPageDetailView( + isExternalPage: page.isExternalUrl, + page: page.localized(in: language)) + .id(page.id + language.rawValue) } .padding() } @@ -144,14 +111,6 @@ struct PageDetailView: View { } } } - - private func setNewId() { - guard page.update(id: newId) else { - newId = page.id - return - } - page.id = newId - } } extension PageDetailView: MainContentView { diff --git a/CHDataManagement/Views/Posts/AddPostView.swift b/CHDataManagement/Views/Posts/AddPostView.swift index d9ab4da..1a243c5 100644 --- a/CHDataManagement/Views/Posts/AddPostView.swift +++ b/CHDataManagement/Views/Posts/AddPostView.swift @@ -74,8 +74,8 @@ struct AddPostView: View { startDate: .now, endDate: nil, tags: [], - german: .init(title: "Titel", content: "Text"), - english: .init(title: "Title", content: "Text")) + german: .init(content: content, title: "Titel", text: "Text"), + english: .init(content: content, title: "Title", text: "Text")) content.posts.insert(post, at: 0) selectedPost = post dismissSheet() diff --git a/CHDataManagement/Views/Posts/DatePickerView.swift b/CHDataManagement/Views/Posts/DatePickerView.swift deleted file mode 100644 index 5efe5d6..0000000 --- a/CHDataManagement/Views/Posts/DatePickerView.swift +++ /dev/null @@ -1,49 +0,0 @@ -import SwiftUI - -struct DatePickerView: View { - - @ObservedObject - var post: Post - - @Binding var showDatePicker: Bool - - var body: some View { - NavigationView { - VStack { - HStack(alignment: .top) { - VStack { - Text("Start date") - .font(.headline) - .padding(.vertical, 3) - DatePicker("", selection: $post.startDate, displayedComponents: .date) - .datePickerStyle(GraphicalDatePickerStyle()) - .labelsHidden() - .padding() - - } - - VStack { - Toggle("End date", isOn: $post.hasEndDate) - .toggleStyle(.switch) - .font(.headline) - DatePicker("Select a date", selection: $post.startDate, displayedComponents: .date) - .datePickerStyle(GraphicalDatePickerStyle()) - .labelsHidden() - .padding() - .disabled(!post.hasEndDate) - } - } - Button("Done") { - showDatePicker = false - } - Spacer() - } - .navigationTitle("Pick a Date") - .padding() - } - } -} - -#Preview { - DatePickerView(post: .mock, showDatePicker: .constant(true)) -} diff --git a/CHDataManagement/Views/Posts/ImagePickerView.swift b/CHDataManagement/Views/Posts/ImagePickerView.swift deleted file mode 100644 index 60f661b..0000000 --- a/CHDataManagement/Views/Posts/ImagePickerView.swift +++ /dev/null @@ -1,59 +0,0 @@ -import SwiftUI - -struct ImagePickerView: View { - - @Binding - var showImagePicker: Bool - - private let selected: (FileResource) -> Void - - @EnvironmentObject - private var content: Content - - @Environment(\.language) - private var language - - init(showImagePicker: Binding, selected: @escaping (FileResource) -> Void) { - self._showImagePicker = showImagePicker - self.selected = selected - } - - @State - private var selectedImage: FileResource? - - var body: some View { - VStack { - Text("Select the image to add") - List(content.images, selection: $selectedImage) { image in - Text("\(image.id)") - .tag(image) - } - .frame(minHeight: 300) - HStack { - Button("Add") { - DispatchQueue.main.async { - if let selectedImage { - print("Added image") - selected(selectedImage) - } else { - print("No image to add") - } - } - showImagePicker = false - } - .disabled(selectedImage == nil) - Button("Cancel", role: .cancel) { - showImagePicker = false - } - } - } - .navigationTitle("Pick an image") - .padding() - } -} - -#Preview { - ImagePickerView(showImagePicker: .constant(true)) { _ in - } - .environmentObject(Content.mock) -} diff --git a/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift b/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift index 24c9338..c555281 100644 --- a/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift +++ b/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift @@ -3,64 +3,25 @@ import SwiftUI struct LocalizedPostDetailView: View { @ObservedObject - private var item: LocalizedPost - - init(post: LocalizedPost, showImagePicker: Bool = false) { - self.item = post - self.showImagePicker = showImagePicker - } - - @State - private var showImagePicker = false + var post: LocalizedPost var body: some View { VStack(alignment: .leading) { - Text("Link Preview Title") - .font(.headline) - OptionalTextField("", text: $item.linkPreviewTitle, - prompt: item.title) - .textFieldStyle(.roundedBorder) - .padding(.bottom) + OptionalStringPropertyView( + title: "Preview Title", + text: $post.linkPreviewTitle, + prompt: post.title, + footer: "The title to use for the post when linking to it") - HStack { - Text("Link Preview Image") - .font(.headline) - IconButton(symbol: .squareAndPencilCircleFill, - size: 22, - color: .blue) { - showImagePicker = true - }.padding(.bottom) + OptionalImagePropertyView( + title: "Preview Image", + selectedImage: $post.linkPreviewImage, + footer: "The image to show for previews of this post") - IconButton(symbol: .trashCircleFill, - size: 22, - color: .red) { - item.linkPreviewImage = nil - }.disabled(item.linkPreviewImage == nil) - Spacer() - } - - .buttonStyle(.plain) - if let image = item.linkPreviewImage { - image.imageToDisplay - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: 400, maxHeight: 300) - .cornerRadius(8) - Text(image.id) - .font(.headline) - } - - Text("Link Preview Description") - .font(.headline) - .padding(.top) - OptionalDescriptionField(text: $item.linkPreviewDescription) - .textFieldStyle(.roundedBorder) - .padding(.bottom) - } - .sheet(isPresented: $showImagePicker) { - ImagePickerView(showImagePicker: $showImagePicker) { image in - item.linkPreviewImage = image - } + OptionalTextFieldPropertyView( + title: "Preview Description", + text: $post.linkPreviewDescription, + footer: "The description to show in previews of the post") } } } diff --git a/CHDataManagement/Views/Posts/PagePickerView.swift b/CHDataManagement/Views/Posts/PagePickerView.swift index 3e8f50e..d25e8e3 100644 --- a/CHDataManagement/Views/Posts/PagePickerView.swift +++ b/CHDataManagement/Views/Posts/PagePickerView.swift @@ -2,21 +2,21 @@ import SwiftUI struct PagePickerView: View { - @Binding var showPagePicker: Bool - - @Binding var selectedPage: Page? - @EnvironmentObject private var content: Content @Environment(\.language) private var language + @Environment(\.dismiss) + var dismiss + + @Binding var selectedPage: Page? + @State private var newSelection: Page? - init(showPagePicker: Binding, selectedPage: Binding) { - self._showPagePicker = showPagePicker + init(selectedPage: Binding) { self._selectedPage = selectedPage self.newSelection = selectedPage.wrappedValue // TODO: Fix assignment not working @@ -35,17 +35,17 @@ struct PagePickerView: View { Button("Use selection") { DispatchQueue.main.async { self.selectedPage = self.newSelection + dismiss() } - showPagePicker = false } Button("Remove page", role: .destructive) { DispatchQueue.main.async { self.selectedPage = nil + dismiss() } - showPagePicker = false } Button("Cancel", role: .cancel) { - showPagePicker = false + dismiss() } } } @@ -55,7 +55,6 @@ struct PagePickerView: View { } #Preview { - PagePickerView(showPagePicker: .constant(true), - selectedPage: .constant(nil)) - .environmentObject(Content.mock) + PagePickerView(selectedPage: .constant(nil)) + .environmentObject(Content.mock) } diff --git a/CHDataManagement/Views/Posts/PostContentView.swift b/CHDataManagement/Views/Posts/PostContentView.swift index 276289f..b7d862f 100644 --- a/CHDataManagement/Views/Posts/PostContentView.swift +++ b/CHDataManagement/Views/Posts/PostContentView.swift @@ -56,7 +56,7 @@ private struct LocalizedContentEditor: View { } var body: some View { - TextEditor(text: $post.content) + TextEditor(text: $post.text) .font(.body) .frame(minHeight: 150) .textEditorStyle(.plain) diff --git a/CHDataManagement/Views/Posts/PostDetailView.swift b/CHDataManagement/Views/Posts/PostDetailView.swift index e5cd0a5..cac2d00 100644 --- a/CHDataManagement/Views/Posts/PostDetailView.swift +++ b/CHDataManagement/Views/Posts/PostDetailView.swift @@ -36,109 +36,51 @@ struct PostDetailView: View { @ObservedObject private var post: Post - @State - private var newId: String - @State private var showLinkedPagePicker = false init(post: Post) { self.post = post - self.newId = post.id - } - - private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted - - private var idExists: Bool { - post.content.posts.contains { $0.id == newId } - } - - private var containsInvalidCharacters: Bool { - newId.rangeOfCharacter(from: allowedCharactersInPostId) != nil } var body: some View { ScrollView { VStack(alignment: .leading) { - Text("ID") - .font(.headline) - HStack { - TextField("", text: $newId) - .textFieldStyle(.roundedBorder) - Button("Update", action: setNewId) - .disabled(newId.isEmpty || containsInvalidCharacters || idExists) - } - .padding(.bottom) + DetailTitle( + title: "Post", + text: "Posts capture quick updates and can link to pages") - HStack { - Text("Draft") - .font(.headline) - Spacer() - Toggle("", isOn: $post.isDraft) - .toggleStyle(.switch) - } - .padding(.bottom) + IdPropertyView( + id: $post.id, + footer: "The id is used to link to post and store them", + validation: post.isValid, + update: { post.update(id: $0) }) - HStack { - Text("Start") - .font(.headline) - Spacer() - DatePicker("", selection: $post.startDate, displayedComponents: .date) - .datePickerStyle(.compact) - .padding(.bottom) - } + BoolPropertyView( + title: "Draft", + value: $post.isDraft, + footer: "Indicate a post as a draft to hide it from the website") - HStack(alignment: .firstTextBaseline) { - Text("Has end date") - .font(.headline) - Spacer() - Toggle("", isOn: $post.hasEndDate) - .toggleStyle(.switch) - .padding(.bottom) - } + DatePropertyView( + title: "Start date", + value: $post.startDate, + footer: "The date when the post content started") - if post.hasEndDate { - HStack(alignment: .firstTextBaseline) { - Text("End date") - .font(.headline) - Spacer() - DatePicker("", selection: $post.endDate, displayedComponents: .date) - .datePickerStyle(.compact) - .padding(.bottom) - } - } - - HStack { - Text("Linked page") - .font(.headline) - IconButton(symbol: .squareAndPencilCircleFill, - size: 22, - color: .blue) { - showLinkedPagePicker = true - } - Spacer() - } - Text(post.linkedPage?.localized(in: language).title ?? "No page linked") + OptionalDatePropertyView( + title: "End date", + isEnabled: $post.hasEndDate, + date: $post.endDate, + footer: "The date when the post content ended") + PagePropertyView( + title: "Linked page", + selectedPage: $post.linkedPage, + footer: "The page to open when clicking on the post") LocalizedPostDetailView(post: post.localized(in: language)) - } .padding() } - .sheet(isPresented: $showLinkedPagePicker) { - PagePickerView( - showPagePicker: $showLinkedPagePicker, - selectedPage: $post.linkedPage) - } - } - - private func setNewId() { - guard post.update(id: newId) else { - newId = post.id - return - } - post.id = newId } } diff --git a/CHDataManagement/Views/Posts/PostImagesView.swift b/CHDataManagement/Views/Posts/PostImagesView.swift index 0d1a41b..ffdcd6b 100644 --- a/CHDataManagement/Views/Posts/PostImagesView.swift +++ b/CHDataManagement/Views/Posts/PostImagesView.swift @@ -50,11 +50,6 @@ struct PostImagesView: View { .padding() } } - .sheet(isPresented: $showImagePicker) { - ImagePickerView(showImagePicker: $showImagePicker) { image in - post.images.append(image) - } - } } private func shiftLeft(_ image: FileResource) { diff --git a/CHDataManagement/Views/Posts/TextEntrySheet.swift b/CHDataManagement/Views/Posts/TextEntrySheet.swift deleted file mode 100644 index 2129019..0000000 --- a/CHDataManagement/Views/Posts/TextEntrySheet.swift +++ /dev/null @@ -1,60 +0,0 @@ -import SwiftUI - -struct TextEntrySheet: View { - - let title: String - - @Binding - var text: String - - @Binding - var isValid: Bool - - @Environment(\.dismiss) - private var dismiss: DismissAction - - var body: some View { - VStack { - Text(title) - .foregroundStyle(.secondary) - TextField("Text", text: $text) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .overlay { - if isValid { - EmptyView() - } else { - RoundedRectangle(cornerRadius: 8) - .strokeBorder(lineWidth: 3) - .foregroundStyle(.red) - } - } - .frame(maxWidth: 300) - HStack { - Button(action: submit) { - Text("Submit") - } - .disabled(!isValid) - Button(role: .cancel, action: cancel) { - Text("Cancel") - } - } - } - .padding() - } - - private func submit() { - dismiss() - } - - private func cancel() { - text = "" - dismiss() - } -} - -#Preview { - TextEntrySheet( - title: "Enter the id for the new post", - text: .constant("new"), - isValid: .constant(false)) -} diff --git a/CHDataManagement/Views/Settings/Content/Pages/PageIssueView.swift b/CHDataManagement/Views/Settings/Content/Pages/PageIssueView.swift index 6ac7093..641416f 100644 --- a/CHDataManagement/Views/Settings/Content/Pages/PageIssueView.swift +++ b/CHDataManagement/Views/Settings/Content/Pages/PageIssueView.swift @@ -120,9 +120,7 @@ struct PageIssueView: View { didSelect(page: page) } } content: { - PagePickerView( - showPagePicker: $showPagePicker, - selectedPage: $selectedPage) + PagePickerView(selectedPage: $selectedPage) } .sheet(isPresented: $showFilePicker) { if let file = selectedFile { diff --git a/CHDataManagement/Views/Settings/GenerationDetailView.swift b/CHDataManagement/Views/Settings/GenerationDetailView.swift index a39b9bc..42259e0 100644 --- a/CHDataManagement/Views/Settings/GenerationDetailView.swift +++ b/CHDataManagement/Views/Settings/GenerationDetailView.swift @@ -5,25 +5,18 @@ struct GenerationDetailView: View { let section: SettingsSection var body: some View { - Group { - switch section { - //case .generation: - // GenerationSettingsView() - case .folders: - PathSettingsView() - case .navigationBar: - NavigationBarSettingsView() - case .postFeed: - PostFeedSettingsView() - case .pages: - PageSettingsDetailView() - case .tagOverview: - TagOverviewDetailView() - } + switch section { + case .folders: + PathSettingsView() + case .navigationBar: + NavigationBarSettingsView() + case .postFeed: + PostFeedSettingsView() + case .pages: + PageSettingsDetailView() + case .tagOverview: + TagOverviewDetailView() } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding() - .navigationTitle("") } } diff --git a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift index e01f4e0..c85f696 100644 --- a/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift +++ b/CHDataManagement/Views/Settings/NavigationBarSettingsView.swift @@ -15,12 +15,9 @@ struct NavigationBarSettingsView: View { var body: some View { ScrollView { VStack(alignment: .leading) { - Text("Navigation Bar") - .font(.largeTitle) - .bold() - Text("Customize the navigation bar for all pages at the top of the website") - .foregroundStyle(.secondary) - .padding(.bottom, 30) + DetailTitle( + title: "Navigation Bar", + text: "Customize the navigation bar for all pages at the top of the website") HStack { Text("Links") diff --git a/CHDataManagement/Views/Settings/PageSettingsDetailView.swift b/CHDataManagement/Views/Settings/PageSettingsDetailView.swift index 8275e93..17bf64b 100644 --- a/CHDataManagement/Views/Settings/PageSettingsDetailView.swift +++ b/CHDataManagement/Views/Settings/PageSettingsDetailView.swift @@ -16,43 +16,43 @@ struct PageSettingsDetailView: View { text: "Change the way pages are displayed") IntegerPropertyView( - value: $content.settings.pages.contentWidth, title: "Content Width", + value: $content.settings.pages.contentWidth, footer: "The maximum width of the content in pages (in pixels)") IntegerPropertyView( - value: $content.settings.pages.largeImageWidth, title: "Fullscreen Image Width", + value: $content.settings.pages.largeImageWidth, footer: "The maximum width of images that are diplayed fullscreen") IntegerPropertyView( - value: $content.settings.pages.pageLinkImageSize, title: "Page Link Image Width", + value: $content.settings.pages.pageLinkImageSize, footer: "The maximum width of images diplayed as thumbnails on page links") FilePropertyView( title: "Default CSS File", - description: "The CSS file containing the styling of all pages", + footer: "The CSS file containing the styling of all pages", selectedFile: $content.settings.pages.defaultCssFile) FilePropertyView( title: "Code Highlighting File", - description: "The JavaScript file to provide syntax highlighting of code blocks", + footer: "The JavaScript file to provide syntax highlighting of code blocks", selectedFile: $content.settings.pages.codeHighlightingJsFile) FilePropertyView( title: "Audio Player CSS File", - description: "The CSS file to provide the style for the audio player", + footer: "The CSS file to provide the style for the audio player", selectedFile: $content.settings.pages.audioPlayerCssFile) FilePropertyView( title: "Audio Player JavaScript File", - description: "The CSS file to provide the functionality for the audio player", + footer: "The CSS file to provide the functionality for the audio player", selectedFile: $content.settings.pages.audioPlayerJsFile) FilePropertyView( title: "3D Model Viewer File", - description: "The JavaScript file to provide the functionality for the 3D model viewer", + footer: "The JavaScript file to provide the functionality for the 3D model viewer", selectedFile: $content.settings.pages.modelViewerJsFile) } } diff --git a/CHDataManagement/Views/Settings/PathSettingsView.swift b/CHDataManagement/Views/Settings/PathSettingsView.swift index 0c64e85..a871573 100644 --- a/CHDataManagement/Views/Settings/PathSettingsView.swift +++ b/CHDataManagement/Views/Settings/PathSettingsView.swift @@ -11,131 +11,66 @@ struct PathSettingsView: View { @EnvironmentObject private var content: Content - @State - private var folderSelection: SecurityScopeBookmark = .contentPath - var body: some View { ScrollView { VStack(alignment: .leading) { - Text("Folder Settings") - .font(.largeTitle) - .bold() - Text("Select the folders for the app to work.") - .foregroundStyle(.secondary) - .padding(.bottom, 30) + DetailTitle( + title: "Folder Settings", + text: "Select the folders for the app to work.") - Text("Content Folder") - .font(.headline) - .padding(.bottom, 1) - Text(contentPath) - Button(action: selectContentFolder) { - Text("Select folder") - } - Text("The folder where the raw content of the website is stored") - .foregroundStyle(.secondary) - .padding(.bottom) + FolderOnDiskPropertyView( + title: "Content Folder", + folder: $contentPath, + footer: "The folder where the raw content of the website is stored") { url in + guard content.storage.save(folderUrl: url, in: .contentPath) else { + return + } + contentPath = url.path() + } - Text("Output Folder") - .font(.headline) - .padding(.bottom, 1) - Text(content.settings.paths.outputDirectoryPath) - Button(action: selectOutputFolder) { - Text("Select folder") - } - Text("The folder where the generated website is stored") - .foregroundStyle(.secondary) - .padding(.bottom) + FolderOnDiskPropertyView( + title: "Output Folder", + folder: $content.settings.paths.outputDirectoryPath, + footer: "The folder where the generated website is stored") { url in + guard content.storage.save(folderUrl: url, in: .outputPath) else { + return + } + content.settings.paths.outputDirectoryPath = url.path() + } - Text("Pages output folder") - .font(.headline) - TextField("", text: $content.settings.paths.pagesOutputFolderPath) - .textFieldStyle(.roundedBorder) - Text("The path in the output folder where the generated pages are stored") - .foregroundStyle(.secondary) - .padding(.bottom) + StringPropertyView( + title: "Pages output folder", + text: $content.settings.paths.pagesOutputFolderPath, + footer: "The path in the output folder where the generated pages are stored") - Text("Tags output folder") - .font(.headline) - TextField("", text: $content.settings.paths.tagsOutputFolderPath) - .textFieldStyle(.roundedBorder) - Text("The path in the output folder where the generated tag pages are stored") - .foregroundStyle(.secondary) - .padding(.bottom) + StringPropertyView( + title: "Tags output folder", + text: $content.settings.paths.tagsOutputFolderPath, + footer: "The path in the output folder where the generated tag pages are stored") - Text("Files output folder") - .font(.headline) - TextField("", text: $content.settings.paths.filesOutputFolderPath) - .textFieldStyle(.roundedBorder) - Text("The path in the output folder where the copied files are stored") - .foregroundStyle(.secondary) - .padding(.bottom) + StringPropertyView( + title: "Files output folder", + text: $content.settings.paths.filesOutputFolderPath, + footer: "The path in the output folder where the copied files are stored") - Text("Images output folder") - .font(.headline) - TextField("", text: $content.settings.paths.imagesOutputFolderPath) - .textFieldStyle(.roundedBorder) - Text("The path in the output folder where the generated images are stored") - .foregroundStyle(.secondary) - .padding(.bottom) + StringPropertyView( + title: "Images output folder", + text: $content.settings.paths.imagesOutputFolderPath, + footer: "The path in the output folder where the generated images are stored") - Text("Videos output folder") - .font(.headline) - TextField("", text: $content.settings.paths.videosOutputFolderPath) - .textFieldStyle(.roundedBorder) - Text("The path in the output folder where the generated videos are stored") - .foregroundStyle(.secondary) - .padding(.bottom) + StringPropertyView( + title: "Videos output folder", + text: $content.settings.paths.videosOutputFolderPath, + footer: "The path in the output folder where the generated videos are stored") - Text("Assets output folder") - .font(.headline) - TextField("", text: $content.settings.paths.assetsOutputFolderPath) - .textFieldStyle(.roundedBorder) - Text("The path in the output folder where assets are stored") - .foregroundStyle(.secondary) - .padding(.bottom) + StringPropertyView( + title: "Assets output folder", + text: $content.settings.paths.assetsOutputFolderPath, + footer: "The path in the output folder where assets are stored") } + .padding() } } - - // MARK: Folder selection - - private func selectContentFolder() { - folderSelection = .contentPath - guard let url = savePanelUsingOpenPanel() else { - return - } - self.contentPath = url.path() - } - - private func selectOutputFolder() { - folderSelection = .outputPath - guard let url = savePanelUsingOpenPanel() else { - return - } - content.settings.paths.outputDirectoryPath = url.path() - } - - private func savePanelUsingOpenPanel() -> URL? { - let panel = NSOpenPanel() - // Sets up so user can only select a single directory - panel.canChooseFiles = false - panel.canChooseDirectories = true - panel.allowsMultipleSelection = false - panel.showsHiddenFiles = false - panel.title = "Select Save Directory" - panel.prompt = "Select Save Directory" - - let response = panel.runModal() - guard response == .OK else { - - return nil - } - guard let url = panel.url else { - return nil - } - content.storage.save(folderUrl: url, in: folderSelection) - return url - } } #Preview { diff --git a/CHDataManagement/Views/Settings/PostFeedSettingsView.swift b/CHDataManagement/Views/Settings/PostFeedSettingsView.swift index 7557712..095e007 100644 --- a/CHDataManagement/Views/Settings/PostFeedSettingsView.swift +++ b/CHDataManagement/Views/Settings/PostFeedSettingsView.swift @@ -15,28 +15,28 @@ struct PostFeedSettingsView: View { text: "Change the way the posts are displayed") IntegerPropertyView( - value: $content.settings.posts.contentWidth, title: "Content Width", + value: $content.settings.posts.contentWidth, footer: "The maximum width of the content the post feed (in pixels)") IntegerPropertyView( - value: $content.settings.posts.postsPerPage, title: "Posts Per Page", + value: $content.settings.posts.postsPerPage, footer: "The maximum number of posts displayed on a single page") FilePropertyView( title: "Default CSS File", - description: "The CSS file containing the styling of all post pages", + footer: "The CSS file containing the styling of all post pages", selectedFile: $content.settings.posts.defaultCssFile) FilePropertyView( title: "Swiper CSS File", - description: "The CSS file containing the styling of image galleries in post feeds", + footer: "The CSS file containing the styling of image galleries in post feeds", selectedFile: $content.settings.posts.swiperCssFile) FilePropertyView( title: "Swiper JavaScript File", - description: "The JavaScript file to load the image gallery code in post feeds", + footer: "The JavaScript file to load the image gallery code in post feeds", selectedFile: $content.settings.posts.swiperJsFile) LocalizedPostFeedSettingsView( diff --git a/CHDataManagement/Views/Settings/TagOverviewDetailView.swift b/CHDataManagement/Views/Settings/TagOverviewDetailView.swift index 86e8742..bc52e74 100644 --- a/CHDataManagement/Views/Settings/TagOverviewDetailView.swift +++ b/CHDataManagement/Views/Settings/TagOverviewDetailView.swift @@ -11,12 +11,9 @@ struct TagOverviewDetailView: View { var body: some View { ScrollView { VStack(alignment: .leading) { - Text("Tag Overview") - .font(.largeTitle) - .bold() - Text("Configure the page showing all tags") - .foregroundStyle(.secondary) - .padding(.bottom, 30) + DetailTitle( + title: "Tag Overview", + text: "Configure the page showing all tags") if let page = content.tagOverview?.localized(in: language) { TagOverviewDetails(page: page) @@ -30,101 +27,48 @@ struct TagOverviewDetailView: View { private func createTagOverviewPage() { content.tagOverview = TagOverviewPage( content: content, - german: .init(title: "Alle Tags", urlString: "alle"), - english: .init(title: "All tags", urlString: "all")) + german: .init(content: content, title: "Alle Tags", urlString: "alle"), + english: .init(content: content, title: "All tags", urlString: "all")) } } private struct TagOverviewDetails: View { + @EnvironmentObject + private var content: Content + @ObservedObject var page: LocalizedTagOverviewPage - @EnvironmentObject - var content: Content - - @State - private var showImagePicker = false - - @State - private var newUrlString: String = "" - - init(page: LocalizedTagOverviewPage) { - self.page = page - } - - private var newUrlCanBeUpdated: Bool { - guard !newUrlString.isEmpty else { return false } - guard content.isValidIdForTagOrPageOrPost(newUrlString) else { return false } - return !content.containsTag(withUrlComponent: newUrlString) - } - var body: some View { VStack(alignment: .leading) { - Text("Title") - .font(.headline) - TextField("", text: $page.title) - .textFieldStyle(.roundedBorder) + StringPropertyView( + title: "Title", + text: $page.title, + footer: "The title of the overview page") - HStack { - Text("Page URL String") - .font(.headline) - TextField("", text: $newUrlString) - .textFieldStyle(.roundedBorder) - Button("Update", action: setNewId) - .disabled(!newUrlCanBeUpdated) - } - .padding(.bottom) + IdPropertyView( + id: $page.urlComponent, + title: "Page URL String", + footer: "The url component to use for the link to the page", + validation: page.isValid, + update: { page.urlComponent = $0 }) - Text("Link Preview Title") - .font(.headline) - OptionalTextField("", text: $page.linkPreviewTitle, - prompt: page.title) - .textFieldStyle(.roundedBorder) - .padding(.bottom) + OptionalStringPropertyView( + title: "Preview Title", + text: $page.linkPreviewTitle, + prompt: page.title, + footer: "The title to use for the page when linking to it") - HStack { - Text("Link Preview Image") - .font(.headline) - IconButton(symbol: .squareAndPencilCircleFill, - size: 22, - color: .blue) { - showImagePicker = true - } + OptionalImagePropertyView( + title: "Preview Image", + selectedImage: $page.linkPreviewImage, + footer: "The image to show for previews of this page") - IconButton(symbol: .trashCircleFill, - size: 22, - color: .red) { - page.linkPreviewImage = nil - }.disabled(page.linkPreviewImage == nil) - } - - .buttonStyle(.plain) - if let image = page.linkPreviewImage { - image.imageToDisplay - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: 400, maxHeight: 300) - .cornerRadius(8) - Text(image.id) - .font(.headline) - } - - Text("Link Preview Description") - .font(.headline) - .padding(.top) - OptionalDescriptionField(text: $page.linkPreviewDescription) - .textFieldStyle(.roundedBorder) - .padding(.bottom) + OptionalTextFieldPropertyView( + title: "Preview Description", + text: $page.linkPreviewDescription, + footer: "The description to show in previews of the page") } - .sheet(isPresented: $showImagePicker) { - ImagePickerView(showImagePicker: $showImagePicker) { image in - page.linkPreviewImage = image - } - } - } - - private func setNewId() { - page.urlString = newUrlString } } diff --git a/CHDataManagement/Views/Tags/AddTagView.swift b/CHDataManagement/Views/Tags/AddTagView.swift index 9d48a4e..2e99aa7 100644 --- a/CHDataManagement/Views/Tags/AddTagView.swift +++ b/CHDataManagement/Views/Tags/AddTagView.swift @@ -28,8 +28,8 @@ struct AddTagView: View { content: content, id: "tag", isVisible: true, - german: .init(urlComponent: "tag", name: "Neuer Tag"), - english: .init(urlComponent: "tag-en", name: "New Tag")) + german: .init(content: .mock, urlComponent: "tag", name: "Neuer Tag"), + english: .init(content: .mock, 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 index d272f1a..82d89cd 100644 --- a/CHDataManagement/Views/Tags/LocalizedTagDetailView.swift +++ b/CHDataManagement/Views/Tags/LocalizedTagDetailView.swift @@ -1,96 +1,55 @@ 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(.headline) - .padding(.bottom) - - Text("Name") - .font(.headline) - TextField("", text: $tag.name) - .textFieldStyle(.roundedBorder) - .padding(.bottom) - - Text("URL String") - .font(.headline) - TextField("", text: $tag.urlComponent) - .textFieldStyle(.roundedBorder) - .padding(.bottom) - - Text("Original url") - .font(.headline) - Text(tag.originalUrl ?? "-") - .padding(.top, 1) - .padding(.bottom) - - Text("Subtitle") - .font(.headline) - OptionalTextField("", text: $tag.subtitle) - .textFieldStyle(.roundedBorder) - .padding(.bottom) - - Text("Link Preview Description") - .font(.headline) - .padding(.top) - OptionalDescriptionField(text: $tag.description) - .textFieldStyle(.roundedBorder) - .padding(.bottom) - - HStack { - Text("Link Preview Image") - .font(.headline) - IconButton(symbol: .squareAndPencilCircleFill, - size: 22, - color: .blue) { - showImagePicker = true - } - - IconButton(symbol: .trashCircleFill, - size: 22, - color: .red) { - tag.linkPreviewImage = nil - }.disabled(tag.linkPreviewImage == nil) - Spacer() - } - - .buttonStyle(.plain) - if let image = tag.linkPreviewImage { - image.imageToDisplay - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxWidth: 400, maxHeight: 300) - .cornerRadius(8) - } - } - .padding() - } - .sheet(isPresented: $showImagePicker) { - ImagePickerView(showImagePicker: $showImagePicker) { image in - tag.linkPreviewImage = image - } + VStack(alignment: .leading) { + StringPropertyView( + title: "Name", + text: $tag.name, + footer: "The displayed name of the tag") + + IdPropertyView( + id: $tag.urlComponent, + title: "Page URL String", + footer: "The url component to use in the url for this tag", + validation: tag.isValid, + update: { tag.urlComponent = $0 }) + + Text("Original url") + .font(.headline) + Text(tag.originalUrl ?? "-") + .foregroundStyle(.secondary) + .padding(.top, 1) + .padding(.bottom) + + OptionalStringPropertyView( + title: "Subtitle", + text: $tag.subtitle, + footer: "The subtitle/tagline to use") + + OptionalImagePropertyView( + title: "Preview Image", + selectedImage: $tag.linkPreviewImage, + footer: "The image to show for previews of this page") + + OptionalTextFieldPropertyView( + title: "Preview Description", + text: $tag.description, + footer: "The description to show in previews of the page") } } } #Preview { - LocalizedTagDetailView( - tagIsVisible: .constant(true), - tag: Tag.mock.english) + LocalizedTagDetailView(tag: Tag.mock.english) } diff --git a/CHDataManagement/Views/Tags/TagDetailView.swift b/CHDataManagement/Views/Tags/TagDetailView.swift index dd233a2..5d91d80 100644 --- a/CHDataManagement/Views/Tags/TagDetailView.swift +++ b/CHDataManagement/Views/Tags/TagDetailView.swift @@ -10,9 +10,22 @@ struct TagDetailView: View { var tag: Tag var body: some View { - LocalizedTagDetailView( - tagIsVisible: $tag.isVisible, - tag: tag.localized(in: language)) + ScrollView { + VStack(alignment: .leading) { + DetailTitle( + title: "Tag", + text: "A tag groups posts and pages together based on a common theme.") + + BoolPropertyView( + title: "Appears in overviews", + value: $tag.isVisible, + footer: "Indicate if the tag should appear in the tag list of posts and pages. If the tag is not visible, then it can still be used as a filter.") + + LocalizedTagDetailView( + tag: tag.localized(in: language)) + } + .padding() + } } }