Import old content, load from disk
This commit is contained in:
parent
0989f06d87
commit
943d8d962b
@ -37,6 +37,16 @@
|
|||||||
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 */; };
|
E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C532CBBF87A0060935B /* FilesView.swift */; };
|
||||||
E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */; };
|
E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */; };
|
||||||
|
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25A0B882CE4021400F33674 /* LocalizedPage.swift */; };
|
||||||
|
E2A37D0E2CE527070000979F /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D0D2CE527040000979F /* Storage.swift */; };
|
||||||
|
E2A37D112CE537800000979F /* PageFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D102CE537670000979F /* PageFile.swift */; };
|
||||||
|
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D142CE68BEA0000979F /* PostFile.swift */; };
|
||||||
|
E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D162CE73F170000979F /* TagFile.swift */; };
|
||||||
|
E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D182CEA36A40000979F /* LocalizedTag.swift */; };
|
||||||
|
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1A2CEA45530000979F /* Tag+Mock.swift */; };
|
||||||
|
E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */; };
|
||||||
|
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */; };
|
||||||
|
E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */; };
|
||||||
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 */; };
|
||||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; };
|
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B85F3A2C428F0D0047CD0C /* Post.swift */; };
|
||||||
@ -59,6 +69,7 @@
|
|||||||
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>"; };
|
||||||
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>"; };
|
||||||
|
E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.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>"; };
|
||||||
@ -82,6 +93,15 @@
|
|||||||
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>"; };
|
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>"; };
|
E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlexibleColumnView.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D0D2CE527040000979F /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D102CE537670000979F /* PageFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageFile.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D142CE68BEA0000979F /* PostFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFile.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D162CE73F170000979F /* TagFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagFile.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D182CEA36A40000979F /* LocalizedTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedTag.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D1A2CEA45530000979F /* Tag+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tag+Mock.swift"; sourceTree = "<group>"; };
|
||||||
|
E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPost.swift; sourceTree = "<group>"; };
|
||||||
|
E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+Sorted.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>"; };
|
||||||
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>"; };
|
||||||
@ -165,6 +185,17 @@
|
|||||||
path = Files;
|
path = Files;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E2A37D0F2CE5375E0000979F /* Storage */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E2A37D162CE73F170000979F /* TagFile.swift */,
|
||||||
|
E2A37D102CE537670000979F /* PageFile.swift */,
|
||||||
|
E2A37D142CE68BEA0000979F /* PostFile.swift */,
|
||||||
|
E2A37D0D2CE527040000979F /* Storage.swift */,
|
||||||
|
);
|
||||||
|
path = Storage;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E2A9CB7F2C7E686C005C89CC /* Tags */ = {
|
E2A9CB7F2C7E686C005C89CC /* Tags */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -179,10 +210,13 @@
|
|||||||
E24252092C52C9260029FF16 /* ContentLanguage.swift */,
|
E24252092C52C9260029FF16 /* ContentLanguage.swift */,
|
||||||
E2A21C502CBBD53C0060935B /* FileResource.swift */,
|
E2A21C502CBBD53C0060935B /* FileResource.swift */,
|
||||||
E2A21C3A2CB9D9A50060935B /* ImageResource.swift */,
|
E2A21C3A2CB9D9A50060935B /* ImageResource.swift */,
|
||||||
|
E25A0B882CE4021400F33674 /* LocalizedPage.swift */,
|
||||||
E2A21C042CB176670060935B /* LocalizedText.swift */,
|
E2A21C042CB176670060935B /* LocalizedText.swift */,
|
||||||
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */,
|
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */,
|
||||||
E2B85F3A2C428F0D0047CD0C /* Post.swift */,
|
E2B85F3A2C428F0D0047CD0C /* Post.swift */,
|
||||||
|
E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */,
|
||||||
E2581DEC2C75202400F1F079 /* Tag.swift */,
|
E2581DEC2C75202400F1F079 /* Tag.swift */,
|
||||||
|
E2A37D182CEA36A40000979F /* LocalizedTag.swift */,
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -236,6 +270,8 @@
|
|||||||
E2B85F552C4BD0AD0047CD0C /* Extensions */ = {
|
E2B85F552C4BD0AD0047CD0C /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2A37D202CEA94E80000979F /* Sequence+Sorted.swift */,
|
||||||
|
E2A37D1E2CEA94330000979F /* Optional+Extensions.swift */,
|
||||||
E2A21C472CBAF8830060935B /* String+Extensions.swift */,
|
E2A21C472CBAF8830060935B /* String+Extensions.swift */,
|
||||||
E2A21C0D2CB189D70060935B /* Color+RGB.swift */,
|
E2A21C0D2CB189D70060935B /* Color+RGB.swift */,
|
||||||
E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */,
|
E2B85F562C4BD0BB0047CD0C /* Binding+Extension.swift */,
|
||||||
@ -263,6 +299,7 @@
|
|||||||
E2DD04722C276F31003BFF1F /* CHDataManagement */ = {
|
E2DD04722C276F31003BFF1F /* CHDataManagement */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2A37D0F2CE5375E0000979F /* Storage */,
|
||||||
E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */,
|
E2DD04732C276F31003BFF1F /* CHDataManagementApp.swift */,
|
||||||
E2B85F392C428F020047CD0C /* Model */,
|
E2B85F392C428F020047CD0C /* Model */,
|
||||||
E2B85F462C42C7CA0047CD0C /* Views */,
|
E2B85F462C42C7CA0047CD0C /* Views */,
|
||||||
@ -280,6 +317,7 @@
|
|||||||
E2DD047C2C276F32003BFF1F /* Preview Content */ = {
|
E2DD047C2C276F32003BFF1F /* Preview Content */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2A37D1A2CEA45530000979F /* Tag+Mock.swift */,
|
||||||
E2A21C1F2CB28ED20060935B /* MockImage.swift */,
|
E2A21C1F2CB28ED20060935B /* MockImage.swift */,
|
||||||
E2A21C292CB2AA4C0060935B /* Post+Mock.swift */,
|
E2A21C292CB2AA4C0060935B /* Post+Mock.swift */,
|
||||||
E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */,
|
E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */,
|
||||||
@ -367,22 +405,29 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */,
|
E2A21C162CB1A3C90060935B /* PostImageGalleryView.swift in Sources */,
|
||||||
|
E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */,
|
||||||
E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */,
|
E2A21C562CBBF9880060935B /* FlexibleColumnView.swift in Sources */,
|
||||||
|
E2A37D192CEA36A90000979F /* LocalizedTag.swift in Sources */,
|
||||||
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */,
|
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */,
|
||||||
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */,
|
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */,
|
||||||
|
E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */,
|
||||||
|
E2A37D112CE537800000979F /* PageFile.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 */,
|
||||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
||||||
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
|
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */,
|
||||||
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */,
|
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */,
|
||||||
|
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */,
|
||||||
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
|
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
|
||||||
E2B85F3D2C4293F80047CD0C /* Feed.swift in Sources */,
|
E2B85F3D2C4293F80047CD0C /* Feed.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 */,
|
||||||
E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */,
|
E2A21C4F2CBB29E50060935B /* ImageDetailsView.swift in Sources */,
|
||||||
|
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
|
||||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
||||||
|
E2A37D172CE73F1A0000979F /* TagFile.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 */,
|
||||||
@ -395,9 +440,12 @@
|
|||||||
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */,
|
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */,
|
||||||
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
|
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
|
||||||
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */,
|
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */,
|
||||||
|
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
|
||||||
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
|
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
|
||||||
|
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */,
|
||||||
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
|
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
|
||||||
E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */,
|
E2A21C542CBBF87A0060935B /* FilesView.swift in Sources */,
|
||||||
|
E2A37D0E2CE527070000979F /* Storage.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 */,
|
||||||
|
@ -66,6 +66,11 @@ struct CHDataManagementApp: App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func importOldContent() {
|
private func importOldContent() {
|
||||||
content.importOldContent()
|
do {
|
||||||
|
try content.loadFromDisk()
|
||||||
|
//content.importOldContent()
|
||||||
|
} catch {
|
||||||
|
print("Failed to load content: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
CHDataManagement/Extensions/Optional+Extensions.swift
Normal file
9
CHDataManagement/Extensions/Optional+Extensions.swift
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Optional {
|
||||||
|
|
||||||
|
func map<T>(_ transform: (Wrapped) throws -> T?) rethrows -> T? {
|
||||||
|
guard let self else { return nil }
|
||||||
|
return try transform(self)
|
||||||
|
}
|
||||||
|
}
|
11
CHDataManagement/Extensions/Sequence+Sorted.swift
Normal file
11
CHDataManagement/Extensions/Sequence+Sorted.swift
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Sequence {
|
||||||
|
|
||||||
|
func sorted<T>(ascending: Bool = true, using conversion: (Element) -> T) -> [Element] where T: Comparable {
|
||||||
|
guard ascending else {
|
||||||
|
return sorted { conversion($0) > conversion($1) }
|
||||||
|
}
|
||||||
|
return sorted { conversion($0) < conversion($1) }
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,71 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct ImportedContent {
|
enum FileType {
|
||||||
|
case image
|
||||||
|
case file
|
||||||
|
case video
|
||||||
|
case resource
|
||||||
|
|
||||||
let posts: [Post]
|
|
||||||
|
|
||||||
let categories: [Tag]
|
init(fileExtension: String) {
|
||||||
|
switch fileExtension.lowercased() {
|
||||||
|
case "jpg", "jpeg", "png", "gif":
|
||||||
|
self = .image
|
||||||
|
case "html", "stl", "f3d", "step", "f3z", "zip", "json", "conf", "css", "js", "cpp", "cddx", "svg", "glb", "mp3", "pdf", "swift":
|
||||||
|
self = .file
|
||||||
|
case "mp4":
|
||||||
|
self = .video
|
||||||
|
case "key", "psd":
|
||||||
|
self = .resource
|
||||||
|
default:
|
||||||
|
print("Unhandled file type: \(fileExtension)")
|
||||||
|
self = .resource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ImportedPage {
|
||||||
|
|
||||||
|
let page: PageFile
|
||||||
|
|
||||||
|
let deContentUrl: URL
|
||||||
|
|
||||||
|
let enContentUrl: URL
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct FileResource {
|
||||||
|
|
||||||
|
let type: FileType
|
||||||
|
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
init(image: String, url: URL) {
|
||||||
|
self.type = .image
|
||||||
|
self.url = url
|
||||||
|
self.name = image
|
||||||
|
}
|
||||||
|
|
||||||
|
init(type: FileType, url: URL, name: String) {
|
||||||
|
self.type = type
|
||||||
|
self.url = url
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Importer {
|
final class Importer {
|
||||||
|
|
||||||
var posts: [Post] = []
|
var posts: [String : PostFile] = [:]
|
||||||
|
|
||||||
var pages: [Page] = []
|
var pages: [String : ImportedPage] = [:]
|
||||||
|
|
||||||
var tags: [Tag] = []
|
var tags: [String : TagFile] = [:]
|
||||||
|
|
||||||
var images: [ImageResource] = []
|
var files: [String : FileResource] = [:]
|
||||||
|
|
||||||
|
var ignoredFiles: [URL] = []
|
||||||
|
|
||||||
var foldersToSearch: [(path: String, tag: String)] = [
|
var foldersToSearch: [(path: String, tag: String)] = [
|
||||||
("/Users/ch/Downloads/Website/projects/electronics", "electronics"),
|
("/Users/ch/Downloads/Website/projects/electronics", "electronics"),
|
||||||
@ -30,93 +80,250 @@ final class Importer {
|
|||||||
("/Users/ch/Downloads/Website/travel", "travel")
|
("/Users/ch/Downloads/Website/travel", "travel")
|
||||||
]
|
]
|
||||||
|
|
||||||
func importOldContent() throws {
|
func importContent() throws {
|
||||||
for (folder, tagName) in foldersToSearch {
|
for (path, name) in foldersToSearch {
|
||||||
let url = URL(filePath: folder)
|
let folder = URL(filePath: path)
|
||||||
let tag = try importTag(name: tagName, folder: url)
|
let pageFolders = try findPageFolders(in: folder)
|
||||||
try importEntries(in: url, tag: tag)
|
|
||||||
tags.append(tag)
|
let tag = try importTag(name: name, folder: folder)
|
||||||
|
|
||||||
|
for pageFolder in pageFolders {
|
||||||
|
try importEntry(at: pageFolder, tag: tag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
posts.sort { $0.startDate > $1.startDate }
|
|
||||||
//pages.sort { $0.startDate > $1.startDate }
|
|
||||||
tags.sort()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func importTag(name: String, folder: URL) throws -> Tag {
|
private func importTag(name: String, folder: URL) throws -> String {
|
||||||
let metadataUrl = folder.appending(path: "metadata.json", directoryHint: .notDirectory)
|
let metadataUrl = folder.appending(path: "metadata.json", directoryHint: .notDirectory)
|
||||||
let data = try Data(contentsOf: metadataUrl)
|
let data = try Data(contentsOf: metadataUrl)
|
||||||
let meta = try JSONDecoder().decode(ImportableTag.self, from: data)
|
let meta = try JSONDecoder().decode(ImportableTag.self, from: data)
|
||||||
|
|
||||||
return .init(
|
let thumbnailUrl = folder.appending(path: "thumbnail.jpg", directoryHint: .notDirectory)
|
||||||
en: meta.info(for: .english)!.title,
|
var thumbnail: FileResource? = nil
|
||||||
de: meta.info(for: .german)!.title)
|
if FileManager.default.fileExists(atPath: thumbnailUrl.path()) {
|
||||||
|
thumbnail = FileResource(type: .image, url: thumbnailUrl, name: "\(name)-thumbnail.jpg")
|
||||||
|
add(resource: thumbnail!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func importEntries(in folder: URL, tag: Tag) throws {
|
func makeTag(metadata: TagLanguage) throws -> LocalizedTagFile {
|
||||||
|
let language = ContentLanguage(rawValue: metadata.language)!
|
||||||
|
let originalUrl = folder
|
||||||
|
.appendingPathComponent("\(language.rawValue).html", isDirectory: false)
|
||||||
|
.path()
|
||||||
|
.replacingOccurrences(of: "/Users/ch/Downloads/Website", with: "")
|
||||||
|
|
||||||
|
return LocalizedTagFile(
|
||||||
|
urlComponent: metadata.title.lowercased().replacingOccurrences(of: " ", with: "-"),
|
||||||
|
name: metadata.title,
|
||||||
|
subtitle: metadata.subtitle,
|
||||||
|
description: metadata.description,
|
||||||
|
thumbnail: thumbnail?.name,
|
||||||
|
originalURL: originalUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
let en = meta.info(for: .english)!
|
||||||
|
let de = meta.info(for: .german)!
|
||||||
|
|
||||||
|
let tagId = en.title.lowercased().replacingOccurrences(of: " ", with: "-")
|
||||||
|
|
||||||
|
let enTag = try makeTag(metadata: en)
|
||||||
|
let deTag = try makeTag(metadata: de)
|
||||||
|
|
||||||
|
let tag = TagFile(
|
||||||
|
id: enTag.urlComponent,
|
||||||
|
german: deTag,
|
||||||
|
english: enTag)
|
||||||
|
tags[tagId] = tag
|
||||||
|
return tagId
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findPageFolders(in folder: URL) throws -> [URL] {
|
||||||
try FileManager.default
|
try FileManager.default
|
||||||
.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
|
.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
|
||||||
.filter { $0.hasDirectoryPath }
|
.filter { $0.hasDirectoryPath }
|
||||||
.forEach { try importEntry(at: $0, tag: tag) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func importEntry(at url: URL, tag: Tag) throws {
|
private func findResources(in folder: URL, pageId: String) throws -> [FileResource] {
|
||||||
|
try FileManager.default
|
||||||
|
.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
|
||||||
|
.filter { !$0.hasDirectoryPath }
|
||||||
|
.compactMap { url in
|
||||||
|
let fileName = url.lastPathComponent
|
||||||
|
let fileExtension = url.pathExtension
|
||||||
|
|
||||||
|
guard fileName != "metadata.json",
|
||||||
|
fileName != "de.md",
|
||||||
|
fileName != "en.md" else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let type = FileType(fileExtension: fileExtension)
|
||||||
|
guard type != .resource else {
|
||||||
|
self.ignoredFiles.append(url)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = pageId + "-" + fileName
|
||||||
|
|
||||||
|
return FileResource(type: type, url: url, name: name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func importEntry(at url: URL, tag: String) throws {
|
||||||
let metadataUrl = url.appending(path: "metadata.json", directoryHint: .notDirectory)
|
let metadataUrl = url.appending(path: "metadata.json", directoryHint: .notDirectory)
|
||||||
guard FileManager.default.fileExists(atPath: metadataUrl.path()) else {
|
guard FileManager.default.fileExists(atPath: metadataUrl.path()) else {
|
||||||
//print("No entry at \(url.path())")
|
print("No entry at \(url.path())")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let data = try Data(contentsOf: metadataUrl)
|
let data = try Data(contentsOf: metadataUrl)
|
||||||
let meta = try JSONDecoder().decode(GenericMetadata.self, from: data)
|
let meta = try JSONDecoder().decode(GenericMetadata.self, from: data)
|
||||||
|
|
||||||
let page = Page(
|
let pageId = meta.customId ?? url.lastPathComponent
|
||||||
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 resources = try findResources(in: url, pageId: pageId)
|
||||||
let en = meta.languages!.first { $0.language == "en" }!
|
|
||||||
|
|
||||||
let thumbnailImageName = meta.thumbnailPath ?? "thumbnail.jpg"
|
guard let languages = meta.languages else {
|
||||||
let thumbnailImageUrl = url.appending(path: thumbnailImageName, directoryHint: .notDirectory)
|
print("No languages for \(url.path())")
|
||||||
var images: [ImageResource] = []
|
return
|
||||||
if tag.id != "articles" {
|
}
|
||||||
if FileManager.default.fileExists(atPath: thumbnailImageUrl.path()) {
|
|
||||||
let thumbnail = ImageResource(
|
let externalFiles = meta.externalFiles ?? []
|
||||||
uniqueId: meta.customId ?? url.lastPathComponent,
|
let requiredFiles = meta.requiredFiles ?? []
|
||||||
altText: .init(en: "An image about \(en.title!)", de: "Ein Bild zu \(de.title!)"),
|
|
||||||
fileUrl: thumbnailImageUrl)
|
let date = meta.date!.toDate()
|
||||||
images.append(thumbnail)
|
let endDate = meta.endDate?.toDate()
|
||||||
self.images.append(thumbnail)
|
|
||||||
|
let de = languages.first { $0.language == "de" }!
|
||||||
|
let en = languages.first { $0.language == "en" }!
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func makePage(_ content: GenericMetadata.LocalizedMetadata) throws -> (LocalizedPageFile, URL, LocalizedPostFile) {
|
||||||
|
let language = ContentLanguage(rawValue: content.language!)!
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
if language == .english {
|
||||||
|
id = pageId
|
||||||
} else {
|
} else {
|
||||||
print("Thumbnail \(thumbnailImageUrl.path()) not found")
|
id = pageId + "-" + language.rawValue
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastPostId = posts.last?.id ?? 0
|
let originalUrl = url
|
||||||
|
.appendingPathComponent("\(language.rawValue).html", isDirectory: false)
|
||||||
|
.path()
|
||||||
|
.replacingOccurrences(of: "/Users/ch/Downloads/Website", with: "")
|
||||||
|
|
||||||
let post = Post(
|
var pageFiles = Set(resources.map { $0.name })
|
||||||
id: lastPostId + 1,
|
let thumbnail = try determineThumbnail(in: resources, folder: url, customPath: meta.thumbnailPath, pageId: id, language: language.rawValue)
|
||||||
isDraft: meta.state == "draft" || meta.state == "hidden",
|
if let thumbnail {
|
||||||
startDate: meta.date!.toDate(),
|
pageFiles.insert(thumbnail.name)
|
||||||
endDate: meta.endDate?.toDate(),
|
}
|
||||||
title: .init(en: en.linkPreviewTitle ?? en.title!,
|
let page = LocalizedPageFile(
|
||||||
de: de.linkPreviewTitle ?? de.title!),
|
url: id,
|
||||||
text: .init(en: en.linkPreviewDescription ?? en.description ?? "No description",
|
files: pageFiles,
|
||||||
de: de.linkPreviewDescription ?? de.description ?? "Keine Beschreibung"),
|
externalFiles: externalFiles,
|
||||||
|
requiredFiles: requiredFiles,
|
||||||
|
title: content.title!,
|
||||||
|
linkPreviewImage: thumbnail?.name,
|
||||||
|
linkPreviewTitle: content.linkPreviewTitle,
|
||||||
|
linkPreviewDescription: content.linkPreviewDescription,
|
||||||
|
lastModifiedDate: nil,
|
||||||
|
originalURL: originalUrl)
|
||||||
|
let contentUrl = url.appendingPathComponent("\(content.language!).md", isDirectory: false)
|
||||||
|
|
||||||
|
let postContent = content.linkPreviewDescription ?? content.description ?? ""
|
||||||
|
|
||||||
|
let post = createPost(page: page, content: postContent)
|
||||||
|
|
||||||
|
return (page, contentUrl, post)
|
||||||
|
}
|
||||||
|
let (dePage, deUrl, dePost) = try makePage(de)
|
||||||
|
let (enPage, enUrl, enPost) = try makePage(en)
|
||||||
|
|
||||||
|
let page = PageFile(
|
||||||
|
isDraft: meta.state == "draft",
|
||||||
tags: [tag],
|
tags: [tag],
|
||||||
images: images)
|
createdDate: date,
|
||||||
|
startDate: date,
|
||||||
|
endDate: endDate,
|
||||||
|
german: dePage,
|
||||||
|
english: enPage)
|
||||||
|
|
||||||
posts.append(post)
|
if pages[pageId] != nil {
|
||||||
|
print("Conflicting page id \(pageId)")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func convertPageContent(_ meta: GenericMetadata.LocalizedMetadata) -> LocalizedPage {
|
pages[pageId] = .init(page: page, deContentUrl: deUrl, enContentUrl: enUrl)
|
||||||
.init(language: ContentLanguage(rawValue: meta.language!)!,
|
|
||||||
urlString: nil,
|
|
||||||
headline: meta.title!)
|
for resource in resources {
|
||||||
|
add(resource: resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
let post = PostFile(
|
||||||
|
isDraft: page.isDraft || meta.state == "hidden",
|
||||||
|
createdDate: page.createdDate,
|
||||||
|
startDate: page.startDate,
|
||||||
|
endDate: page.endDate,
|
||||||
|
tags: page.tags,
|
||||||
|
german: dePost,
|
||||||
|
english: enPost,
|
||||||
|
linkedPageId: pageId)
|
||||||
|
|
||||||
|
posts[pageId] = post
|
||||||
|
}
|
||||||
|
|
||||||
|
private func add(resource: FileResource) {
|
||||||
|
guard let existingFile = files[resource.name] else {
|
||||||
|
files[resource.name] = resource
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard existingFile.url != resource.url else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
print("Conflicting name for file \(resource.name)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineThumbnail(in resources: [FileResource], folder: URL, customPath: String?, pageId: String, language: String) throws -> FileResource? {
|
||||||
|
guard let thumbnailUrl = findThumbnailUrl(in: folder, customPath: customPath, language: language) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return resources.first { $0.url == thumbnailUrl }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func determineThumbnail(in folder: URL, customPath: String?, pageId: String, language: String) throws -> FileResource? {
|
||||||
|
guard let thumbnailUrl = findThumbnailUrl(in: folder, customPath: customPath, language: language) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let id = pageId + "-" + thumbnailUrl.lastPathComponent
|
||||||
|
return FileResource(image: id, url: thumbnailUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findThumbnailUrl(in folder: URL, customPath: String?, language: String) -> URL? {
|
||||||
|
if let customPath {
|
||||||
|
return folder.appending(path: customPath, directoryHint: .notDirectory)
|
||||||
|
}
|
||||||
|
let thumbnailImageUrl = folder.appending(path: "thumbnail.jpg", directoryHint: .notDirectory)
|
||||||
|
if FileManager.default.fileExists(atPath: thumbnailImageUrl.path()) {
|
||||||
|
return thumbnailImageUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
let localizedThumbnail = folder.appending(path: "thumbnail-\(language).jpg", directoryHint: .notDirectory)
|
||||||
|
if FileManager.default.fileExists(atPath: localizedThumbnail.path()) {
|
||||||
|
return localizedThumbnail
|
||||||
|
}
|
||||||
|
print("No thumbnail found in \(folder.path())")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func createPost(page: LocalizedPageFile, content: String) -> LocalizedPostFile {
|
||||||
|
let images = page.linkPreviewImage.map { [$0] } ?? []
|
||||||
|
|
||||||
|
return LocalizedPostFile(
|
||||||
|
images: Set(images),
|
||||||
|
title: page.linkPreviewTitle ?? page.title,
|
||||||
|
content: content,
|
||||||
|
lastModifiedDate: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
final class Content: ObservableObject {
|
final class Content: ObservableObject {
|
||||||
|
|
||||||
@ -17,6 +18,9 @@ final class Content: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
var files: [FileResources] = []
|
var files: [FileResources] = []
|
||||||
|
|
||||||
|
@AppStorage("contentPath")
|
||||||
|
var contentPath: String = ""
|
||||||
|
|
||||||
func generateFeed(for language: ContentLanguage, bookmarkKey: String) {
|
func generateFeed(for language: ContentLanguage, bookmarkKey: String) {
|
||||||
let posts = posts.map { $0.feedEntry(for: language) }
|
let posts = posts.map { $0.feedEntry(for: language) }
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
@ -56,19 +60,178 @@ final class Content: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func importOldContent() {
|
func importOldContent() {
|
||||||
let importer = Importer()
|
let storage = Storage(baseFolder: URL(filePath: "/Users/ch/Downloads/Content"))
|
||||||
do {
|
do {
|
||||||
try importer.importOldContent()
|
try storage.createFolderStructure()
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
print(error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.posts = importer.posts
|
|
||||||
self.tags = importer.tags
|
let importer = Importer()
|
||||||
#warning("TODO: Copy page sources to data folder")
|
do {
|
||||||
self.pages = importer.pages
|
try importer.importContent()
|
||||||
self.images = importer.images
|
} catch {
|
||||||
#warning("TODO: Copy images to data folder")
|
print(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (_, file) in importer.files.sorted(by: { $0.key < $1.key }) {
|
||||||
|
storage.copyFile(at: file.url, fileId: file.name)
|
||||||
|
// TODO: Store alt text for image and videos
|
||||||
|
}
|
||||||
|
var missingPages: [String] = []
|
||||||
|
for (pageId, page) in importer.pages.sorted(by: { $0.key < $1.key }) {
|
||||||
|
storage.save(pageMetadata: page.page, for: pageId)
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: page.deContentUrl.path()) {
|
||||||
|
storage.copyPageContent(from: page.deContentUrl, for: pageId, language: .german)
|
||||||
|
} else {
|
||||||
|
missingPages.append(pageId + " (DE)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: page.enContentUrl.path()) {
|
||||||
|
storage.copyPageContent(from: page.enContentUrl, for: pageId, language: .english)
|
||||||
|
} else {
|
||||||
|
missingPages.append(pageId + " (EN)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (tagId, tag) in importer.tags {
|
||||||
|
storage.save(tagMetadata: tag, for: tagId)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (postId, post) in importer.posts {
|
||||||
|
storage.save(post: post, for: postId)
|
||||||
|
}
|
||||||
|
|
||||||
|
let ignoredFiles = importer.ignoredFiles
|
||||||
|
.map { $0.path() }
|
||||||
|
.sorted()
|
||||||
|
|
||||||
|
print("Ignored files:")
|
||||||
|
for file in ignoredFiles {
|
||||||
|
print(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Missing pages:")
|
||||||
|
for page in missingPages {
|
||||||
|
print(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try loadFromDisk()
|
||||||
|
} catch {
|
||||||
|
print("Failed to load from disk: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convert(_ tag: LocalizedTagFile) -> LocalizedTag {
|
||||||
|
LocalizedTag(
|
||||||
|
urlComponent: tag.urlComponent,
|
||||||
|
name: tag.name,
|
||||||
|
subtitle: tag.subtitle,
|
||||||
|
description: tag.description,
|
||||||
|
thumbnail: tag.thumbnail,
|
||||||
|
originalUrl: tag.originalURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadFromDisk() throws {
|
||||||
|
let storage = Storage(baseFolder: URL(filePath: contentPath))
|
||||||
|
|
||||||
|
let tagData = try storage.loadAllTags()
|
||||||
|
let pagesData = try storage.loadAllPages()
|
||||||
|
let postsData = try storage.loadAllPosts()
|
||||||
|
let filesData = try storage.loadAllFiles()
|
||||||
|
|
||||||
|
let tags = tagData.reduce(into: [:]) { (tags, data) in
|
||||||
|
tags[data.key] = Tag(german: convert(data.value.german),
|
||||||
|
english: convert(data.value.english))
|
||||||
|
}
|
||||||
|
|
||||||
|
let pages: [String : Page] = loadPages(pagesData, tags: tags)
|
||||||
|
|
||||||
|
let images: [String : ImageResource] = filesData.reduce(into: [:]) { dict, item in
|
||||||
|
let (file, url) = item
|
||||||
|
let ext = file.components(separatedBy: ".").last!.lowercased()
|
||||||
|
let type = FileType(fileExtension: ext)
|
||||||
|
guard type == .image else { return }
|
||||||
|
dict[file] = ImageResource(uniqueId: file, altText: .init(en: "", de: ""), fileUrl: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
let files: [FileResources] = filesData.compactMap { file, url in
|
||||||
|
let ext = file.components(separatedBy: ".").last!.lowercased()
|
||||||
|
let type = FileType(fileExtension: ext)
|
||||||
|
guard type == .file else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return FileResources(uniqueId: file, description: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
let posts = postsData.map { postId, post in
|
||||||
|
let linkedPage = post.linkedPageId.map { pages[$0] }
|
||||||
|
|
||||||
|
let german = LocalizedPost(
|
||||||
|
title: post.german.title,
|
||||||
|
content: post.german.content,
|
||||||
|
lastModified: post.german.lastModifiedDate,
|
||||||
|
images: post.german.images.compactMap { images[$0] })
|
||||||
|
|
||||||
|
let english = LocalizedPost(
|
||||||
|
title: post.english.title,
|
||||||
|
content: post.english.content,
|
||||||
|
lastModified: post.english.lastModifiedDate,
|
||||||
|
images: post.english.images.compactMap { images[$0] })
|
||||||
|
|
||||||
|
return Post(
|
||||||
|
id: postId,
|
||||||
|
isDraft: post.isDraft,
|
||||||
|
createdDate: post.createdDate,
|
||||||
|
startDate: post.startDate,
|
||||||
|
endDate: post.endDate,
|
||||||
|
tags: post.tags.map { tags[$0]! },
|
||||||
|
german: german,
|
||||||
|
english: english,
|
||||||
|
linkedPage: linkedPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tags = tags.values.sorted()
|
||||||
|
self.pages = pages.values.sorted(ascending: false) { $0.startDate }
|
||||||
|
self.files = files.sorted { $0.uniqueId }
|
||||||
|
self.images = images.values.sorted { $0.id }
|
||||||
|
self.posts = posts.sorted(ascending: false) { $0.startDate }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag]) -> [String : Page] {
|
||||||
|
pagesData.reduce(into: [:]) { pages, data in
|
||||||
|
let (pageId, page) = data
|
||||||
|
let germanPage = LocalizedPage(
|
||||||
|
urlString: page.german.url,
|
||||||
|
title: page.german.title,
|
||||||
|
lastModified: page.german.lastModifiedDate,
|
||||||
|
originalUrl: page.german.originalURL,
|
||||||
|
files: page.german.files,
|
||||||
|
externalFiles: page.german.externalFiles,
|
||||||
|
requiredFiles: page.german.requiredFiles)
|
||||||
|
|
||||||
|
let englishPage = LocalizedPage(
|
||||||
|
urlString: page.english.url,
|
||||||
|
title: page.english.title,
|
||||||
|
lastModified: page.english.lastModifiedDate,
|
||||||
|
originalUrl: page.english.originalURL,
|
||||||
|
files: page.english.files,
|
||||||
|
externalFiles: page.english.externalFiles,
|
||||||
|
requiredFiles: page.english.requiredFiles)
|
||||||
|
|
||||||
|
pages[pageId] = Page(
|
||||||
|
id: pageId,
|
||||||
|
isDraft: page.isDraft,
|
||||||
|
createdDate: page.createdDate,
|
||||||
|
startDate: page.startDate,
|
||||||
|
endDate: page.endDate,
|
||||||
|
german: germanPage,
|
||||||
|
english: englishPage,
|
||||||
|
tags: page.tags.map { tags[$0]! })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func accessFolderFromBookmark(key: String, operation: (URL) -> Void) {
|
static func accessFolderFromBookmark(key: String, operation: (URL) -> Void) {
|
||||||
@ -100,5 +263,4 @@ final class Content: ObservableObject {
|
|||||||
print("Failed to access folder: \(folderURL.path)")
|
print("Failed to access folder: \(folderURL.path)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,3 +6,7 @@ enum ContentLanguage: String {
|
|||||||
|
|
||||||
case german = "de"
|
case german = "de"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension ContentLanguage: Codable {
|
||||||
|
|
||||||
|
}
|
||||||
|
72
CHDataManagement/Model/LocalizedPage.swift
Normal file
72
CHDataManagement/Model/LocalizedPage.swift
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/**
|
||||||
|
A localized page contains the page content of a single language,
|
||||||
|
including the title, url path and required resources
|
||||||
|
|
||||||
|
*/
|
||||||
|
final class LocalizedPage: ObservableObject {
|
||||||
|
|
||||||
|
/**
|
||||||
|
The string to use when creating the url for the page.
|
||||||
|
|
||||||
|
Defaults to ``id`` if unset.
|
||||||
|
*/
|
||||||
|
@Published
|
||||||
|
var urlString: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
The headline to use when showing the entry on it's own page
|
||||||
|
*/
|
||||||
|
@Published
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var lastModified: Date?
|
||||||
|
|
||||||
|
/**
|
||||||
|
The url used on the old version of the website.
|
||||||
|
|
||||||
|
Needed to redirect links to their new locations.
|
||||||
|
*/
|
||||||
|
let originalUrl: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
All files which occur in the content and are stored.
|
||||||
|
- Note: This property defaults to an empty set.
|
||||||
|
*/
|
||||||
|
@Published
|
||||||
|
var files: Set<String> = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
All files which may occur in the content but are 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> = []
|
||||||
|
|
||||||
|
init(urlString: String,
|
||||||
|
title: String,
|
||||||
|
lastModified: Date? = nil,
|
||||||
|
originalUrl: String? = nil,
|
||||||
|
files: Set<String> = [],
|
||||||
|
externalFiles: Set<String> = [],
|
||||||
|
requiredFiles: Set<String> = []) {
|
||||||
|
self.urlString = urlString
|
||||||
|
self.title = title
|
||||||
|
self.lastModified = lastModified
|
||||||
|
self.originalUrl = originalUrl
|
||||||
|
self.files = files
|
||||||
|
self.externalFiles = externalFiles
|
||||||
|
self.requiredFiles = requiredFiles
|
||||||
|
}
|
||||||
|
}
|
54
CHDataManagement/Model/LocalizedPost.swift
Normal file
54
CHDataManagement/Model/LocalizedPost.swift
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class LocalizedPost: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var title: String
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var content: String
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var lastModified: Date?
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var images: [ImageResource]
|
||||||
|
|
||||||
|
init(title: String? = nil,
|
||||||
|
content: String,
|
||||||
|
lastModified: Date? = nil,
|
||||||
|
images: [ImageResource] = []) {
|
||||||
|
self.title = title ?? ""
|
||||||
|
self.content = content
|
||||||
|
self.lastModified = lastModified
|
||||||
|
self.images = images
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayImages: [Image] {
|
||||||
|
images.map { $0.imageToDisplay }
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func editableTitle() -> Binding<String> {
|
||||||
|
Binding(
|
||||||
|
get: {
|
||||||
|
self.title
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
self.title = newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func editableContent() -> Binding<String> {
|
||||||
|
Binding(
|
||||||
|
get: {
|
||||||
|
self.content
|
||||||
|
},
|
||||||
|
set: { newValue in
|
||||||
|
self.content = newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
48
CHDataManagement/Model/LocalizedTag.swift
Normal file
48
CHDataManagement/Model/LocalizedTag.swift
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class LocalizedTag: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var urlComponent: String
|
||||||
|
|
||||||
|
/// A custom name, different from the tag id
|
||||||
|
@Published
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var subtitle: String?
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var description: String?
|
||||||
|
|
||||||
|
/// The image id of the thumbnail
|
||||||
|
@Published
|
||||||
|
var thumbnail: String?
|
||||||
|
|
||||||
|
/// The original url in the previous site layout
|
||||||
|
let originalUrl: String?
|
||||||
|
|
||||||
|
init(urlComponent: String,
|
||||||
|
name: String,
|
||||||
|
subtitle: String? = nil,
|
||||||
|
description: String? = nil,
|
||||||
|
thumbnail: String? = nil,
|
||||||
|
originalUrl: String? = nil) {
|
||||||
|
self.urlComponent = urlComponent
|
||||||
|
self.name = name
|
||||||
|
self.subtitle = subtitle
|
||||||
|
self.description = description
|
||||||
|
self.thumbnail = thumbnail
|
||||||
|
self.originalUrl = originalUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LocalizedTag {
|
||||||
|
|
||||||
|
func data() -> FeedEntryData.Tag {
|
||||||
|
.init(
|
||||||
|
name: name,
|
||||||
|
url: "tags/\(urlComponent).html"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -12,23 +12,25 @@ final class Page: ObservableObject {
|
|||||||
var isDraft: Bool
|
var isDraft: Bool
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var metadata: [LocalizedPage]
|
var createdDate: Date
|
||||||
|
|
||||||
/**
|
|
||||||
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
|
@Published
|
||||||
var externalFiles: Set<String> = []
|
var startDate: Date
|
||||||
|
|
||||||
/**
|
|
||||||
Specifies additional files which should be copied to the destination when generating the content.
|
|
||||||
- Note: This property defaults to an empty set.
|
|
||||||
*/
|
|
||||||
@Published
|
@Published
|
||||||
var requiredFiles: Set<String> = []
|
var hasEndDate: Bool
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var endDate: Date
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var german: LocalizedPage
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var english: LocalizedPage
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var tags: [Tag]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Additional images required by the element.
|
Additional images required by the element.
|
||||||
@ -38,36 +40,31 @@ final class Page: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
var images: Set<String> = []
|
var images: Set<String> = []
|
||||||
|
|
||||||
init(id: String, isDraft: Bool, metadata: [LocalizedPage], externalFiles: Set<String> = [], requiredFiles: Set<String> = [], images: Set<String> = []) {
|
init(id: String,
|
||||||
|
isDraft: Bool,
|
||||||
|
createdDate: Date,
|
||||||
|
startDate: Date,
|
||||||
|
endDate: Date?,
|
||||||
|
german: LocalizedPage,
|
||||||
|
english: LocalizedPage,
|
||||||
|
tags: [Tag]) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.isDraft = isDraft
|
self.isDraft = isDraft
|
||||||
self.metadata = metadata
|
self.createdDate = createdDate
|
||||||
self.externalFiles = externalFiles
|
self.startDate = startDate
|
||||||
self.requiredFiles = requiredFiles
|
self.hasEndDate = endDate != nil
|
||||||
self.images = images
|
self.endDate = endDate ?? startDate
|
||||||
|
self.german = german
|
||||||
|
self.english = english
|
||||||
|
self.tags = tags
|
||||||
}
|
}
|
||||||
|
|
||||||
func metadata(for language: ContentLanguage) -> LocalizedPage? {
|
func metadata(for language: ContentLanguage) -> LocalizedPage? {
|
||||||
metadata.first { $0.language == language }
|
switch language {
|
||||||
|
case .german: return german
|
||||||
|
case .english: return english
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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: Identifiable {
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
import SwiftUI
|
import Foundation
|
||||||
|
|
||||||
final class Post: ObservableObject {
|
final class Post: ObservableObject {
|
||||||
|
|
||||||
let id: Int
|
let id: String
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var isDraft: Bool
|
var isDraft: Bool
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var createdDate: Date
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var startDate: Date
|
var startDate: Date
|
||||||
|
|
||||||
@ -19,33 +22,42 @@ final class Post: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
var tags: [Tag]
|
var tags: [Tag]
|
||||||
|
|
||||||
let title: LocalizedText
|
@Published
|
||||||
|
var german: LocalizedPost
|
||||||
|
|
||||||
let text: LocalizedText
|
@Published
|
||||||
|
var english: LocalizedPost
|
||||||
var images: [ImageResource]
|
|
||||||
|
|
||||||
/// The page linked to by this post
|
/// The page linked to by this post
|
||||||
@Published
|
@Published
|
||||||
var linkedPage: Page?
|
var linkedPage: Page?
|
||||||
|
|
||||||
init(id: Int,
|
init(id: String,
|
||||||
isDraft: Bool = false,
|
isDraft: Bool,
|
||||||
|
createdDate: Date,
|
||||||
startDate: Date,
|
startDate: Date,
|
||||||
endDate: Date? = nil,
|
endDate: Date?,
|
||||||
title: LocalizedText,
|
|
||||||
text: LocalizedText,
|
|
||||||
tags: [Tag],
|
tags: [Tag],
|
||||||
images: [ImageResource]) {
|
german: LocalizedPost,
|
||||||
|
english: LocalizedPost,
|
||||||
|
linkedPage: Page? = nil) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.isDraft = isDraft
|
self.isDraft = isDraft
|
||||||
|
self.createdDate = createdDate
|
||||||
self.startDate = startDate
|
self.startDate = startDate
|
||||||
self.hasEndDate = endDate != nil
|
self.hasEndDate = endDate != nil
|
||||||
self.endDate = endDate ?? startDate
|
self.endDate = endDate ?? startDate
|
||||||
self.title = title
|
|
||||||
self.text = text
|
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
self.images = images
|
self.german = german
|
||||||
|
self.english = english
|
||||||
|
self.linkedPage = linkedPage
|
||||||
|
}
|
||||||
|
|
||||||
|
func localized(in language: ContentLanguage) -> LocalizedPost {
|
||||||
|
switch language {
|
||||||
|
case .english: return english
|
||||||
|
case .german: return german
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,8 +154,7 @@ extension Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func paragraphs(in language: ContentLanguage) -> [String] {
|
private func paragraphs(in language: ContentLanguage) -> [String] {
|
||||||
text
|
localized(in: language).content
|
||||||
.getText(for: language)
|
|
||||||
.components(separatedBy: "\n")
|
.components(separatedBy: "\n")
|
||||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
.filter { $0 != "" }
|
.filter { $0 != "" }
|
||||||
@ -154,17 +165,16 @@ extension Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func feedEntry(for language: ContentLanguage) -> FeedEntryData {
|
func feedEntry(for language: ContentLanguage) -> FeedEntryData {
|
||||||
.init(
|
let post = localized(in: language)
|
||||||
|
return .init(
|
||||||
entryId: "\(id)",
|
entryId: "\(id)",
|
||||||
title: title.getText(for: language),
|
title: post.title,
|
||||||
textAboveTitle: dateText(in: language),
|
textAboveTitle: dateText(in: language),
|
||||||
link: linkToPageInFeed(for: language),
|
link: linkToPageInFeed(for: language),
|
||||||
tags: tags.map { $0.data(in: language) },
|
tags: tags.map { $0.data(in: language) },
|
||||||
text: paragraphs(in: language),
|
text: paragraphs(in: language),
|
||||||
images: images.map { $0.feedEntryImage(for: language) })
|
images: post.images.map {
|
||||||
}
|
$0.feedEntryImage(for: language)
|
||||||
|
})
|
||||||
var displayImages: [Image] {
|
|
||||||
images.map { $0.imageToDisplay }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,18 @@ import Foundation
|
|||||||
final class Tag: ObservableObject {
|
final class Tag: ObservableObject {
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
name.getText(for: .english).lowercased().replacingOccurrences(of: " ", with: "-")
|
english.urlComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var name: LocalizedText
|
var german: LocalizedTag
|
||||||
|
|
||||||
init(en: String, de: String) {
|
@Published
|
||||||
self.name = .init(en: en, de: de)
|
var english: LocalizedTag
|
||||||
|
|
||||||
|
init(german: LocalizedTag, english: LocalizedTag) {
|
||||||
|
self.german = german
|
||||||
|
self.english = english
|
||||||
}
|
}
|
||||||
|
|
||||||
var linkName: String {
|
var linkName: String {
|
||||||
@ -24,22 +28,13 @@ final class Tag: ObservableObject {
|
|||||||
|
|
||||||
extension Tag {
|
extension Tag {
|
||||||
|
|
||||||
func getUrl(for language: ContentLanguage) -> String {
|
|
||||||
"/\(language.rawValue)/tags/\(id).html"
|
|
||||||
}
|
|
||||||
|
|
||||||
func data(in language: ContentLanguage) -> FeedEntryData.Tag {
|
func data(in language: ContentLanguage) -> FeedEntryData.Tag {
|
||||||
.init(
|
switch language {
|
||||||
name: name.getText(for: language),
|
case .english:
|
||||||
url: getUrl(for: language)
|
return english.data()
|
||||||
)
|
case .german:
|
||||||
|
return german.data()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
extension Tag: ExpressibleByStringLiteral {
|
|
||||||
|
|
||||||
convenience init(stringLiteral value: StringLiteralType) {
|
|
||||||
self.init(en: value.capitalized, de: value.capitalized)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,13 +6,32 @@ extension Page {
|
|||||||
.init(
|
.init(
|
||||||
id: "my-id",
|
id: "my-id",
|
||||||
isDraft: true,
|
isDraft: true,
|
||||||
metadata: [
|
createdDate: Date(),
|
||||||
.init(language: .english, headline: "Title"),
|
startDate: Date().addingTimeInterval(-86400),
|
||||||
.init(language: .german, headline: "Titel")
|
endDate: nil,
|
||||||
],
|
german: .german,
|
||||||
externalFiles: [],
|
english: .english,
|
||||||
requiredFiles: [],
|
tags: [.mock])
|
||||||
images: [])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension LocalizedPage {
|
||||||
|
|
||||||
|
static let english = LocalizedPage(
|
||||||
|
urlString: "my-project",
|
||||||
|
title: "My First Project",
|
||||||
|
lastModified: nil,
|
||||||
|
originalUrl: "projects/electronics/my-first-project/en.html",
|
||||||
|
files: [],
|
||||||
|
externalFiles: [],
|
||||||
|
requiredFiles: [])
|
||||||
|
|
||||||
|
static let german = LocalizedPage(
|
||||||
|
urlString: "mein-projekt",
|
||||||
|
title: "Mein Erstes Projekt",
|
||||||
|
lastModified: nil,
|
||||||
|
originalUrl: "projects/electronics/my-first-project/de.html",
|
||||||
|
files: [],
|
||||||
|
externalFiles: [],
|
||||||
|
requiredFiles: [])
|
||||||
|
}
|
||||||
|
@ -2,52 +2,46 @@
|
|||||||
extension Post {
|
extension Post {
|
||||||
|
|
||||||
static var empty: Post {
|
static var empty: Post {
|
||||||
.init(id: 0,
|
.init(id: "empty",
|
||||||
isDraft: true,
|
isDraft: true,
|
||||||
|
createdDate: .now,
|
||||||
startDate: .now,
|
startDate: .now,
|
||||||
title: .init(en: "The title", de: "Der Titel"),
|
endDate: nil,
|
||||||
text: .init(en: "", de: ""),
|
|
||||||
tags: [],
|
tags: [],
|
||||||
images: [])
|
german: .init(content: "Text"),
|
||||||
|
english: .init(content: "Text"),
|
||||||
|
linkedPage: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
static var mock: Post {
|
static var mock: Post {
|
||||||
Post(
|
Post(
|
||||||
id: 1,
|
id: "mock",
|
||||||
isDraft: false,
|
isDraft: false,
|
||||||
|
createdDate: .now,
|
||||||
startDate: .now,
|
startDate: .now,
|
||||||
endDate: nil,
|
endDate: nil,
|
||||||
title: .init(en: "The title", de: "Der Titel"),
|
tags: [.nature, .sports, .hiking],
|
||||||
text: .init(
|
german: .init(title: "Der Titel", content: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend."),
|
||||||
en: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
|
english: .init(title: "The title", content: "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 {
|
static var fullMock: Post {
|
||||||
.init(
|
.init(
|
||||||
id: 2,
|
id: "full",
|
||||||
isDraft: true,
|
isDraft: true,
|
||||||
|
createdDate: .now,
|
||||||
startDate: .now.addingTimeInterval(-86400), endDate: .now,
|
startDate: .now.addingTimeInterval(-86400), endDate: .now,
|
||||||
title: .init(en: "A longer title", de: "Ein langer Titel"),
|
tags: [.nature, .sports, .hiking, .mountains],
|
||||||
text: .init(
|
german: .init(
|
||||||
en: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
|
title: "Ein langer Titel",
|
||||||
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."
|
content: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend.",
|
||||||
),
|
images: MockImage.images),
|
||||||
tags: [
|
english: .init(
|
||||||
Tag(en: "Nature", de: "Natur"),
|
title: "A longer title",
|
||||||
Tag(en: "Sports", de: "Sport"),
|
content: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
|
||||||
Tag(en: "Hiking", de: "Wandern"),
|
images: MockImage.images)
|
||||||
Tag(en: "Mountains", de: "Berge")
|
|
||||||
],
|
|
||||||
images: MockImage.images
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
CHDataManagement/Preview Content/Tag+Mock.swift
Normal file
47
CHDataManagement/Preview Content/Tag+Mock.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Tag {
|
||||||
|
|
||||||
|
static let mock = Tag(
|
||||||
|
german: .german,
|
||||||
|
english: .english)
|
||||||
|
|
||||||
|
static let nature = Tag(
|
||||||
|
german: .init(urlComponent: "natur", name: "Natur"),
|
||||||
|
english: .init(urlComponent: "nature", name: "Nature")
|
||||||
|
)
|
||||||
|
|
||||||
|
static let sports = Tag(
|
||||||
|
german: .init(urlComponent: "sport", name: "Sport"),
|
||||||
|
english: .init(urlComponent: "sports", name: "Sports")
|
||||||
|
)
|
||||||
|
|
||||||
|
static let hiking = Tag(
|
||||||
|
german: .init(urlComponent: "wandern", name: "Wandern"),
|
||||||
|
english: .init(urlComponent: "hiking", name: "Hiking")
|
||||||
|
)
|
||||||
|
|
||||||
|
static let mountains = Tag(
|
||||||
|
german: .init(urlComponent: "berge", name: "Berge"),
|
||||||
|
english: .init(urlComponent: "mountains", name: "Mountains")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LocalizedTag {
|
||||||
|
|
||||||
|
static let english = LocalizedTag(
|
||||||
|
urlComponent: "electronics",
|
||||||
|
name: "Electronics",
|
||||||
|
subtitle: "Projects with electronics",
|
||||||
|
description: "Some description of the tag",
|
||||||
|
thumbnail: "electronic-thumbnail.jpg",
|
||||||
|
originalUrl: "projects/electronics")
|
||||||
|
|
||||||
|
static let german = LocalizedTag(
|
||||||
|
urlComponent: "elektronik",
|
||||||
|
name: "Elektronik",
|
||||||
|
subtitle: "Projekte mit Elektronik",
|
||||||
|
description: "Eine Beschreibung des Tags",
|
||||||
|
thumbnail: "electronic-thumbnail.jpg",
|
||||||
|
originalUrl: "projects/electronics")
|
||||||
|
}
|
62
CHDataManagement/Storage/PageFile.swift
Normal file
62
CHDataManagement/Storage/PageFile.swift
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PageFile {
|
||||||
|
|
||||||
|
let isDraft: Bool
|
||||||
|
|
||||||
|
let tags: [String]
|
||||||
|
|
||||||
|
let createdDate: Date
|
||||||
|
|
||||||
|
let startDate: Date
|
||||||
|
|
||||||
|
let endDate: Date?
|
||||||
|
|
||||||
|
let german: LocalizedPageFile
|
||||||
|
|
||||||
|
let english: LocalizedPageFile
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PageFile: Codable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
The structure to store the metadata of a localized page
|
||||||
|
*/
|
||||||
|
struct LocalizedPageFile {
|
||||||
|
|
||||||
|
let url: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
The files (images, videos, other files) used in the page.
|
||||||
|
*/
|
||||||
|
let files: Set<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
The additional files required for the page to function correctly, but which are not stored with the content.
|
||||||
|
*/
|
||||||
|
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>
|
||||||
|
|
||||||
|
let title: String
|
||||||
|
|
||||||
|
let linkPreviewImage: String?
|
||||||
|
|
||||||
|
let linkPreviewTitle: String?
|
||||||
|
|
||||||
|
let linkPreviewDescription: String?
|
||||||
|
|
||||||
|
let lastModifiedDate: Date?
|
||||||
|
|
||||||
|
let originalURL: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LocalizedPageFile: Codable {
|
||||||
|
|
||||||
|
}
|
42
CHDataManagement/Storage/PostFile.swift
Normal file
42
CHDataManagement/Storage/PostFile.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PostFile {
|
||||||
|
|
||||||
|
let isDraft: Bool
|
||||||
|
|
||||||
|
let createdDate: Date
|
||||||
|
|
||||||
|
let startDate: Date
|
||||||
|
|
||||||
|
let endDate: Date?
|
||||||
|
|
||||||
|
let tags: [String]
|
||||||
|
|
||||||
|
let german: LocalizedPostFile
|
||||||
|
|
||||||
|
let english: LocalizedPostFile
|
||||||
|
|
||||||
|
let linkedPageId: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PostFile: Codable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
The structure to store the metadata of a localized post
|
||||||
|
*/
|
||||||
|
struct LocalizedPostFile {
|
||||||
|
|
||||||
|
let images: Set<String>
|
||||||
|
|
||||||
|
let title: String?
|
||||||
|
|
||||||
|
let content: String
|
||||||
|
|
||||||
|
let lastModifiedDate: Date?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LocalizedPostFile: Codable {
|
||||||
|
|
||||||
|
}
|
283
CHDataManagement/Storage/Storage.swift
Normal file
283
CHDataManagement/Storage/Storage.swift
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/**
|
||||||
|
A class that handles the storage of the website data.
|
||||||
|
|
||||||
|
BaseFolder
|
||||||
|
- pages: Contains the markdown files of the localized pages, file name is the url
|
||||||
|
- images: Contains the raw images
|
||||||
|
- files: Contains additional files
|
||||||
|
- videos: Contains raw video files
|
||||||
|
- posts: Contains the markdown files for localized posts, file name is the post id
|
||||||
|
-
|
||||||
|
*/
|
||||||
|
final class Storage {
|
||||||
|
|
||||||
|
private(set) var baseFolder: URL
|
||||||
|
|
||||||
|
private let encoder = JSONEncoder()
|
||||||
|
|
||||||
|
private let decoder = JSONDecoder()
|
||||||
|
|
||||||
|
private let fm = FileManager.default
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create the storage.
|
||||||
|
*/
|
||||||
|
init(baseFolder: URL) {
|
||||||
|
self.baseFolder = baseFolder
|
||||||
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Helper
|
||||||
|
|
||||||
|
private func subFolder(_ name: String) -> URL {
|
||||||
|
baseFolder.appending(path: name, directoryHint: .isDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func files(in folder: URL) throws -> [URL] {
|
||||||
|
do {
|
||||||
|
return try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
|
||||||
|
.filter { !$0.hasDirectoryPath }
|
||||||
|
} catch {
|
||||||
|
print("Failed to get files in folder \(folder.path): \(error)")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fileNames(in folder: URL) throws -> [String] {
|
||||||
|
try fm.contentsOfDirectory(atPath: folder.path())
|
||||||
|
.filter { !$0.hasPrefix(".") }
|
||||||
|
.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func files(in folder: URL, type: String) throws -> [URL] {
|
||||||
|
try files(in: folder).filter { $0.pathExtension == type }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Folders
|
||||||
|
|
||||||
|
func update(baseFolder: URL, moveContent: Bool) throws {
|
||||||
|
let oldFolder = self.baseFolder
|
||||||
|
self.baseFolder = baseFolder
|
||||||
|
try createFolderStructure()
|
||||||
|
guard moveContent else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: Move all files
|
||||||
|
}
|
||||||
|
|
||||||
|
private func create(folder: URL) throws {
|
||||||
|
guard !FileManager.default.fileExists(atPath: folder.path) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFolderStructure() throws {
|
||||||
|
try create(folder: pagesFolder)
|
||||||
|
try create(folder: imagesFolder)
|
||||||
|
try create(folder: filesFolder)
|
||||||
|
try create(folder: videosFolder)
|
||||||
|
try create(folder: postsFolder)
|
||||||
|
try create(folder: tagsFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Pages
|
||||||
|
|
||||||
|
/// The folder path where the markdown and metadata files of the pages are stored (by their id/url component)
|
||||||
|
private var pagesFolder: URL { subFolder("pages") }
|
||||||
|
|
||||||
|
private func pageFileUrl(pageId: String) -> URL {
|
||||||
|
pagesFolder.appending(path: pageId, directoryHint: .notDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pageContentUrl(pageId: String, language: ContentLanguage) -> URL {
|
||||||
|
pagesFolder.appending(path: "\(pageId)-\(language.rawValue).md", directoryHint: .notDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pageMetadataUrl(pageId: String) -> URL {
|
||||||
|
pagesFolder.appending(path: pageId + ".json", directoryHint: .notDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func save(pageContent: String, for pageId: String, language: ContentLanguage) -> Bool {
|
||||||
|
let contentUrl = pageContentUrl(pageId: pageId, language: language)
|
||||||
|
return write(content: pageContent, to: contentUrl, type: "page", id: pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func save(pageMetadata: PageFile, for pageId: String) -> Bool {
|
||||||
|
let contentUrl = pageMetadataUrl(pageId: pageId)
|
||||||
|
return write(pageMetadata, type: "page", id: pageId, to: contentUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func copyPageContent(from url: URL, for pageId: String, language: ContentLanguage) -> Bool {
|
||||||
|
let contentUrl = pageContentUrl(pageId: pageId, language: language)
|
||||||
|
return copy(file: url, to: contentUrl, type: "page content", id: pageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAllPages() throws -> [String : PageFile] {
|
||||||
|
try loadAll(in: pagesFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Posts
|
||||||
|
|
||||||
|
/// The folder path where the markdown files of the posts are stored (by their unique id/url component)
|
||||||
|
private var postsFolder: URL { subFolder("posts") }
|
||||||
|
|
||||||
|
private func postFileUrl(postId: String) -> URL {
|
||||||
|
postsFolder.appending(path: postId, directoryHint: .notDirectory).appendingPathExtension("json")
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func save(post: PostFile, for postId: String) -> Bool {
|
||||||
|
let contentUrl = postFileUrl(postId: postId)
|
||||||
|
return write(post, type: "post", id: postId, to: contentUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAllPosts() throws -> [String : PostFile] {
|
||||||
|
try loadAll(in: postsFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func post(at url: URL) throws -> PostFile {
|
||||||
|
try read(at: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postContent(for postId: String) throws -> PostFile {
|
||||||
|
let url = postFileUrl(postId: postId)
|
||||||
|
return try post(at: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Tags
|
||||||
|
|
||||||
|
/// The folder path where the source images are stored (by their unique name)
|
||||||
|
private var tagsFolder: URL { subFolder("tags") }
|
||||||
|
|
||||||
|
private func tagFileUrl(tagId: String) -> URL {
|
||||||
|
tagsFolder.appending(path: tagId, directoryHint: .notDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func tagMetadataUrl(tagId: String) -> URL {
|
||||||
|
tagFileUrl(tagId: tagId).appendingPathExtension("json")
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func save(tagMetadata: TagFile, for tagId: String) -> Bool {
|
||||||
|
let contentUrl = tagMetadataUrl(tagId: tagId)
|
||||||
|
return write(tagMetadata, type: "tag", id: tagId, to: contentUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAllTags() throws -> [String : TagFile] {
|
||||||
|
try loadAll(in: tagsFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Images
|
||||||
|
|
||||||
|
/// The folder path where the source images are stored (by their unique name)
|
||||||
|
private var imagesFolder: URL { subFolder("images") }
|
||||||
|
|
||||||
|
private func imageUrl(image: String) -> URL {
|
||||||
|
imagesFolder.appending(path: image, directoryHint: .notDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func copyImage(at url: URL, imageId: String) -> Bool {
|
||||||
|
let contentUrl = imageUrl(image: imageId)
|
||||||
|
return copy(file: url, to: contentUrl, type: "image", id: imageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Files
|
||||||
|
|
||||||
|
/// The folder path where other files are stored (by their unique name)
|
||||||
|
private var filesFolder: URL { subFolder("files") }
|
||||||
|
|
||||||
|
private func fileUrl(file: String) -> URL {
|
||||||
|
filesFolder.appending(path: file, directoryHint: .notDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func copyFile(at url: URL, fileId: String) -> Bool {
|
||||||
|
let contentUrl = fileUrl(file: fileId)
|
||||||
|
return copy(file: url, to: contentUrl, type: "file", id: fileId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAllFiles() throws -> [String : URL] {
|
||||||
|
try files(in: filesFolder).reduce(into: [:]) { files, url in
|
||||||
|
files[url.lastPathComponent] = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Videos
|
||||||
|
|
||||||
|
/// The folder path where source videos are stored (by their unique name)
|
||||||
|
private var videosFolder: URL { subFolder("videos") }
|
||||||
|
|
||||||
|
private func videoUrl(video: String) -> URL {
|
||||||
|
videosFolder.appending(path: video, directoryHint: .notDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func copyVideo(at url: URL, videoId: String) -> Bool {
|
||||||
|
let contentUrl = videoUrl(video: videoId)
|
||||||
|
return copy(file: url, to: contentUrl, type: "video", id: videoId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAllVideos() throws -> [String] {
|
||||||
|
try fileNames(in: videosFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Writing files
|
||||||
|
|
||||||
|
private func write<T>(_ value: T, type: String, id: String, to file: URL) -> Bool where T: Encodable {
|
||||||
|
let content: Data
|
||||||
|
do {
|
||||||
|
content = try encoder.encode(value)
|
||||||
|
} catch {
|
||||||
|
print("Failed to encode content of \(type) '\(id)': \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try content.write(to: file, options: .atomic)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
print("Failed to save content for \(type) '\(id)': \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func copy(file: URL, to destination: URL, type: String, id: String) -> Bool {
|
||||||
|
do {
|
||||||
|
try fm.copyItem(at: file, to: destination)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
print("Failed to copy content file for \(type) '\(id)': \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func write(content: String, to file: URL, type: String, id: String) -> Bool {
|
||||||
|
do {
|
||||||
|
try content.write(to: file, atomically: true, encoding: .utf8)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
print("Failed to save content for \(type) '\(id)': \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func read<T>(at url: URL) throws -> T where T: Decodable {
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
return try decoder.decode(T.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadAll<T>(in folder: URL) throws -> [String : T] where T: Decodable {
|
||||||
|
try files(in: folder, type: "json").reduce(into: [:]) { items, url in
|
||||||
|
let id = url.deletingPathExtension().lastPathComponent
|
||||||
|
let item: T = try read(at: url)
|
||||||
|
items[id] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
39
CHDataManagement/Storage/TagFile.swift
Normal file
39
CHDataManagement/Storage/TagFile.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct TagFile {
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
|
||||||
|
let german: LocalizedTagFile
|
||||||
|
|
||||||
|
let english: LocalizedTagFile
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TagFile: Codable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LocalizedTagFile {
|
||||||
|
|
||||||
|
/// The id of the tag, used also as a url component
|
||||||
|
let urlComponent: String
|
||||||
|
|
||||||
|
/// A custom name, different from the tag id
|
||||||
|
let name: String
|
||||||
|
|
||||||
|
let subtitle: String?
|
||||||
|
|
||||||
|
let description: String?
|
||||||
|
|
||||||
|
/// The image id of the thumbnail
|
||||||
|
let thumbnail: String?
|
||||||
|
|
||||||
|
/// The original url in the previous site layout
|
||||||
|
let originalURL: String?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LocalizedTagFile: Codable {
|
||||||
|
|
||||||
|
}
|
@ -8,7 +8,7 @@ struct PageDetailView: View {
|
|||||||
var language: ContentLanguage
|
var language: ContentLanguage
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text(page.metadata(for: language)?.headline ?? "No headline")
|
Text(page.metadata(for: language)?.title ?? "No headline")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,17 +55,15 @@ struct PostList: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func addNewPost() {
|
private func addNewPost() {
|
||||||
let largestId = posts.map { $0.id }.max() ?? 0
|
|
||||||
|
|
||||||
let post = Post(
|
let post = Post(
|
||||||
id: largestId + 1,
|
id: "new",
|
||||||
isDraft: true,
|
isDraft: true,
|
||||||
|
createdDate: .now,
|
||||||
startDate: .now,
|
startDate: .now,
|
||||||
endDate: nil,
|
endDate: nil,
|
||||||
title: .init(en: "Title", de: "Titel"),
|
|
||||||
text: .init(en: "Text", de: "Text"),
|
|
||||||
tags: [],
|
tags: [],
|
||||||
images: [])
|
german: .init(title: "Titel", content: "Text"),
|
||||||
|
english: .init(title: "Title", content: "Text"))
|
||||||
posts.insert(post, at: 0)
|
posts.insert(post, at: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ struct PostView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
if !post.images.isEmpty {
|
if !post.localized(in: language).images.isEmpty {
|
||||||
PostImageGalleryView(images: post.displayImages)
|
PostImageGalleryView(images: post.localized(in: language).displayImages)
|
||||||
.aspectRatio(1.33, contentMode: .fill)
|
.aspectRatio(1.33, contentMode: .fill)
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
@ -26,14 +26,17 @@ struct PostView: View {
|
|||||||
Toggle("Draft", isOn: $post.isDraft)
|
Toggle("Draft", isOn: $post.isDraft)
|
||||||
}
|
}
|
||||||
.foregroundStyle(Color(r: 96, g: 186, b: 255))
|
.foregroundStyle(Color(r: 96, g: 186, b: 255))
|
||||||
TextField("", text: post.title.text(for: language))
|
TextField("", text: post.localized(in: language).editableTitle())
|
||||||
.font(.system(size: 24, weight: .bold))
|
.font(.system(size: 24, weight: .bold))
|
||||||
.foregroundStyle(Color.white)
|
.foregroundStyle(Color.white)
|
||||||
.textFieldStyle(.plain)
|
.textFieldStyle(.plain)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
FlowHStack {
|
FlowHStack {
|
||||||
ForEach(post.tags, id: \.id) { tag in
|
ForEach(post.tags, id: \.id) { tag in
|
||||||
TagView(tag: tag.name)
|
TagView(tag: .init(
|
||||||
|
en: tag.english.name,
|
||||||
|
de: tag.german.name)
|
||||||
|
)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
remove(tag: tag)
|
remove(tag: tag)
|
||||||
}
|
}
|
||||||
@ -49,7 +52,7 @@ struct PostView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
TextEditor(text: post.text.text(for: language))
|
TextEditor(text: post.localized(in: language).editableContent())
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundStyle(Color(r: 221, g: 221, b: 221))
|
.foregroundStyle(Color(r: 221, g: 221, b: 221))
|
||||||
.textEditorStyle(.plain)
|
.textEditorStyle(.plain)
|
||||||
|
@ -52,13 +52,19 @@ struct SettingsView: View {
|
|||||||
private func selectContentFolder() {
|
private func selectContentFolder() {
|
||||||
isSelectingContentFolder = true
|
isSelectingContentFolder = true
|
||||||
//showFileImporter = true
|
//showFileImporter = true
|
||||||
savePanelUsingOpenPanel(key: "contentPathBookmark")
|
guard let url = savePanelUsingOpenPanel(key: "contentPathBookmark") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.contentPath = url.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func selectOutputFolder() {
|
private func selectOutputFolder() {
|
||||||
isSelectingContentFolder = false
|
isSelectingContentFolder = false
|
||||||
//showFileImporter = true
|
//showFileImporter = true
|
||||||
savePanelUsingOpenPanel(key: "outputPathBookmark")
|
guard let url = savePanelUsingOpenPanel(key: "outputPathBookmark") else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.outputPath = url.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func didSelectContentFolder(_ result: Result<URL, any Error>) {
|
private func didSelectContentFolder(_ result: Result<URL, any Error>) {
|
||||||
@ -99,7 +105,7 @@ struct SettingsView: View {
|
|||||||
content.generateFeed(for: language, bookmarkKey: "outputPathBookmark")
|
content.generateFeed(for: language, bookmarkKey: "outputPathBookmark")
|
||||||
}
|
}
|
||||||
|
|
||||||
func savePanelUsingOpenPanel(key: String) {
|
func savePanelUsingOpenPanel(key: String) -> URL? {
|
||||||
let panel = NSOpenPanel()
|
let panel = NSOpenPanel()
|
||||||
// Sets up so user can only select a single directory
|
// Sets up so user can only select a single directory
|
||||||
panel.canChooseFiles = false
|
panel.canChooseFiles = false
|
||||||
@ -112,12 +118,13 @@ struct SettingsView: View {
|
|||||||
let response = panel.runModal()
|
let response = panel.runModal()
|
||||||
guard response == .OK else {
|
guard response == .OK else {
|
||||||
|
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
guard let url = panel.url else {
|
guard let url = panel.url else {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
saveSecurityScopedBookmark(url, key: key)
|
saveSecurityScopedBookmark(url, key: key)
|
||||||
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveSecurityScopedBookmark(_ url: URL, key: String) {
|
func saveSecurityScopedBookmark(_ url: URL, key: String) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user