Unified detail views, model

This commit is contained in:
Christoph Hagen 2024-12-16 09:54:21 +01:00
parent 1e67a99866
commit 31d1ecb8bd
57 changed files with 853 additions and 954 deletions

View File

@ -13,9 +13,7 @@
E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; }; E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; };
E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850182CEE561B0090B18B /* PageOnDisk.swift */; }; E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850182CEE561B0090B18B /* PageOnDisk.swift */; };
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501C2CEE6CB30090B18B /* VerticalCenter.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 */; }; 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 */; }; E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850262CF3B42D0090B18B /* PostDetailView.swift */; };
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; }; E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; };
E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; }; E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; };
@ -36,6 +34,16 @@
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */; }; E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */; };
E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990292D0F5A10009F8D77 /* DetailTitle.swift */; }; E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990292D0F5A10009F8D77 /* DetailTitle.swift */; };
E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902B2D0F6FC0009F8D77 /* ItemId.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 */; }; E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; }; E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; };
E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.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 */; }; E2A21C082CB17B870060935B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C072CB17B810060935B /* TagView.swift */; };
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0D2CB189D70060935B /* Color+RGB.swift */; }; E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0D2CB189D70060935B /* Color+RGB.swift */; };
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0F2CB18B390060935B /* FlowHStack.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 */; }; E2A21C202CB28ED20060935B /* MockImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C1F2CB28ED20060935B /* MockImage.swift */; };
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C272CB29B290060935B /* FeedEntryData.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 */; }; 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 = "<group>"; }; E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
E21850182CEE561B0090B18B /* PageOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageOnDisk.swift; sourceTree = "<group>"; }; E21850182CEE561B0090B18B /* PageOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageOnDisk.swift; sourceTree = "<group>"; };
E218501C2CEE6CB30090B18B /* VerticalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCenter.swift; sourceTree = "<group>"; }; E218501C2CEE6CB30090B18B /* VerticalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCenter.swift; sourceTree = "<group>"; };
E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = "<group>"; };
E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = "<group>"; }; E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = "<group>"; };
E21850242CF38BCE0090B18B /* TextEntrySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntrySheet.swift; sourceTree = "<group>"; };
E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; }; E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; };
E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = "<group>"; }; E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = "<group>"; };
E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = "<group>"; }; E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = "<group>"; };
@ -217,6 +222,16 @@
E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerPropertyView.swift; sourceTree = "<group>"; }; E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerPropertyView.swift; sourceTree = "<group>"; };
E22990292D0F5A10009F8D77 /* DetailTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTitle.swift; sourceTree = "<group>"; }; E22990292D0F5A10009F8D77 /* DetailTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTitle.swift; sourceTree = "<group>"; };
E229902B2D0F6FC0009F8D77 /* ItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemId.swift; sourceTree = "<group>"; }; E229902B2D0F6FC0009F8D77 /* ItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemId.swift; sourceTree = "<group>"; };
E229902D2D0F7278009F8D77 /* IdPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdPropertyView.swift; sourceTree = "<group>"; };
E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolPropertyView.swift; sourceTree = "<group>"; };
E22990312D0F7678009F8D77 /* DatePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePropertyView.swift; sourceTree = "<group>"; };
E22990332D0F77E4009F8D77 /* PagePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePropertyView.swift; sourceTree = "<group>"; };
E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalStringPropertyView.swift; sourceTree = "<group>"; };
E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalImagePropertyView.swift; sourceTree = "<group>"; };
E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPropertyView.swift; sourceTree = "<group>"; };
E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextFieldPropertyView.swift; sourceTree = "<group>"; };
E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringPropertyView.swift; sourceTree = "<group>"; };
E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderOnDiskPropertyView.swift; sourceTree = "<group>"; };
E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = "<group>"; }; E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = "<group>"; };
E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; }; E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = "<group>"; }; E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = "<group>"; };
@ -325,7 +340,6 @@
E2A21C072CB17B810060935B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; }; E2A21C072CB17B810060935B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
E2A21C0D2CB189D70060935B /* Color+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+RGB.swift"; sourceTree = "<group>"; }; E2A21C0D2CB189D70060935B /* Color+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+RGB.swift"; sourceTree = "<group>"; };
E2A21C0F2CB18B390060935B /* FlowHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowHStack.swift; sourceTree = "<group>"; }; E2A21C0F2CB18B390060935B /* FlowHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowHStack.swift; sourceTree = "<group>"; };
E2A21C112CB18D520060935B /* DatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerView.swift; sourceTree = "<group>"; };
E2A21C1F2CB28ED20060935B /* MockImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImage.swift; sourceTree = "<group>"; }; E2A21C1F2CB28ED20060935B /* MockImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImage.swift; sourceTree = "<group>"; };
E2A21C272CB29B290060935B /* FeedEntryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntryData.swift; sourceTree = "<group>"; }; E2A21C272CB29B290060935B /* FeedEntryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntryData.swift; sourceTree = "<group>"; };
E2A21C292CB2AA4C0060935B /* Post+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+Mock.swift"; sourceTree = "<group>"; }; E2A21C292CB2AA4C0060935B /* Post+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+Mock.swift"; sourceTree = "<group>"; };
@ -589,9 +603,19 @@
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */, E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */,
E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */, E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */,
E2A21C0F2CB18B390060935B /* FlowHStack.swift */, E2A21C0F2CB18B390060935B /* FlowHStack.swift */,
E22990292D0F5A10009F8D77 /* DetailTitle.swift */,
E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */,
E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */, E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */,
E22990252D0F5822009F8D77 /* FilePropertyView.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; path = Generic;
sourceTree = "<group>"; sourceTree = "<group>";
@ -707,11 +731,8 @@
E21850262CF3B42D0090B18B /* PostDetailView.swift */, E21850262CF3B42D0090B18B /* PostDetailView.swift */,
E29D313E2D04822C0051B7F4 /* AddPostView.swift */, E29D313E2D04822C0051B7F4 /* AddPostView.swift */,
E21850222CF10C840090B18B /* TagSelectionView.swift */, E21850222CF10C840090B18B /* TagSelectionView.swift */,
E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */,
E21850082CEE01BF0090B18B /* PagePickerView.swift */, E21850082CEE01BF0090B18B /* PagePickerView.swift */,
E2A21C112CB18D520060935B /* DatePickerView.swift */,
E2A21C072CB17B810060935B /* TagView.swift */, E2A21C072CB17B810060935B /* TagView.swift */,
E21850242CF38BCE0090B18B /* TextEntrySheet.swift */,
E29D31312D03B5610051B7F4 /* LocalizedPostDetailView.swift */, E29D31312D03B5610051B7F4 /* LocalizedPostDetailView.swift */,
E218502C2CF791440090B18B /* PostImagesView.swift */, E218502C2CF791440090B18B /* PostImagesView.swift */,
); );
@ -888,10 +909,10 @@
E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */, E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */,
E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */, E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */,
E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */,
E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */,
E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */, E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */,
E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */, E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */,
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */, E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */,
E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */,
E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */, E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */,
E21850172CEE55FC0090B18B /* FileType.swift in Sources */, E21850172CEE55FC0090B18B /* FileType.swift in Sources */,
E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */, E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */,
@ -913,6 +934,7 @@
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */, E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */,
E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */, E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */,
E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */, E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */,
E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */,
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */, E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */,
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */, E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */, E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */,
@ -934,6 +956,7 @@
E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */, E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */,
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */,
E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */,
E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */, E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */,
E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */, E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */,
@ -952,7 +975,7 @@
E29D315D2D06D6EA0051B7F4 /* TextFileType.swift in Sources */, E29D315D2D06D6EA0051B7F4 /* TextFileType.swift in Sources */,
E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */, E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */,
E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */, E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */,
E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */, E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */,
E21850332CFAFA2F0090B18B /* Settings.swift in Sources */, E21850332CFAFA2F0090B18B /* Settings.swift in Sources */,
E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */, E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */,
E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */, E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */,
@ -967,12 +990,14 @@
E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */, E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */,
E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */, E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */,
E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */,
E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */,
E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */,
E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */,
E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */, E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */,
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */, E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */,
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */,
E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */, E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */,
E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */, E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */,
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
@ -989,11 +1014,11 @@
E29D31472D04892E0051B7F4 /* FileListView.swift in Sources */, E29D31472D04892E0051B7F4 /* FileListView.swift in Sources */,
E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */, E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */,
E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */, E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */,
E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */,
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */, E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */,
E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */, E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */,
E2DD04742C276F31003BFF1F /* MainView.swift in Sources */, E2DD04742C276F31003BFF1F /* MainView.swift in Sources */,
E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */,
E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */, E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */,
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */, E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
E29D319F2D0C46310051B7F4 /* PageIssueChecker.swift in Sources */, E29D319F2D0C46310051B7F4 /* PageIssueChecker.swift in Sources */,
@ -1028,10 +1053,13 @@
E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */, E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */,
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */, E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */,
E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */, E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */,
E229903A2D0F7E48009F8D77 /* GenericPropertyView.swift in Sources */,
E29D31AA2D0CEE3F0051B7F4 /* AudioPlayer.swift in Sources */, E29D31AA2D0CEE3F0051B7F4 /* AudioPlayer.swift in Sources */,
E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */, E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */,
E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */, E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */,
E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */, E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */,
E22990362D0F79D2009F8D77 /* OptionalStringPropertyView.swift in Sources */,
E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */,
E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */, E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */,
E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */, E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */,
E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */, E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */,

