First version
This commit is contained in:
parent
7c812de089
commit
0989f06d87
@ -7,29 +7,17 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
E227BE282C3330CE00F0CB47 /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227BE272C3330CE00F0CB47 /* Article.swift */; };
|
|
||||||
E227BE2A2C355AF700F0CB47 /* Use.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227BE292C355AF700F0CB47 /* Use.swift */; };
|
|
||||||
E227BE2D2C3E976D00F0CB47 /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227BE2C2C3E976D00F0CB47 /* Path.swift */; };
|
|
||||||
E227BE2F2C3E97DF00F0CB47 /* Svg+Dimensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227BE2E2C3E97DF00F0CB47 /* Svg+Dimensions.swift */; };
|
|
||||||
E227BE312C3E9B2700F0CB47 /* MetricIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227BE302C3E9B2700F0CB47 /* MetricIcon.swift */; };
|
|
||||||
E227BE332C3EA51500F0CB47 /* TagLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227BE322C3EA51500F0CB47 /* TagLink.swift */; };
|
|
||||||
E227BE352C415EC000F0CB47 /* Source+Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227BE342C415EC000F0CB47 /* Source+Attributes.swift */; };
|
|
||||||
E227BE372C415F8900F0CB47 /* Image+Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227BE362C415F8900F0CB47 /* Image+Attributes.swift */; };
|
|
||||||
E227BE392C41611100F0CB47 /* ArticleImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E227BE382C41611100F0CB47 /* ArticleImage.swift */; };
|
|
||||||
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
||||||
E24252032C5163CF0029FF16 /* Importer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252022C5163CF0029FF16 /* Importer.swift */; };
|
E24252032C5163CF0029FF16 /* Importer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252022C5163CF0029FF16 /* Importer.swift */; };
|
||||||
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252052C51684E0029FF16 /* GenericMetadata.swift */; };
|
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252052C51684E0029FF16 /* GenericMetadata.swift */; };
|
||||||
E24252082C5168750029FF16 /* GenericMetadata+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */; };
|
E24252082C5168750029FF16 /* GenericMetadata+Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */; };
|
||||||
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 */; };
|
||||||
E2581DEF2C75203800F1F079 /* TagsSubtitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEE2C75203800F1F079 /* TagsSubtitle.swift */; };
|
|
||||||
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DF02C7523F400F1F079 /* ImportableTag.swift */; };
|
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DF02C7523F400F1F079 /* ImportableTag.swift */; };
|
||||||
E28101192C50E03A0066F5BE /* EntryContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28101182C50E03A0066F5BE /* EntryContentView.swift */; };
|
|
||||||
E2A21C012CB16A820060935B /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C002CB16A820060935B /* PostView.swift */; };
|
E2A21C012CB16A820060935B /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C002CB16A820060935B /* PostView.swift */; };
|
||||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; };
|
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; };
|
||||||
E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C042CB176670060935B /* LocalizedText.swift */; };
|
E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C042CB176670060935B /* LocalizedText.swift */; };
|
||||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C072CB17B810060935B /* TagView.swift */; };
|
E2A21C082CB17B870060935B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C072CB17B810060935B /* TagView.swift */; };
|
||||||
E2A21C0C2CB17C190060935B /* TagListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0B2CB17C150060935B /* TagListView.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 */; };
|
E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C112CB18D520060935B /* DatePickerView.swift */; };
|
||||||
@ -42,24 +30,20 @@
|
|||||||
E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C312CB5BCAC0060935B /* PageDetailView.swift */; };
|
E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C312CB5BCAC0060935B /* PageDetailView.swift */; };
|
||||||
E2A21C362CB9A3D70060935B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C352CB9A3D70060935B /* SettingsView.swift */; };
|
E2A21C362CB9A3D70060935B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C352CB9A3D70060935B /* SettingsView.swift */; };
|
||||||
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C3A2CB9D9A50060935B /* ImageResource.swift */; };
|
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C3A2CB9D9A50060935B /* ImageResource.swift */; };
|
||||||
E2A21C3E2CBA53860060935B /* Elementary in Frameworks */ = {isa = PBXBuildFile; productRef = E2A21C3D2CBA53860060935B /* Elementary */; };
|
|
||||||
E2A21C412CBA53FA0060935B /* Elementary in Frameworks */ = {isa = PBXBuildFile; productRef = E2A21C402CBA53FA0060935B /* Elementary */; };
|
|
||||||
E2A21C442CBA560F0060935B /* Elementary in Frameworks */ = {isa = PBXBuildFile; productRef = E2A21C432CBA560F0060935B /* Elementary */; };
|
|
||||||
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */; };
|
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C452CBAE2E50060935B /* FeedEntryContent.swift */; };
|
||||||
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C472CBAF8830060935B /* String+Extensions.swift */; };
|
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C472CBAF8830060935B /* String+Extensions.swift */; };
|
||||||
E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C4C2CBB16B50060935B /* ImagesView.swift */; };
|
E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C4C2CBB16B50060935B /* ImagesView.swift */; };
|
||||||
E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */; };
|
E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */; };
|
||||||
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C502CBBD53C0060935B /* FileResource.swift */; };
|
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C502CBBD53C0060935B /* FileResource.swift */; };
|
||||||
|
E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C532CBBF87A0060935B /* FilesView.swift */; };
|
||||||
|
E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */; };
|
||||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; };
|
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; };
|
||||||
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; };
|
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2B85F352C426BEE0047CD0C /* SFSafeSymbols */; };
|
||||||
E2B85F382C4289F10047CD0C /* TopNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F372C4289F10047CD0C /* TopNavigationBar.swift */; };
|
|
||||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; };
|
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; };
|
||||||
E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* Feed.swift */; };
|
E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3C2C4293F80047CD0C /* Feed.swift */; };
|
||||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F402C4294790047CD0C /* PageHead.swift */; };
|
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F402C4294790047CD0C /* PageHead.swift */; };
|
||||||
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F422C4294F60047CD0C /* FeedEntry.swift */; };
|
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F422C4294F60047CD0C /* FeedEntry.swift */; };
|
||||||
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F442C429ED60047CD0C /* ImageGallery.swift */; };
|
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F442C429ED60047CD0C /* ImageGallery.swift */; };
|
||||||
E2B85F522C4BB3220047CD0C /* OptionalTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F512C4BB3220047CD0C /* OptionalTextField.swift */; };
|
|
||||||
E2B85F542C4BCCAC0047CD0C /* DetailTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F532C4BCCAC0047CD0C /* DetailTextField.swift */; };
|
|
||||||
E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */; };
|
E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */; };
|
||||||
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */; };
|
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */; };
|
||||||
E2DD047A2C276F32003BFF1F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2DD04792C276F32003BFF1F /* Assets.xcassets */; };
|
E2DD047A2C276F32003BFF1F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2DD04792C276F32003BFF1F /* Assets.xcassets */; };
|
||||||
@ -69,28 +53,16 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
E227BE272C3330CE00F0CB47 /* Article.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = "<group>"; };
|
|
||||||
E227BE292C355AF700F0CB47 /* Use.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Use.swift; sourceTree = "<group>"; };
|
|
||||||
E227BE2C2C3E976D00F0CB47 /* Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Path.swift; sourceTree = "<group>"; };
|
|
||||||
E227BE2E2C3E97DF00F0CB47 /* Svg+Dimensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Svg+Dimensions.swift"; sourceTree = "<group>"; };
|
|
||||||
E227BE302C3E9B2700F0CB47 /* MetricIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricIcon.swift; sourceTree = "<group>"; };
|
|
||||||
E227BE322C3EA51500F0CB47 /* TagLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagLink.swift; sourceTree = "<group>"; };
|
|
||||||
E227BE342C415EC000F0CB47 /* Source+Attributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+Attributes.swift"; sourceTree = "<group>"; };
|
|
||||||
E227BE362C415F8900F0CB47 /* Image+Attributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+Attributes.swift"; sourceTree = "<group>"; };
|
|
||||||
E227BE382C41611100F0CB47 /* ArticleImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleImage.swift; sourceTree = "<group>"; };
|
|
||||||
E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = "<group>"; };
|
E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = "<group>"; };
|
||||||
E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = "<group>"; };
|
E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = "<group>"; };
|
||||||
E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.swift"; sourceTree = "<group>"; };
|
E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.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>"; };
|
||||||
E2581DEE2C75203800F1F079 /* TagsSubtitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsSubtitle.swift; sourceTree = "<group>"; };
|
|
||||||
E2581DF02C7523F400F1F079 /* ImportableTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportableTag.swift; sourceTree = "<group>"; };
|
E2581DF02C7523F400F1F079 /* ImportableTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportableTag.swift; sourceTree = "<group>"; };
|
||||||
E28101182C50E03A0066F5BE /* EntryContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryContentView.swift; sourceTree = "<group>"; };
|
|
||||||
E2A21C002CB16A820060935B /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
E2A21C002CB16A820060935B /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
||||||
E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = "<group>"; };
|
E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = "<group>"; };
|
||||||
E2A21C042CB176670060935B /* LocalizedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedText.swift; sourceTree = "<group>"; };
|
E2A21C042CB176670060935B /* LocalizedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedText.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
E2A21C0B2CB17C150060935B /* TagListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagListView.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>"; };
|
E2A21C112CB18D520060935B /* DatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerView.swift; sourceTree = "<group>"; };
|
||||||
@ -108,15 +80,14 @@
|
|||||||
E2A21C4C2CBB16B50060935B /* ImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesView.swift; sourceTree = "<group>"; };
|
E2A21C4C2CBB16B50060935B /* ImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesView.swift; sourceTree = "<group>"; };
|
||||||
E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDetailsView.swift; sourceTree = "<group>"; };
|
E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDetailsView.swift; sourceTree = "<group>"; };
|
||||||
E2A21C502CBBD53C0060935B /* FileResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResource.swift; sourceTree = "<group>"; };
|
E2A21C502CBBD53C0060935B /* FileResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResource.swift; sourceTree = "<group>"; };
|
||||||
|
E2A21C532CBBF87A0060935B /* FilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesView.swift; sourceTree = "<group>"; };
|
||||||
|
E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleColumnView.swift; sourceTree = "<group>"; };
|
||||||
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
|
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
|
||||||
E2B85F372C4289F10047CD0C /* TopNavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopNavigationBar.swift; sourceTree = "<group>"; };
|
|
||||||
E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
|
E2B85F3A2C428F0D0047CD0C /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
|
||||||
E2B85F3C2C4293F80047CD0C /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
|
E2B85F3C2C4293F80047CD0C /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; };
|
||||||
E2B85F402C4294790047CD0C /* PageHead.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHead.swift; sourceTree = "<group>"; };
|
E2B85F402C4294790047CD0C /* PageHead.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHead.swift; sourceTree = "<group>"; };
|
||||||
E2B85F422C4294F60047CD0C /* FeedEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntry.swift; sourceTree = "<group>"; };
|
E2B85F422C4294F60047CD0C /* FeedEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntry.swift; sourceTree = "<group>"; };
|
||||||
E2B85F442C429ED60047CD0C /* ImageGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGallery.swift; sourceTree = "<group>"; };
|
E2B85F442C429ED60047CD0C /* ImageGallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGallery.swift; sourceTree = "<group>"; };
|
||||||
E2B85F512C4BB3220047CD0C /* OptionalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextField.swift; sourceTree = "<group>"; };
|
|
||||||
E2B85F532C4BCCAC0047CD0C /* DetailTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTextField.swift; sourceTree = "<group>"; };
|
|
||||||
E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Extension.swift"; sourceTree = "<group>"; };
|
E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Extension.swift"; sourceTree = "<group>"; };
|
||||||
E2DD04702C276F31003BFF1F /* CHDataManagement.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CHDataManagement.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
E2DD04702C276F31003BFF1F /* CHDataManagement.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CHDataManagement.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CHDataManagementApp.swift; sourceTree = "<group>"; };
|
E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CHDataManagementApp.swift; sourceTree = "<group>"; };
|
||||||
@ -132,9 +103,6 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
E2A21C412CBA53FA0060935B /* Elementary in Frameworks */,
|
|
||||||
E2A21C3E2CBA53860060935B /* Elementary in Frameworks */,
|
|
||||||
E2A21C442CBA560F0060935B /* Elementary in Frameworks */,
|
|
||||||
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */,
|
E2B85F362C426BEE0047CD0C /* SFSafeSymbols in Frameworks */,
|
||||||
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */,
|
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */,
|
||||||
);
|
);
|
||||||
@ -143,52 +111,17 @@
|
|||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
E227BE262C3330C100F0CB47 /* Elementary */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
E227BE2B2C3E976000F0CB47 /* Custom Elements */,
|
|
||||||
E227BE302C3E9B2700F0CB47 /* MetricIcon.swift */,
|
|
||||||
E227BE322C3EA51500F0CB47 /* TagLink.swift */,
|
|
||||||
E2581DEE2C75203800F1F079 /* TagsSubtitle.swift */,
|
|
||||||
);
|
|
||||||
path = Elementary;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
E227BE2B2C3E976000F0CB47 /* Custom Elements */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
E227BE292C355AF700F0CB47 /* Use.swift */,
|
|
||||||
E227BE2C2C3E976D00F0CB47 /* Path.swift */,
|
|
||||||
E227BE2E2C3E97DF00F0CB47 /* Svg+Dimensions.swift */,
|
|
||||||
E227BE342C415EC000F0CB47 /* Source+Attributes.swift */,
|
|
||||||
E227BE362C415F8900F0CB47 /* Image+Attributes.swift */,
|
|
||||||
E227BE382C41611100F0CB47 /* ArticleImage.swift */,
|
|
||||||
E2B85F372C4289F10047CD0C /* TopNavigationBar.swift */,
|
|
||||||
);
|
|
||||||
path = "Custom Elements";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
E24252042C5168430029FF16 /* Import */ = {
|
E24252042C5168430029FF16 /* Import */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E24252022C5163CF0029FF16 /* Importer.swift */,
|
E24252022C5163CF0029FF16 /* Importer.swift */,
|
||||||
|
E2581DF02C7523F400F1F079 /* ImportableTag.swift */,
|
||||||
E24252052C51684E0029FF16 /* GenericMetadata.swift */,
|
E24252052C51684E0029FF16 /* GenericMetadata.swift */,
|
||||||
E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */,
|
E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */,
|
||||||
);
|
);
|
||||||
path = Import;
|
path = Import;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
E2A21C062CB17B7A0060935B /* Unused */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
E2A21C0B2CB17C150060935B /* TagListView.swift */,
|
|
||||||
E2B85F512C4BB3220047CD0C /* OptionalTextField.swift */,
|
|
||||||
E2B85F532C4BCCAC0047CD0C /* DetailTextField.swift */,
|
|
||||||
E28101182C50E03A0066F5BE /* EntryContentView.swift */,
|
|
||||||
);
|
|
||||||
path = Unused;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
E2A21C322CB5BCAC0060935B /* Pages */ = {
|
E2A21C322CB5BCAC0060935B /* Pages */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -219,10 +152,19 @@
|
|||||||
children = (
|
children = (
|
||||||
E2A21C4C2CBB16B50060935B /* ImagesView.swift */,
|
E2A21C4C2CBB16B50060935B /* ImagesView.swift */,
|
||||||
E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */,
|
E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */,
|
||||||
|
E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */,
|
||||||
);
|
);
|
||||||
path = Images;
|
path = Images;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E2A21C522CBBF86D0060935B /* Files */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E2A21C532CBBF87A0060935B /* FilesView.swift */,
|
||||||
|
);
|
||||||
|
path = Files;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E2A9CB7F2C7E686C005C89CC /* Tags */ = {
|
E2A9CB7F2C7E686C005C89CC /* Tags */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -233,15 +175,14 @@
|
|||||||
E2B85F392C428F020047CD0C /* Model */ = {
|
E2B85F392C428F020047CD0C /* Model */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2E06DFA2CA4A6570019C2AF /* Content.swift */,
|
||||||
|
E24252092C52C9260029FF16 /* ContentLanguage.swift */,
|
||||||
E2A21C502CBBD53C0060935B /* FileResource.swift */,
|
E2A21C502CBBD53C0060935B /* FileResource.swift */,
|
||||||
E2A21C3A2CB9D9A50060935B /* ImageResource.swift */,
|
E2A21C3A2CB9D9A50060935B /* ImageResource.swift */,
|
||||||
E2A21C042CB176670060935B /* LocalizedText.swift */,
|
E2A21C042CB176670060935B /* LocalizedText.swift */,
|
||||||
E2E06DFA2CA4A6570019C2AF /* Content.swift */,
|
|
||||||
E2B85F3A2C428F0D0047CD0C /* Post.swift */,
|
|
||||||
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */,
|
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */,
|
||||||
E24252092C52C9260029FF16 /* ContentLanguage.swift */,
|
E2B85F3A2C428F0D0047CD0C /* Post.swift */,
|
||||||
E2581DEC2C75202400F1F079 /* Tag.swift */,
|
E2581DEC2C75202400F1F079 /* Tag.swift */,
|
||||||
E2581DF02C7523F400F1F079 /* ImportableTag.swift */,
|
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -250,7 +191,6 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E2B85F3C2C4293F80047CD0C /* Feed.swift */,
|
E2B85F3C2C4293F80047CD0C /* Feed.swift */,
|
||||||
E227BE272C3330CE00F0CB47 /* Article.swift */,
|
|
||||||
);
|
);
|
||||||
path = Pages;
|
path = Pages;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -270,6 +210,7 @@
|
|||||||
E2B85F462C42C7CA0047CD0C /* Views */ = {
|
E2B85F462C42C7CA0047CD0C /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2A21C522CBBF86D0060935B /* Files */,
|
||||||
E2A21C492CBB168F0060935B /* Images */,
|
E2A21C492CBB168F0060935B /* Images */,
|
||||||
E2A21C372CB9A4F10060935B /* Generic */,
|
E2A21C372CB9A4F10060935B /* Generic */,
|
||||||
E2A21C342CB9A3CA0060935B /* Settings */,
|
E2A21C342CB9A3CA0060935B /* Settings */,
|
||||||
@ -322,13 +263,11 @@
|
|||||||
E2DD04722C276F31003BFF1F /* CHDataManagement */ = {
|
E2DD04722C276F31003BFF1F /* CHDataManagement */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
E2A21C062CB17B7A0060935B /* Unused */,
|
|
||||||
E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */,
|
E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */,
|
||||||
E2B85F392C428F020047CD0C /* Model */,
|
E2B85F392C428F020047CD0C /* Model */,
|
||||||
E2B85F462C42C7CA0047CD0C /* Views */,
|
E2B85F462C42C7CA0047CD0C /* Views */,
|
||||||
E2B85F3F2C42946E0047CD0C /* Page Elements */,
|
E2B85F3F2C42946E0047CD0C /* Page Elements */,
|
||||||
E2B85F3E2C4293FF0047CD0C /* Pages */,
|
E2B85F3E2C4293FF0047CD0C /* Pages */,
|
||||||
E227BE262C3330C100F0CB47 /* Elementary */,
|
|
||||||
E2DD04792C276F32003BFF1F /* Assets.xcassets */,
|
E2DD04792C276F32003BFF1F /* Assets.xcassets */,
|
||||||
E2DD047B2C276F32003BFF1F /* CHDataManagement.entitlements */,
|
E2DD047B2C276F32003BFF1F /* CHDataManagement.entitlements */,
|
||||||
E2B85F552C4BD0AD0047CD0C /* Extensions */,
|
E2B85F552C4BD0AD0047CD0C /* Extensions */,
|
||||||
@ -368,9 +307,6 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
E2B85F352C426BEE0047CD0C /* SFSafeSymbols */,
|
E2B85F352C426BEE0047CD0C /* SFSafeSymbols */,
|
||||||
E24252002C50E0A40029FF16 /* HighlightedTextEditor */,
|
E24252002C50E0A40029FF16 /* HighlightedTextEditor */,
|
||||||
E2A21C3D2CBA53860060935B /* Elementary */,
|
|
||||||
E2A21C402CBA53FA0060935B /* Elementary */,
|
|
||||||
E2A21C432CBA560F0060935B /* Elementary */,
|
|
||||||
);
|
);
|
||||||
productName = CHDataManagement;
|
productName = CHDataManagement;
|
||||||
productReference = E2DD04702C276F31003BFF1F /* CHDataManagement.app */;
|
productReference = E2DD04702C276F31003BFF1F /* CHDataManagement.app */;
|
||||||
@ -403,7 +339,6 @@
|
|||||||
packageReferences = (
|
packageReferences = (
|
||||||
E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||||
E24251FF2C50E0A40029FF16 /* XCRemoteSwiftPackageReference "HighlightedTextEditor" */,
|
E24251FF2C50E0A40029FF16 /* XCRemoteSwiftPackageReference "HighlightedTextEditor" */,
|
||||||
E2A21C422CBA560F0060935B /* XCRemoteSwiftPackageReference "elementary" */,
|
|
||||||
);
|
);
|
||||||
productRefGroup = E2DD04712C276F31003BFF1F /* Products */;
|
productRefGroup = E2DD04712C276F31003BFF1F /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@ -432,57 +367,44 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */,
|
E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */,
|
||||||
|
E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */,
|
||||||
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */,
|
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */,
|
||||||
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */,
|
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */,
|
||||||
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */,
|
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */,
|
||||||
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */,
|
E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */,
|
||||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */,
|
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */,
|
||||||
E227BE332C3EA51500F0CB47 /* TagLink.swift in Sources */,
|
|
||||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
||||||
E2A21C0C2CB17C190060935B /* TagListView.swift in Sources */,
|
|
||||||
E2B85F542C4BCCAC0047CD0C /* DetailTextField.swift in Sources */,
|
|
||||||
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
|
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
|
||||||
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */,
|
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */,
|
||||||
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
|
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
|
||||||
E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */,
|
E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */,
|
||||||
E227BE312C3E9B2700F0CB47 /* MetricIcon.swift in Sources */,
|
|
||||||
E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */,
|
E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */,
|
||||||
E24252082C5168750029FF16 /* GenericMetadata+Localized.swift in Sources */,
|
E24252082C5168750029FF16 /* GenericMetadata+Localized.swift in Sources */,
|
||||||
E2581DED2C75202400F1F079 /* Tag.swift in Sources */,
|
E2581DED2C75202400F1F079 /* Tag.swift in Sources */,
|
||||||
E227BE372C415F8900F0CB47 /* Image+Attributes.swift in Sources */,
|
|
||||||
E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */,
|
E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */,
|
||||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
||||||
E227BE282C3330CE00F0CB47 /* Article.swift in Sources */,
|
|
||||||
E24252032C5163CF0029FF16 /* Importer.swift in Sources */,
|
E24252032C5163CF0029FF16 /* Importer.swift in Sources */,
|
||||||
E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */,
|
E2A21C332CB5BCAC0060935B /* PageDetailView.swift in Sources */,
|
||||||
E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */,
|
E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */,
|
||||||
E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */,
|
E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */,
|
||||||
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
|
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
|
||||||
E2B85F382C4289F10047CD0C /* TopNavigationBar.swift in Sources */,
|
|
||||||
E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */,
|
E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */,
|
||||||
E2581DEF2C75203800F1F079 /* TagsSubtitle.swift in Sources */,
|
|
||||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
||||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
||||||
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */,
|
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */,
|
||||||
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */,
|
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */,
|
||||||
E227BE2F2C3E97DF00F0CB47 /* Svg+Dimensions.swift in Sources */,
|
|
||||||
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
|
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
|
||||||
E28101192C50E03A0066F5BE /* EntryContentView.swift in Sources */,
|
|
||||||
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */,
|
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */,
|
||||||
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
|
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
|
||||||
E227BE352C415EC000F0CB47 /* Source+Attributes.swift in Sources */,
|
|
||||||
E227BE2D2C3E976D00F0CB47 /* Path.swift in Sources */,
|
|
||||||
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
|
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
|
||||||
|
E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */,
|
||||||
E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */,
|
E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */,
|
||||||
E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */,
|
E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */,
|
||||||
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */,
|
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */,
|
||||||
E227BE2A2C355AF700F0CB47 /* Use.swift in Sources */,
|
|
||||||
E2B85F522C4BB3220047CD0C /* OptionalTextField.swift in Sources */,
|
|
||||||
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */,
|
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */,
|
||||||
E2A21C362CB9A3D70060935B /* SettingsView.swift in Sources */,
|
E2A21C362CB9A3D70060935B /* SettingsView.swift in Sources */,
|
||||||
E2A21C012CB16A820060935B /* PostView.swift in Sources */,
|
E2A21C012CB16A820060935B /* PostView.swift in Sources */,
|
||||||
E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */,
|
E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */,
|
||||||
E227BE392C41611100F0CB47 /* ArticleImage.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -715,14 +637,6 @@
|
|||||||
minimumVersion = 2.1.2;
|
minimumVersion = 2.1.2;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
E2A21C422CBA560F0060935B /* XCRemoteSwiftPackageReference "elementary" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/sliemeobn/elementary";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 0.3.4;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
||||||
@ -739,19 +653,6 @@
|
|||||||
package = E24251FF2C50E0A40029FF16 /* XCRemoteSwiftPackageReference "HighlightedTextEditor" */;
|
package = E24251FF2C50E0A40029FF16 /* XCRemoteSwiftPackageReference "HighlightedTextEditor" */;
|
||||||
productName = HighlightedTextEditor;
|
productName = HighlightedTextEditor;
|
||||||
};
|
};
|
||||||
E2A21C3D2CBA53860060935B /* Elementary */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = Elementary;
|
|
||||||
};
|
|
||||||
E2A21C402CBA53FA0060935B /* Elementary */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
productName = Elementary;
|
|
||||||
};
|
|
||||||
E2A21C432CBA560F0060935B /* Elementary */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = E2A21C422CBA560F0060935B /* XCRemoteSwiftPackageReference "elementary" */;
|
|
||||||
productName = Elementary;
|
|
||||||
};
|
|
||||||
E2B85F352C426BEE0047CD0C /* SFSafeSymbols */ = {
|
E2B85F352C426BEE0047CD0C /* SFSafeSymbols */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
package = E2B85F342C426BED0047CD0C /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||||
|
@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"originHash" : "c1a67d708d6f681f2c183d65d661dd3b41db4b2eb186a732bdf66ec00610d102",
|
"originHash" : "a865991f5fa01ecfb2e7afd44ef74d1e86f52c8f7eec6be4e188382e4051b34c",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
|
||||||
"identity" : "elementary",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/sliemeobn/elementary",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "5ed7c2d87190cf73cf4fd2df28be5ee6695af30d",
|
|
||||||
"version" : "0.3.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identity" : "highlightedtexteditor",
|
"identity" : "highlightedtexteditor",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
@ -1,32 +1,71 @@
|
|||||||
//
|
|
||||||
// CHDataManagementApp.swift
|
|
||||||
// CHDataManagement
|
|
||||||
//
|
|
||||||
// Created by CH on 22.06.24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftData
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
|
||||||
|
enum ContentDisplayType {
|
||||||
|
case markdown
|
||||||
|
case html
|
||||||
|
case rendered
|
||||||
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct CHDataManagementApp: App {
|
struct CHDataManagementApp: App {
|
||||||
var sharedModelContainer: ModelContainer = {
|
|
||||||
let schema = Schema([
|
|
||||||
Item.self,
|
|
||||||
])
|
|
||||||
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
|
|
||||||
|
|
||||||
do {
|
var navigationTitle: String {
|
||||||
return try ModelContainer(for: schema, configurations: [modelConfiguration])
|
""
|
||||||
} catch {
|
|
||||||
fatalError("Could not create ModelContainer: \(error)")
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
@ObservedObject
|
||||||
|
var content: Content = .init()
|
||||||
|
|
||||||
|
@State
|
||||||
|
var selectedLanguage: ContentLanguage = .english
|
||||||
|
|
||||||
|
@State
|
||||||
|
var contentDisplayType: ContentDisplayType = .markdown
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
TabView {
|
||||||
|
Tab("Posts", systemImage: SFSymbol.rectangleAndPencilAndEllipsis.rawValue) {
|
||||||
|
PostList(posts: $content.posts)
|
||||||
|
.environment(\.language, selectedLanguage)
|
||||||
|
.background(Color(r: 2, g: 15, b: 26))
|
||||||
}
|
}
|
||||||
.modelContainer(sharedModelContainer)
|
Tab("Pages", systemImage: SFSymbol.textBelowPhoto.rawValue) {
|
||||||
|
Text("TODO")
|
||||||
|
}
|
||||||
|
Tab("Tags", systemImage: SFSymbol.tag.rawValue) {
|
||||||
|
Text("TODO")
|
||||||
|
}
|
||||||
|
Tab("Images", systemImage: SFSymbol.photo.rawValue) {
|
||||||
|
ImagesView()
|
||||||
|
.environmentObject(content)
|
||||||
|
}
|
||||||
|
Tab("Files", systemImage: SFSymbol.doc.rawValue) {
|
||||||
|
FilesView()
|
||||||
|
.environmentObject(content)
|
||||||
|
}
|
||||||
|
Tab("Settings", systemImage: SFSymbol.gear.rawValue) {
|
||||||
|
SettingsView()
|
||||||
|
.environmentObject(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Picker("", selection: $selectedLanguage) {
|
||||||
|
Text("English")
|
||||||
|
.tag(ContentLanguage.english)
|
||||||
|
Text("German")
|
||||||
|
.tag(ContentLanguage.german)
|
||||||
|
}.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear(perform: importOldContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func importOldContent() {
|
||||||
|
content.importOldContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentView.swift
|
|
||||||
// CHDataManagement
|
|
||||||
//
|
|
||||||
// Created by CH on 22.06.24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
@Environment(\.modelContext) private var modelContext
|
|
||||||
@Query private var items: [Item]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationSplitView {
|
|
||||||
List {
|
|
||||||
ForEach(items) { item in
|
|
||||||
NavigationLink {
|
|
||||||
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
|
|
||||||
} label: {
|
|
||||||
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onDelete(perform: deleteItems)
|
|
||||||
}
|
|
||||||
#if os(macOS)
|
|
||||||
.navigationSplitViewColumnWidth(min: 180, ideal: 200)
|
|
||||||
#endif
|
|
||||||
.toolbar {
|
|
||||||
#if os(iOS)
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
|
||||||
EditButton()
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
ToolbarItem {
|
|
||||||
Button(action: addItem) {
|
|
||||||
Label("Add Item", systemImage: "plus")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} detail: {
|
|
||||||
Text("Select an item")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func addItem() {
|
|
||||||
withAnimation {
|
|
||||||
let newItem = Item(timestamp: Date())
|
|
||||||
modelContext.insert(newItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func deleteItems(offsets: IndexSet) {
|
|
||||||
withAnimation {
|
|
||||||
for index in offsets {
|
|
||||||
modelContext.delete(items[index])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentView()
|
|
||||||
.modelContainer(for: Item.self, inMemory: true)
|
|
||||||
}
|
|
14
CHDataManagement/Extensions/Binding+Extension.swift
Normal file
14
CHDataManagement/Extensions/Binding+Extension.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public extension Binding where Value: Equatable, Value: Sendable {
|
||||||
|
|
||||||
|
init(_ source: Binding<Value?>, replacingNilWith nilProxy: Value) {
|
||||||
|
self.init(
|
||||||
|
get: { source.wrappedValue ?? nilProxy },
|
||||||
|
set: { newValue in
|
||||||
|
if newValue == nilProxy { source.wrappedValue = nil }
|
||||||
|
else { source.wrappedValue = newValue }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
15
CHDataManagement/Extensions/Color+RGB.swift
Normal file
15
CHDataManagement/Extensions/Color+RGB.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension Color {
|
||||||
|
|
||||||
|
init(_ r: Int, _ g: Int, _ b: Int) {
|
||||||
|
self.init(r: r, g: g, b: b)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(r: Int, g: Int, b: Int) {
|
||||||
|
self.init(
|
||||||
|
red: Double(r) / 255,
|
||||||
|
green: Double(g) / 255,
|
||||||
|
blue: Double(b) / 255)
|
||||||
|
}
|
||||||
|
}
|
15
CHDataManagement/Extensions/Environment+Language.swift
Normal file
15
CHDataManagement/Extensions/Environment+Language.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LanguageKey: EnvironmentKey {
|
||||||
|
|
||||||
|
static let defaultValue: ContentLanguage = .english
|
||||||
|
}
|
||||||
|
|
||||||
|
extension EnvironmentValues {
|
||||||
|
|
||||||
|
var language: ContentLanguage {
|
||||||
|
get { self[LanguageKey.self] }
|
||||||
|
set { self[LanguageKey.self] = newValue }
|
||||||
|
}
|
||||||
|
}
|
11
CHDataManagement/Extensions/String+Extensions.swift
Normal file
11
CHDataManagement/Extensions/String+Extensions.swift
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
extension String {
|
||||||
|
|
||||||
|
func htmlEscaped() -> String {
|
||||||
|
replacingOccurrences(of: "&", with: "&")
|
||||||
|
.replacingOccurrences(of: "\"", with: """)
|
||||||
|
.replacingOccurrences(of: "'", with: "'")
|
||||||
|
.replacingOccurrences(of: "<", with: "<")
|
||||||
|
.replacingOccurrences(of: ">", with: ">")
|
||||||
|
}
|
||||||
|
}
|
153
CHDataManagement/Import/GenericMetadata+Localized.swift
Normal file
153
CHDataManagement/Import/GenericMetadata+Localized.swift
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension GenericMetadata {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Metadata localized for a specific language.
|
||||||
|
*/
|
||||||
|
struct LocalizedMetadata {
|
||||||
|
|
||||||
|
/**
|
||||||
|
The language for which the content is specified.
|
||||||
|
- Note: This field is mandatory
|
||||||
|
*/
|
||||||
|
let language: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The title used in the page header.
|
||||||
|
- Note: This field is mandatory
|
||||||
|
*/
|
||||||
|
let title: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The subtitle used in the page header.
|
||||||
|
*/
|
||||||
|
let subtitle: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The description text used in the page header
|
||||||
|
*/
|
||||||
|
let description: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The title to use for the link preview.
|
||||||
|
|
||||||
|
If `nil` is specified, then the localized element `title` is used.
|
||||||
|
*/
|
||||||
|
let linkPreviewTitle: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The file name of the link preview image.
|
||||||
|
- Note: The image must be located in the element folder.
|
||||||
|
- Note: If `nil` is specified, then the (localized) thumbnail is used.
|
||||||
|
*/
|
||||||
|
let linkPreviewImage: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The description text for the link preview.
|
||||||
|
- Note: If `nil` is specified, then first the (localized) element `subtitle` is used.
|
||||||
|
If this is `nil` too, then the localized `description` of the element is used.
|
||||||
|
*/
|
||||||
|
let linkPreviewDescription: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The text on the link to show the section page when previewing multiple sections on an overview page.
|
||||||
|
- Note: If this value is inherited from the parent, if it is not defined. There must be at least one
|
||||||
|
element in the path that defines this property.
|
||||||
|
*/
|
||||||
|
let moreLinkText: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The text on the back navigation link of **contained** elements.
|
||||||
|
|
||||||
|
This text does not appear on the section page, but on the pages contained within the section.
|
||||||
|
- Note: If this property is not specified, then the root `backLinkText` is used.
|
||||||
|
- Note: The root element must specify this property.
|
||||||
|
*/
|
||||||
|
let backLinkText: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The text to show as a title for placeholder boxes
|
||||||
|
|
||||||
|
Placeholders are included in missing pages.
|
||||||
|
- Note: If no value is specified, then this property is inherited from the parent. The root element must specify this property.
|
||||||
|
*/
|
||||||
|
let placeholderTitle: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The text to show as a description for placeholder boxes
|
||||||
|
|
||||||
|
Placeholders are included in missing pages.
|
||||||
|
- Note: If no value is specified, then this property is inherited from the parent. The root element must specify this property.
|
||||||
|
*/
|
||||||
|
let placeholderText: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
An optional suffix to add to the title on a page.
|
||||||
|
|
||||||
|
This can be useful to express a different author, project grouping, etc.
|
||||||
|
*/
|
||||||
|
let titleSuffix: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
An optional suffix to add to the thumbnail title of a page.
|
||||||
|
|
||||||
|
This can be useful to express a different author, project grouping, etc.
|
||||||
|
*/
|
||||||
|
let thumbnailSuffix: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
A text to place in the top right corner of a large thumbnail.
|
||||||
|
|
||||||
|
The text should be a very short string to fit into the corner, like `soon`, or `draft`
|
||||||
|
|
||||||
|
- Note: This property is ignored if `thumbnailStyle` is not `large`.
|
||||||
|
*/
|
||||||
|
let cornerText: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The external url to use instead of automatically generating the page.
|
||||||
|
|
||||||
|
This property can be used for links to other parts of the site, like additional services.
|
||||||
|
It can also be set to manually write a page.
|
||||||
|
*/
|
||||||
|
let externalUrl: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The text to display for content related to the current page.
|
||||||
|
|
||||||
|
This property is mandatory at root level, and is propagated to child elements.
|
||||||
|
*/
|
||||||
|
let relatedContentText: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The text to display on a navigation element pointing to this element as the previous page.
|
||||||
|
|
||||||
|
This property is mandatory at root level, and is propagated to child elements.
|
||||||
|
*/
|
||||||
|
let navigationTextAsPreviousPage: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The text to display on the navigation element pointing to this element as the next page.
|
||||||
|
|
||||||
|
This property is mandatory at root level, and is propagated to child elements.
|
||||||
|
*/
|
||||||
|
let navigationTextAsNextPage: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The text to display above a slideshow for most recent items.
|
||||||
|
Only used for elements that define `showMostRecentSection = true`
|
||||||
|
*/
|
||||||
|
let mostRecentTitle: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The text to display above a slideshow for featured items.
|
||||||
|
Only used for elements that define `showFeaturedSection = true`
|
||||||
|
*/
|
||||||
|
let featuredTitle: String?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GenericMetadata.LocalizedMetadata: Codable {
|
||||||
|
|
||||||
|
}
|
137
CHDataManagement/Import/GenericMetadata.swift
Normal file
137
CHDataManagement/Import/GenericMetadata.swift
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/**
|
||||||
|
The metadata for all site elements.
|
||||||
|
*/
|
||||||
|
struct GenericMetadata {
|
||||||
|
|
||||||
|
/**
|
||||||
|
A custom id to uniquely identify the element on the site.
|
||||||
|
|
||||||
|
The id is used for short-hand links to pages, in the form of ``
|
||||||
|
for thumbnail previews or `[text](page:page_id)` for simple links.
|
||||||
|
|
||||||
|
If no custom id is set, then the name of the element folder is used.
|
||||||
|
*/
|
||||||
|
let customId: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The author of the content.
|
||||||
|
|
||||||
|
If no author is set, then the author from the parent element is used.
|
||||||
|
*/
|
||||||
|
let author: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The (start) date of the element.
|
||||||
|
|
||||||
|
The date is printed on content pages and may also used for sorting elements,
|
||||||
|
depending on the `useManualSorting` property of the parent.
|
||||||
|
*/
|
||||||
|
let date: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The end date of the element.
|
||||||
|
|
||||||
|
This property can be used to specify a date range for a content page.
|
||||||
|
*/
|
||||||
|
let endDate: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The deployment state of the page.
|
||||||
|
|
||||||
|
- Note: This property defaults to ``PageState.standard`
|
||||||
|
*/
|
||||||
|
let state: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The sort index of the page for manual sorting.
|
||||||
|
|
||||||
|
- Note: This property is only used (and must be set) if `useManualSorting` option of the parent is set.
|
||||||
|
*/
|
||||||
|
let sortIndex: Int?
|
||||||
|
|
||||||
|
/**
|
||||||
|
All files which may occur in content but is stored externally.
|
||||||
|
|
||||||
|
Missing files which would otherwise produce a warning are ignored when included here.
|
||||||
|
- Note: This property defaults to an empty set.
|
||||||
|
*/
|
||||||
|
let externalFiles: Set<String>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
Specifies additional files which should be copied to the destination when generating the content.
|
||||||
|
- Note: This property defaults to an empty set.
|
||||||
|
*/
|
||||||
|
let requiredFiles: Set<String>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
Additional images required by the element.
|
||||||
|
|
||||||
|
These images are specified as: `source_name destination_name width (height)`.
|
||||||
|
*/
|
||||||
|
let images: Set<String>?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The path to the thumbnail file.
|
||||||
|
|
||||||
|
This property is optional, and defaults to ``Element.defaultThumbnailName``.
|
||||||
|
Note: The generator first looks for localized versions of the thumbnail by appending `-[lang]` to the file name,
|
||||||
|
e.g. `customThumb-en.jpg`. If no file is found, then the specified file is tried.
|
||||||
|
*/
|
||||||
|
let thumbnailPath: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The style of thumbnail to use when generating overviews.
|
||||||
|
|
||||||
|
- Note: This property is only relevant for sections.
|
||||||
|
- Note: This property is inherited from the parent if not specified.
|
||||||
|
*/
|
||||||
|
let thumbnailStyle: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
Sort the child elements by their `sortIndex` property when generating overviews, instead of using the `date`.
|
||||||
|
|
||||||
|
- Note: This property is only relevant for sections.
|
||||||
|
- Note: This property defaults to `false`
|
||||||
|
*/
|
||||||
|
let useManualSorting: Bool?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The number of items to show when generating overviews of this element.
|
||||||
|
- Note: This property is only relevant for sections.
|
||||||
|
- Note: This property is inherited from the parent if not specified.
|
||||||
|
*/
|
||||||
|
let overviewItemCount: Int?
|
||||||
|
|
||||||
|
/**
|
||||||
|
Indicate the header type to be generated automatically.
|
||||||
|
|
||||||
|
If this option is set to `none`, then custom header code should be present in the page source files
|
||||||
|
- Note: If not specified, this property defaults to `left`.
|
||||||
|
- Note: Overview pages are always using `center`.
|
||||||
|
*/
|
||||||
|
let headerType: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
Indicate that the overview section should contain a `Newest Content` section before the other sections.
|
||||||
|
- Note: If not specified, this property defaults to `false`
|
||||||
|
*/
|
||||||
|
let showMostRecentSection: Bool?
|
||||||
|
|
||||||
|
/**
|
||||||
|
Indicate that the overview section should contain a `Featured Content` section before the other sections.
|
||||||
|
The elements are the page ids of the elements contained in the feature.
|
||||||
|
- Note: If not specified, this property defaults to `false`
|
||||||
|
*/
|
||||||
|
let featuredPages: [String]?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The localized metadata for each language.
|
||||||
|
*/
|
||||||
|
let languages: [LocalizedMetadata]?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GenericMetadata: Codable {
|
||||||
|
|
||||||
|
}
|
33
CHDataManagement/Import/ImportableTag.swift
Normal file
33
CHDataManagement/Import/ImportableTag.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ImportableTag {
|
||||||
|
|
||||||
|
let languages: [TagLanguage]
|
||||||
|
|
||||||
|
func info(for language: ContentLanguage) -> TagLanguage? {
|
||||||
|
languages.first { $0.language == language.rawValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImportableTag: Codable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TagLanguage {
|
||||||
|
|
||||||
|
let language: String
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
let subtitle: String?
|
||||||
|
|
||||||
|
let description: String?
|
||||||
|
|
||||||
|
let moreLinkText: String?
|
||||||
|
|
||||||
|
let backLinkText: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TagLanguage: Codable {
|
||||||
|
|
||||||
|
}
|
134
CHDataManagement/Import/Importer.swift
Normal file
134
CHDataManagement/Import/Importer.swift
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ImportedContent {
|
||||||
|
|
||||||
|
let posts: [Post]
|
||||||
|
|
||||||
|
let categories: [Tag]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Importer {
|
||||||
|
|
||||||
|
var posts: [Post] = []
|
||||||
|
|
||||||
|
var pages: [Page] = []
|
||||||
|
|
||||||
|
var tags: [Tag] = []
|
||||||
|
|
||||||
|
var images: [ImageResource] = []
|
||||||
|
|
||||||
|
var foldersToSearch: [(path: String, tag: String)] = [
|
||||||
|
("/Users/ch/Downloads/Website/projects/electronics", "electronics"),
|
||||||
|
("/Users/ch/Downloads/Website/projects/endeavor", "endeavor"),
|
||||||
|
("/Users/ch/Downloads/Website/projects/furniture", "furniture"),
|
||||||
|
("/Users/ch/Downloads/Website/projects/lighting", "lighting"),
|
||||||
|
("/Users/ch/Downloads/Website/projects/other", "other"),
|
||||||
|
("/Users/ch/Downloads/Website/projects/sewing", "sewing"),
|
||||||
|
("/Users/ch/Downloads/Website/projects/software", "software"),
|
||||||
|
("/Users/ch/Downloads/Website/articles", "articles"),
|
||||||
|
("/Users/ch/Downloads/Website/photography", "photography"),
|
||||||
|
("/Users/ch/Downloads/Website/travel", "travel")
|
||||||
|
]
|
||||||
|
|
||||||
|
func importOldContent() throws {
|
||||||
|
for (folder, tagName) in foldersToSearch {
|
||||||
|
let url = URL(filePath: folder)
|
||||||
|
let tag = try importTag(name: tagName, folder: url)
|
||||||
|
try importEntries(in: url, tag: tag)
|
||||||
|
tags.append(tag)
|
||||||
|
}
|
||||||
|
posts.sort { $0.startDate > $1.startDate }
|
||||||
|
//pages.sort { $0.startDate > $1.startDate }
|
||||||
|
tags.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func importTag(name: String, folder: URL) throws -> Tag {
|
||||||
|
let metadataUrl = folder.appending(path: "metadata.json", directoryHint: .notDirectory)
|
||||||
|
let data = try Data(contentsOf: metadataUrl)
|
||||||
|
let meta = try JSONDecoder().decode(ImportableTag.self, from: data)
|
||||||
|
|
||||||
|
return .init(
|
||||||
|
en: meta.info(for: .english)!.title,
|
||||||
|
de: meta.info(for: .german)!.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func importEntries(in folder: URL, tag: Tag) throws {
|
||||||
|
try FileManager.default
|
||||||
|
.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
|
||||||
|
.filter { $0.hasDirectoryPath }
|
||||||
|
.forEach { try importEntry(at: $0, tag: tag) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func importEntry(at url: URL, tag: Tag) throws {
|
||||||
|
let metadataUrl = url.appending(path: "metadata.json", directoryHint: .notDirectory)
|
||||||
|
guard FileManager.default.fileExists(atPath: metadataUrl.path()) else {
|
||||||
|
//print("No entry at \(url.path())")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let data = try Data(contentsOf: metadataUrl)
|
||||||
|
let meta = try JSONDecoder().decode(GenericMetadata.self, from: data)
|
||||||
|
|
||||||
|
let page = Page(
|
||||||
|
id: meta.customId ?? url.lastPathComponent,
|
||||||
|
isDraft: meta.state == "draft",
|
||||||
|
metadata: meta.languages!.map(convertPageContent),
|
||||||
|
externalFiles: meta.externalFiles ?? [],
|
||||||
|
requiredFiles: meta.requiredFiles ?? [],
|
||||||
|
images: meta.images ?? [])
|
||||||
|
pages.append(page)
|
||||||
|
|
||||||
|
let de = meta.languages!.first { $0.language == "de" }!
|
||||||
|
let en = meta.languages!.first { $0.language == "en" }!
|
||||||
|
|
||||||
|
let thumbnailImageName = meta.thumbnailPath ?? "thumbnail.jpg"
|
||||||
|
let thumbnailImageUrl = url.appending(path: thumbnailImageName, directoryHint: .notDirectory)
|
||||||
|
var images: [ImageResource] = []
|
||||||
|
if tag.id != "articles" {
|
||||||
|
if FileManager.default.fileExists(atPath: thumbnailImageUrl.path()) {
|
||||||
|
let thumbnail = ImageResource(
|
||||||
|
uniqueId: meta.customId ?? url.lastPathComponent,
|
||||||
|
altText: .init(en: "An image about \(en.title!)", de: "Ein Bild zu \(de.title!)"),
|
||||||
|
fileUrl: thumbnailImageUrl)
|
||||||
|
images.append(thumbnail)
|
||||||
|
self.images.append(thumbnail)
|
||||||
|
} else {
|
||||||
|
print("Thumbnail \(thumbnailImageUrl.path()) not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastPostId = posts.last?.id ?? 0
|
||||||
|
|
||||||
|
let post = Post(
|
||||||
|
id: lastPostId + 1,
|
||||||
|
isDraft: meta.state == "draft" || meta.state == "hidden",
|
||||||
|
startDate: meta.date!.toDate(),
|
||||||
|
endDate: meta.endDate?.toDate(),
|
||||||
|
title: .init(en: en.linkPreviewTitle ?? en.title!,
|
||||||
|
de: de.linkPreviewTitle ?? de.title!),
|
||||||
|
text: .init(en: en.linkPreviewDescription ?? en.description ?? "No description",
|
||||||
|
de: de.linkPreviewDescription ?? de.description ?? "Keine Beschreibung"),
|
||||||
|
tags: [tag],
|
||||||
|
images: images)
|
||||||
|
|
||||||
|
posts.append(post)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convertPageContent(_ meta: GenericMetadata.LocalizedMetadata) -> LocalizedPage {
|
||||||
|
.init(language: ContentLanguage(rawValue: meta.language!)!,
|
||||||
|
urlString: nil,
|
||||||
|
headline: meta.title!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
|
||||||
|
private static let metadataDate: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateFormat = "dd.MM.yy"
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
func toDate() -> Date {
|
||||||
|
String.metadataDate.date(from: self)!
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
//
|
|
||||||
// Item.swift
|
|
||||||
// CHDataManagement
|
|
||||||
//
|
|
||||||
// Created by CH on 22.06.24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SwiftData
|
|
||||||
|
|
||||||
@Model
|
|
||||||
final class Item {
|
|
||||||
var timestamp: Date
|
|
||||||
|
|
||||||
init(timestamp: Date) {
|
|
||||||
self.timestamp = timestamp
|
|
||||||
}
|
|
||||||
}
|
|
104
CHDataManagement/Model/Content.swift
Normal file
104
CHDataManagement/Model/Content.swift
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class Content: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var posts: [Post] = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var pages: [Page] = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var tags: [Tag] = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var images: [ImageResource] = []
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var files: [FileResources] = []
|
||||||
|
|
||||||
|
func generateFeed(for language: ContentLanguage, bookmarkKey: String) {
|
||||||
|
let posts = posts.map { $0.feedEntry(for: language) }
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
|
||||||
|
let navigationItems: [FeedNavigationLink] = [
|
||||||
|
.init(text: .init(en: "Projects", de: "Projekte"),
|
||||||
|
url: .init(en: "/projects", de: "/projekte")),
|
||||||
|
.init(text: .init(en: "Adventures", de: "Abenteuer"),
|
||||||
|
url: .init(en: "/adventures", de: "/abenteuer")),
|
||||||
|
.init(text: .init(en: "Services", de: "Dienste"),
|
||||||
|
url: .init(en: "/services", de: "/dienste")),
|
||||||
|
.init(text: .init(en: "Tags", de: "Kategorien"),
|
||||||
|
url: .init(en: "/tags", de: "/kategorien")),
|
||||||
|
]
|
||||||
|
|
||||||
|
let feed = Feed(
|
||||||
|
language: language,
|
||||||
|
title: .init(en: "Blog | CH", de: "Blog | CH"),
|
||||||
|
description: .init(en: "The latests posts, projects and adventures",
|
||||||
|
de: "Die neusten Beiträge, Projekte und Abenteuer"),
|
||||||
|
iconDescription: .init(en: "An icon consisting of the letters C and H in blue and orange",
|
||||||
|
de: "Ein Logo aus den Buchstaben C und H in Blau und Orange"),
|
||||||
|
navigationItems: navigationItems,
|
||||||
|
posts: posts)
|
||||||
|
let fileContent = feed.content
|
||||||
|
Content.accessFolderFromBookmark(key: bookmarkKey) { folder in
|
||||||
|
let outputFile = folder.appendingPathComponent("feed.html", isDirectory: false)
|
||||||
|
do {
|
||||||
|
try fileContent
|
||||||
|
.data(using: .utf8)!
|
||||||
|
.write(to: outputFile)
|
||||||
|
} catch {
|
||||||
|
print("Failed to save: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func importOldContent() {
|
||||||
|
let importer = Importer()
|
||||||
|
do {
|
||||||
|
try importer.importOldContent()
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.posts = importer.posts
|
||||||
|
self.tags = importer.tags
|
||||||
|
#warning("TODO: Copy page sources to data folder")
|
||||||
|
self.pages = importer.pages
|
||||||
|
self.images = importer.images
|
||||||
|
#warning("TODO: Copy images to data folder")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func accessFolderFromBookmark(key: String, operation: (URL) -> Void) {
|
||||||
|
guard let bookmarkData = UserDefaults.standard.data(forKey: key) else {
|
||||||
|
print("No bookmark data to access folder")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var isStale = false
|
||||||
|
let folderURL: URL
|
||||||
|
do {
|
||||||
|
// Resolve the bookmark to get the folder URL
|
||||||
|
folderURL = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
||||||
|
} catch {
|
||||||
|
print("Failed to resolve bookmark: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isStale {
|
||||||
|
print("Bookmark is stale, consider saving a new bookmark.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start accessing the security-scoped resource
|
||||||
|
if folderURL.startAccessingSecurityScopedResource() {
|
||||||
|
print("Accessing folder: \(folderURL.path)")
|
||||||
|
|
||||||
|
operation(folderURL)
|
||||||
|
folderURL.stopAccessingSecurityScopedResource()
|
||||||
|
} else {
|
||||||
|
print("Failed to access folder: \(folderURL.path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
8
CHDataManagement/Model/ContentLanguage.swift
Normal file
8
CHDataManagement/Model/ContentLanguage.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ContentLanguage: String {
|
||||||
|
|
||||||
|
case english = "en"
|
||||||
|
|
||||||
|
case german = "de"
|
||||||
|
}
|
16
CHDataManagement/Model/FileResource.swift
Normal file
16
CHDataManagement/Model/FileResource.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class FileResources: ObservableObject {
|
||||||
|
|
||||||
|
/// Globally unique id
|
||||||
|
@Published
|
||||||
|
var uniqueId: String
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var description: String
|
||||||
|
|
||||||
|
init(uniqueId: String, description: String) {
|
||||||
|
self.uniqueId = uniqueId
|
||||||
|
self.description = description
|
||||||
|
}
|
||||||
|
}
|
91
CHDataManagement/Model/ImageResource.swift
Normal file
91
CHDataManagement/Model/ImageResource.swift
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class ImageResource: ObservableObject {
|
||||||
|
|
||||||
|
/// Globally unique id
|
||||||
|
@Published
|
||||||
|
var id: String
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var altText: LocalizedText
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var size: CGSize = .zero
|
||||||
|
|
||||||
|
var aspectRatio: CGFloat {
|
||||||
|
guard size.height > 0 else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return size.width / size.height
|
||||||
|
}
|
||||||
|
|
||||||
|
private let source: ImageSource
|
||||||
|
|
||||||
|
init(uniqueId: String, altText: LocalizedText, fileUrl: URL) {
|
||||||
|
self.id = uniqueId
|
||||||
|
self.source = .file(fileUrl)
|
||||||
|
self.altText = altText
|
||||||
|
}
|
||||||
|
|
||||||
|
init(resourceName: String) {
|
||||||
|
self.id = resourceName
|
||||||
|
self.source = .resource(resourceName)
|
||||||
|
self.altText = .init(en: "A test image included in the bundle", de: "Ein Test-Image aus dem Bundle")
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ImageSource {
|
||||||
|
case file(URL)
|
||||||
|
case resource(String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImageResource: Identifiable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImageResource: Equatable {
|
||||||
|
|
||||||
|
static func == (lhs: ImageResource, rhs: ImageResource) -> Bool {
|
||||||
|
lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImageResource {
|
||||||
|
|
||||||
|
var imageToDisplay: Image {
|
||||||
|
switch source {
|
||||||
|
case .file(let url):
|
||||||
|
return image(at: url)
|
||||||
|
case .resource(let name):
|
||||||
|
return .init(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func image(at url: URL) -> Image {
|
||||||
|
let imageData: Data
|
||||||
|
do {
|
||||||
|
imageData = try Data(contentsOf: url)
|
||||||
|
} catch {
|
||||||
|
print("Failed to load image data from \(url.path): \(error)")
|
||||||
|
return failureImage
|
||||||
|
}
|
||||||
|
guard let loadedImage = NSImage(data: imageData) else {
|
||||||
|
print("Failed to create image from \(url.path)")
|
||||||
|
return failureImage
|
||||||
|
}
|
||||||
|
self.size = loadedImage.size
|
||||||
|
return .init(nsImage: loadedImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var failureImage: SwiftUI.Image {
|
||||||
|
Image(systemSymbol: .exclamationmarkTriangle)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ImageResource {
|
||||||
|
|
||||||
|
func feedEntryImage(for language: ContentLanguage) -> FeedEntryData.Image {
|
||||||
|
.init(mainImageUrl: "images/\(id)", altText: altText.getText(for: language))
|
||||||
|
}
|
||||||
|
}
|
48
CHDataManagement/Model/LocalizedText.swift
Normal file
48
CHDataManagement/Model/LocalizedText.swift
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A simple container for localized text
|
||||||
|
|
||||||
|
final class LocalizedText: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var en: String
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var de: String
|
||||||
|
|
||||||
|
init(en: String, de: String) {
|
||||||
|
self.en = en
|
||||||
|
self.de = de
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
en
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(text: String, for language: ContentLanguage) {
|
||||||
|
switch language {
|
||||||
|
case .english: self.en = text
|
||||||
|
case .german: self.de = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getText(for language: ContentLanguage) -> String {
|
||||||
|
switch language {
|
||||||
|
case .english: return en
|
||||||
|
case .german: return de
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func text(for language: ContentLanguage) -> Binding<String> {
|
||||||
|
Binding(
|
||||||
|
get: {
|
||||||
|
self.getText(for: language)
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
self.set(text: newValue, for: language)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
89
CHDataManagement/Model/Page.swift
Normal file
89
CHDataManagement/Model/Page.swift
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class Page: ObservableObject {
|
||||||
|
|
||||||
|
/**
|
||||||
|
The unique id of the entry
|
||||||
|
*/
|
||||||
|
@Published
|
||||||
|
var id: String
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var isDraft: Bool
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var metadata: [LocalizedPage]
|
||||||
|
|
||||||
|
/**
|
||||||
|
All files which may occur in content but is stored externally.
|
||||||
|
|
||||||
|
Missing files which would otherwise produce a warning are ignored when included here.
|
||||||
|
- Note: This property defaults to an empty set.
|
||||||
|
*/
|
||||||
|
@Published
|
||||||
|
var externalFiles: Set<String> = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
Specifies additional files which should be copied to the destination when generating the content.
|
||||||
|
- Note: This property defaults to an empty set.
|
||||||
|
*/
|
||||||
|
@Published
|
||||||
|
var requiredFiles: Set<String> = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
Additional images required by the element.
|
||||||
|
|
||||||
|
These images are specified as: `source_name destination_name width (height)`.
|
||||||
|
*/
|
||||||
|
@Published
|
||||||
|
var images: Set<String> = []
|
||||||
|
|
||||||
|
init(id: String, isDraft: Bool, metadata: [LocalizedPage], externalFiles: Set<String> = [], requiredFiles: Set<String> = [], images: Set<String> = []) {
|
||||||
|
self.id = id
|
||||||
|
self.isDraft = isDraft
|
||||||
|
self.metadata = metadata
|
||||||
|
self.externalFiles = externalFiles
|
||||||
|
self.requiredFiles = requiredFiles
|
||||||
|
self.images = images
|
||||||
|
}
|
||||||
|
|
||||||
|
func metadata(for language: ContentLanguage) -> LocalizedPage? {
|
||||||
|
metadata.first { $0.language == language }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LocalizedPage {
|
||||||
|
|
||||||
|
let language: ContentLanguage
|
||||||
|
|
||||||
|
/**
|
||||||
|
The string to use when creating the url for the page.
|
||||||
|
|
||||||
|
Defaults to ``id`` if unset.
|
||||||
|
*/
|
||||||
|
var urlString: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The headline to use when showing the entry on it's own page
|
||||||
|
*/
|
||||||
|
var headline: String
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Page: Identifiable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Page: Equatable {
|
||||||
|
|
||||||
|
static func == (lhs: Page, rhs: Page) -> Bool {
|
||||||
|
lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Page: Hashable {
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
}
|
170
CHDataManagement/Model/Post.swift
Normal file
170
CHDataManagement/Model/Post.swift
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class Post: ObservableObject {
|
||||||
|
|
||||||
|
let id: Int
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var isDraft: Bool
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var startDate: Date
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var hasEndDate: Bool
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var endDate: Date
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var tags: [Tag]
|
||||||
|
|
||||||
|
let title: LocalizedText
|
||||||
|
|
||||||
|
let text: LocalizedText
|
||||||
|
|
||||||
|
var images: [ImageResource]
|
||||||
|
|
||||||
|
/// The page linked to by this post
|
||||||
|
@Published
|
||||||
|
var linkedPage: Page?
|
||||||
|
|
||||||
|
init(id: Int,
|
||||||
|
isDraft: Bool = false,
|
||||||
|
startDate: Date,
|
||||||
|
endDate: Date? = nil,
|
||||||
|
title: LocalizedText,
|
||||||
|
text: LocalizedText,
|
||||||
|
tags: [Tag],
|
||||||
|
images: [ImageResource]) {
|
||||||
|
self.id = id
|
||||||
|
self.isDraft = isDraft
|
||||||
|
self.startDate = startDate
|
||||||
|
self.hasEndDate = endDate != nil
|
||||||
|
self.endDate = endDate ?? startDate
|
||||||
|
self.title = title
|
||||||
|
self.text = text
|
||||||
|
self.tags = tags
|
||||||
|
self.images = images
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Post: Identifiable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Post: Equatable {
|
||||||
|
|
||||||
|
static func == (lhs: Post, rhs: Post) -> Bool {
|
||||||
|
lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Post: Hashable {
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Feed entry
|
||||||
|
|
||||||
|
extension Post {
|
||||||
|
|
||||||
|
private static let englishDate: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.locale = .init(identifier: "en")
|
||||||
|
df.dateFormat = "d. MMMM yyyy"
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let germanDate: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.locale = .init(identifier: "de")
|
||||||
|
df.dateFormat = "d. MMMM yyyy"
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let englishDayAndMonth: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.locale = .init(identifier: "en")
|
||||||
|
df.dateFormat = "d. MMMM"
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let germanDayAndMonth: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.locale = .init(identifier: "de")
|
||||||
|
df.dateFormat = "d. MMMM"
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static let day: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateFormat = "d."
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
|
private static func dayAndMonth(of date: Date, in language: ContentLanguage) -> String {
|
||||||
|
switch language {
|
||||||
|
case .english: return englishDayAndMonth.string(from: date)
|
||||||
|
case .german: return germanDayAndMonth.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func dateString(for date: Date, in language: ContentLanguage) -> String {
|
||||||
|
switch language {
|
||||||
|
case .english: return englishDate.string(from: date)
|
||||||
|
case .german: return germanDate.string(from: date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func datePrefixString(in language: ContentLanguage) -> String {
|
||||||
|
guard Calendar.current.isDate(startDate, equalTo: endDate, toGranularity: .year) else {
|
||||||
|
// Different year, return full string
|
||||||
|
return startDate.formatted(date: .long, time: .omitted)
|
||||||
|
}
|
||||||
|
guard Calendar.current.isDate(startDate, equalTo: endDate, toGranularity: .month) else {
|
||||||
|
// Different month
|
||||||
|
return Post.dayAndMonth(of: startDate, in: language)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Post.day.string(from: startDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dateText(in language: ContentLanguage) -> String {
|
||||||
|
guard hasEndDate else {
|
||||||
|
return Post.dateString(for: startDate, in: language)
|
||||||
|
}
|
||||||
|
let endText = Post.dateString(for: endDate, in: language)
|
||||||
|
return "\(datePrefixString(in: language)) - \(endText)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func paragraphs(in language: ContentLanguage) -> [String] {
|
||||||
|
text
|
||||||
|
.getText(for: language)
|
||||||
|
.components(separatedBy: "\n")
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { $0 != "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkToPageInFeed(for language: ContentLanguage) -> FeedEntryData.Link? {
|
||||||
|
nil //.init(url: <#T##String#>, text: <#T##String#>)
|
||||||
|
}
|
||||||
|
|
||||||
|
func feedEntry(for language: ContentLanguage) -> FeedEntryData {
|
||||||
|
.init(
|
||||||
|
entryId: "\(id)",
|
||||||
|
title: title.getText(for: language),
|
||||||
|
textAboveTitle: dateText(in: language),
|
||||||
|
link: linkToPageInFeed(for: language),
|
||||||
|
tags: tags.map { $0.data(in: language) },
|
||||||
|
text: paragraphs(in: language),
|
||||||
|
images: images.map { $0.feedEntryImage(for: language) })
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayImages: [Image] {
|
||||||
|
images.map { $0.imageToDisplay }
|
||||||
|
}
|
||||||
|
}
|
69
CHDataManagement/Model/Tag.swift
Normal file
69
CHDataManagement/Model/Tag.swift
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class Tag: ObservableObject {
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
name.getText(for: .english).lowercased().replacingOccurrences(of: " ", with: "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var name: LocalizedText
|
||||||
|
|
||||||
|
init(en: String, de: String) {
|
||||||
|
self.name = .init(en: en, de: de)
|
||||||
|
}
|
||||||
|
|
||||||
|
var linkName: String {
|
||||||
|
id.lowercased().replacingOccurrences(of: " ", with: "-")
|
||||||
|
}
|
||||||
|
|
||||||
|
var url: String {
|
||||||
|
"/tags/\(linkName).html"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tag {
|
||||||
|
|
||||||
|
func getUrl(for language: ContentLanguage) -> String {
|
||||||
|
"/\(language.rawValue)/tags/\(id).html"
|
||||||
|
}
|
||||||
|
|
||||||
|
func data(in language: ContentLanguage) -> FeedEntryData.Tag {
|
||||||
|
.init(
|
||||||
|
name: name.getText(for: language),
|
||||||
|
url: getUrl(for: language)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tag: ExpressibleByStringLiteral {
|
||||||
|
|
||||||
|
convenience init(stringLiteral value: StringLiteralType) {
|
||||||
|
self.init(en: value.capitalized, de: value.capitalized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tag: Identifiable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tag: Equatable {
|
||||||
|
|
||||||
|
static func == (_ lhs: Tag, _ rhs: Tag) -> Bool {
|
||||||
|
lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tag: Hashable {
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Tag: Comparable {
|
||||||
|
|
||||||
|
static func < (lhs: Tag, rhs: Tag) -> Bool {
|
||||||
|
lhs.id < rhs.id
|
||||||
|
}
|
||||||
|
}
|
41
CHDataManagement/Page Elements/FeedEntry.swift
Normal file
41
CHDataManagement/Page Elements/FeedEntry.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct FeedEntry {
|
||||||
|
|
||||||
|
let data: FeedEntryData
|
||||||
|
|
||||||
|
init(data: FeedEntryData) {
|
||||||
|
self.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
func addContent(to result: inout String) {
|
||||||
|
#warning("TODO: Select CSS classes based on existence of link (hover effects, mouse pointer")
|
||||||
|
result += "<div class='card'>"
|
||||||
|
ImageGallery(id: data.entryId, images: data.images)
|
||||||
|
.addContent(to: &result)
|
||||||
|
|
||||||
|
if let url = data.link?.url {
|
||||||
|
result += "<div class=\"card-content\" onclick=\"window.location.href='\(url)'\">"
|
||||||
|
} else {
|
||||||
|
result += "<div class=\"card-content\">"
|
||||||
|
}
|
||||||
|
result += "<h3>\(data.textAboveTitle)</h3>"
|
||||||
|
if let title = data.title {
|
||||||
|
result += "<h2>\(title.htmlEscaped())</h2>"
|
||||||
|
}
|
||||||
|
if !data.tags.isEmpty {
|
||||||
|
result += "<div class=\"tags\">"
|
||||||
|
for tag in data.tags {
|
||||||
|
result += "<a class=\"tag\" href=\"\(tag.url)\">\(tag.name)</a>"
|
||||||
|
}
|
||||||
|
result += "</div>"
|
||||||
|
}
|
||||||
|
for paragraph in data.text {
|
||||||
|
result += "<p>\(paragraph)</p>"
|
||||||
|
}
|
||||||
|
if let url = data.link {
|
||||||
|
result += "<div class=\"link-center\"><div class=\"link\">\(url.text)</div></div>"
|
||||||
|
}
|
||||||
|
result += "</div></div>" // Closes card-content and card
|
||||||
|
}
|
||||||
|
}
|
44
CHDataManagement/Page Elements/FeedEntryContent.swift
Normal file
44
CHDataManagement/Page Elements/FeedEntryContent.swift
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//import Elementary
|
||||||
|
|
||||||
|
struct FeedEntryContent<Content> {
|
||||||
|
|
||||||
|
let url: String?
|
||||||
|
|
||||||
|
let inner: Content
|
||||||
|
|
||||||
|
init(url: String?, inner: Content) {
|
||||||
|
self.url = url
|
||||||
|
self.inner = inner
|
||||||
|
}
|
||||||
|
|
||||||
|
func addContent(to result: inout String, inner: () -> Void) -> Void {
|
||||||
|
if let url {
|
||||||
|
result += "<div class=\"card-content\" onclick=\"window.location.href='\(url)'\">"
|
||||||
|
} else {
|
||||||
|
result += "<div class=\"card-content\">"
|
||||||
|
}
|
||||||
|
inner()
|
||||||
|
result += "</div>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
extension FeedEntryContent: HTML where Content: HTML {
|
||||||
|
|
||||||
|
init(url: String?, @HTMLBuilder content: () -> Content) {
|
||||||
|
self.init(url: url, inner: content())
|
||||||
|
}
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
if let url {
|
||||||
|
div(.class("card-content"), .on(.click, "window.location.href='\(url)'")) {
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
div(.class("card-content")) {
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
51
CHDataManagement/Page Elements/FeedEntryData.swift
Normal file
51
CHDataManagement/Page Elements/FeedEntryData.swift
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
|
||||||
|
struct FeedEntryData {
|
||||||
|
|
||||||
|
let entryId: String
|
||||||
|
|
||||||
|
let title: String?
|
||||||
|
|
||||||
|
let textAboveTitle: String
|
||||||
|
|
||||||
|
let link: Link?
|
||||||
|
|
||||||
|
let tags: [Tag]
|
||||||
|
|
||||||
|
let text: [String]
|
||||||
|
|
||||||
|
let images: [Image]
|
||||||
|
|
||||||
|
init(entryId: String, title: String?, textAboveTitle: String, link: Link?, tags: [Tag], text: [String], images: [Image]) {
|
||||||
|
self.entryId = entryId
|
||||||
|
self.title = title
|
||||||
|
self.textAboveTitle = textAboveTitle
|
||||||
|
self.link = link
|
||||||
|
self.tags = tags
|
||||||
|
self.text = text
|
||||||
|
self.images = images
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Link {
|
||||||
|
|
||||||
|
let url: String
|
||||||
|
|
||||||
|
let text: String
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Tag {
|
||||||
|
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
let url: String
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Image {
|
||||||
|
|
||||||
|
let mainImageUrl: String
|
||||||
|
|
||||||
|
let altText: String
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
83
CHDataManagement/Page Elements/ImageGallery.swift
Normal file
83
CHDataManagement/Page Elements/ImageGallery.swift
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ImageGallery {
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
|
||||||
|
let images: [FeedEntryData.Image]
|
||||||
|
|
||||||
|
init(id: String, images: [FeedEntryData.Image]) {
|
||||||
|
self.id = id
|
||||||
|
self.images = images
|
||||||
|
}
|
||||||
|
|
||||||
|
func addContent(to result: inout String) {
|
||||||
|
guard !images.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result += "<div id=\"s\(id)\" class=\"swiper\"><div class=\"swiper-wrapper\">"
|
||||||
|
|
||||||
|
guard images.count > 1 else {
|
||||||
|
let image = images[0]
|
||||||
|
result += "<div class=\"swiper-slide\"><img src=\(image.mainImageUrl) loading=\"lazy\" alt=\"\(image.altText.htmlEscaped())\"></div>"
|
||||||
|
result += "</div></div>" // Close swiper, swiper-wrapper
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for image in images {
|
||||||
|
// TODO: Use different images based on device
|
||||||
|
result += "<div class=\"swiper-slide\">"
|
||||||
|
result += "<img src=\(image.mainImageUrl) loading=\"lazy\" alt=\"\(image.altText.htmlEscaped())\">"
|
||||||
|
result += "<div class=\"swiper-lazy-preloader swiper-lazy-preloader-white\"></div>"
|
||||||
|
result += "</div>" // Close swiper-slide
|
||||||
|
}
|
||||||
|
|
||||||
|
result += "<div class=\"swiper-button-next\"></div>"
|
||||||
|
result += "<div class=\"swiper-button-prev\"></div>"
|
||||||
|
result += "<div class=\"swiper-pagination\"></div>"
|
||||||
|
result += "</div></div>" // Close swiper, swiper-wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
static func swiperInit(id: String) -> String {
|
||||||
|
"""
|
||||||
|
var swiper\(id) = new Swiper("#\(id)", {
|
||||||
|
loop: true,
|
||||||
|
slidesPerView: 1,
|
||||||
|
spaceBetween: 30,
|
||||||
|
centeredSlides: true,
|
||||||
|
keyboard: { enabled: true },
|
||||||
|
pagination: {
|
||||||
|
el: "#\(id) .swiper-pagination",
|
||||||
|
clickable: true
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
nextEl: "#\(id) .swiper-button-next",
|
||||||
|
prevEl: "#\(id) .swiper-button-prev"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
extension ImageGallery: HTML {
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
div(.id(id), .class("swiper")) {
|
||||||
|
div(.class("swiper-wrapper")) {
|
||||||
|
for image in images {
|
||||||
|
div(.class("swiper-slide")) {
|
||||||
|
// TODO: Use different images based on device
|
||||||
|
img(.src(image.mainImageUrl), .lazyLoad)
|
||||||
|
div(.class("swiper-lazy-preloader"), .class("swiper-lazy-preloader-white")) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(.class("swiper-button-next")) { }
|
||||||
|
div(.class("swiper-button-prev")) { }
|
||||||
|
div(.class("swiper-pagination")) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
36
CHDataManagement/Page Elements/PageHead.swift
Normal file
36
CHDataManagement/Page Elements/PageHead.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import Foundation
|
||||||
|
//import Elementary
|
||||||
|
|
||||||
|
struct PageHead {
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
let description: String
|
||||||
|
|
||||||
|
var content: String {
|
||||||
|
"""
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>\(title)</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
|
||||||
|
<meta name="description" content="\(description)">
|
||||||
|
<link rel="stylesheet" href="/assets/swiper/swiper.css" />
|
||||||
|
<link rel="stylesheet" href="/assets/css/style.css" />
|
||||||
|
</head>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
extension PageHead: HTML {
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
meta(.charset(.utf8))
|
||||||
|
meta(.title(title))
|
||||||
|
meta(.name(.viewport), .content("width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"))
|
||||||
|
meta(.name(.description), .content(description))
|
||||||
|
link(.rel(.stylesheet), .href("style.css"))
|
||||||
|
link(.rel(.stylesheet), .href("swiper.css"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
79
CHDataManagement/Pages/Feed.swift
Normal file
79
CHDataManagement/Pages/Feed.swift
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct FeedNavigationLink {
|
||||||
|
|
||||||
|
let text: LocalizedText
|
||||||
|
|
||||||
|
let url: LocalizedText
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Feed {
|
||||||
|
|
||||||
|
private let navigationIconPath = "/assets/icons/ch.svg"
|
||||||
|
|
||||||
|
let language: ContentLanguage
|
||||||
|
|
||||||
|
let title: LocalizedText
|
||||||
|
|
||||||
|
let description: LocalizedText
|
||||||
|
|
||||||
|
let iconDescription: LocalizedText
|
||||||
|
|
||||||
|
let navigationItems: [FeedNavigationLink]
|
||||||
|
|
||||||
|
let posts: [FeedEntryData]
|
||||||
|
|
||||||
|
var content: String {
|
||||||
|
#warning("TODO: Split feed into multiple pages")
|
||||||
|
var result = ""
|
||||||
|
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
|
||||||
|
let head = PageHead(
|
||||||
|
title: title.getText(for: language),
|
||||||
|
description: description.getText(for: language))
|
||||||
|
result += head.content
|
||||||
|
result += "<body>"
|
||||||
|
addNavbar(to: &result)
|
||||||
|
result += "<div class=\"content\"><div style=\"height: 70px;\"></div>"
|
||||||
|
for post in posts {
|
||||||
|
FeedEntry(data: post)
|
||||||
|
.addContent(to: &result)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSwiperInits(to: &result)
|
||||||
|
result += "</div></body></html>" // Close content
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
#warning("TODO: Set correct navigation links and texts")
|
||||||
|
private func addNavbar(to result: inout String) {
|
||||||
|
result += "<nav class=\"navbar\"><div class=\"navbar-fade\"></div><div class=\"nav-center\">"
|
||||||
|
let middleIndex = navigationItems.count / 2
|
||||||
|
let leftNavigationItems = navigationItems[..<middleIndex]
|
||||||
|
let rightNavigationItems = navigationItems[middleIndex...]
|
||||||
|
|
||||||
|
for item in leftNavigationItems {
|
||||||
|
result += "<a class=\"nav-animate\" href=\"\(item.url.getText(for: language))\">\(item.text.getText(for: language))</a>"
|
||||||
|
}
|
||||||
|
|
||||||
|
result += "<a id=\"nav-image\" href=\"/\">"
|
||||||
|
result += "<img class=\"navbar-icon\" src=\"\(navigationIconPath)\" alt=\"\(iconDescription.getText(for: language))\">"
|
||||||
|
|
||||||
|
for item in rightNavigationItems {
|
||||||
|
result += "<a class=\"nav-animate\" href=\"\(item.url.getText(for: language))\">\(item.text.getText(for: language))</a>"
|
||||||
|
}
|
||||||
|
result += "</div></nav>" // Close nav-center, navbar
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addSwiperInits(to result: inout String) {
|
||||||
|
if posts.contains(where: { $0.images.count > 1 }) {
|
||||||
|
result += "<script src=\"/assets/swiper/swiper.min.js\"></script><script>"
|
||||||
|
for post in posts {
|
||||||
|
guard post.images.count > 1 else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result += ImageGallery.swiperInit(id: post.entryId)
|
||||||
|
}
|
||||||
|
result += "</script>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
19
CHDataManagement/Preview Content/MockImage.swift
Normal file
19
CHDataManagement/Preview Content/MockImage.swift
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// An image loaded from the app resources for test purposes
|
||||||
|
struct MockImage {
|
||||||
|
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
static var images: [ImageResource] {
|
||||||
|
["image1", "image2", "image3", "image4"]
|
||||||
|
.map(ImageResource.init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MockImage: ExpressibleByStringLiteral {
|
||||||
|
|
||||||
|
init(stringLiteral value: StringLiteralType) {
|
||||||
|
self.name = value
|
||||||
|
}
|
||||||
|
}
|
18
CHDataManagement/Preview Content/Page+Mock.swift
Normal file
18
CHDataManagement/Preview Content/Page+Mock.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Page {
|
||||||
|
|
||||||
|
static var empty: Page {
|
||||||
|
.init(
|
||||||
|
id: "my-id",
|
||||||
|
isDraft: true,
|
||||||
|
metadata: [
|
||||||
|
.init(language: .english, headline: "Title"),
|
||||||
|
.init(language: .german, headline: "Titel")
|
||||||
|
],
|
||||||
|
externalFiles: [],
|
||||||
|
requiredFiles: [],
|
||||||
|
images: [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
53
CHDataManagement/Preview Content/Post+Mock.swift
Normal file
53
CHDataManagement/Preview Content/Post+Mock.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
extension Post {
|
||||||
|
|
||||||
|
static var empty: Post {
|
||||||
|
.init(id: 0,
|
||||||
|
isDraft: true,
|
||||||
|
startDate: .now,
|
||||||
|
title: .init(en: "The title", de: "Der Titel"),
|
||||||
|
text: .init(en: "", de: ""),
|
||||||
|
tags: [],
|
||||||
|
images: [])
|
||||||
|
}
|
||||||
|
|
||||||
|
static var mock: Post {
|
||||||
|
Post(
|
||||||
|
id: 1,
|
||||||
|
isDraft: false,
|
||||||
|
startDate: .now,
|
||||||
|
endDate: nil,
|
||||||
|
title: .init(en: "The title", de: "Der Titel"),
|
||||||
|
text: .init(
|
||||||
|
en: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
|
||||||
|
de: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend."
|
||||||
|
),
|
||||||
|
tags: [
|
||||||
|
Tag(en: "Nature", de: "Natur"),
|
||||||
|
Tag(en: "Sports", de: "Sport"),
|
||||||
|
Tag(en: "Hiking", de: "Wandern")
|
||||||
|
],
|
||||||
|
images: []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var fullMock: Post {
|
||||||
|
.init(
|
||||||
|
id: 2,
|
||||||
|
isDraft: true,
|
||||||
|
startDate: .now.addingTimeInterval(-86400), endDate: .now,
|
||||||
|
title: .init(en: "A longer title", de: "Ein langer Titel"),
|
||||||
|
text: .init(
|
||||||
|
en: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
|
||||||
|
de: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend."
|
||||||
|
),
|
||||||
|
tags: [
|
||||||
|
Tag(en: "Nature", de: "Natur"),
|
||||||
|
Tag(en: "Sports", de: "Sport"),
|
||||||
|
Tag(en: "Hiking", de: "Wandern"),
|
||||||
|
Tag(en: "Mountains", de: "Berge")
|
||||||
|
],
|
||||||
|
images: MockImage.images
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
21
CHDataManagement/Preview Content/Preview Assets.xcassets/image1.imageset/Contents.json
vendored
Normal file
21
CHDataManagement/Preview Content/Preview Assets.xcassets/image1.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "image1.jpg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
CHDataManagement/Preview Content/Preview Assets.xcassets/image1.imageset/image1.jpg
vendored
Normal file
BIN
CHDataManagement/Preview Content/Preview Assets.xcassets/image1.imageset/image1.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.6 MiB |
21
CHDataManagement/Preview Content/Preview Assets.xcassets/image2.imageset/Contents.json
vendored
Normal file
21
CHDataManagement/Preview Content/Preview Assets.xcassets/image2.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "image2.jpg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
CHDataManagement/Preview Content/Preview Assets.xcassets/image2.imageset/image2.jpg
vendored
Normal file
BIN
CHDataManagement/Preview Content/Preview Assets.xcassets/image2.imageset/image2.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 MiB |
21
CHDataManagement/Preview Content/Preview Assets.xcassets/image3.imageset/Contents.json
vendored
Normal file
21
CHDataManagement/Preview Content/Preview Assets.xcassets/image3.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "image3.jpg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
CHDataManagement/Preview Content/Preview Assets.xcassets/image3.imageset/image3.jpg
vendored
Normal file
BIN
CHDataManagement/Preview Content/Preview Assets.xcassets/image3.imageset/image3.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 MiB |
21
CHDataManagement/Preview Content/Preview Assets.xcassets/image4.imageset/Contents.json
vendored
Normal file
21
CHDataManagement/Preview Content/Preview Assets.xcassets/image4.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "image4.jpg",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
CHDataManagement/Preview Content/Preview Assets.xcassets/image4.imageset/image4.jpg
vendored
Normal file
BIN
CHDataManagement/Preview Content/Preview Assets.xcassets/image4.imageset/image4.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 MiB |
20
CHDataManagement/Views/Files/FilesView.swift
Normal file
20
CHDataManagement/Views/Files/FilesView.swift
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FilesView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var content: Content
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
FilesView()
|
||||||
|
}
|
53
CHDataManagement/Views/Generic/FlowHStack.swift
Normal file
53
CHDataManagement/Views/Generic/FlowHStack.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FlowHStack: Layout {
|
||||||
|
|
||||||
|
var horizontalSpacing: CGFloat = 8
|
||||||
|
|
||||||
|
var verticalSpacing: CGFloat = 8
|
||||||
|
|
||||||
|
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
||||||
|
let subviewSizes = subviews.map { $0.sizeThatFits(proposal) }
|
||||||
|
let maxSubviewHeight = subviewSizes.map { $0.height }.max() ?? .zero
|
||||||
|
var currentRowWidth: CGFloat = .zero
|
||||||
|
var totalHeight: CGFloat = maxSubviewHeight
|
||||||
|
var totalWidth: CGFloat = .zero
|
||||||
|
|
||||||
|
for size in subviewSizes {
|
||||||
|
let requestedRowWidth = currentRowWidth + horizontalSpacing + size.width
|
||||||
|
let availableRowWidth = proposal.width ?? .zero
|
||||||
|
let willOverflow = requestedRowWidth > availableRowWidth
|
||||||
|
|
||||||
|
if willOverflow {
|
||||||
|
totalHeight += verticalSpacing + maxSubviewHeight
|
||||||
|
currentRowWidth = size.width
|
||||||
|
} else {
|
||||||
|
currentRowWidth = requestedRowWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
totalWidth = max(totalWidth, currentRowWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CGSize(width: totalWidth, height: totalHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
||||||
|
let subviewSizes = subviews.map { $0.sizeThatFits(proposal) }
|
||||||
|
let maxSubviewHeight = subviewSizes.map { $0.height }.max() ?? .zero
|
||||||
|
var point = CGPoint(x: bounds.minX, y: bounds.minY)
|
||||||
|
|
||||||
|
for index in subviews.indices {
|
||||||
|
let requestedWidth = point.x + subviewSizes[index].width
|
||||||
|
let availableWidth = bounds.maxX
|
||||||
|
let willOverflow = requestedWidth > availableWidth
|
||||||
|
|
||||||
|
if willOverflow {
|
||||||
|
point.x = bounds.minX
|
||||||
|
point.y += maxSubviewHeight + verticalSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
subviews[index].place(at: point, proposal: ProposedViewSize(subviewSizes[index]))
|
||||||
|
point.x += subviewSizes[index].width + horizontalSpacing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
CHDataManagement/Views/Generic/HorizontalCenter.swift
Normal file
33
CHDataManagement/Views/Generic/HorizontalCenter.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/**
|
||||||
|
A view that centers the content horizontally using an `HStack`
|
||||||
|
*/
|
||||||
|
struct HorizontalCenter<Content> : View where Content : View {
|
||||||
|
|
||||||
|
let alignment: VerticalAlignment
|
||||||
|
|
||||||
|
let spacing: CGFloat?
|
||||||
|
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content) {
|
||||||
|
self.alignment = alignment
|
||||||
|
self.spacing = spacing
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: alignment, spacing: spacing) {
|
||||||
|
Spacer()
|
||||||
|
content
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
HorizontalCenter {
|
||||||
|
Text("Test")
|
||||||
|
}
|
||||||
|
}
|
49
CHDataManagement/Views/Images/FlexibleColumnView.swift
Normal file
49
CHDataManagement/Views/Images/FlexibleColumnView.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FlexibleColumnView<Content, Inner>: View where Content: Identifiable, Inner: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var items: [Content]
|
||||||
|
|
||||||
|
let maximumItemWidth: CGFloat
|
||||||
|
|
||||||
|
let spacing: CGFloat
|
||||||
|
|
||||||
|
private let content: (_ item: Content, _ width: CGFloat) -> Inner
|
||||||
|
|
||||||
|
init(items: Binding<[Content]>, maximumItemWidth: CGFloat = 300, spacing: CGFloat = 20, content: @escaping (_ item: Content, _ width: CGFloat) -> Inner) {
|
||||||
|
self._items = items
|
||||||
|
self.maximumItemWidth = maximumItemWidth
|
||||||
|
self.spacing = spacing
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
let totalWidth = geometry.size.width
|
||||||
|
let columnCount = max(Int((totalWidth + spacing) / (maximumItemWidth + spacing)), 1)
|
||||||
|
let totalSpacing = spacing * CGFloat(columnCount + 1)
|
||||||
|
let trueItemWidth = (totalWidth - totalSpacing) / CGFloat(columnCount)
|
||||||
|
|
||||||
|
let columns = Array(repeating: GridItem(.flexible(), spacing: spacing), count: columnCount)
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
LazyVGrid(columns: columns, spacing: spacing) {
|
||||||
|
ForEach(items) { item in
|
||||||
|
content(item, trueItemWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(spacing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
FlexibleColumnView(items: .constant(MockImage.images), maximumItemWidth: 150) { image, width in
|
||||||
|
image.imageToDisplay
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.frame(width: width)
|
||||||
|
}
|
||||||
|
}
|
59
CHDataManagement/Views/Images/ImageDetailsView.swift
Normal file
59
CHDataManagement/Views/Images/ImageDetailsView.swift
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ImageDetailsView: View {
|
||||||
|
|
||||||
|
@Environment(\.language)
|
||||||
|
var language
|
||||||
|
|
||||||
|
let image: ImageResource
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var newId: String
|
||||||
|
|
||||||
|
init(image: ImageResource) {
|
||||||
|
self.image = image
|
||||||
|
self.newId = image.id
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Unique identifier")
|
||||||
|
.font(.headline)
|
||||||
|
HStack {
|
||||||
|
TextField("", text: $newId)
|
||||||
|
Button(action: setNewId) {
|
||||||
|
Text("Update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text("Description")
|
||||||
|
.font(.headline)
|
||||||
|
TextField("", text: image.altText.text(for: language))
|
||||||
|
Text("Info")
|
||||||
|
.font(.headline)
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Original Size")
|
||||||
|
Text("Aspect ratio")
|
||||||
|
}
|
||||||
|
VStack(alignment: .trailing) {
|
||||||
|
Text("\(Int(image.size.width)) x \(Int(image.size.height))")
|
||||||
|
Text("\(image.aspectRatio)")
|
||||||
|
}
|
||||||
|
}.padding(.vertical)
|
||||||
|
Text("Versions")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setNewId() {
|
||||||
|
#warning("Check if ID is unique")
|
||||||
|
// TODO: Clean id
|
||||||
|
image.id = newId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ImageDetailsView(image: MockImage.images.first!)
|
||||||
|
}
|
62
CHDataManagement/Views/Images/ImagesView.swift
Normal file
62
CHDataManagement/Views/Images/ImagesView.swift
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct ImagesView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var content: Content
|
||||||
|
|
||||||
|
let maximumItemWidth: CGFloat = 300
|
||||||
|
|
||||||
|
let aspectRatio: CGFloat = 1.5
|
||||||
|
|
||||||
|
let spacing: CGFloat = 20
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var selectedImage: ImageResource?
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showImageDetails = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
FlexibleColumnView(items: $content.images) { image, width in
|
||||||
|
let isSelected = selectedImage == image
|
||||||
|
let borderColor: Color = isSelected ? .accentColor : .clear
|
||||||
|
return image.imageToDisplay
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.border(borderColor, width: 5)
|
||||||
|
.frame(width: width)
|
||||||
|
.onTapGesture { didTap(image: image) }
|
||||||
|
}
|
||||||
|
.inspector(isPresented: $showImageDetails) {
|
||||||
|
if let selectedImage {
|
||||||
|
ImageDetailsView(image: selectedImage)
|
||||||
|
} else {
|
||||||
|
Text("Select an image to show its details")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(action: { showImageDetails.toggle() }) {
|
||||||
|
Label("Details", systemSymbol: .infoCircle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didTap(image: ImageResource) {
|
||||||
|
if selectedImage == image {
|
||||||
|
selectedImage = nil
|
||||||
|
} else {
|
||||||
|
selectedImage = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
let content = Content()
|
||||||
|
content.images = MockImage.images
|
||||||
|
return ImagesView()
|
||||||
|
.environmentObject(content)
|
||||||
|
}
|
17
CHDataManagement/Views/Pages/PageDetailView.swift
Normal file
17
CHDataManagement/Views/Pages/PageDetailView.swift
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PageDetailView: View {
|
||||||
|
|
||||||
|
@ObservedObject var page: Page
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var language: ContentLanguage
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text(page.metadata(for: language)?.headline ?? "No headline")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PageDetailView(page: .empty, language: .constant(.english))
|
||||||
|
}
|
49
CHDataManagement/Views/Posts/DatePickerView.swift
Normal file
49
CHDataManagement/Views/Posts/DatePickerView.swift
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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))
|
||||||
|
}
|
83
CHDataManagement/Views/Posts/PostImageGalleryView.swift
Normal file
83
CHDataManagement/Views/Posts/PostImageGalleryView.swift
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
private struct NavigationIcon: View {
|
||||||
|
|
||||||
|
let symbol: SFSymbol
|
||||||
|
|
||||||
|
let edge: Edge.Set
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
SwiftUI.Image(systemSymbol: symbol)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.padding(5)
|
||||||
|
.padding(edge, 2)
|
||||||
|
.fontWeight(.light)
|
||||||
|
.foregroundStyle(.white)
|
||||||
|
.frame(width: 30, height: 30)
|
||||||
|
.background(Color.black.opacity(0.6).clipShape(Circle()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PostImageGalleryView: View {
|
||||||
|
|
||||||
|
let images: [Image]
|
||||||
|
|
||||||
|
@State private var currentIndex = 0
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
images[currentIndex]
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
if images.count > 1 {
|
||||||
|
HStack {
|
||||||
|
Button(action: previous) {
|
||||||
|
NavigationIcon(symbol: .chevronLeft, edge: .trailing)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding()
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
Button(action: next) {
|
||||||
|
NavigationIcon(symbol: .chevronRight, edge: .leading)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
ForEach(0..<images.count, id: \.self) { index in
|
||||||
|
Circle()
|
||||||
|
.fill(index == currentIndex ? Color.white : Color.gray) // Change color based on current index
|
||||||
|
.frame(width: 10, height: 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func previous() {
|
||||||
|
if currentIndex > 0 {
|
||||||
|
currentIndex -= 1
|
||||||
|
} else {
|
||||||
|
currentIndex = images.count - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func next() {
|
||||||
|
if currentIndex < images.count - 1 {
|
||||||
|
currentIndex += 1
|
||||||
|
} else {
|
||||||
|
currentIndex = 0 // Wrap to first image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(traits: .fixedLayout(width: 300, height: 300)) {
|
||||||
|
PostImageGalleryView(images: MockImage.images.map { $0.imageToDisplay })
|
||||||
|
}
|
75
CHDataManagement/Views/Posts/PostList.swift
Normal file
75
CHDataManagement/Views/Posts/PostList.swift
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
private struct CenteredPost<Content>: View where Content: View {
|
||||||
|
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(@ViewBuilder content: () -> Content) {
|
||||||
|
self.content = content()
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HorizontalCenter {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.listRowBackground(PostList.background)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PostList: View {
|
||||||
|
|
||||||
|
static let background = Color(r: 2, g: 15, b: 26)
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var posts: [Post]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
if posts.isEmpty {
|
||||||
|
CenteredPost {
|
||||||
|
Text("No posts yet.")
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
}
|
||||||
|
CenteredPost {
|
||||||
|
Button(action: addNewPost) {
|
||||||
|
Text("Add post")
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
}
|
||||||
|
ForEach(posts) { post in
|
||||||
|
CenteredPost {
|
||||||
|
PostView(post: post)
|
||||||
|
.frame(maxWidth: 600)
|
||||||
|
}
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowInsets(.init(top: 0, leading: 0, bottom: 30, trailing: 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.background(PostList.background)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addNewPost() {
|
||||||
|
let largestId = posts.map { $0.id }.max() ?? 0
|
||||||
|
|
||||||
|
let post = Post(
|
||||||
|
id: largestId + 1,
|
||||||
|
isDraft: true,
|
||||||
|
startDate: .now,
|
||||||
|
endDate: nil,
|
||||||
|
title: .init(en: "Title", de: "Titel"),
|
||||||
|
text: .init(en: "Text", de: "Text"),
|
||||||
|
tags: [],
|
||||||
|
images: [])
|
||||||
|
posts.insert(post, at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PostList(posts: .constant([.mock, .fullMock]))
|
||||||
|
}
|
88
CHDataManagement/Views/Posts/PostView.swift
Normal file
88
CHDataManagement/Views/Posts/PostView.swift
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PostView: View {
|
||||||
|
|
||||||
|
@Environment(\.language)
|
||||||
|
var language
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var post: Post
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showDatePicker = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .center) {
|
||||||
|
if !post.images.isEmpty {
|
||||||
|
PostImageGalleryView(images: post.displayImages)
|
||||||
|
.aspectRatio(1.33, contentMode: .fill)
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
HStack(alignment: .center, spacing: 0) {
|
||||||
|
Text(post.dateText(in: language))
|
||||||
|
.font(.system(size: 19, weight: .semibold))
|
||||||
|
.onTapGesture { showDatePicker = true }
|
||||||
|
Spacer()
|
||||||
|
Toggle("Draft", isOn: $post.isDraft)
|
||||||
|
}
|
||||||
|
.foregroundStyle(Color(r: 96, g: 186, b: 255))
|
||||||
|
TextField("", text: post.title.text(for: language))
|
||||||
|
.font(.system(size: 24, weight: .bold))
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.textFieldStyle(.plain)
|
||||||
|
.lineLimit(2)
|
||||||
|
FlowHStack {
|
||||||
|
ForEach(post.tags, id: \.id) { tag in
|
||||||
|
TagView(tag: tag.name)
|
||||||
|
.onTapGesture {
|
||||||
|
remove(tag: tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button(action: showTagList) {
|
||||||
|
SwiftUI.Image(systemSymbol: .plusCircleFill)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(1, contentMode: .fit)
|
||||||
|
.frame(height: 18)
|
||||||
|
.foregroundColor(TagView.foreground)
|
||||||
|
.opacity(0.7)
|
||||||
|
.padding(.top, 3)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
TextEditor(text: post.text.text(for: language))
|
||||||
|
.font(.body)
|
||||||
|
.foregroundStyle(Color(r: 221, g: 221, b: 221))
|
||||||
|
.textEditorStyle(.plain)
|
||||||
|
.padding(.leading, -5)
|
||||||
|
.scrollDisabled(true)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.background(Color(r: 4, g: 31, b: 52))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.sheet(isPresented: $showDatePicker) {
|
||||||
|
DatePickerView(post: post, showDatePicker: $showDatePicker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func remove(tag: Tag) {
|
||||||
|
post.tags = post.tags.filter {$0.id != tag.id }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showTagList() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview(traits: .fixedLayout(width: 450, height: 600)) {
|
||||||
|
List {
|
||||||
|
PostView(post: .fullMock)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color(r: 2, g: 15, b: 26))
|
||||||
|
.environment(\.language, ContentLanguage.german)
|
||||||
|
PostView(post: .mock)
|
||||||
|
.listRowSeparator(.hidden)
|
||||||
|
.listRowBackground(Color(r: 2, g: 15, b: 26))
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
}
|
57
CHDataManagement/Views/Posts/TagView.swift
Normal file
57
CHDataManagement/Views/Posts/TagView.swift
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct TagView: View {
|
||||||
|
|
||||||
|
static let background = Color(r: 9, g: 62, b: 103)
|
||||||
|
|
||||||
|
static let foreground = Color(r: 96, g: 186, b: 255)
|
||||||
|
|
||||||
|
@Environment(\.language)
|
||||||
|
var language: ContentLanguage
|
||||||
|
|
||||||
|
let tag: LocalizedText
|
||||||
|
|
||||||
|
let icon: SFSymbol
|
||||||
|
|
||||||
|
let iconSize: CGFloat
|
||||||
|
|
||||||
|
init(tag: LocalizedText, icon: SFSymbol = .xCircleFill, iconSize: CGFloat = 12.0) {
|
||||||
|
self.tag = tag
|
||||||
|
self.icon = icon
|
||||||
|
self.iconSize = iconSize
|
||||||
|
}
|
||||||
|
|
||||||
|
static var add: TagView {
|
||||||
|
.init(tag: LocalizedText(en: "Add", de: "Mehr"), icon: .plusCircleFill)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(tag.getText(for: language))
|
||||||
|
.font(.subheadline)
|
||||||
|
.padding(.leading, 2)
|
||||||
|
SwiftUI.Image(systemSymbol: icon)
|
||||||
|
.font(.system(size: iconSize, weight: .black, design: .rounded))
|
||||||
|
.opacity(0.7)
|
||||||
|
.padding(.leading, -5)
|
||||||
|
}
|
||||||
|
.foregroundColor(TagView.foreground)
|
||||||
|
.font(.caption2)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(TagView.background)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
HStack {
|
||||||
|
TagView(tag: LocalizedText(en: "Some", de: "Etwas"))
|
||||||
|
.environment(\.language, ContentLanguage.german)
|
||||||
|
TagView(tag: LocalizedText(en: "Some", de: "Etwas"))
|
||||||
|
.environment(\.language, ContentLanguage.english)
|
||||||
|
TagView.add
|
||||||
|
}
|
||||||
|
}
|
136
CHDataManagement/Views/Settings/SettingsView.swift
Normal file
136
CHDataManagement/Views/Settings/SettingsView.swift
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
|
||||||
|
@Environment(\.language)
|
||||||
|
var language
|
||||||
|
|
||||||
|
@AppStorage("contentPath")
|
||||||
|
var contentPath: String = ""
|
||||||
|
|
||||||
|
@AppStorage("outputPath")
|
||||||
|
var outputPath: String = ""
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var content: Content
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var isSelectingContentFolder = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showFileImporter = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("Content Folder")
|
||||||
|
.font(.headline)
|
||||||
|
TextField("Content Folder", text: $contentPath)
|
||||||
|
Button(action: selectContentFolder) {
|
||||||
|
Text("Select folder")
|
||||||
|
}
|
||||||
|
Text("Output Folder")
|
||||||
|
.font(.headline)
|
||||||
|
TextField("Output Folder", text: $outputPath)
|
||||||
|
Button(action: selectOutputFolder) {
|
||||||
|
Text("Select folder")
|
||||||
|
}
|
||||||
|
Text("Feed")
|
||||||
|
.font(.headline)
|
||||||
|
Button(action: generateFeed) {
|
||||||
|
Text("Generate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.fileImporter(
|
||||||
|
isPresented: $showFileImporter,
|
||||||
|
allowedContentTypes: [.folder],
|
||||||
|
onCompletion: didSelectContentFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Folder selection
|
||||||
|
|
||||||
|
private func selectContentFolder() {
|
||||||
|
isSelectingContentFolder = true
|
||||||
|
//showFileImporter = true
|
||||||
|
savePanelUsingOpenPanel(key: "contentPathBookmark")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectOutputFolder() {
|
||||||
|
isSelectingContentFolder = false
|
||||||
|
//showFileImporter = true
|
||||||
|
savePanelUsingOpenPanel(key: "outputPathBookmark")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didSelectContentFolder(_ result: Result<URL, any Error>) {
|
||||||
|
switch result {
|
||||||
|
case .success(let url):
|
||||||
|
didSelect(folder: url)
|
||||||
|
case .failure(let error):
|
||||||
|
print("Failed to select content folder: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didSelect(folder: URL) {
|
||||||
|
let path = folder.absoluteString
|
||||||
|
.replacingOccurrences(of: "file://", with: "")
|
||||||
|
if isSelectingContentFolder {
|
||||||
|
self.contentPath = path
|
||||||
|
saveSecurityScopedBookmark(folder, key: "contentPathBookmark")
|
||||||
|
} else {
|
||||||
|
self.outputPath = path
|
||||||
|
saveSecurityScopedBookmark(folder, key: "outputPathBookmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Feed
|
||||||
|
|
||||||
|
private func generateFeed() {
|
||||||
|
guard outputPath != "" else {
|
||||||
|
print("Invalid output path")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let url = URL(fileURLWithPath: outputPath)
|
||||||
|
|
||||||
|
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||||
|
print("Missing output folder")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content.generateFeed(for: language, bookmarkKey: "outputPathBookmark")
|
||||||
|
}
|
||||||
|
|
||||||
|
func savePanelUsingOpenPanel(key: String) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
guard let url = panel.url else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
saveSecurityScopedBookmark(url, key: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveSecurityScopedBookmark(_ url: URL, key: String) {
|
||||||
|
do {
|
||||||
|
let bookmarkData = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
|
||||||
|
UserDefaults.standard.set(bookmarkData, forKey: key)
|
||||||
|
print("Security-scoped bookmark saved.")
|
||||||
|
} catch {
|
||||||
|
print("Failed to create security-scoped bookmark: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
SettingsView()
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user