View File

@ -8,13 +8,6 @@ final class FeedPageGenerator {
self.content = content 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 swiperIncludes: [HeaderElement] {
var result = [HeaderElement]() var result = [HeaderElement]()
if let swiperCss = content.settings.posts.swiperCssFile { if let swiperCss = content.settings.posts.swiperCssFile {
@ -57,7 +50,7 @@ final class FeedPageGenerator {
language: language, language: language,
title: title, title: title,
description: description, description: description,
links: navigationBar(in: language), links: content.navigationBar(in: language),
headers: headers, headers: headers,
additionalFooter: footer) { content in additionalFooter: footer) { content in
if showTitle { if showTitle {

View File

@ -22,13 +22,6 @@ final class LocalizedWebsiteGenerator {
private let imageGenerator: ImageGenerator 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) { init(content: Content, language: ContentLanguage) {
self.language = language self.language = language
self.content = content self.content = content
@ -61,7 +54,6 @@ final class LocalizedWebsiteGenerator {
language: language, language: language,
content: content, content: content,
imageGenerator: imageGenerator, imageGenerator: imageGenerator,
navigationBarLinks: navigationBarLinks,
showTitle: false, showTitle: false,
pageTitle: localizedPostSettings.title, pageTitle: localizedPostSettings.title,
pageDescription: localizedPostSettings.description, pageDescription: localizedPostSettings.description,
@ -82,7 +74,6 @@ final class LocalizedWebsiteGenerator {
language: language, language: language,
content: content, content: content,
imageGenerator: imageGenerator, imageGenerator: imageGenerator,
navigationBarLinks: navigationBarLinks,
showTitle: true, showTitle: true,
pageTitle: localized.name, pageTitle: localized.name,
pageDescription: localized.description ?? "", pageDescription: localized.description ?? "",
@ -115,8 +106,7 @@ final class LocalizedWebsiteGenerator {
} }
let pageGenerator = PageGenerator( let pageGenerator = PageGenerator(
content: content, content: content,
imageGenerator: imageGenerator, imageGenerator: imageGenerator)
navigationBarLinks: navigationBarLinks)
let content: String let content: String
let results: PageGenerationResults let results: PageGenerationResults

View File

@ -4,12 +4,9 @@ final class PageGenerator {
private let imageGenerator: ImageGenerator private let imageGenerator: ImageGenerator
private let navigationBarLinks: [NavigationBar.Link] init(content: Content, imageGenerator: ImageGenerator) {
init(content: Content, imageGenerator: ImageGenerator, navigationBarLinks: [NavigationBar.Link]) {
self.content = content self.content = content
self.imageGenerator = imageGenerator self.imageGenerator = imageGenerator
self.navigationBarLinks = navigationBarLinks
} }
func makeHeaders(requiredItems: [HeaderFile]) -> [HeaderElement] { func makeHeaders(requiredItems: [HeaderFile]) -> [HeaderElement] {
@ -51,7 +48,7 @@ final class PageGenerator {
tags: tags, tags: tags,
linkTitle: localized.linkPreviewTitle ?? localized.title, linkTitle: localized.linkPreviewTitle ?? localized.title,
description: localized.linkPreviewDescription ?? "", description: localized.linkPreviewDescription ?? "",
navigationBarLinks: navigationBarLinks, navigationBarLinks: content.navigationBar(in: language),
pageContent: pageContent, pageContent: pageContent,
headers: headers, headers: headers,
footers: contentGenerator.results.requiredFooters.sorted(), footers: contentGenerator.results.requiredFooters.sorted(),

View File

@ -8,9 +8,6 @@ final class PostListPageGenerator {
private let imageGenerator: ImageGenerator private let imageGenerator: ImageGenerator
#warning("Get from settings")
private let navigationBarLinks: [NavigationBar.Link]
private let showTitle: Bool private let showTitle: Bool
private let pageTitle: String private let pageTitle: String
@ -20,11 +17,10 @@ final class PostListPageGenerator {
/// The url of the page, excluding the extension /// The url of the page, excluding the extension
private let pageUrlPrefix: String 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.language = language
self.content = content self.content = content
self.imageGenerator = imageGenerator self.imageGenerator = imageGenerator
self.navigationBarLinks = navigationBarLinks
self.showTitle = showTitle self.showTitle = showTitle
self.pageTitle = pageTitle self.pageTitle = pageTitle
self.pageDescription = pageDescription self.pageDescription = pageDescription
@ -50,14 +46,14 @@ final class PostListPageGenerator {
let startIndex = (pageIndex - 1) * postsPerPage let startIndex = (pageIndex - 1) * postsPerPage
let endIndex = min(pageIndex * postsPerPage, totalCount) let endIndex = min(pageIndex * postsPerPage, totalCount)
let postsOnPage = posts[startIndex..<endIndex] let postsOnPage = posts[startIndex..<endIndex]
guard createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage, bar: navigationBarLinks) else { guard createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage) else {
return false return false
} }
} }
return true return true
} }
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>, bar: [NavigationBar.Link]) -> Bool { private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>) -> Bool {
let posts: [FeedEntryData] = posts.map { post in let posts: [FeedEntryData] = posts.map { post in
let localized: LocalizedPost = post.localized(in: language) let localized: LocalizedPost = post.localized(in: language)
@ -78,7 +74,7 @@ final class PostListPageGenerator {
textAboveTitle: post.dateText(in: language), textAboveTitle: post.dateText(in: language),
link: linkUrl, link: linkUrl,
tags: tags, tags: tags,
text: [localized.content], // TODO: Convert from markdown to html text: localized.text.components(separatedBy: "\n"),
images: localized.images.map(createImageSet)) images: localized.images.map(createImageSet))
} }

View File

@ -1,8 +1,6 @@
import SwiftUI import SwiftUI
import SFSafeSymbols import SFSafeSymbols
#warning("Allow selection of pages as navigation bar items")
#warning("Show all warnings on page content") #warning("Show all warnings on page content")
#warning("Button to delete file") #warning("Button to delete file")
#warning("Fix podcast") #warning("Fix podcast")
@ -14,6 +12,10 @@ import SFSafeSymbols
#warning("Calculate file sizes") #warning("Calculate file sizes")
#warning("Specify image aspect ratio to prevent page jumps") #warning("Specify image aspect ratio to prevent page jumps")
#warning("Add version and source url properties to file resources") #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 @main
struct MainView: App { struct MainView: App {

View File

@ -35,4 +35,11 @@ extension Content {
func tag(_ tagId: String) -> Tag? { func tag(_ tagId: String) -> Tag? {
tags.first { $0.id == tagId } 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))
}
}
} }

View File

@ -4,6 +4,7 @@ extension Content {
private func convert(_ tag: LocalizedTagFile, images: [String : FileResource]) -> LocalizedTag { private func convert(_ tag: LocalizedTagFile, images: [String : FileResource]) -> LocalizedTag {
LocalizedTag( LocalizedTag(
content: self,
urlComponent: tag.urlComponent, urlComponent: tag.urlComponent,
name: tag.name, name: tag.name,
subtitle: tag.subtitle, subtitle: tag.subtitle,
@ -14,8 +15,9 @@ extension Content {
private func convert(_ post: LocalizedPostFile, images: [String : FileResource]) -> LocalizedPost { private func convert(_ post: LocalizedPostFile, images: [String : FileResource]) -> LocalizedPost {
LocalizedPost( LocalizedPost(
content: self,
title: post.title, title: post.title,
content: post.content, text: post.content,
lastModified: post.lastModifiedDate, lastModified: post.lastModifiedDate,
images: post.images.compactMap { images[$0] }, images: post.images.compactMap { images[$0] },
linkPreviewImage: post.linkPreviewImage.map { images[$0] }, linkPreviewImage: post.linkPreviewImage.map { images[$0] },
@ -107,8 +109,8 @@ extension Content {
let tagOverview = tagOverviewData.map { file in let tagOverview = tagOverviewData.map { file in
TagOverviewPage( TagOverviewPage(
content: self, content: self,
german: .init(file: file.german, image: file.german.linkPreviewImage.map { files[$0] }), german: .init(content: self, file: file.german, image: file.german.linkPreviewImage.map { files[$0] }),
english: .init(file: file.english, image: file.english.linkPreviewImage.map { files[$0] })) english: .init(content: self, file: file.english, image: file.english.linkPreviewImage.map { files[$0] }))
} }
self.tags = tags.values.sorted() self.tags = tags.values.sorted()

View File

@ -95,7 +95,7 @@ private extension LocalizedPost {
var postFile: LocalizedPostFile { var postFile: LocalizedPostFile {
.init(images: images.map { $0.id }, .init(images: images.map { $0.id },
title: title.nonEmpty, title: title.nonEmpty,
content: content, content: text,
lastModifiedDate: lastModified, lastModifiedDate: lastModified,
linkPreviewImage: linkPreviewImage?.id, linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle, linkPreviewTitle: linkPreviewTitle,

View File

@ -18,6 +18,10 @@ extension Content {
!posts.contains { $0.id == id } !posts.contains { $0.id == id }
} }
func isNewIdForFile(_ id: String) -> Bool {
!files.contains { $0.id == id }
}
func isValidIdForTagOrPageOrPost(_ id: String) -> Bool { func isValidIdForTagOrPageOrPost(_ id: String) -> Bool {
id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil
} }
@ -26,6 +30,13 @@ extension Content {
id.rangeOfCharacter(from: Content.disallowedCharactersInFileIds) == nil 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 { func containsTag(withUrlComponent urlComponent: String) -> Bool {
(tagOverview?.contains(urlComponent: urlComponent) ?? false) || (tagOverview?.contains(urlComponent: urlComponent) ?? false) ||
tags.contains { $0.contains(urlComponent: urlComponent) } tags.contains { $0.contains(urlComponent: urlComponent) }

View File

@ -111,6 +111,13 @@ final class FileResource: Item {
// MARK: File // MARK: File
func isValid(id: String) -> Bool {
!id.isEmpty &&
content.isValidIdForFile(id) &&
content.isNewIdForFile(id)
}
@discardableResult
func update(id newId: String) -> Bool { func update(id newId: String) -> Bool {
guard !isExternallyStored else { guard !isExternallyStored else {
id = newId id = newId

View File

@ -34,11 +34,11 @@ final class TagOverviewPage: Item {
} }
private func internalPath(for language: ContentLanguage) -> String { 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 { func contains(urlComponent: String) -> Bool {
english.urlString == urlComponent || german.urlString == urlComponent english.urlComponent == urlComponent || german.urlComponent == urlComponent
} }
var file: TagOverviewFile { var file: TagOverviewFile {
@ -53,16 +53,16 @@ extension TagOverviewPage: LocalizedItem {
final class LocalizedTagOverviewPage: ObservableObject { final class LocalizedTagOverviewPage: ObservableObject {
unowned let content: Content
@Published @Published
var title: String var title: String
/** /**
The string to use when creating the url for the page. The string to use when creating the url for the page.
Defaults to ``id`` if unset.
*/ */
@Published @Published
var urlString: String var urlComponent: String
@Published @Published
var linkPreviewImage: FileResource? var linkPreviewImage: FileResource?
@ -73,27 +73,35 @@ final class LocalizedTagOverviewPage: ObservableObject {
@Published @Published
var linkPreviewDescription: String? 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.title = title
self.urlString = urlString self.urlComponent = urlString
self.linkPreviewImage = linkPreviewImage self.linkPreviewImage = linkPreviewImage
self.linkPreviewTitle = linkPreviewTitle self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription self.linkPreviewDescription = linkPreviewDescription
} }
init(file: LocalizedTagOverviewFile, image: FileResource?) { init(content: Content, file: LocalizedTagOverviewFile, image: FileResource?) {
self.content = content
self.title = file.title self.title = file.title
self.urlString = file.url self.urlComponent = file.url
self.linkPreviewImage = image self.linkPreviewImage = image
self.linkPreviewTitle = file.linkPreviewTitle self.linkPreviewTitle = file.linkPreviewTitle
self.linkPreviewDescription = file.linkPreviewDescription self.linkPreviewDescription = file.linkPreviewDescription
} }
var file: LocalizedTagOverviewFile { var file: LocalizedTagOverviewFile {
.init(url: urlString, .init(url: urlComponent,
title: title, title: title,
linkPreviewImage: linkPreviewImage?.id, linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle, linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription) linkPreviewDescription: linkPreviewDescription)
} }
func isValid(urlComponent: String) -> Bool {
!urlComponent.isEmpty &&
content.isValidIdForTagOrPageOrPost(urlComponent) &&
!content.containsTag(withUrlComponent: urlComponent)
}
} }

View File

@ -89,4 +89,9 @@ final class LocalizedPage: ObservableObject {
self.linkPreviewTitle = linkPreviewTitle self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription self.linkPreviewDescription = linkPreviewDescription
} }
func isValid(urlComponent: String) -> Bool {
content.isValidIdForTagOrPageOrPost(urlComponent) &&
!content.containsPage(withUrlComponent: urlComponent)
}
} }

View File

@ -3,11 +3,13 @@ import SwiftUI
final class LocalizedPost: ObservableObject { final class LocalizedPost: ObservableObject {
unowned let content: Content
@Published @Published
var title: String var title: String
@Published @Published
var content: String var text: String
@Published @Published
var lastModified: Date? var lastModified: Date?
@ -24,15 +26,17 @@ final class LocalizedPost: ObservableObject {
@Published @Published
var linkPreviewDescription: String? var linkPreviewDescription: String?
init(title: String? = nil, init(content: Content,
content: String, title: String? = nil,
text: String,
lastModified: Date? = nil, lastModified: Date? = nil,
images: [FileResource] = [], images: [FileResource] = [],
linkPreviewImage: FileResource? = nil, linkPreviewImage: FileResource? = nil,
linkPreviewTitle: String? = nil, linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil) { linkPreviewDescription: String? = nil) {
self.title = title ?? ""
self.content = content self.content = content
self.title = title ?? ""
self.text = text
self.lastModified = lastModified self.lastModified = lastModified
self.images = images self.images = images
self.linkPreviewImage = linkPreviewImage self.linkPreviewImage = linkPreviewImage

View File

@ -2,6 +2,8 @@ import Foundation
final class LocalizedTag: ObservableObject { final class LocalizedTag: ObservableObject {
unowned let content: Content
@Published @Published
var urlComponent: String var urlComponent: String
@ -22,12 +24,14 @@ final class LocalizedTag: ObservableObject {
/// The original url in the previous site layout /// The original url in the previous site layout
let originalUrl: String? let originalUrl: String?
init(urlComponent: String, init(content: Content,
urlComponent: String,
name: String, name: String,
subtitle: String? = nil, subtitle: String? = nil,
description: String? = nil, description: String? = nil,
thumbnail: FileResource? = nil, thumbnail: FileResource? = nil,
originalUrl: String? = nil) { originalUrl: String? = nil) {
self.content = content
self.urlComponent = urlComponent self.urlComponent = urlComponent
self.name = name self.name = name
self.subtitle = subtitle self.subtitle = subtitle
@ -35,4 +39,9 @@ final class LocalizedTag: ObservableObject {
self.linkPreviewImage = thumbnail self.linkPreviewImage = thumbnail
self.originalUrl = originalUrl self.originalUrl = originalUrl
} }
func isValid(urlComponent: String) -> Bool {
content.isValidIdForTagOrPageOrPost(urlComponent) &&
!content.containsTag(withUrlComponent: urlComponent)
}
} }

View File

@ -66,9 +66,16 @@ final class Page: Item {
super.init(content: content, id: id) 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 { func update(id newId: String) -> Bool {
guard content.storage.move(page: id, to: newId) else { 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 return false
} }
id = newId id = newId

View File

@ -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 { func update(id newId: String) -> Bool {
do { do {
try content.storage.move(post: id, to: newId) try content.storage.move(post: id, to: newId)

View File

@ -13,9 +13,9 @@ final class Tag: Item {
override init(content: Content, id: String) { override init(content: Content, id: String) {
self.isVisible = true 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 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) super.init(content: content, id: id)
} }

View File

@ -9,8 +9,10 @@ extension Post {
startDate: .now, startDate: .now,
endDate: nil, endDate: nil,
tags: [], tags: [],
german: .init(content: "Text"), german: .init(content: .mock,
english: .init(content: "Text"), text: "Text"),
english: .init(content: .mock,
text: "Text"),
linkedPage: nil) linkedPage: nil)
} }
@ -23,8 +25,14 @@ extension Post {
startDate: .now, startDate: .now,
endDate: nil, endDate: nil,
tags: [.nature, .sports, .hiking], 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."), german: .init(
english: .init(title: "The title", content: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height") 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 { extension LocalizedPost {
static let german = LocalizedPost( static let german = LocalizedPost(
content: .mock,
title: "Ein langer Titel", 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) images: MockImage.images)
static let english = LocalizedPost( static let english = LocalizedPost(
content: .mock,
title: "A longer title", 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) images: MockImage.images)
} }

View File

@ -11,35 +11,36 @@ extension Tag {
static let nature = Tag( static let nature = Tag(
content: .mock, content: .mock,
id: "nature", id: "nature",
german: .init(urlComponent: "natur", name: "Natur"), german: .init(content: .mock, urlComponent: "natur", name: "Natur"),
english: .init(urlComponent: "nature", name: "Nature") english: .init(content: .mock, urlComponent: "nature", name: "Nature")
) )
static let sports = Tag( static let sports = Tag(
content: .mock, content: .mock,
id: "sports", id: "sports",
german: .init(urlComponent: "sport", name: "Sport"), german: .init(content: .mock, urlComponent: "sport", name: "Sport"),
english: .init(urlComponent: "sports", name: "Sports") english: .init(content: .mock, urlComponent: "sports", name: "Sports")
) )
static let hiking = Tag( static let hiking = Tag(
content: .mock, content: .mock,
id: "hiking", id: "hiking",
german: .init(urlComponent: "wandern", name: "Wandern"), german: .init(content: .mock, urlComponent: "wandern", name: "Wandern"),
english: .init(urlComponent: "hiking", name: "Hiking") english: .init(content: .mock, urlComponent: "hiking", name: "Hiking")
) )
static let mountains = Tag( static let mountains = Tag(
content: .mock, content: .mock,
id: "mountains", id: "mountains",
german: .init(urlComponent: "berge", name: "Berge"), german: .init(content: .mock, urlComponent: "berge", name: "Berge"),
english: .init(urlComponent: "mountains", name: "Mountains") english: .init(content: .mock, urlComponent: "mountains", name: "Mountains")
) )
} }
extension LocalizedTag { extension LocalizedTag {
static let english = LocalizedTag( static let english = LocalizedTag(
content: .mock,
urlComponent: "electronics", urlComponent: "electronics",
name: "Electronics", name: "Electronics",
subtitle: "Projects with electronics", subtitle: "Projects with electronics",
@ -48,6 +49,7 @@ extension LocalizedTag {
originalUrl: "projects/electronics") originalUrl: "projects/electronics")
static let german = LocalizedTag( static let german = LocalizedTag(
content: .mock,
urlComponent: "elektronik", urlComponent: "elektronik",
name: "Elektronik", name: "Elektronik",
subtitle: "Projekte mit Elektronik", subtitle: "Projekte mit Elektronik",

View File

@ -435,12 +435,15 @@ final class Storage {
// MARK: Folder access // MARK: Folder access
func save(folderUrl url: URL, in bookmark: SecurityScopeBookmark) { @discardableResult
func save(folderUrl url: URL, in bookmark: SecurityScopeBookmark) -> Bool {
do { do {
let bookmarkData = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil) let bookmarkData = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
UserDefaults.standard.set(bookmarkData, forKey: bookmark.rawValue) UserDefaults.standard.set(bookmarkData, forKey: bookmark.rawValue)
return true
} catch { } catch {
print("Failed to create security-scoped bookmark: \(error)") print("Failed to create security-scoped bookmark: \(error)")
return false
} }
} }

View File

@ -5,44 +5,29 @@ struct FileDetailView: View {
@ObservedObject @ObservedObject
var file: FileResource 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 { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("File Name") DetailTitle(
.font(.headline) title: "File",
HStack { text: "A file that can be used in a post or page")
TextField("", text: $newId)
.textFieldStyle(.roundedBorder) IdPropertyView(
Button(action: setNewId) { id: $file.id,
Text("Update") title: "Name",
} footer: "The unique name of the file, which is also used to reference it in posts and pages.",
.disabled(newId.isEmpty || containsInvalidCharacters || idExists) validation: file.isValid,
} update: { file.update(id: $0) })
Text("German Description")
.font(.headline) StringPropertyView(
TextField("", text: $file.german) title: "German Description",
.textFieldStyle(.roundedBorder) text: $file.german,
Text("English Description") footer: "The description for the file in German. Descriptions are used for images and to explain the content of a file.")
.font(.headline)
TextField("", text: $file.english) StringPropertyView(
.textFieldStyle(.roundedBorder) 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 { if file.type.isImage {
Text("Image size") Text("Image size")
.font(.headline) .font(.headline)
@ -53,12 +38,6 @@ struct FileDetailView: View {
Spacer() Spacer()
}.padding() }.padding()
} }
private func setNewId() {
if !file.update(id: newId) {
newId = file.id
}
}
} }
extension FileDetailView: MainContentView { extension FileDetailView: MainContentView {

View File

@ -1,6 +1,6 @@
import SwiftUI import SwiftUI
private enum FileFilterType: String, Hashable, CaseIterable, Identifiable { enum FileFilterType: String, Hashable, CaseIterable, Identifiable {
case images case images
case text case text
case videos case videos
@ -38,11 +38,19 @@ struct FileListView: View {
var selectedFile: FileResource? var selectedFile: FileResource?
@State @State
private var selectedFileType: FileFilterType = .images private var selectedFileType: FileFilterType
@State @State
private var searchString = "" private var searchString = ""
let allowedType: FileFilterType?
init(selectedFile: Binding<FileResource?>, allowedType: FileFilterType? = nil) {
self._selectedFile = selectedFile
self.allowedType = allowedType
self.selectedFileType = allowedType ?? .images
}
var filesBySelectedType: [FileResource] { var filesBySelectedType: [FileResource] {
content.files.filter { selectedFileType.matches($0.type) } content.files.filter { selectedFileType.matches($0.type) }
} }
@ -63,6 +71,7 @@ struct FileListView: View {
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
.padding(.trailing, 7) .padding(.trailing, 7)
.disabled(allowedType != nil)
TextField("", text: $searchString, prompt: Text("Search")) TextField("", text: $searchString, prompt: Text("Search"))
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.padding(.horizontal, 8) .padding(.horizontal, 8)

View File

@ -2,15 +2,18 @@ import SwiftUI
struct FileSelectionView: View { struct FileSelectionView: View {
@Binding
private var selectedFile: FileResource?
@Environment(\.dismiss) @Environment(\.dismiss)
private var dismiss private var dismiss
init(selectedFile: Binding<FileResource?>) { @Binding
private var selectedFile: FileResource?
let allowedType: FileFilterType?
init(selectedFile: Binding<FileResource?>, allowedType: FileFilterType? = nil) {
self._selectedFile = selectedFile self._selectedFile = selectedFile
self.newSelection = selectedFile.wrappedValue self.newSelection = selectedFile.wrappedValue
self.allowedType = allowedType
} }
@State @State
@ -18,7 +21,7 @@ struct FileSelectionView: View {
var body: some View { var body: some View {
VStack { VStack {
FileListView(selectedFile: $newSelection) FileListView(selectedFile: $newSelection, allowedType: allowedType)
.frame(minHeight: 500, idealHeight: 600) .frame(minHeight: 500, idealHeight: 600)
HStack { HStack {
Button("Cancel") { Button("Cancel") {

View File

@ -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)
}
}
}

View File

@ -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()
}
}
}
}

View File

@ -2,9 +2,9 @@ import SwiftUI
struct FilePropertyView: View { struct FilePropertyView: View {
let title: String let title: LocalizedStringKey
let description: String let footer: LocalizedStringKey
@Binding @Binding
var selectedFile: FileResource? var selectedFile: FileResource?
@ -13,9 +13,7 @@ struct FilePropertyView: View {
private var showFileSelectionSheet = false private var showFileSelectionSheet = false
var body: some View { var body: some View {
VStack(alignment: .leading) { GenericPropertyView(title: title, footer: footer) {
Text(title)
.font(.headline)
HStack { HStack {
Text(selectedFile?.id ?? "No file selected") Text(selectedFile?.id ?? "No file selected")
Spacer() Spacer()
@ -23,9 +21,6 @@ struct FilePropertyView: View {
showFileSelectionSheet = true showFileSelectionSheet = true
} }
} }
Text(description)
.foregroundStyle(.secondary)
.padding(.bottom)
} }
.sheet(isPresented: $showFileSelectionSheet) { .sheet(isPresented: $showFileSelectionSheet) {
FileSelectionView(selectedFile: $selectedFile) FileSelectionView(selectedFile: $selectedFile)

View File

@ -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<String>, 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
}
}

View File

@ -0,0 +1,27 @@
import SwiftUI
struct GenericPropertyView<Content>: 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)
}
}
}

View File

@ -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<String>,
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
}
}
}

View File

@ -2,22 +2,17 @@ import SwiftUI
struct IntegerPropertyView: View { struct IntegerPropertyView: View {
let title: LocalizedStringKey
@Binding @Binding
var value: Int var value: Int
let title: String let footer: LocalizedStringKey
let footer: String
var body: some View { var body: some View {
VStack(alignment: .leading) { GenericPropertyView(title: title, footer: footer) {
Text(title)
.font(.headline)
IntegerField("", number: $value) IntegerField("", number: $value)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
Text(footer)
.foregroundStyle(.secondary)
.padding(.bottom)
} }
} }
} }

View File

@ -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)
}
}
}

View File

@ -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<String?>, 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)
}
}
}

View File

@ -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<String?>, 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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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<String>, 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)
}
}
}

View File

@ -3,101 +3,41 @@ import SFSafeSymbols
struct LocalizedPageDetailView: View { struct LocalizedPageDetailView: View {
let isExternalPage: Bool
@ObservedObject @ObservedObject
private var page: LocalizedPage 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 body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack { IdPropertyView(
Text("Page URL String") id: $page.urlString,
.font(.headline) title: "Page URL String",
TextField("", text: $newUrlString) footer: "The url component to use for the link to the page",
.textFieldStyle(.roundedBorder) validation: page.isValid,
Button("Update", action: setNewId) update: { page.urlString = $0 })
.disabled(newUrlString.isEmpty || containsInvalidCharacters || idExists) .disabled(isExternalPage)
}
.padding(.bottom)
Text("Link Preview Title") OptionalStringPropertyView(
.font(.headline) title: "Preview Title",
OptionalTextField("", text: $page.linkPreviewTitle, text: $page.linkPreviewTitle,
prompt: page.title) prompt: page.title,
.textFieldStyle(.roundedBorder) footer: "The title to use for the page when linking to it")
.padding(.bottom)
HStack { OptionalImagePropertyView(
Text("Link Preview Image") title: "Preview Image",
.font(.headline) selectedImage: $page.linkPreviewImage,
IconButton(symbol: .squareAndPencilCircleFill, footer: "The image to show for previews of this page")
size: 22,
color: .blue) {
showImagePicker = true
}
IconButton(symbol: .trashCircleFill, OptionalTextFieldPropertyView(
size: 22, title: "Preview Description",
color: .red) { text: $page.linkPreviewDescription,
page.linkPreviewImage = nil footer: "The description to show in previews of the page")
}.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)
} }
.sheet(isPresented: $showImagePicker) {
ImagePickerView(showImagePicker: $showImagePicker) { image in
page.linkPreviewImage = image
}
}
}
private func setNewId() {
page.urlString = newUrlString
} }
} }
#Preview { #Preview {
LocalizedPageDetailView(page: .english) LocalizedPageDetailView(isExternalPage: false, page: .english)
.environmentObject(Content.mock) .environmentObject(Content.mock)
} }

View File

@ -15,30 +15,19 @@ struct PageDetailView: View {
@State @State
private var isGeneratingWebsite = false private var isGeneratingWebsite = false
@State
private var newId: String
@State @State
private var didGenerateWebsite: Bool? private var didGenerateWebsite: Bool?
init(page: Page) { init(page: Page) {
self.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 { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
DetailTitle(
title: "Page",
text: "A page contains longer content")
HStack { HStack {
Button(action: generate) { Button(action: generate) {
Text("Generate") Text("Generate")
@ -54,62 +43,40 @@ struct PageDetailView: View {
} }
} }
} }
HStack { IdPropertyView(
TextField("", text: $newId) id: $page.id,
.textFieldStyle(.roundedBorder) footer: "The page id is used to link to it internally.",
Button("Update", action: setNewId) validation: page.isValid,
.disabled(newId.isEmpty || containsInvalidCharacters || idExists) update: { page.update(id: $0) })
}
.padding(.bottom)
Text("External url") OptionalStringPropertyView(
.font(.headline) title: "External url",
OptionalTextField("", text: $page.externalLink, text: $page.externalLink,
prompt: "External url") 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")
.textFieldStyle(.roundedBorder)
.padding(.bottom)
HStack { BoolPropertyView(
Text("Draft") title: "Draft",
.font(.headline) value: $page.isDraft,
Spacer() footer: "Indicate a page as a draft to hide it from the website")
Toggle("", isOn: $page.isDraft) .disabled(page.isExternalUrl)
.toggleStyle(.switch)
}
.padding(.bottom)
HStack { DatePropertyView(
Text("Start") title: "Start date",
.font(.headline) value: $page.startDate,
Spacer() footer: "The date when the page content started")
DatePicker("", selection: $page.startDate, displayedComponents: .date) .disabled(page.isExternalUrl)
.datePickerStyle(.compact)
.padding(.bottom)
}
HStack(alignment: .firstTextBaseline) { OptionalDatePropertyView(
Text("Has end date") title: "End date",
.font(.headline) isEnabled: $page.hasEndDate,
Spacer() date: $page.endDate,
Toggle("", isOn: $page.hasEndDate) footer: "The date when the page content ended")
.toggleStyle(.switch) .disabled(page.isExternalUrl)
.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)
LocalizedPageDetailView(
isExternalPage: page.isExternalUrl,
page: page.localized(in: language))
.id(page.id + language.rawValue)
} }
.padding() .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 { extension PageDetailView: MainContentView {

View File

@ -74,8 +74,8 @@ struct AddPostView: View {
startDate: .now, startDate: .now,
endDate: nil, endDate: nil,
tags: [], tags: [],
german: .init(title: "Titel", content: "Text"), german: .init(content: content, title: "Titel", text: "Text"),
english: .init(title: "Title", content: "Text")) english: .init(content: content, title: "Title", text: "Text"))
content.posts.insert(post, at: 0) content.posts.insert(post, at: 0)
selectedPost = post selectedPost = post
dismissSheet() dismissSheet()

View File

@ -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))
}

View File

@ -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<Bool>, 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)
}

View File

@ -3,64 +3,25 @@ import SwiftUI
struct LocalizedPostDetailView: View { struct LocalizedPostDetailView: View {
@ObservedObject @ObservedObject
private var item: LocalizedPost var post: LocalizedPost
init(post: LocalizedPost, showImagePicker: Bool = false) {
self.item = post
self.showImagePicker = showImagePicker
}
@State
private var showImagePicker = false
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Link Preview Title") OptionalStringPropertyView(
.font(.headline) title: "Preview Title",
OptionalTextField("", text: $item.linkPreviewTitle, text: $post.linkPreviewTitle,
prompt: item.title) prompt: post.title,
.textFieldStyle(.roundedBorder) footer: "The title to use for the post when linking to it")
.padding(.bottom)
HStack { OptionalImagePropertyView(
Text("Link Preview Image") title: "Preview Image",
.font(.headline) selectedImage: $post.linkPreviewImage,
IconButton(symbol: .squareAndPencilCircleFill, footer: "The image to show for previews of this post")
size: 22,
color: .blue) {
showImagePicker = true
}.padding(.bottom)
IconButton(symbol: .trashCircleFill, OptionalTextFieldPropertyView(
size: 22, title: "Preview Description",
color: .red) { text: $post.linkPreviewDescription,
item.linkPreviewImage = nil footer: "The description to show in previews of the post")
}.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
}
} }
} }
} }

View File

@ -2,21 +2,21 @@ import SwiftUI
struct PagePickerView: View { struct PagePickerView: View {
@Binding var showPagePicker: Bool
@Binding var selectedPage: Page?
@EnvironmentObject @EnvironmentObject
private var content: Content private var content: Content
@Environment(\.language) @Environment(\.language)
private var language private var language
@Environment(\.dismiss)
var dismiss
@Binding var selectedPage: Page?
@State @State
private var newSelection: Page? private var newSelection: Page?
init(showPagePicker: Binding<Bool>, selectedPage: Binding<Page?>) { init(selectedPage: Binding<Page?>) {
self._showPagePicker = showPagePicker
self._selectedPage = selectedPage self._selectedPage = selectedPage
self.newSelection = selectedPage.wrappedValue self.newSelection = selectedPage.wrappedValue
// TODO: Fix assignment not working // TODO: Fix assignment not working
@ -35,17 +35,17 @@ struct PagePickerView: View {
Button("Use selection") { Button("Use selection") {
DispatchQueue.main.async { DispatchQueue.main.async {
self.selectedPage = self.newSelection self.selectedPage = self.newSelection
dismiss()
} }
showPagePicker = false
} }
Button("Remove page", role: .destructive) { Button("Remove page", role: .destructive) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.selectedPage = nil self.selectedPage = nil
dismiss()
} }
showPagePicker = false
} }
Button("Cancel", role: .cancel) { Button("Cancel", role: .cancel) {
showPagePicker = false dismiss()
} }
} }
} }
@ -55,7 +55,6 @@ struct PagePickerView: View {
} }
#Preview { #Preview {
PagePickerView(showPagePicker: .constant(true), PagePickerView(selectedPage: .constant(nil))
selectedPage: .constant(nil)) .environmentObject(Content.mock)
.environmentObject(Content.mock)
} }

View File

@ -56,7 +56,7 @@ private struct LocalizedContentEditor: View {
} }
var body: some View { var body: some View {
TextEditor(text: $post.content) TextEditor(text: $post.text)
.font(.body) .font(.body)
.frame(minHeight: 150) .frame(minHeight: 150)
.textEditorStyle(.plain) .textEditorStyle(.plain)

View File

@ -36,109 +36,51 @@ struct PostDetailView: View {
@ObservedObject @ObservedObject
private var post: Post private var post: Post
@State
private var newId: String
@State @State
private var showLinkedPagePicker = false private var showLinkedPagePicker = false
init(post: Post) { init(post: Post) {
self.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 { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("ID") DetailTitle(
.font(.headline) title: "Post",
HStack { text: "Posts capture quick updates and can link to pages")
TextField("", text: $newId)
.textFieldStyle(.roundedBorder)
Button("Update", action: setNewId)
.disabled(newId.isEmpty || containsInvalidCharacters || idExists)
}
.padding(.bottom)
HStack { IdPropertyView(
Text("Draft") id: $post.id,
.font(.headline) footer: "The id is used to link to post and store them",
Spacer() validation: post.isValid,
Toggle("", isOn: $post.isDraft) update: { post.update(id: $0) })
.toggleStyle(.switch)
}
.padding(.bottom)
HStack { BoolPropertyView(
Text("Start") title: "Draft",
.font(.headline) value: $post.isDraft,
Spacer() footer: "Indicate a post as a draft to hide it from the website")
DatePicker("", selection: $post.startDate, displayedComponents: .date)
.datePickerStyle(.compact)
.padding(.bottom)
}
HStack(alignment: .firstTextBaseline) { DatePropertyView(
Text("Has end date") title: "Start date",
.font(.headline) value: $post.startDate,
Spacer() footer: "The date when the post content started")
Toggle("", isOn: $post.hasEndDate)
.toggleStyle(.switch)
.padding(.bottom)
}
if post.hasEndDate { OptionalDatePropertyView(
HStack(alignment: .firstTextBaseline) { title: "End date",
Text("End date") isEnabled: $post.hasEndDate,
.font(.headline) date: $post.endDate,
Spacer() footer: "The date when the post content ended")
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")
PagePropertyView(
title: "Linked page",
selectedPage: $post.linkedPage,
footer: "The page to open when clicking on the post")
LocalizedPostDetailView(post: post.localized(in: language)) LocalizedPostDetailView(post: post.localized(in: language))
} }
.padding() .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
} }
} }

View File

@ -50,11 +50,6 @@ struct PostImagesView: View {
.padding() .padding()
} }
} }
.sheet(isPresented: $showImagePicker) {
ImagePickerView(showImagePicker: $showImagePicker) { image in
post.images.append(image)
}
}
} }
private func shiftLeft(_ image: FileResource) { private func shiftLeft(_ image: FileResource) {

View File

@ -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))
}

View File

@ -120,9 +120,7 @@ struct PageIssueView: View {
didSelect(page: page) didSelect(page: page)
} }
} content: { } content: {
PagePickerView( PagePickerView(selectedPage: $selectedPage)
showPagePicker: $showPagePicker,
selectedPage: $selectedPage)
} }
.sheet(isPresented: $showFilePicker) { .sheet(isPresented: $showFilePicker) {
if let file = selectedFile { if let file = selectedFile {

View File

@ -5,25 +5,18 @@ struct GenerationDetailView: View {
let section: SettingsSection let section: SettingsSection
var body: some View { var body: some View {
Group { switch section {
switch section { case .folders:
//case .generation: PathSettingsView()
// GenerationSettingsView() case .navigationBar:
case .folders: NavigationBarSettingsView()
PathSettingsView() case .postFeed:
case .navigationBar: PostFeedSettingsView()
NavigationBarSettingsView() case .pages:
case .postFeed: PageSettingsDetailView()
PostFeedSettingsView() case .tagOverview:
case .pages: TagOverviewDetailView()
PageSettingsDetailView()
case .tagOverview:
TagOverviewDetailView()
}
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
.navigationTitle("")
} }
} }

View File

@ -15,12 +15,9 @@ struct NavigationBarSettingsView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Navigation Bar") DetailTitle(
.font(.largeTitle) title: "Navigation Bar",
.bold() text: "Customize the navigation bar for all pages at the top of the website")
Text("Customize the navigation bar for all pages at the top of the website")
.foregroundStyle(.secondary)
.padding(.bottom, 30)
HStack { HStack {
Text("Links") Text("Links")

View File

@ -16,43 +16,43 @@ struct PageSettingsDetailView: View {
text: "Change the way pages are displayed") text: "Change the way pages are displayed")
IntegerPropertyView( IntegerPropertyView(
value: $content.settings.pages.contentWidth,
title: "Content Width", title: "Content Width",
value: $content.settings.pages.contentWidth,
footer: "The maximum width of the content in pages (in pixels)") footer: "The maximum width of the content in pages (in pixels)")
IntegerPropertyView( IntegerPropertyView(
value: $content.settings.pages.largeImageWidth,
title: "Fullscreen Image Width", title: "Fullscreen Image Width",
value: $content.settings.pages.largeImageWidth,
footer: "The maximum width of images that are diplayed fullscreen") footer: "The maximum width of images that are diplayed fullscreen")
IntegerPropertyView( IntegerPropertyView(
value: $content.settings.pages.pageLinkImageSize,
title: "Page Link Image Width", title: "Page Link Image Width",
value: $content.settings.pages.pageLinkImageSize,
footer: "The maximum width of images diplayed as thumbnails on page links") footer: "The maximum width of images diplayed as thumbnails on page links")
FilePropertyView( FilePropertyView(
title: "Default CSS File", 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) selectedFile: $content.settings.pages.defaultCssFile)
FilePropertyView( FilePropertyView(
title: "Code Highlighting File", 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) selectedFile: $content.settings.pages.codeHighlightingJsFile)
FilePropertyView( FilePropertyView(
title: "Audio Player CSS File", 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) selectedFile: $content.settings.pages.audioPlayerCssFile)
FilePropertyView( FilePropertyView(
title: "Audio Player JavaScript File", 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) selectedFile: $content.settings.pages.audioPlayerJsFile)
FilePropertyView( FilePropertyView(
title: "3D Model Viewer File", 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) selectedFile: $content.settings.pages.modelViewerJsFile)
} }
} }

View File

@ -11,131 +11,66 @@ struct PathSettingsView: View {
@EnvironmentObject @EnvironmentObject
private var content: Content private var content: Content
@State
private var folderSelection: SecurityScopeBookmark = .contentPath
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Folder Settings") DetailTitle(
.font(.largeTitle) title: "Folder Settings",
.bold() text: "Select the folders for the app to work.")
Text("Select the folders for the app to work.")
.foregroundStyle(.secondary)
.padding(.bottom, 30)
Text("Content Folder") FolderOnDiskPropertyView(
.font(.headline) title: "Content Folder",
.padding(.bottom, 1) folder: $contentPath,
Text(contentPath) footer: "The folder where the raw content of the website is stored") { url in
Button(action: selectContentFolder) { guard content.storage.save(folderUrl: url, in: .contentPath) else {
Text("Select folder") return
} }
Text("The folder where the raw content of the website is stored") contentPath = url.path()
.foregroundStyle(.secondary) }
.padding(.bottom)
Text("Output Folder") FolderOnDiskPropertyView(
.font(.headline) title: "Output Folder",
.padding(.bottom, 1) folder: $content.settings.paths.outputDirectoryPath,
Text(content.settings.paths.outputDirectoryPath) footer: "The folder where the generated website is stored") { url in
Button(action: selectOutputFolder) { guard content.storage.save(folderUrl: url, in: .outputPath) else {
Text("Select folder") return
} }
Text("The folder where the generated website is stored") content.settings.paths.outputDirectoryPath = url.path()
.foregroundStyle(.secondary) }
.padding(.bottom)
Text("Pages output folder") StringPropertyView(
.font(.headline) title: "Pages output folder",
TextField("", text: $content.settings.paths.pagesOutputFolderPath) text: $content.settings.paths.pagesOutputFolderPath,
.textFieldStyle(.roundedBorder) footer: "The path in the output folder where the generated pages are stored")
Text("The path in the output folder where the generated pages are stored")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Tags output folder") StringPropertyView(
.font(.headline) title: "Tags output folder",
TextField("", text: $content.settings.paths.tagsOutputFolderPath) text: $content.settings.paths.tagsOutputFolderPath,
.textFieldStyle(.roundedBorder) footer: "The path in the output folder where the generated tag pages are stored")
Text("The path in the output folder where the generated tag pages are stored")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Files output folder") StringPropertyView(
.font(.headline) title: "Files output folder",
TextField("", text: $content.settings.paths.filesOutputFolderPath) text: $content.settings.paths.filesOutputFolderPath,
.textFieldStyle(.roundedBorder) footer: "The path in the output folder where the copied files are stored")
Text("The path in the output folder where the copied files are stored")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Images output folder") StringPropertyView(
.font(.headline) title: "Images output folder",
TextField("", text: $content.settings.paths.imagesOutputFolderPath) text: $content.settings.paths.imagesOutputFolderPath,
.textFieldStyle(.roundedBorder) footer: "The path in the output folder where the generated images are stored")
Text("The path in the output folder where the generated images are stored")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Videos output folder") StringPropertyView(
.font(.headline) title: "Videos output folder",
TextField("", text: $content.settings.paths.videosOutputFolderPath) text: $content.settings.paths.videosOutputFolderPath,
.textFieldStyle(.roundedBorder) footer: "The path in the output folder where the generated videos are stored")
Text("The path in the output folder where the generated videos are stored")
.foregroundStyle(.secondary)
.padding(.bottom)
Text("Assets output folder") StringPropertyView(
.font(.headline) title: "Assets output folder",
TextField("", text: $content.settings.paths.assetsOutputFolderPath) text: $content.settings.paths.assetsOutputFolderPath,
.textFieldStyle(.roundedBorder) footer: "The path in the output folder where assets are stored")
Text("The path in the output folder where assets are stored")
.foregroundStyle(.secondary)
.padding(.bottom)
} }
.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 { #Preview {

View File

@ -15,28 +15,28 @@ struct PostFeedSettingsView: View {
text: "Change the way the posts are displayed") text: "Change the way the posts are displayed")
IntegerPropertyView( IntegerPropertyView(
value: $content.settings.posts.contentWidth,
title: "Content Width", title: "Content Width",
value: $content.settings.posts.contentWidth,
footer: "The maximum width of the content the post feed (in pixels)") footer: "The maximum width of the content the post feed (in pixels)")
IntegerPropertyView( IntegerPropertyView(
value: $content.settings.posts.postsPerPage,
title: "Posts Per Page", title: "Posts Per Page",
value: $content.settings.posts.postsPerPage,
footer: "The maximum number of posts displayed on a single page") footer: "The maximum number of posts displayed on a single page")
FilePropertyView( FilePropertyView(
title: "Default CSS File", 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) selectedFile: $content.settings.posts.defaultCssFile)
FilePropertyView( FilePropertyView(
title: "Swiper CSS File", 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) selectedFile: $content.settings.posts.swiperCssFile)
FilePropertyView( FilePropertyView(
title: "Swiper JavaScript File", 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) selectedFile: $content.settings.posts.swiperJsFile)
LocalizedPostFeedSettingsView( LocalizedPostFeedSettingsView(

View File

@ -11,12 +11,9 @@ struct TagOverviewDetailView: View {
var body: some View { var body: some View {
ScrollView { ScrollView {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Tag Overview") DetailTitle(
.font(.largeTitle) title: "Tag Overview",
.bold() text: "Configure the page showing all tags")
Text("Configure the page showing all tags")
.foregroundStyle(.secondary)
.padding(.bottom, 30)
if let page = content.tagOverview?.localized(in: language) { if let page = content.tagOverview?.localized(in: language) {
TagOverviewDetails(page: page) TagOverviewDetails(page: page)
@ -30,101 +27,48 @@ struct TagOverviewDetailView: View {
private func createTagOverviewPage() { private func createTagOverviewPage() {
content.tagOverview = TagOverviewPage( content.tagOverview = TagOverviewPage(
content: content, content: content,
german: .init(title: "Alle Tags", urlString: "alle"), german: .init(content: content, title: "Alle Tags", urlString: "alle"),
english: .init(title: "All tags", urlString: "all")) english: .init(content: content, title: "All tags", urlString: "all"))
} }
} }
private struct TagOverviewDetails: View { private struct TagOverviewDetails: View {
@EnvironmentObject
private var content: Content
@ObservedObject @ObservedObject
var page: LocalizedTagOverviewPage 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 { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Title") StringPropertyView(
.font(.headline) title: "Title",
TextField("", text: $page.title) text: $page.title,
.textFieldStyle(.roundedBorder) footer: "The title of the overview page")
HStack { IdPropertyView(
Text("Page URL String") id: $page.urlComponent,
.font(.headline) title: "Page URL String",
TextField("", text: $newUrlString) footer: "The url component to use for the link to the page",
.textFieldStyle(.roundedBorder) validation: page.isValid,
Button("Update", action: setNewId) update: { page.urlComponent = $0 })
.disabled(!newUrlCanBeUpdated)
}
.padding(.bottom)
Text("Link Preview Title") OptionalStringPropertyView(
.font(.headline) title: "Preview Title",
OptionalTextField("", text: $page.linkPreviewTitle, text: $page.linkPreviewTitle,
prompt: page.title) prompt: page.title,
.textFieldStyle(.roundedBorder) footer: "The title to use for the page when linking to it")
.padding(.bottom)
HStack { OptionalImagePropertyView(
Text("Link Preview Image") title: "Preview Image",
.font(.headline) selectedImage: $page.linkPreviewImage,
IconButton(symbol: .squareAndPencilCircleFill, footer: "The image to show for previews of this page")
size: 22,
color: .blue) {
showImagePicker = true
}
IconButton(symbol: .trashCircleFill, OptionalTextFieldPropertyView(
size: 22, title: "Preview Description",
color: .red) { text: $page.linkPreviewDescription,
page.linkPreviewImage = nil footer: "The description to show in previews of the page")
}.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)
} }
.sheet(isPresented: $showImagePicker) {
ImagePickerView(showImagePicker: $showImagePicker) { image in
page.linkPreviewImage = image
}
}
}
private func setNewId() {
page.urlString = newUrlString
} }
} }

View File

@ -28,8 +28,8 @@ struct AddTagView: View {
content: content, content: content,
id: "tag", id: "tag",
isVisible: true, isVisible: true,
german: .init(urlComponent: "tag", name: "Neuer Tag"), german: .init(content: .mock, urlComponent: "tag", name: "Neuer Tag"),
english: .init(urlComponent: "tag-en", name: "New Tag")) english: .init(content: .mock, urlComponent: "tag-en", name: "New Tag"))
// Add to top of the list, and resort when changing the name // Add to top of the list, and resort when changing the name
content.tags.insert(newTag, at: 0) content.tags.insert(newTag, at: 0)
dismiss() dismiss()

View File

@ -2,9 +2,6 @@ import SwiftUI
struct LocalizedTagDetailView: View { struct LocalizedTagDetailView: View {
@Binding
var tagIsVisible: Bool
@ObservedObject @ObservedObject
var tag: LocalizedTag var tag: LocalizedTag
@ -15,82 +12,44 @@ struct LocalizedTagDetailView: View {
private var showImagePicker = false private var showImagePicker = false
var body: some View { var body: some View {
ScrollView { VStack(alignment: .leading) {
VStack(alignment: .leading) { StringPropertyView(
Toggle("Appears in overviews", isOn: $tagIsVisible) title: "Name",
.toggleStyle(.switch) text: $tag.name,
.font(.headline) footer: "The displayed name of the tag")
.padding(.bottom)
Text("Name") IdPropertyView(
.font(.headline) id: $tag.urlComponent,
TextField("", text: $tag.name) title: "Page URL String",
.textFieldStyle(.roundedBorder) footer: "The url component to use in the url for this tag",
.padding(.bottom) validation: tag.isValid,
update: { tag.urlComponent = $0 })
Text("URL String") Text("Original url")
.font(.headline) .font(.headline)
TextField("", text: $tag.urlComponent) Text(tag.originalUrl ?? "-")
.textFieldStyle(.roundedBorder) .foregroundStyle(.secondary)
.padding(.bottom) .padding(.top, 1)
.padding(.bottom)
Text("Original url") OptionalStringPropertyView(
.font(.headline) title: "Subtitle",
Text(tag.originalUrl ?? "-") text: $tag.subtitle,
.padding(.top, 1) footer: "The subtitle/tagline to use")
.padding(.bottom)
Text("Subtitle") OptionalImagePropertyView(
.font(.headline) title: "Preview Image",
OptionalTextField("", text: $tag.subtitle) selectedImage: $tag.linkPreviewImage,
.textFieldStyle(.roundedBorder) footer: "The image to show for previews of this page")
.padding(.bottom)
Text("Link Preview Description") OptionalTextFieldPropertyView(
.font(.headline) title: "Preview Description",
.padding(.top) text: $tag.description,
OptionalDescriptionField(text: $tag.description) footer: "The description to show in previews of the page")
.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
}
} }
} }
} }
#Preview { #Preview {
LocalizedTagDetailView( LocalizedTagDetailView(tag: Tag.mock.english)
tagIsVisible: .constant(true),
tag: Tag.mock.english)
} }

View File

@ -10,9 +10,22 @@ struct TagDetailView: View {
var tag: Tag var tag: Tag
var body: some View { var body: some View {
LocalizedTagDetailView( ScrollView {
tagIsVisible: $tag.isVisible, VStack(alignment: .leading) {
tag: tag.localized(in: language)) 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()
}
} }
} }