diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index bd896d8..db60bf3 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -16,21 +16,18 @@ E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; }; E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; }; E21850332CFAFA2F0090B18B /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850322CFAFA200090B18B /* Settings.swift */; }; - E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850342CFAFA570090B18B /* SettingsFile.swift */; }; E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; }; - E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */; }; E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */; }; E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */; }; E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */; }; - E22990192D0E3546009F8D77 /* ItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990182D0E3546009F8D77 /* ItemType.swift */; }; + E22990192D0E3546009F8D77 /* ItemReference.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990182D0E3546009F8D77 /* ItemReference.swift */; }; E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229901D2D0E4362009F8D77 /* LocalizedItem.swift */; }; E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */; }; - E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990212D0ED129009F8D77 /* TagOverviewFile.swift */; }; E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990232D0EDBD0009F8D77 /* HeaderElement.swift */; }; E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990252D0F5822009F8D77 /* FilePropertyView.swift */; }; E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */; }; E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990292D0F5A10009F8D77 /* DetailTitle.swift */; }; - E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902B2D0F6FC0009F8D77 /* ItemId.swift */; }; + E229902C2D0F6FC6009F8D77 /* LocalizedItemId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902B2D0F6FC0009F8D77 /* LocalizedItemId.swift */; }; E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902D2D0F7278009F8D77 /* IdPropertyView.swift */; }; E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */; }; E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990312D0F7678009F8D77 /* DatePropertyView.swift */; }; @@ -53,7 +50,6 @@ E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5082CFD964E00AEF16D /* TagContentView.swift */; }; E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */; }; E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */; }; - E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5142CFF00B900AEF16D /* Content+Load.swift */; }; E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5162CFF00F200AEF16D /* Content+Save.swift */; }; E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5182CFF035200AEF16D /* Array+Split.swift */; }; E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */; }; @@ -64,8 +60,6 @@ E25DA5272CFF745700AEF16D /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5262CFF745200AEF16D /* URL+Extensions.swift */; }; E25DA52C2CFFC3EC00AEF16D /* SDWebImageAVIFCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52B2CFFC3EC00AEF16D /* SDWebImageAVIFCoder */; }; E25DA52F2CFFC91B00AEF16D /* SDWebImageWebPCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E25DA52E2CFFC91B00AEF16D /* SDWebImageWebPCoder */; }; - E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */; }; - E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */; }; E25DA5412D00446C00AEF16D /* PostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5402D00446700AEF16D /* PostSettings.swift */; }; E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5442D00952D00AEF16D /* SettingsSection.swift */; }; E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */; }; @@ -82,7 +76,6 @@ E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58A2D020C9200AEF16D /* PageImage.swift */; }; E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58E2D02368A00AEF16D /* PageSettings.swift */; }; E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5902D023A7E00AEF16D /* IntegerField.swift */; }; - E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */; }; E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */; }; E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5982D02401A00AEF16D /* PageGenerator.swift */; }; E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA59A2D024A2900AEF16D /* DateItem.swift */; }; @@ -91,7 +84,6 @@ E29D31242D0366860051B7F4 /* TagList.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31232D0366820051B7F4 /* TagList.swift */; }; E29D31262D0370A80051B7F4 /* VideoCommand+Option.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31252D0370A50051B7F4 /* VideoCommand+Option.swift */; }; E29D31282D0371930051B7F4 /* ContentPageVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31272D0371870051B7F4 /* ContentPageVideo.swift */; }; - E29D312A2D039B090051B7F4 /* FileDescriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31292D039B050051B7F4 /* FileDescriptions.swift */; }; E29D312C2D039DB80051B7F4 /* PageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D312B2D039DB30051B7F4 /* PageDetailView.swift */; }; E29D312E2D03A0D70051B7F4 /* LocalizedPageDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D312D2D03A0CF0051B7F4 /* LocalizedPageDetailView.swift */; }; E29D31302D03A2C50051B7F4 /* DescriptionField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */; }; @@ -129,7 +121,6 @@ E29D31902D0B34870051B7F4 /* GenerationAnomaly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */; }; E29D31942D0B7D280051B7F4 /* SimpleImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31932D0B7D250051B7F4 /* SimpleImage.swift */; }; E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31952D0C18690051B7F4 /* PathSettings.swift */; }; - E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */; }; E29D319B2D0C452B0051B7F4 /* PageIssue.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D319A2D0C452B0051B7F4 /* PageIssue.swift */; }; E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D319C2D0C45B60051B7F4 /* PageIssueView.swift */; }; E29D319F2D0C46310051B7F4 /* PageIssueChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29D319E2D0C46290051B7F4 /* PageIssueChecker.swift */; }; @@ -163,9 +154,6 @@ E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C502CBBD53C0060935B /* FileResource.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 */; }; @@ -187,6 +175,14 @@ E2DD047E2C276F32003BFF1F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */; }; E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E06DFA2CA4A6570019C2AF /* Content.swift */; }; E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */; }; + E2FD1D0D2D2DBBA600B48627 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */; }; + E2FD1D192D2DC4F500B48627 /* LoadingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */; }; + E2FD1D1B2D2DC63800B48627 /* LinkPreviewDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */; }; + E2FD1D1D2D2DE31800B48627 /* ItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D1C2D2DE31600B48627 /* ItemType.swift */; }; + E2FD1D1F2D2E9CC200B48627 /* ItemId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D1E2D2E9CBE00B48627 /* ItemId.swift */; }; + E2FD1D212D2EB22900B48627 /* ModelLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D202D2EB22700B48627 /* ModelLoader.swift */; }; + E2FD1D232D2EB27000B48627 /* LoadingResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D222D2EB26C00B48627 /* LoadingResult.swift */; }; + E2FD1D252D2EBA8000B48627 /* TagOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D242D2EBA7C00B48627 /* TagOverview.swift */; }; E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */; }; E2FE0EE82D16D4A3002963B7 /* ConvertThrowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */; }; E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */; }; @@ -196,8 +192,6 @@ E2FE0EF82D1D8110002963B7 /* IconCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF72D1D810C002963B7 /* IconCommand.swift */; }; E2FE0EFA2D25AFBA002963B7 /* PageHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */; }; E2FE0EFC2D266D22002963B7 /* NavigationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */; }; - E2FE0EFE2D266DA5002963B7 /* NavigationSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */; }; - E2FE0F002D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EFF2D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift */; }; E2FE0F022D266FCB002963B7 /* LocalizedNavigationSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F012D266FCB002963B7 /* LocalizedNavigationSettings.swift */; }; E2FE0F042D267206002963B7 /* LocalizedNavigationBarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F032D2671FC002963B7 /* LocalizedNavigationBarSettingsView.swift */; }; E2FE0F062D267350002963B7 /* TextFieldPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F052D26734E002963B7 /* TextFieldPropertyView.swift */; }; @@ -209,7 +203,6 @@ E2FE0F152D26918F002963B7 /* HtmlCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F142D269188002963B7 /* HtmlCommand.swift */; }; E2FE0F172D2698D5002963B7 /* LocalizedPageId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */; }; E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F182D2723E3002963B7 /* ImageSet.swift */; }; - E2FE0F1B2D274FDF002963B7 /* LinkPreviewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1A2D274FDA002963B7 /* LinkPreviewItem.swift */; }; E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */; }; E2FE0F202D29A70E002963B7 /* Array+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */; }; E2FE0F222D2A84A0002963B7 /* VideoCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F212D2A849B002963B7 /* VideoCommand.swift */; }; @@ -223,7 +216,6 @@ E2FE0F362D2B27F9002963B7 /* BlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F352D2B27F6002963B7 /* BlockProcessor.swift */; }; E2FE0F382D2B32F4002963B7 /* SingleFilePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F372D2B32ED002963B7 /* SingleFilePlayer.swift */; }; E2FE0F3A2D2B3E4F002963B7 /* AudioPlayerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F392D2B3E4E002963B7 /* AudioPlayerSettings.swift */; }; - E2FE0F3C2D2B3F45002963B7 /* AudioPlayerSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F3B2D2B3F42002963B7 /* AudioPlayerSettingsFile.swift */; }; E2FE0F3E2D2B4225002963B7 /* AudioSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F3D2D2B4225002963B7 /* AudioSettingsDetailView.swift */; }; E2FE0F402D2B45D3002963B7 /* SwiftBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F3F2D2B45CD002963B7 /* SwiftBlock.swift */; }; E2FE0F422D2B4821002963B7 /* OtherCodeBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F412D2B480B002963B7 /* OtherCodeBlock.swift */; }; @@ -238,13 +230,11 @@ E2FE0F572D2BCFD4002963B7 /* BlockLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F562D2BCFD4002963B7 /* BlockLineProcessor.swift */; }; E2FE0F592D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F582D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift */; }; E2FE0F5B2D2BCFF2002963B7 /* KeyedBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F5A2D2BCFF2002963B7 /* KeyedBlockProcessor.swift */; }; - E2FE0F5E2D2BE190002963B7 /* FileResourceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F5D2D2BE18B002963B7 /* FileResourceFile.swift */; }; E2FE0F602D2C0422002963B7 /* VideoBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F5F2D2C041E002963B7 /* VideoBlock.swift */; }; E2FE0F622D2C0D8D002963B7 /* VersionedVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F612D2C0D8D002963B7 /* VersionedVideo.swift */; }; E2FE0F642D2C2F4D002963B7 /* ButtonBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F632D2C2F46002963B7 /* ButtonBlock.swift */; }; E2FE0F662D2C3B3A002963B7 /* LabelsBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F652D2C3B33002963B7 /* LabelsBlock.swift */; }; E2FE0F682D2D2CF6002963B7 /* LocalizedPageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F672D2D2CF0002963B7 /* LocalizedPageSettings.swift */; }; - E2FE0F6A2D2D2D55002963B7 /* LocalizedPageSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F692D2D2D4F002963B7 /* LocalizedPageSettingsFile.swift */; }; E2FE0F6C2D2D335E002963B7 /* LocalizedPageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F6B2D2D3358002963B7 /* LocalizedPageSettingsView.swift */; }; E2FE0F6E2D2D3689002963B7 /* LocalizedAudioPlayerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F6D2D2D3685002963B7 /* LocalizedAudioPlayerSettings.swift */; }; E2FE0F702D2D5235002963B7 /* DraftIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F6F2D2D5231002963B7 /* DraftIndicator.swift */; }; @@ -260,21 +250,18 @@ E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = ""; }; E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = ""; }; E21850322CFAFA200090B18B /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; - E21850342CFAFA570090B18B /* SettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFile.swift; sourceTree = ""; }; E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = ""; }; - E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Mock.swift"; sourceTree = ""; }; E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostFeedSettingsView.swift; sourceTree = ""; }; E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSelectionView.swift; sourceTree = ""; }; E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewPage.swift; sourceTree = ""; }; - E22990182D0E3546009F8D77 /* ItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = ""; }; + E22990182D0E3546009F8D77 /* ItemReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemReference.swift; sourceTree = ""; }; E229901D2D0E4362009F8D77 /* LocalizedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedItem.swift; sourceTree = ""; }; E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewDetailView.swift; sourceTree = ""; }; - E22990212D0ED129009F8D77 /* TagOverviewFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewFile.swift; sourceTree = ""; }; E22990232D0EDBD0009F8D77 /* HeaderElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderElement.swift; sourceTree = ""; }; E22990252D0F5822009F8D77 /* FilePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePropertyView.swift; sourceTree = ""; }; E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerPropertyView.swift; sourceTree = ""; }; E22990292D0F5A10009F8D77 /* DetailTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTitle.swift; sourceTree = ""; }; - E229902B2D0F6FC0009F8D77 /* ItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemId.swift; sourceTree = ""; }; + E229902B2D0F6FC0009F8D77 /* LocalizedItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedItemId.swift; sourceTree = ""; }; E229902D2D0F7278009F8D77 /* IdPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdPropertyView.swift; sourceTree = ""; }; E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolPropertyView.swift; sourceTree = ""; }; E22990312D0F7678009F8D77 /* DatePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePropertyView.swift; sourceTree = ""; }; @@ -297,7 +284,6 @@ E25DA5082CFD964E00AEF16D /* TagContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentView.swift; sourceTree = ""; }; E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTagAssignmentView.swift; sourceTree = ""; }; E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTagAssignmentView.swift; sourceTree = ""; }; - E25DA5142CFF00B900AEF16D /* Content+Load.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Load.swift"; sourceTree = ""; }; E25DA5162CFF00F200AEF16D /* Content+Save.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Save.swift"; sourceTree = ""; }; E25DA5182CFF035200AEF16D /* Array+Split.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Split.swift"; sourceTree = ""; }; E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedPageNavigation.swift; sourceTree = ""; }; @@ -306,8 +292,6 @@ E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGenerator.swift; sourceTree = ""; }; E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSSize+Scaling.swift"; sourceTree = ""; }; E25DA5262CFF745200AEF16D /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = ""; }; - E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsFile.swift; sourceTree = ""; }; - E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettingsFile.swift; sourceTree = ""; }; E25DA5402D00446700AEF16D /* PostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettings.swift; sourceTree = ""; }; E25DA5442D00952D00AEF16D /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsView.swift; sourceTree = ""; }; @@ -322,7 +306,6 @@ E25DA58A2D020C9200AEF16D /* PageImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageImage.swift; sourceTree = ""; }; E25DA58E2D02368A00AEF16D /* PageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettings.swift; sourceTree = ""; }; E25DA5902D023A7E00AEF16D /* IntegerField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerField.swift; sourceTree = ""; }; - E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsFile.swift; sourceTree = ""; }; E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsDetailView.swift; sourceTree = ""; }; E25DA5982D02401A00AEF16D /* PageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerator.swift; sourceTree = ""; }; E25DA59A2D024A2900AEF16D /* DateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateItem.swift; sourceTree = ""; }; @@ -331,7 +314,6 @@ E29D31232D0366820051B7F4 /* TagList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagList.swift; sourceTree = ""; }; E29D31252D0370A50051B7F4 /* VideoCommand+Option.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VideoCommand+Option.swift"; sourceTree = ""; }; E29D31272D0371870051B7F4 /* ContentPageVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPageVideo.swift; sourceTree = ""; }; - E29D31292D039B050051B7F4 /* FileDescriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDescriptions.swift; sourceTree = ""; }; E29D312B2D039DB30051B7F4 /* PageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageDetailView.swift; sourceTree = ""; }; E29D312D2D03A0CF0051B7F4 /* LocalizedPageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageDetailView.swift; sourceTree = ""; }; E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionField.swift; sourceTree = ""; }; @@ -369,7 +351,6 @@ E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationAnomaly.swift; sourceTree = ""; }; E29D31932D0B7D250051B7F4 /* SimpleImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleImage.swift; sourceTree = ""; }; E29D31952D0C18690051B7F4 /* PathSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettings.swift; sourceTree = ""; }; - E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettingsFile.swift; sourceTree = ""; }; E29D319A2D0C452B0051B7F4 /* PageIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssue.swift; sourceTree = ""; }; E29D319C2D0C45B60051B7F4 /* PageIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssueView.swift; sourceTree = ""; }; E29D319E2D0C46290051B7F4 /* PageIssueChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssueChecker.swift; sourceTree = ""; }; @@ -401,9 +382,6 @@ E2A21C472CBAF8830060935B /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; E2A21C502CBBD53C0060935B /* FileResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResource.swift; sourceTree = ""; }; E2A37D0D2CE527040000979F /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; - E2A37D102CE537670000979F /* PageFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageFile.swift; sourceTree = ""; }; - E2A37D142CE68BEA0000979F /* PostFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFile.swift; sourceTree = ""; }; - E2A37D162CE73F170000979F /* TagFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagFile.swift; sourceTree = ""; }; E2A37D182CEA36A40000979F /* LocalizedTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedTag.swift; sourceTree = ""; }; E2A37D1A2CEA45530000979F /* Tag+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tag+Mock.swift"; sourceTree = ""; }; E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPost.swift; sourceTree = ""; }; @@ -426,6 +404,14 @@ E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; E2E06DFA2CA4A6570019C2AF /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = ""; }; E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Page+Mock.swift"; sourceTree = ""; }; + E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = ""; }; + E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingContext.swift; sourceTree = ""; }; + E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewDetailView.swift; sourceTree = ""; }; + E2FD1D1C2D2DE31600B48627 /* ItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = ""; }; + E2FD1D1E2D2E9CBE00B48627 /* ItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemId.swift; sourceTree = ""; }; + E2FD1D202D2EB22700B48627 /* ModelLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelLoader.swift; sourceTree = ""; }; + E2FD1D222D2EB26C00B48627 /* LoadingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingResult.swift; sourceTree = ""; }; + E2FD1D242D2EBA7C00B48627 /* TagOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverview.swift; sourceTree = ""; }; E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = ""; }; E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = ""; }; E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = ""; }; @@ -435,8 +421,6 @@ E2FE0EF72D1D810C002963B7 /* IconCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconCommand.swift; sourceTree = ""; }; E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHeader.swift; sourceTree = ""; }; E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettings.swift; sourceTree = ""; }; - E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettingsFile.swift; sourceTree = ""; }; - E2FE0EFF2D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationSettingsFile.swift; sourceTree = ""; }; E2FE0F012D266FCB002963B7 /* LocalizedNavigationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationSettings.swift; sourceTree = ""; }; E2FE0F032D2671FC002963B7 /* LocalizedNavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationBarSettingsView.swift; sourceTree = ""; }; E2FE0F052D26734E002963B7 /* TextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldPropertyView.swift; sourceTree = ""; }; @@ -448,7 +432,6 @@ E2FE0F142D269188002963B7 /* HtmlCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlCommand.swift; sourceTree = ""; }; E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageId.swift; sourceTree = ""; }; E2FE0F182D2723E3002963B7 /* ImageSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSet.swift; sourceTree = ""; }; - E2FE0F1A2D274FDA002963B7 /* LinkPreviewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewItem.swift; sourceTree = ""; }; E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewGenerator.swift; sourceTree = ""; }; E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = ""; }; E2FE0F212D2A849B002963B7 /* VideoCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCommand.swift; sourceTree = ""; }; @@ -462,7 +445,6 @@ E2FE0F352D2B27F6002963B7 /* BlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockProcessor.swift; sourceTree = ""; }; E2FE0F372D2B32ED002963B7 /* SingleFilePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFilePlayer.swift; sourceTree = ""; }; E2FE0F392D2B3E4E002963B7 /* AudioPlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerSettings.swift; sourceTree = ""; }; - E2FE0F3B2D2B3F42002963B7 /* AudioPlayerSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerSettingsFile.swift; sourceTree = ""; }; E2FE0F3D2D2B4225002963B7 /* AudioSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSettingsDetailView.swift; sourceTree = ""; }; E2FE0F3F2D2B45CD002963B7 /* SwiftBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBlock.swift; sourceTree = ""; }; E2FE0F412D2B480B002963B7 /* OtherCodeBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherCodeBlock.swift; sourceTree = ""; }; @@ -477,13 +459,11 @@ E2FE0F562D2BCFD4002963B7 /* BlockLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockLineProcessor.swift; sourceTree = ""; }; E2FE0F582D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedKeyBlockProcessor.swift; sourceTree = ""; }; E2FE0F5A2D2BCFF2002963B7 /* KeyedBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedBlockProcessor.swift; sourceTree = ""; }; - E2FE0F5D2D2BE18B002963B7 /* FileResourceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResourceFile.swift; sourceTree = ""; }; E2FE0F5F2D2C041E002963B7 /* VideoBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoBlock.swift; sourceTree = ""; }; E2FE0F612D2C0D8D002963B7 /* VersionedVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionedVideo.swift; sourceTree = ""; }; E2FE0F632D2C2F46002963B7 /* ButtonBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonBlock.swift; sourceTree = ""; }; E2FE0F652D2C3B33002963B7 /* LabelsBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelsBlock.swift; sourceTree = ""; }; E2FE0F672D2D2CF0002963B7 /* LocalizedPageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageSettings.swift; sourceTree = ""; }; - E2FE0F692D2D2D4F002963B7 /* LocalizedPageSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageSettingsFile.swift; sourceTree = ""; }; E2FE0F6B2D2D3358002963B7 /* LocalizedPageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageSettingsView.swift; sourceTree = ""; }; E2FE0F6D2D2D3685002963B7 /* LocalizedAudioPlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedAudioPlayerSettings.swift; sourceTree = ""; }; E2FE0F6F2D2D5231002963B7 /* DraftIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftIndicator.swift; sourceTree = ""; }; @@ -510,47 +490,18 @@ E229901A2D0E3F09009F8D77 /* Item */ = { isa = PBXGroup; children = ( - E2FE0F1A2D274FDA002963B7 /* LinkPreviewItem.swift */, + E2FD1D1E2D2E9CBE00B48627 /* ItemId.swift */, + E2FD1D1C2D2DE31600B48627 /* ItemType.swift */, E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */, - E229902B2D0F6FC0009F8D77 /* ItemId.swift */, + E229902B2D0F6FC0009F8D77 /* LocalizedItemId.swift */, E229901D2D0E4362009F8D77 /* LocalizedItem.swift */, E29D31A22D0CC98B0051B7F4 /* Item.swift */, - E22990182D0E3546009F8D77 /* ItemType.swift */, + E22990182D0E3546009F8D77 /* ItemReference.swift */, E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */, ); path = Item; sourceTree = ""; }; - E25DA5112CFF001900AEF16D /* Model */ = { - isa = PBXGroup; - children = ( - E29D31292D039B050051B7F4 /* FileDescriptions.swift */, - E25DA5322D0041C400AEF16D /* Settings */, - E2A37D102CE537670000979F /* PageFile.swift */, - E2A37D142CE68BEA0000979F /* PostFile.swift */, - E2A37D162CE73F170000979F /* TagFile.swift */, - E2FE0F5D2D2BE18B002963B7 /* FileResourceFile.swift */, - ); - path = Model; - sourceTree = ""; - }; - E25DA5322D0041C400AEF16D /* Settings */ = { - isa = PBXGroup; - children = ( - E2FE0F3B2D2B3F42002963B7 /* AudioPlayerSettingsFile.swift */, - E2FE0EFF2D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift */, - E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */, - E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */, - E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */, - E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */, - E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */, - E2FE0F692D2D2D4F002963B7 /* LocalizedPageSettingsFile.swift */, - E21850342CFAFA570090B18B /* SettingsFile.swift */, - E22990212D0ED129009F8D77 /* TagOverviewFile.swift */, - ); - path = Settings; - sourceTree = ""; - }; E25DA53B2D0042EA00AEF16D /* Settings */ = { isa = PBXGroup; children = ( @@ -752,7 +703,6 @@ E22990492D10BB90009F8D77 /* SecurityScopeBookmark.swift */, E22990472D10B7B7009F8D77 /* StorageAccessError.swift */, E22990452D10B7A6009F8D77 /* SecurityScopeStatus.swift */, - E25DA5112CFF001900AEF16D /* Model */, E2A37D0D2CE527040000979F /* Storage.swift */, ); path = Storage; @@ -775,13 +725,14 @@ E2B85F392C428F020047CD0C /* Model */ = { isa = PBXGroup; children = ( + E2FD1D262D2EBBA300B48627 /* Loading */, + E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */, E229901A2D0E3F09009F8D77 /* Item */, E25DA53B2D0042EA00AEF16D /* Settings */, E2E06DFA2CA4A6570019C2AF /* Content.swift */, E29D31A02D0C75C50051B7F4 /* Content+Validation.swift */, E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */, E25DA5162CFF00F200AEF16D /* Content+Save.swift */, - E25DA5142CFF00B900AEF16D /* Content+Load.swift */, E24252092C52C9260029FF16 /* ContentLanguage.swift */, E25DA59A2D024A2900AEF16D /* DateItem.swift */, E21850162CEE55FB0090B18B /* FileType.swift */, @@ -793,6 +744,7 @@ E2A9CB7D2C7BCF2A005C89CC /* Page.swift */, E25A0B882CE4021400F33674 /* LocalizedPage.swift */, E29D31C22D0DBEF00051B7F4 /* Song.swift */, + E2FD1D242D2EBA7C00B48627 /* TagOverview.swift */, ); path = Model; sourceTree = ""; @@ -816,6 +768,7 @@ E2B85F462C42C7CA0047CD0C /* Views */ = { isa = PBXGroup; children = ( + E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */, E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */, E2A21C372CB9A4F10060935B /* Generic */, E2B85F4B2C4B8B7F0047CD0C /* Posts */, @@ -899,7 +852,6 @@ isa = PBXGroup; children = ( E25DA5762D018B9500AEF16D /* File+Mock.swift */, - E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */, E218500A2CEE02FA0090B18B /* Content+Mock.swift */, E2A37D1A2CEA45530000979F /* Tag+Mock.swift */, E2A21C1F2CB28ED20060935B /* MockImage.swift */, @@ -910,6 +862,16 @@ path = "Preview Content"; sourceTree = ""; }; + E2FD1D262D2EBBA300B48627 /* Loading */ = { + isa = PBXGroup; + children = ( + E2FD1D202D2EB22700B48627 /* ModelLoader.swift */, + E2FD1D222D2EB26C00B48627 /* LoadingResult.swift */, + E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */, + ); + path = Loading; + sourceTree = ""; + }; E2FE0F072D2689DC002963B7 /* Post Lists */ = { isa = PBXGroup; children = ( @@ -1103,9 +1065,10 @@ files = ( E29D31242D0366860051B7F4 /* TagList.swift in Sources */, E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */, + E2FD1D1D2D2DE31800B48627 /* ItemType.swift in Sources */, E2FE0F482D2BC7D1002963B7 /* MarkdownProcessor.swift in Sources */, - E2FE0EFE2D266DA5002963B7 /* NavigationSettingsFile.swift in Sources */, E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */, + E2FD1D252D2EBA8000B48627 /* TagOverview.swift in Sources */, E2FE0F152D26918F002963B7 /* HtmlCommand.swift in Sources */, E2FE0F202D29A70E002963B7 /* Array+Remove.swift in Sources */, E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */, @@ -1127,7 +1090,6 @@ E2FE0EF42D1D6D2E002963B7 /* GeneralIcons.swift in Sources */, E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */, E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */, - E2FE0F3C2D2B3F45002963B7 /* AudioPlayerSettingsFile.swift in Sources */, E2FE0F462D2BC777002963B7 /* MarkdownImageProcessor.swift in Sources */, E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */, E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */, @@ -1138,11 +1100,9 @@ E2FE0F042D267206002963B7 /* LocalizedNavigationBarSettingsView.swift in Sources */, E2FE0F382D2B32F4002963B7 /* SingleFilePlayer.swift in Sources */, E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */, - E2A37D112CE537800000979F /* PageFile.swift in Sources */, E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */, E2B85F452C429ED60047CD0C /* ImageGallery.swift in Sources */, E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */, - E2FE0F6A2D2D2D55002963B7 /* LocalizedPageSettingsFile.swift in Sources */, E29D31852D0AE8EE0051B7F4 /* KnownHeaderElement.swift in Sources */, E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */, E22990422D107A95009F8D77 /* ImageVersion.swift in Sources */, @@ -1160,6 +1120,7 @@ E29D31942D0B7D280051B7F4 /* SimpleImage.swift in Sources */, E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */, E2FE0F362D2B27F9002963B7 /* BlockProcessor.swift in Sources */, + E2FD1D1B2D2DC63800B48627 /* LinkPreviewDetailView.swift in Sources */, E22990462D10B7A7009F8D77 /* SecurityScopeStatus.swift in Sources */, E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */, E2FE0F4F2D2BCD80002963B7 /* TagLinkCommand.swift in Sources */, @@ -1167,9 +1128,8 @@ E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */, E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */, E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */, - E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */, E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */, - E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */, + E229902C2D0F6FC6009F8D77 /* LocalizedItemId.swift in Sources */, E25DA5952D023BD100AEF16D /* PageSettingsDetailView.swift in Sources */, E21850092CEE01C30090B18B /* PagePickerView.swift in Sources */, E29D31492D0489BB0051B7F4 /* AddFileView.swift in Sources */, @@ -1179,18 +1139,14 @@ E29D31302D03A2C50051B7F4 /* DescriptionField.swift in Sources */, E29D31BE2D0DB85A0051B7F4 /* AudioPlayerCommand.swift in Sources */, E29D31552D06D2CE0051B7F4 /* TagListView.swift in Sources */, - E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */, E2FE0F3A2D2B3E4F002963B7 /* AudioPlayerSettings.swift in Sources */, - E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */, E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */, E2FE0F092D2689F0002963B7 /* TagPageGeneratorSource.swift in Sources */, E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */, - E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */, E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */, E29D31262D0370A80051B7F4 /* VideoCommand+Option.swift in Sources */, E2FE0EF82D1D8110002963B7 /* IconCommand.swift in Sources */, E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */, - E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */, E29D318E2D0B2E680051B7F4 /* PageSettingsContentView.swift in Sources */, E22990242D0EDBD0009F8D77 /* HeaderElement.swift in Sources */, E29D31BC2D0DB5120051B7F4 /* CommandProcessor.swift in Sources */, @@ -1221,18 +1177,16 @@ E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */, E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, - E21850352CFAFA5A0090B18B /* SettingsFile.swift in Sources */, E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */, E22990172D0E330F009F8D77 /* TagOverviewPage.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, - E2FE0F1B2D274FDF002963B7 /* LinkPreviewItem.swift in Sources */, E2FE0F062D267350002963B7 /* TextFieldPropertyView.swift in Sources */, + E2FD1D232D2EB27000B48627 /* LoadingResult.swift in Sources */, E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, - E2FE0F5E2D2BE190002963B7 /* FileResourceFile.swift in Sources */, E2FE0F2A2D2AFBE6002963B7 /* ImageCompareIcons.swift in Sources */, E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, @@ -1241,11 +1195,10 @@ E2FE0F592D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift in Sources */, E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */, E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */, + E2FD1D212D2EB22900B48627 /* ModelLoader.swift in Sources */, E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */, E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, - E22990222D0ED12E009F8D77 /* TagOverviewFile.swift in Sources */, E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, - E2FE0F002D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */, E2FE0F572D2BCFD4002963B7 /* BlockLineProcessor.swift in Sources */, @@ -1263,6 +1216,7 @@ E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */, E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */, E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */, + E2FD1D1F2D2E9CC200B48627 /* ItemId.swift in Sources */, E2FE0EFA2D25AFBA002963B7 /* PageHeader.swift in Sources */, E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */, E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */, @@ -1283,7 +1237,7 @@ E2FE0F112D268E7E002963B7 /* MarkdownCodeProcessor.swift in Sources */, E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */, E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, - E22990192D0E3546009F8D77 /* ItemType.swift in Sources */, + E22990192D0E3546009F8D77 /* ItemReference.swift in Sources */, E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */, E2FE0F0F2D268D4F002963B7 /* BoxCommand.swift in Sources */, E22990482D10B7B7009F8D77 /* StorageAccessError.swift in Sources */, @@ -1295,7 +1249,6 @@ E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */, E2FE0F6C2D2D335E002963B7 /* LocalizedPageSettingsView.swift in Sources */, E218502D2CF791440090B18B /* PostImagesView.swift in Sources */, - E29D312A2D039B090051B7F4 /* FileDescriptions.swift in Sources */, E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */, E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */, E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */, @@ -1311,13 +1264,12 @@ E29D31AA2D0CEE3F0051B7F4 /* AudioPlayer.swift in Sources */, E2FE0F4B2D2BCCAA002963B7 /* MarkdownHeadlineProcessor.swift in Sources */, E2FE0F532D2BCE17002963B7 /* SvgCommand.swift in Sources */, - E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */, E2FE0F3E2D2B4225002963B7 /* AudioSettingsDetailView.swift in Sources */, E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */, E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */, + E2FD1D0D2D2DBBA600B48627 /* LinkPreview.swift in Sources */, E22990362D0F79D2009F8D77 /* OptionalStringPropertyView.swift in Sources */, E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */, - E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */, E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */, E2FE0F222D2A84A0002963B7 /* VideoCommand.swift in Sources */, E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */, @@ -1328,9 +1280,9 @@ E2FE0F262D2AF9B0002963B7 /* ImageCompareCommand.swift in Sources */, E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */, E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */, - E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */, E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */, E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */, + E2FD1D192D2DC4F500B48627 /* LoadingContext.swift in Sources */, E29D318B2D0B07EE0051B7F4 /* ContentBox.swift in Sources */, E2FE0F4D2D2BCD30002963B7 /* PageLinkCommand.swift in Sources */, ); diff --git a/CHDataManagement/Extensions/String+Extensions.swift b/CHDataManagement/Extensions/String+Extensions.swift index c3eb3ac..294f10c 100644 --- a/CHDataManagement/Extensions/String+Extensions.swift +++ b/CHDataManagement/Extensions/String+Extensions.swift @@ -120,6 +120,14 @@ extension String { } } +extension String { + + func removingPrefix(_ prefix: String) -> String? { + guard self.hasPrefix(prefix) else { return nil } + return String(self.dropFirst(prefix.count)) + } +} + extension String { var fileNameWithoutExtension: String { diff --git a/CHDataManagement/Generator/Commands/PageLinkCommand.swift b/CHDataManagement/Generator/Commands/PageLinkCommand.swift index 27f1427..b160b99 100644 --- a/CHDataManagement/Generator/Commands/PageLinkCommand.swift +++ b/CHDataManagement/Generator/Commands/PageLinkCommand.swift @@ -38,9 +38,14 @@ struct PageLinkCommand: CommandProcessor { let localized = page.localized(in: language) let url = page.absoluteUrl(in: language) - let title = localized.linkPreviewTitle ?? localized.title - let description = localized.linkPreviewDescription ?? "" - let image = makePageImage(item: localized) + let title = localized.linkPreview.title ?? localized.title + let description = localized.linkPreview.description ?? "" + let image = localized.linkPreview.image.map { + let size = content.settings.pages.pageLinkImageSize + let imageSet = $0.imageSet(width: size, height: size, language: language) + results.require(imageSet: imageSet) + return imageSet + } return RelatedPageLink( title: title, @@ -49,13 +54,4 @@ struct PageLinkCommand: CommandProcessor { image: image) .content } - - private func makePageImage(item: LinkPreviewItem) -> ImageSet? { - item.linkPreviewImage.map { image in - let size = content.settings.pages.pageLinkImageSize - let imageSet = image.imageSet(width: size, height: size, language: language) - results.require(imageSet: imageSet) - return imageSet - } - } } diff --git a/CHDataManagement/Generator/Commands/TagLinkCommand.swift b/CHDataManagement/Generator/Commands/TagLinkCommand.swift index 29d30db..bcd6ff2 100644 --- a/CHDataManagement/Generator/Commands/TagLinkCommand.swift +++ b/CHDataManagement/Generator/Commands/TagLinkCommand.swift @@ -33,8 +33,13 @@ struct TagLinkCommand: CommandProcessor { let localized = tag.localized(in: language) let url = tag.absoluteUrl(in: language) let title = localized.name - let description = localized.linkPreviewDescription ?? "" - let image = makePageImage(item: localized) + let description = localized.linkPreview.description ?? "" + let image = localized.linkPreview.image.map { + let size = content.settings.pages.pageLinkImageSize + let imageSet = $0.imageSet(width: size, height: size, language: language) + results.require(imageSet: imageSet) + return imageSet + } return RelatedPageLink( title: title, @@ -43,13 +48,4 @@ struct TagLinkCommand: CommandProcessor { image: image) .content } - - private func makePageImage(item: LinkPreviewItem) -> ImageSet? { - item.linkPreviewImage.map { image in - let size = content.settings.pages.pageLinkImageSize - let imageSet = image.imageSet(width: size, height: size, language: language) - results.require(imageSet: imageSet) - return imageSet - } - } } diff --git a/CHDataManagement/Generator/ImageSet.swift b/CHDataManagement/Generator/ImageSet.swift index bf02f56..7d59567 100644 --- a/CHDataManagement/Generator/ImageSet.swift +++ b/CHDataManagement/Generator/ImageSet.swift @@ -10,11 +10,11 @@ struct ImageSet: HtmlProducer { let quality: CGFloat - let description: String + let description: String? let extraAttributes: String - init(image: FileResource, maxWidth: Int, maxHeight: Int, description: String, quality: CGFloat = 0.7, extraAttributes: String? = nil) { + init(image: FileResource, maxWidth: Int, maxHeight: Int, description: String?, quality: CGFloat = 0.7, extraAttributes: String? = nil) { self.image = image self.maxWidth = maxWidth self.maxHeight = maxHeight @@ -39,6 +39,13 @@ struct ImageSet: HtmlProducer { ] } + private var imageAltText: String { + guard let description else { + return "" + } + return " alt='\(description.htmlEscaped())'" + } + func populate(_ result: inout String) { let fileExtension = image.type.fileExtension.map { "." + $0 } ?? "" @@ -48,7 +55,7 @@ struct ImageSet: HtmlProducer { result += "" result += "" result += "" - result += "\(description.htmlEscaped())" + result += "" result += "" } } diff --git a/CHDataManagement/Generator/Page Generators/PageGenerator.swift b/CHDataManagement/Generator/Page Generators/PageGenerator.swift index f79c740..f106ce9 100644 --- a/CHDataManagement/Generator/Page Generators/PageGenerator.swift +++ b/CHDataManagement/Generator/Page Generators/PageGenerator.swift @@ -58,8 +58,8 @@ final class PageGenerator { let pageHeader = PageHeader( language: language, - title: localized.linkPreviewTitle ?? localized.title, - description: localized.linkPreviewDescription, + title: localized.linkPreview.title ?? localized.title, + description: localized.linkPreview.description, iconUrl: iconUrl, languageButton: languageButton, links: content.navigationBar(in: language), diff --git a/CHDataManagement/Generator/Page Generators/TagOverviewGenerator.swift b/CHDataManagement/Generator/Page Generators/TagOverviewGenerator.swift index 7294f95..a914b78 100644 --- a/CHDataManagement/Generator/Page Generators/TagOverviewGenerator.swift +++ b/CHDataManagement/Generator/Page Generators/TagOverviewGenerator.swift @@ -10,7 +10,7 @@ private struct TagData { init(tag: Tag, language: ContentLanguage) { let localized = tag.localized(in: language) self.url = tag.absoluteUrl(in: language) - self.title = localized.linkPreviewTitle ?? localized.name + self.title = localized.linkPreview.title ?? localized.name self.localized = localized } } @@ -81,12 +81,12 @@ final class TagOverviewGenerator { self.results = results } - func generatePages(tags: [Tag], overview: TagOverviewPage) { + func generatePages(tags: [Tag], overview: Tag) { let localized = overview.localized(in: language) let header = TagHeaderContent( language: language, - title: localized.linkPreviewTitle ?? localized.title, - description: localized.linkPreviewDescription, + title: localized.linkPreview.title ?? localized.title, + description: localized.linkPreview.description, iconUrl: content.settings.navigation.localized(in: language).rootUrl, links: content.navigationBar(in: language), headers: content.postPageHeaders, @@ -123,8 +123,13 @@ final class TagOverviewGenerator { additionalFooter: "") { content in content += "

\(header.title)

" for tag in tags { - let description = tag.localized.linkPreviewDescription ?? "" - let image = self.makePageImage(item: tag.localized) + let description = tag.localized.linkPreview.description ?? "" + let image = tag.localized.linkPreview.image.map { + let size = self.content.settings.pages.pageLinkImageSize + let imageSet = $0.imageSet(width: size, height: size, language: self.language) + self.results.require(imageSet: imageSet) + return imageSet + } content += RelatedPageLink( title: tag.title, @@ -148,13 +153,4 @@ final class TagOverviewGenerator { return } } - - private func makePageImage(item: LinkPreviewItem) -> ImageSet? { - item.linkPreviewImage.map { image in - let size = content.settings.pages.pageLinkImageSize - let imageSet = image.imageSet(width: size, height: size, language: language) - results.require(imageSet: imageSet) - return imageSet - } - } } diff --git a/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift b/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift index d10af14..4e953b3 100644 --- a/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift +++ b/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift @@ -22,7 +22,7 @@ struct TagPageGeneratorSource: PostListPageGeneratorSource { } var pageDescription: String { - tag.localized(in: language).linkPreviewDescription ?? "" + tag.localized(in: language).linkPreview.description ?? "" } func pageUrlPrefix(for language: ContentLanguage) -> String { diff --git a/CHDataManagement/Generator/Results/GenerationResults.swift b/CHDataManagement/Generator/Results/GenerationResults.swift index 24f22bb..278a651 100644 --- a/CHDataManagement/Generator/Results/GenerationResults.swift +++ b/CHDataManagement/Generator/Results/GenerationResults.swift @@ -56,7 +56,7 @@ final class GenerationResults: ObservableObject { var redirects: [String : String] = [:] /// The cache of previously used GenerationResults - private var cache: [ItemId : PageGenerationResults] = [:] + private var cache: [LocalizedItemId : PageGenerationResults] = [:] private(set) var general: PageGenerationResults! @@ -66,14 +66,14 @@ final class GenerationResults: ObservableObject { // MARK: Life cycle init() { - let id = ItemId(language: .english, itemType: .general) + let id = LocalizedItemId(language: .english, itemType: .general) let general = PageGenerationResults(itemId: id, delegate: self) self.general = general cache[id] = general self.resultCount = 1 } - func makeResults(_ itemId: ItemId) -> PageGenerationResults { + func makeResults(_ itemId: LocalizedItemId) -> PageGenerationResults { guard let result = cache[itemId] else { let result = PageGenerationResults(itemId: itemId, delegate: self) cache[itemId] = result @@ -83,18 +83,18 @@ final class GenerationResults: ObservableObject { return result } - func makeResults(for type: ItemType, in language: ContentLanguage) -> PageGenerationResults { - let itemId = ItemId(language: language, itemType: type) + func makeResults(for type: ItemReference, in language: ContentLanguage) -> PageGenerationResults { + let itemId = LocalizedItemId(language: language, itemType: type) return makeResults(itemId) } func makeResults(for page: Page, in language: ContentLanguage) -> PageGenerationResults { - let itemId = ItemId(language: language, itemType: .page(page)) + let itemId = LocalizedItemId(language: language, itemType: .page(page)) return makeResults(itemId) } func makeResults(for tag: Tag, in language: ContentLanguage) -> PageGenerationResults { - let itemId = ItemId(language: language, itemType: .tagPage(tag)) + let itemId = LocalizedItemId(language: language, itemType: .tagPage(tag)) return makeResults(itemId) } @@ -209,9 +209,9 @@ final class GenerationResults: ObservableObject { } } -private extension Dictionary where Value == Set { +private extension Dictionary where Value == Set { - mutating func remove(keys: S, of item: ItemId) where S: Sequence, S.Element == Key { + mutating func remove(keys: S, of item: LocalizedItemId) where S: Sequence, S.Element == Key { for key in keys { guard var value = self[key] else { continue } value.remove(item) diff --git a/CHDataManagement/Generator/Results/PageGenerationResults.swift b/CHDataManagement/Generator/Results/PageGenerationResults.swift index 424a748..4283fcd 100644 --- a/CHDataManagement/Generator/Results/PageGenerationResults.swift +++ b/CHDataManagement/Generator/Results/PageGenerationResults.swift @@ -17,7 +17,7 @@ extension ImageToGenerate: Hashable { final class PageGenerationResults: ObservableObject { - let itemId: ItemId + let itemId: LocalizedItemId private unowned let delegate: GenerationResults @@ -73,13 +73,13 @@ final class PageGenerationResults: ObservableObject { private(set) var warnings: Set /// The files that could not be saved to the output folder - private(set) var unsavedOutputFiles: [String: Set] = [:] + private(set) var unsavedOutputFiles: [String: Set] = [:] private(set) var pageIsEmpty: Bool private(set) var redirect: (originalUrl: String, newUrl: String)? - init(itemId: ItemId, delegate: GenerationResults) { + init(itemId: LocalizedItemId, delegate: GenerationResults) { self.itemId = itemId self.delegate = delegate inaccessibleFiles = [] @@ -245,7 +245,7 @@ final class PageGenerationResults: ObservableObject { delegate.warning(warning) } - func unsavedOutput(_ path: String, source: ItemType) { + func unsavedOutput(_ path: String, source: ItemReference) { unsavedOutputFiles[path, default: []].insert(source) delegate.unsaved(path) } diff --git a/CHDataManagement/Main/InitialSetupView.swift b/CHDataManagement/Main/InitialSetupView.swift index 1fc03cc..7fe735d 100644 --- a/CHDataManagement/Main/InitialSetupView.swift +++ b/CHDataManagement/Main/InitialSetupView.swift @@ -23,6 +23,7 @@ struct InitialSetupView: View { if let message { Text(message) .padding(.bottom) + .lineLimit(10) } } .padding() @@ -52,14 +53,25 @@ struct InitialSetupView: View { set(message: "Failed to set content path") return } - DispatchQueue.main.async { - do { - try content.loadFromDisk() - } catch { - set(message: "Failed to load database: \(error)") + + DispatchQueue.global().async { + let loader = ModelLoader(content: content, storage: content.storage) + let result = loader.load() + guard result.errors.isEmpty else { + let message = "Failed to load database\n" + result.errors.sorted().joined(separator: "\n") + set(message: message) return } - dismiss() + + DispatchQueue.main.async { + content.files = result.files + content.posts = result.posts + content.pages = result.pages + content.tags = result.tags + content.settings = result.settings + content.tagOverview = result.tagOverview + dismiss() + } } } diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index 7113b2f..8e80b2d 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -69,6 +69,12 @@ struct MainView: App { @State private var showInitialSetupSheet = false + @State + private var showLoadErrorSheet = false + + @State + private var loadErrors: [String] = [] + @ViewBuilder var sidebar: some View { switch selectedTab { @@ -202,14 +208,30 @@ struct MainView: App { .environment(\.language, language) .environmentObject(content) } + .sheet(isPresented: $showLoadErrorSheet) { + VStack { + Text("Failed to load database") + .font(.headline) + List(loadErrors, id: \.self) { error in + HStack { + Text(error) + Spacer() + } + } + .frame(minHeight: 200) + Button("Dismiss", action: { showLoadErrorSheet = false }) + .padding() + } + .padding() + } } } private func save() { - do { - try content.saveToDisk() - } catch { - print("Failed to save content: \(error.localizedDescription)") + guard content.saveToDisk() else { + print("Failed to save content") + #warning("Show error message") + return } } @@ -218,10 +240,12 @@ struct MainView: App { showInitialSheet() return } - do { - try content.loadFromDisk() - } catch { - print("Failed to load content: \(error.localizedDescription)") + content.loadFromDisk { errors in + guard !errors.isEmpty else { + return + } + self.loadErrors = errors + self.showLoadErrorSheet = true } } diff --git a/CHDataManagement/Model/Content+Load.swift b/CHDataManagement/Model/Content+Load.swift deleted file mode 100644 index 96fef3d..0000000 --- a/CHDataManagement/Model/Content+Load.swift +++ /dev/null @@ -1,158 +0,0 @@ -import Foundation - -extension Content { - - private func convert(_ tag: LocalizedTagFile, images: [String : FileResource]) -> LocalizedTag { - LocalizedTag( - content: self, - urlComponent: tag.urlComponent, - name: tag.name, - linkPreviewTitle: tag.linkPreviewTitle, - linkPreviewDescription: tag.linkPreviewDescription, - linkPreviewImage: tag.linkPreviewImage.map { images[$0] }, - originalUrl: tag.originalURL) - } - - private func convert(_ page: LocalizedPageFile, images: [String : FileResource]) -> LocalizedPage { - LocalizedPage( - content: self, - urlString: page.url, - title: page.title, - lastModified: page.lastModifiedDate, - originalUrl: page.originalURL, - linkPreviewImage: page.linkPreviewImage.map { images[$0] }, - linkPreviewTitle: page.linkPreviewTitle, - linkPreviewDescription: page.linkPreviewDescription, - hideTitle: page.hideTitle ?? false) - } - - func loadFromDisk() throws { - guard storage.contentScope != nil else { - print("Storage not initialized, not loading content") - throw StorageAccessError.noBookmarkData - } - - let settings = storage.loadSettings() ?? .default - - guard let tagData = storage.loadAllTags() else { - print("Failed to load file tags") - return - } - if tagData.isEmpty { print("No tags loaded") } - - guard let pagesData = storage.loadAllPages() else { - print("Failed to load file pages") - return - } - if pagesData.isEmpty { print("No pages loaded") } - - guard let postsData = storage.loadAllPosts() else { - print("Failed to load file posts") - return - } - if postsData.isEmpty { print("No posts loaded") } - - guard let fileList = storage.loadAllFiles() else { - print("Failed to load file list") - return - } - if fileList.isEmpty { print("No files loaded") } - - print("Loaded data from disk, processing...") - // All data loaded from storage, start constructing the data model - - let files: [String : FileResource] = fileList.reduce(into: [:]) { (files, data) in - let fileId = data.key - let fileData = data.value.data - let isExternal = data.value.isExternal - files[fileId] = FileResource(content: self, id: fileId, file: fileData, isExternalFile: isExternal) - } - - let images = files.filter { $0.value.type.isImage } - - let tags = tagData.reduce(into: [:]) { (tags, data) in - tags[data.key] = Tag( - content: self, - id: data.value.id, - isVisible: data.value.isVisible, - german: convert(data.value.german, images: images), - english: convert(data.value.english, images: images)) - } - - let pages: [String : Page] = loadPages(pagesData, tags: tags, files: files) - - let posts: [String : Post] = postsData.reduce(into: [:]) { dict, data in - let (postId, post) = data - let linkedPage = post.linkedPageId.map { pages[$0] } - let german = LocalizedPost(content: self, file: post.german, images: images) - let english = LocalizedPost(content: self, file: post.english, images: images) - - dict[postId] = Post( - content: self, - 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) - } - - let tagOverview = settings.tagOverview.map { file in - TagOverviewPage( - content: self, - german: .init(content: self, file: file.german, image: file.german.linkPreviewImage.map { files[$0] }), - english: .init(content: self, file: file.english, image: file.english.linkPreviewImage.map { files[$0] })) - } - - self.tags = tags.values.sorted() - self.pages = pages.values.sorted(ascending: false) { $0.startDate } - self.files = files.values.sorted { $0.id } - self.posts = posts.values.sorted(ascending: false) { $0.startDate } - self.tagOverview = tagOverview - self.settings = .init(file: settings, files: files) { raw in - #warning("Notify about missing links") - guard let type = ItemType(rawValue: raw, posts: posts, pages: pages, tags: tags) else { - return nil - } - switch type { - case .general: - return nil - case .post(let post): - return post - case .feed: - return nil // TODO: Provide feed object - case .page(let page): - return page - case .tagPage(let tag): - return tag - case .tagOverview: - return tagOverview - } - } - - print("Content loaded") - } - - private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], files: [String : FileResource]) -> [String : Page] { - pagesData.reduce(into: [:]) { pages, data in - let (pageId, page) = data - pages[pageId] = Page( - content: self, - id: pageId, - externalLink: page.externalLink, - isDraft: page.isDraft, - createdDate: page.createdDate, - hideDate: page.hideDate ?? false, - startDate: page.startDate, - endDate: page.endDate, - german: convert(page.german, images: files), - english: convert(page.english, images: files), - tags: page.tags.compactMap { tags[$0] }, - requiredFiles: page.requiredFiles?.compactMap { files[$0] } ?? []) - } - } - -} diff --git a/CHDataManagement/Model/Content+Save.swift b/CHDataManagement/Model/Content+Save.swift index c23ad91..751aa70 100644 --- a/CHDataManagement/Model/Content+Save.swift +++ b/CHDataManagement/Model/Content+Save.swift @@ -2,22 +2,25 @@ import Foundation extension Content { - func saveToDisk() throws { + func saveToDisk() -> Bool { + guard didLoadContent else { return false } guard storage.contentScope != nil else { print("Storage not initialized, not saving content") - return + return false } var failedSaves = 0 - failedSaves += pages.count { !storage.save(pageMetadata: $0.pageFile, for: $0.id) } - failedSaves += posts.count { !storage.save(post: $0.postFile, for: $0.id) } - failedSaves += tags.count { !storage.save(tagMetadata: $0.file, for: $0.id) } - failedSaves.increment(!storage.save(settings: settings.file(tagOverview: tagOverview))) - failedSaves += files.count { !storage.save(fileInfo: $0.fileInfo, for: $0.id) } + failedSaves += pages.count { !storage.save(pageMetadata: $0.data, for: $0.id) } + failedSaves += posts.count { !storage.save(post: $0.data, for: $0.id) } + failedSaves += tags.count { !storage.save(tagMetadata: $0.data, for: $0.id) } + failedSaves.increment(!storage.save(settings: settings.data(tagOverview: tagOverview))) + failedSaves += files.count { !storage.save(fileInfo: $0.data, for: $0.id) } if failedSaves > 0 { print("Save partially failed with \(failedSaves) errors") + return false } + return true } func removeUnlinkedFiles() -> Bool { @@ -37,49 +40,3 @@ extension Content { return success } } - -private extension Page { - - var pageFile: PageFile { - .init(isDraft: isDraft, - externalLink: externalLink, - tags: tags.map { $0.id }, - hideDate: hideDate ? true : nil, - createdDate: createdDate, - startDate: startDate, - endDate: hasEndDate ? endDate : nil, - german: german.pageFile, - english: english.pageFile, - requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted()) - } -} - -private extension LocalizedPage { - - var pageFile: LocalizedPageFile { - .init(url: urlString, - title: title, - linkPreviewImage: linkPreviewImage?.id, - linkPreviewTitle: linkPreviewTitle, - linkPreviewDescription: linkPreviewDescription, - lastModifiedDate: lastModified, - originalURL: originalUrl, - hideTitle: hideTitle ? true : nil) - } -} - - -private extension Post { - - var postFile: PostFile { - .init( - isDraft: isDraft, - createdDate: createdDate, - startDate: startDate, - endDate: hasEndDate ? endDate : nil, - tags: tags.map { $0.id }, - german: german.postFile, - english: english.postFile, - linkedPageId: linkedPage?.id) - } -} diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index ccb8a1f..3e8fbcb 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -4,6 +4,9 @@ import Combine final class Content: ObservableObject { + @Published + var didLoadContent = false + @ObservedObject var storage: Storage @@ -23,7 +26,7 @@ final class Content: ObservableObject { var files: [FileResource] @Published - var tagOverview: TagOverviewPage? + var tagOverview: Tag? @Published var results: GenerationResults @@ -47,7 +50,7 @@ final class Content: ObservableObject { pages: [Page], tags: [Tag], files: [FileResource], - tagOverview: TagOverviewPage?) { + tagOverview: Tag?) { self.settings = settings self.posts = posts self.pages = pages @@ -112,16 +115,11 @@ final class Content: ObservableObject { pages.insert(page, at: 0) } - func update(contentPath: URL) { + func update(contentPath: URL, callback: @escaping ([String]) -> ()) { guard storage.save(contentPath: contentPath) else { return } - clear() - do { - try loadFromDisk() - } catch { - print("Failed to reload content: \(error)") - } + loadFromDisk(callback: callback) } func remove(_ file: FileResource) { @@ -146,4 +144,29 @@ final class Content: ObservableObject { func file(withOutputPath: String) -> FileResource? { files.first { $0.absoluteUrl == withOutputPath } } + + func loadFromDisk(callback: @escaping (_ errors: [String]) -> ()) { + DispatchQueue.global().async { + let loader = ModelLoader(content: self, storage: self.storage) + let result = loader.load() + guard result.errors.isEmpty else { + DispatchQueue.main.async { + self.didLoadContent = false + callback(result.errors.sorted()) + } + return + } + + DispatchQueue.main.async { + self.files = result.files + self.posts = result.posts + self.pages = result.pages + self.tags = result.tags + self.settings = result.settings + self.tagOverview = result.tagOverview + self.didLoadContent = true + callback([]) + } + } + } } diff --git a/CHDataManagement/Model/FileResource.swift b/CHDataManagement/Model/FileResource.swift index bcc250d..3b27be4 100644 --- a/CHDataManagement/Model/FileResource.swift +++ b/CHDataManagement/Model/FileResource.swift @@ -1,34 +1,43 @@ import Foundation import SwiftUI -final class FileResource: Item { +final class FileResource: Item, LocalizedItem { let type: FileType + /// Indicate if the file content is stored by the app @Published var isExternallyStored: Bool + /// The file/image description in German @Published - var german: String + var german: String? + /// The file/image description in English @Published - var english: String + var english: String? + /// A version string of this resource, mostly for assets @Published var version: String? + /// A URL where the resource was copied/downloaded from @Published var sourceUrl: String? + /// The list of generated image versions for this image @Published var generatedImageVersions: Set + /// A custom file path in the output folder where this file is located @Published var customOutputPath: String? + /// The date when the file was added @Published var addedDate: Date + /// The date when the file was last modified @Published var modifiedDate: Date @@ -53,8 +62,8 @@ final class FileResource: Item { modifiedDate: Date = .now) { self.type = FileType(fileExtension: id.fileExtension) self.isExternallyStored = isExternallyStored - self.german = german ?? "" - self.english = english ?? "" + self.german = german + self.english = english self.version = version self.sourceUrl = sourceUrl self.generatedImageVersions = generatedImageVersions @@ -64,20 +73,6 @@ final class FileResource: Item { super.init(content: content, id: id) } - init(content: Content, id: String, file: FileResourceFile, isExternalFile: Bool) { - self.type = FileType(fileExtension: id.fileExtension) - self.isExternallyStored = isExternalFile - self.german = file.germanDescription ?? "" - self.english = file.englishDescription ?? "" - self.version = file.version - self.sourceUrl = file.sourceUrl - self.generatedImageVersions = Set(file.generatedImages ?? []) - self.customOutputPath = file.customOutputPath - self.addedDate = file.addedDate - self.modifiedDate = file.modifiedDate - super.init(content: content, id: id) - } - /** Only for bundle images */ @@ -101,7 +96,7 @@ final class FileResource: Item { content.storage.fileContent(for: id) ?? "" } - func dataContent() -> Data? { + func dataContent() -> Foundation.Data? { content.storage.fileData(for: id) } @@ -131,7 +126,6 @@ final class FileResource: Item { // Image must have changed, so force regeneration DispatchQueue.main.async { self.imageDimensions = size - self.didChange() self.removeGeneratedImages() } } @@ -299,12 +293,34 @@ final class FileResource: Item { } } +extension FileResource: CustomStringConvertible { + + var description: String { + id + } +} + extension FileResource { - var fileInfo: FileResourceFile { + convenience init(content: Content, id: String, data: FileResource.Data, isExternalFile: Bool) { + self.init( + content: content, + id: id, + isExternallyStored: isExternalFile, + english: data.englishDescription, + german: data.germanDescription, + version: data.version, + sourceUrl: data.sourceUrl, + generatedImageVersions: Set(data.generatedImages ?? []), + customOutputPath: data.customOutputPath, + addedDate: data.addedDate, + modifiedDate: data.modifiedDate) + } + + var data: Data { .init( - englishDescription: english.nonEmpty, - germanDescription: german.nonEmpty, + englishDescription: english, + germanDescription: german, generatedImages: generatedImageVersions.sorted().nonEmpty, customOutputPath: customOutputPath, version: version, @@ -312,15 +328,16 @@ extension FileResource { addedDate: addedDate, modifiedDate: modifiedDate) } -} -extension FileResource: LocalizedItem { - -} - -extension FileResource: CustomStringConvertible { - - var description: String { - id + /// This struct holds metadata about a file resource that is stored in the content folder. + struct Data: Codable { + let englishDescription: String? + let germanDescription: String? + let generatedImages: [String]? + let customOutputPath: String? + let version: String? + let sourceUrl: String? + let addedDate: Date + let modifiedDate: Date } } diff --git a/CHDataManagement/Model/Item/Item.swift b/CHDataManagement/Model/Item/Item.swift index dd70b80..c7f0369 100644 --- a/CHDataManagement/Model/Item/Item.swift +++ b/CHDataManagement/Model/Item/Item.swift @@ -39,12 +39,20 @@ class Item: ObservableObject, Identifiable { var itemType: ItemType { fatalError() } + + var itemReference: ItemReference { + fatalError() + } + + var itemId: ItemId { + .init(type: itemType, id: id) + } } extension Item: Equatable { static func == (lhs: Item, rhs: Item) -> Bool { - lhs.id == rhs.id + lhs.id == rhs.id && lhs.itemType == rhs.itemType } } @@ -52,12 +60,13 @@ extension Item: Hashable { func hash(into hasher: inout Hasher) { hasher.combine(id) + hasher.combine(itemType) } } extension Item: Comparable { static func < (lhs: Item, rhs: Item) -> Bool { - lhs.id < rhs.id + lhs.id < rhs.id && lhs.itemType < rhs.itemType } } diff --git a/CHDataManagement/Model/Item/ItemId.swift b/CHDataManagement/Model/Item/ItemId.swift index f83ccce..11712a7 100644 --- a/CHDataManagement/Model/Item/ItemId.swift +++ b/CHDataManagement/Model/Item/ItemId.swift @@ -1,33 +1,11 @@ struct ItemId { - let language: ContentLanguage + let type: ItemType - let itemType: ItemType + let id: String? } -extension ItemId: Equatable { - - static func == (lhs: ItemId, rhs: ItemId) -> Bool { - lhs.language == rhs.language && - lhs.itemType == rhs.itemType - } -} - -extension ItemId: Hashable { - - func hash(into hasher: inout Hasher) { - hasher.combine(language) - hasher.combine(itemType.id) - } -} - -extension ItemId: Comparable { +extension ItemId: Codable { - static func < (lhs: ItemId, rhs: ItemId) -> Bool { - guard lhs.itemType == rhs.itemType else { - return lhs.itemType < rhs.itemType - } - return lhs.language < rhs.language - } } diff --git a/CHDataManagement/Model/Item/ItemReference.swift b/CHDataManagement/Model/Item/ItemReference.swift new file mode 100644 index 0000000..111e939 --- /dev/null +++ b/CHDataManagement/Model/Item/ItemReference.swift @@ -0,0 +1,68 @@ + +enum ItemReference { + + case general + + case post(Post) + + case feed + + case page(Page) + + case tagPage(Tag) + + case tagOverview +} + +extension ItemReference: Equatable { + +} + +extension ItemReference: Hashable { + +} + +extension ItemReference: Identifiable { + + var id: String { + switch self { + case .general: + return "0-general" + case .feed: + return "1-feed" + case .post(let post): + return "2-post-\(post.id)" + case .page(let page): + return "3-page-\(page.id)" + case .tagPage(let tag): + return "5-tag-\(tag.id)" + case .tagOverview: + return "4-tag-overview" + } + } + + init?(context: LoadingContext, rawValue: String) { + if rawValue == "0-general" { + self = .general + } else if rawValue == "1-feed" { + self = .feed + } else if rawValue == "4-tag-overview" { + self = .tagOverview + } else if let id = rawValue.removingPrefix("3-page-"), let page = context.page(id) { + self = .page(page) + } else if let id = rawValue.removingPrefix("2-post-"), let post = context.post(id) { + self = .post(post) + } else if let id = rawValue.removingPrefix("5-tag-"), let tag = context.tag(id) { + self = .tagPage(tag) + } else { + return nil + } + } +} + +extension ItemReference: Comparable { + + static func < (lhs: ItemReference, rhs: ItemReference) -> Bool { + lhs.id < rhs.id + } +} diff --git a/CHDataManagement/Model/Item/ItemType.swift b/CHDataManagement/Model/Item/ItemType.swift index b0c8211..b443295 100644 --- a/CHDataManagement/Model/Item/ItemType.swift +++ b/CHDataManagement/Model/Item/ItemType.swift @@ -1,76 +1,21 @@ -enum ItemType { +enum ItemType: String, Equatable, Hashable { - case general + case post = "post" - case post(Post) + case page = "page" - case feed + case tag = "tag" - case page(Page) - - case tagPage(Tag) - - case tagOverview -} - -extension ItemType: Equatable { - -} - -extension ItemType: Hashable { - -} - -extension ItemType: Identifiable { - - var id: String { - switch self { - case .general: - return "0-general" - case .feed: - return "1-feed" - case .post(let post): - return "2-post-\(post.id)" - case .page(let page): - return "3-page-\(page.id)" - case .tagPage(let tag): - return "5-tag-\(tag.id)" - case .tagOverview: - return "4-tag-overview" - } - } - - init?(rawValue: String, posts: [String : Post], pages: [String : Page], tags: [String : Tag]) { - if rawValue == "0-general" { - self = .general - } else if rawValue == "1-feed" { - self = .feed - } else if rawValue == "4-tag-overview" { - self = .tagOverview - } else if let id = rawValue.removingPrefix("3-page-"), let page = pages[id] { - self = .page(page) - } else if let id = rawValue.removingPrefix("2-post-"), let post = posts[id] { - self = .post(post) - } else if let id = rawValue.removingPrefix("5-tag-"), let tag = tags[id] { - self = .tagPage(tag) - } else { - return nil - } - } + case tagOverview = "tag-overview" } extension ItemType: Comparable { - - static func < (lhs: ItemType, rhs: ItemType) -> Bool { - lhs.id < rhs.id + public static func < (lhs: ItemType, rhs: ItemType) -> Bool { + lhs.rawValue < rhs.rawValue } } -extension String { - - func removingPrefix(_ prefix: String) -> String? { - guard self.hasPrefix(prefix) else { return nil } - return String(self.dropFirst(prefix.count)) - } +extension ItemType: Codable { + } diff --git a/CHDataManagement/Model/Item/LinkPreviewItem.swift b/CHDataManagement/Model/Item/LinkPreviewItem.swift deleted file mode 100644 index 4b8a864..0000000 --- a/CHDataManagement/Model/Item/LinkPreviewItem.swift +++ /dev/null @@ -1,18 +0,0 @@ - -protocol LinkPreviewItem: AnyObject { - - var linkPreviewImage: FileResource? { get set } - - var linkPreviewTitle: String? { get } - - var linkPreviewDescription: String? { get } -} - -extension LinkPreviewItem { - - func remove(linkPreviewImage file: FileResource) { - if linkPreviewImage == file { - linkPreviewImage = nil - } - } -} diff --git a/CHDataManagement/Model/Item/LocalizedItemId.swift b/CHDataManagement/Model/Item/LocalizedItemId.swift new file mode 100644 index 0000000..4a39dad --- /dev/null +++ b/CHDataManagement/Model/Item/LocalizedItemId.swift @@ -0,0 +1,33 @@ + +struct LocalizedItemId { + + let language: ContentLanguage + + let itemType: ItemReference +} + +extension LocalizedItemId: Equatable { + + static func == (lhs: LocalizedItemId, rhs: LocalizedItemId) -> Bool { + lhs.language == rhs.language && + lhs.itemType == rhs.itemType + } +} + +extension LocalizedItemId: Hashable { + + func hash(into hasher: inout Hasher) { + hasher.combine(language) + hasher.combine(itemType.id) + } +} + +extension LocalizedItemId: Comparable { + + static func < (lhs: LocalizedItemId, rhs: LocalizedItemId) -> Bool { + guard lhs.itemType == rhs.itemType else { + return lhs.itemType < rhs.itemType + } + return lhs.language < rhs.language + } +} diff --git a/CHDataManagement/Model/Item/TagOverviewPage.swift b/CHDataManagement/Model/Item/TagOverviewPage.swift index 38753de..1975d42 100644 --- a/CHDataManagement/Model/Item/TagOverviewPage.swift +++ b/CHDataManagement/Model/Item/TagOverviewPage.swift @@ -1,5 +1,5 @@ import Foundation - +/* final class TagOverviewPage: Item { static let id = "all-tags" @@ -105,3 +105,4 @@ final class LocalizedTagOverviewPage: ObservableObject { !content.containsTag(withUrlComponent: urlComponent) } } +*/ diff --git a/CHDataManagement/Model/LinkPreview.swift b/CHDataManagement/Model/LinkPreview.swift new file mode 100644 index 0000000..a11f287 --- /dev/null +++ b/CHDataManagement/Model/LinkPreview.swift @@ -0,0 +1,58 @@ +import Foundation + +/** + The information to use when constructing the link preview of a page. + + The information will be placed in the `` of the page as `` tags. + */ +final class LinkPreview: ObservableObject { + + /// The description to show when linking to a page (contained in the `` of the page) + @Published + var title: String? + + /// The image id of the thumbnail to attach to the link preview (contained in the `` of the page) + @Published + var description: String? + + /// The title to show for a link preview (contained in the `` of the page) + @Published + var image: FileResource? + + init(title: String? = nil, description: String? = nil, image: FileResource? = nil) { + self.title = title + self.description = description + self.image = image + } + + /** + Remove a file if it is used in the link preview. + */ + func remove(_ file: FileResource) { + if image == file { + image = nil + } + } + + // MARK: Storage + + var data: Data { + .init(title: title, description: description, image: image?.id) + } + + init(context: LoadingContext, data: Data) { + self.title = data.title + self.description = data.description + self.image = data.image.map(context.image) + } +} + +extension LinkPreview { + + /// The object to serialize a link preview for storage + struct Data: Codable { + let title: String? + let description: String? + let image: String? + } +} diff --git a/CHDataManagement/Model/Loading/LoadingContext.swift b/CHDataManagement/Model/Loading/LoadingContext.swift new file mode 100644 index 0000000..0f3d9aa --- /dev/null +++ b/CHDataManagement/Model/Loading/LoadingContext.swift @@ -0,0 +1,110 @@ + +final class LoadingContext { + + let content: Content + + var files: [String: FileResource] = [:] + + var pages: [String : Page] = [:] + + var tags: [String : Tag] = [:] + + var posts: [String : Post] = [:] + + var errors: Set = [] + + var tagOverview: TagOverview? + + var settings: Settings? + + init(content: Content) { + self.content = content + } + + func results() -> LoadingResult { + .init( + settings: settings ?? .default, + posts: posts.values.sorted(ascending: false) { $0.startDate }, + pages: pages.values.sorted(ascending: false) { $0.startDate }, + tags: tags.values.sorted(), + files: files.values.sorted { $0.id }, + tagOverview: tagOverview, + errors: errors.sorted()) + } + + func error(_ message: String) { + errors.insert(message) + } + + func post(_ postId: String) -> Post? { + if let post = posts[postId] { + return post + } + error("Missing post \(postId)") + return nil + } + + func tag(_ tagId: String) -> Tag? { + if let tag = tags[tagId] { + return tag + } + error("Missing tag \(tagId)") + return nil + } + + func page(_ pageId: String) -> Page? { + if let page = pages[pageId] { + return page + } + error("Missing page \(pageId)") + return nil + } + + func file(_ fileId: String) -> FileResource? { + if let file = files[fileId] { + return file + } + error("Missing file \(fileId)") + return nil + } + + func image(_ imageId: String) -> FileResource? { + guard let image = file(imageId) else { + return nil + } + if image.type.isImage { + return image + } + error("Image \(imageId) is not an image") + return nil + } + + func item(itemId: ItemId) -> Item? { + switch itemId.type { + case .post: + guard let id = itemId.id else { + error("Missing post id in itemId") + return nil + } + return post(id) + case .page: + guard let id = itemId.id else { + error("Missing page id in itemId") + return nil + } + return page(id) + case .tag: + guard let id = itemId.id else { + error("Missing tag id in itemId") + return nil + } + return tag(id) + case .tagOverview: + guard let tagOverview else { + error("Missing tag overview") + return nil + } + return tagOverview + } + } +} diff --git a/CHDataManagement/Model/Loading/LoadingResult.swift b/CHDataManagement/Model/Loading/LoadingResult.swift new file mode 100644 index 0000000..cc8f819 --- /dev/null +++ b/CHDataManagement/Model/Loading/LoadingResult.swift @@ -0,0 +1,17 @@ + +struct LoadingResult { + + let settings: Settings + + let posts: [Post] + + let pages: [Page] + + let tags: [Tag] + + let files: [FileResource] + + let tagOverview: Tag? + + let errors: [String] +} diff --git a/CHDataManagement/Model/Loading/ModelLoader.swift b/CHDataManagement/Model/Loading/ModelLoader.swift new file mode 100644 index 0000000..0fd6dc7 --- /dev/null +++ b/CHDataManagement/Model/Loading/ModelLoader.swift @@ -0,0 +1,96 @@ + +final class ModelLoader { + + let content: Content + + let storage: Storage + + let context: LoadingContext + + init(content: Content, storage: Storage) { + self.content = content + self.storage = storage + self.context = .init(content: content) + } + + func load() -> LoadingResult { + loadInternal() + return context.results() + } + + private func loadInternal() { + guard storage.contentScope != nil else { + context.error("Storage not initialized, not loading content") + return + } + + loadFiles() + loadTags() + loadPages() + loadPosts() + loadSettings() + } + + private func loadFiles() { + guard let files = storage.loadAllFiles() else { + context.error("Failed to load file list") + return + } + if files.isEmpty { print("No files loaded") } + + files.forEach { (fileId, data) in + let fileData = data.data + let isExternal = data.isExternal + context.files[fileId] = FileResource(content: content, id: fileId, data: fileData, isExternalFile: isExternal) + } + } + + private func loadTags() { + guard let tags = storage.loadAllTags() else { + context.error("Failed to load file tags") + return + } + if tags.isEmpty { print("No tags loaded") } + + tags.forEach { (tagId, data) in + context.tags[tagId] = Tag(context: context, id: tagId, data: data) + } + } + + private func loadPages() { + guard let pages = storage.loadAllPages() else { + context.error("Failed to load file pages") + return + } + if pages.isEmpty { print("No pages loaded") } + + pages.forEach { pageId, data in + context.pages[pageId] = Page(context: context, id: pageId, data: data) + } + } + + private func loadPosts() { + guard let posts = storage.loadAllPosts() else { + context.error("Failed to load file posts") + return + } + if posts.isEmpty { print("No posts loaded") } + + posts.forEach { postId, data in + context.posts[postId] = Post(context: context, id: postId, data: data) + } + } + + private func loadSettings() { + guard let settings = storage.loadSettings() else { + context.error("Failed to load settings") + return + } + + context.tagOverview = settings.tagOverview.map { data in + TagOverview(context: context, id: "all-tags", data: data) + } + + context.settings = Settings(context: context, data: settings) + } +} diff --git a/CHDataManagement/Model/LocalizedPage.swift b/CHDataManagement/Model/LocalizedPage.swift index dd94420..1e60161 100644 --- a/CHDataManagement/Model/LocalizedPage.swift +++ b/CHDataManagement/Model/LocalizedPage.swift @@ -35,13 +35,7 @@ final class LocalizedPage: ObservableObject { let originalUrl: String? @Published - var linkPreviewImage: FileResource? - - @Published - var linkPreviewTitle: String? - - @Published - var linkPreviewDescription: String? + var linkPreview: LinkPreview @Published var hideTitle: Bool @@ -51,18 +45,14 @@ final class LocalizedPage: ObservableObject { title: String, lastModified: Date? = nil, originalUrl: String? = nil, - linkPreviewImage: FileResource? = nil, - linkPreviewTitle: String? = nil, - linkPreviewDescription: String? = nil, + linkPreview: LinkPreview = .init(), hideTitle: Bool = false) { self.content = content self.urlString = urlString self.title = title self.lastModified = lastModified self.originalUrl = originalUrl - self.linkPreviewImage = linkPreviewImage - self.linkPreviewTitle = linkPreviewTitle - self.linkPreviewDescription = linkPreviewDescription + self.linkPreview = linkPreview self.hideTitle = hideTitle } @@ -72,6 +62,37 @@ final class LocalizedPage: ObservableObject { } } -extension LocalizedPage: LinkPreviewItem { +extension LocalizedPage { + + convenience init(context: LoadingContext, data: LocalizedPage.Data) { + self.init( + content: context.content, + urlString: data.url, + title: data.title, + lastModified: data.lastModifiedDate, + originalUrl: data.originalURL, + linkPreview: .init(context: context, data: data.linkPreview), + hideTitle: data.hideTitle ?? false) + } + + /// The structure to store the metadata of a localized page + struct Data: Codable { + let url: String + let title: String + let linkPreview: LinkPreview.Data + let lastModifiedDate: Date? + let originalURL: String? + let hideTitle: Bool? + } + + var data: Data { + .init( + url: urlString, + title: title, + linkPreview: linkPreview.data, + lastModifiedDate: lastModified, + originalURL: originalUrl, + hideTitle: hideTitle ? true : nil) + } } diff --git a/CHDataManagement/Model/LocalizedPost.swift b/CHDataManagement/Model/LocalizedPost.swift index 12a0330..a651a72 100644 --- a/CHDataManagement/Model/LocalizedPost.swift +++ b/CHDataManagement/Model/LocalizedPost.swift @@ -22,13 +22,7 @@ final class LocalizedPost: ObservableObject { var pageLinkText: String? @Published - var linkPreviewImage: FileResource? - - @Published - var linkPreviewTitle: String? - - @Published - var linkPreviewDescription: String? + var linkPreview: LinkPreview init(content: Content, title: String? = nil, @@ -36,41 +30,14 @@ final class LocalizedPost: ObservableObject { lastModified: Date? = nil, images: [FileResource] = [], pageLinkText: String? = nil, - linkPreviewImage: FileResource? = nil, - linkPreviewTitle: String? = nil, - linkPreviewDescription: String? = nil) { + linkPreview: LinkPreview = .init()) { self.content = content self.title = title self.text = text self.lastModified = lastModified self.images = images self.pageLinkText = pageLinkText - self.linkPreviewImage = linkPreviewImage - self.linkPreviewTitle = linkPreviewTitle - self.linkPreviewDescription = linkPreviewDescription - } - - init(content: Content, file: LocalizedPostFile, images: [String : FileResource]) { - self.content = content - self.title = file.title - self.text = file.content - self.lastModified = file.lastModifiedDate - self.images = file.images.compactMap { images[$0] } - self.pageLinkText = file.pageLinkText - self.linkPreviewImage = file.linkPreviewImage.map { images[$0] } - self.linkPreviewTitle = file.linkPreviewTitle - self.linkPreviewDescription = file.linkPreviewDescription - } - - var postFile: LocalizedPostFile { - .init(images: images.map { $0.id }, - title: title, - content: text, - lastModifiedDate: lastModified, - pageLinkText: pageLinkText, - linkPreviewImage: linkPreviewImage?.id, - linkPreviewTitle: linkPreviewTitle, - linkPreviewDescription: linkPreviewDescription) + self.linkPreview = linkPreview } func contains(_ string: String) -> Bool { @@ -84,10 +51,41 @@ final class LocalizedPost: ObservableObject { if images.contains(file) { images.remove(file) } - remove(linkPreviewImage: file) + linkPreview.remove(file) } } -extension LocalizedPost: LinkPreviewItem { +// MARK: Storage +extension LocalizedPost { + + convenience init(context: LoadingContext, data: Data) { + self.init( + content: context.content, + title: data.title, + text: data.text, + lastModified: data.lastModifiedDate, + images: data.images.compactMap(context.image), + pageLinkText: data.pageLinkText, + linkPreview: .init(context: context, data: data.linkPreview)) + } + + var data: Data { + .init(images: images.map { $0.id }, + title: title, + text: text, + lastModifiedDate: lastModified, + pageLinkText: pageLinkText, + linkPreview: linkPreview.data) + } + + /// The structure to store the metadata of a localized post + struct Data: Codable { + let images: [String] + let title: String? + let text: String + let lastModifiedDate: Date? + let pageLinkText: String? + let linkPreview: LinkPreview.Data + } } diff --git a/CHDataManagement/Model/LocalizedTag.swift b/CHDataManagement/Model/LocalizedTag.swift index 9d5995f..35c48af 100644 --- a/CHDataManagement/Model/LocalizedTag.swift +++ b/CHDataManagement/Model/LocalizedTag.swift @@ -12,14 +12,7 @@ final class LocalizedTag: ObservableObject { var name: String @Published - var linkPreviewTitle: String? - - @Published - var linkPreviewDescription: String? - - /// The image id of the thumbnail - @Published - var linkPreviewImage: FileResource? + var linkPreview: LinkPreview /// The original url in the previous site layout let originalUrl: String? @@ -27,42 +20,51 @@ final class LocalizedTag: ObservableObject { init(content: Content, urlComponent: String, name: String, - linkPreviewTitle: String? = nil, - linkPreviewDescription: String? = nil, - linkPreviewImage: FileResource? = nil, + linkPreview: LinkPreview = .init(), originalUrl: String? = nil) { self.content = content self.urlComponent = urlComponent self.name = name - self.linkPreviewTitle = linkPreviewTitle - self.linkPreviewDescription = linkPreviewDescription - self.linkPreviewImage = linkPreviewImage + self.linkPreview = linkPreview self.originalUrl = originalUrl } func isValid(urlComponent: String) -> Bool { + !urlComponent.isEmpty && content.isValidIdForTagOrPageOrPost(urlComponent) && !content.containsTag(withUrlComponent: urlComponent) } /// The title to display when considering multiple items of this tag var title: String { - linkPreviewTitle ?? name + linkPreview.title ?? name } } -extension LocalizedTag: LinkPreviewItem { - -} +// MARK: Storage extension LocalizedTag { - var tagFile: LocalizedTagFile { + convenience init(context: LoadingContext, data: Data) { + self.init( + content: context.content, + urlComponent: data.urlComponent, + name: data.name, + linkPreview: .init(context: context, data: data.linkPreview), + originalUrl: data.originalUrl) + } + + struct Data: Codable { + let urlComponent: String + let name: String + let linkPreview: LinkPreview.Data + let originalUrl: String? + } + + var data: Data { .init(urlComponent: urlComponent, name: name, - linkPreviewTitle: linkPreviewTitle, - linkPreviewDescription: linkPreviewDescription, - linkPreviewImage: linkPreviewImage?.id, - originalURL: originalUrl) + linkPreview: linkPreview.data, + originalUrl: originalUrl) } } diff --git a/CHDataManagement/Model/Page.swift b/CHDataManagement/Model/Page.swift index 38ee208..955e540 100644 --- a/CHDataManagement/Model/Page.swift +++ b/CHDataManagement/Model/Page.swift @@ -1,6 +1,8 @@ import Foundation -final class Page: Item { +final class Page: Item, DateItem, LocalizedItem { + + override var itemType: ItemType { .page } /** The external link this page points to. @@ -38,9 +40,7 @@ final class Page: Item { @Published var tags: [Tag] - /** - Additional files to copy, because the page content references them - */ + /// Additional files to copy, because the page content references them @Published var requiredFiles: [FileResource] @@ -141,7 +141,7 @@ final class Page: Item { content.settings.paths.pagesOutputFolderPath + "/" + localized(in: language).urlString } - override var itemType: ItemType { + override var itemReference: ItemReference { .page(self) } @@ -161,15 +161,57 @@ final class Page: Item { if requiredFiles.contains(file) { requiredFiles.remove(file) } - english.remove(linkPreviewImage: file) - german.remove(linkPreviewImage: file) + english.linkPreview.remove(file) + german.linkPreview.remove(file) } } -extension Page: DateItem { +// MARK: Storage -} +extension Page { -extension Page: LocalizedItem { - + convenience init(context: LoadingContext, id: String, data: Data) { + self.init( + content: context.content, + id: id, + externalLink: data.externalLink, + isDraft: data.isDraft, + createdDate: data.createdDate, + hideDate: data.hideDate ?? false, + startDate: data.startDate, + endDate: data.endDate, + german: .init(context: context, data: data.german), + english: .init(context: context, data: data.english), + tags: data.tags.compactMap(context.tag), + requiredFiles: data.requiredFiles?.compactMap(context.file) ?? []) + } + + /// The structure to store the metadata of a page on disk + struct Data: Codable { + let isDraft: Bool + let externalLink: String? + let tags: [String] + let hideDate: Bool? + let createdDate: Date + let startDate: Date + let endDate: Date? + let german: LocalizedPage.Data + let english: LocalizedPage.Data + let requiredFiles: [String]? + } + + + var data: Data { + .init( + isDraft: isDraft, + externalLink: externalLink, + tags: tags.map { $0.id }, + hideDate: hideDate ? true : nil, + createdDate: createdDate, + startDate: startDate, + endDate: hasEndDate ? endDate : nil, + german: german.data, + english: english.data, + requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted()) + } } diff --git a/CHDataManagement/Model/Post.swift b/CHDataManagement/Model/Post.swift index 30c8f1b..5bc407b 100644 --- a/CHDataManagement/Model/Post.swift +++ b/CHDataManagement/Model/Post.swift @@ -1,6 +1,8 @@ import Foundation -final class Post: Item { +final class Post: Item, DateItem, LocalizedItem { + + override var itemType: ItemType { .post } @Published var isDraft: Bool @@ -142,10 +144,42 @@ final class Post: Item { } } -extension Post: DateItem { - -} - -extension Post: LocalizedItem { +extension Post { + convenience init(context: LoadingContext, id: String, data: Data) { + self.init( + content: context.content, + id: id, + isDraft: data.isDraft, + createdDate: data.createdDate, + startDate: data.startDate, + endDate: data.endDate, + tags: data.tags.compactMap(context.tag), + german: .init(context: context, data: data.german), + english: .init(context: context, data: data.english), + linkedPage: data.linkedPageId.map(context.page)) + } + + struct Data: Codable { + let isDraft: Bool + let createdDate: Date + let startDate: Date + let endDate: Date? + let tags: [String] + let german: LocalizedPost.Data + let english: LocalizedPost.Data + let linkedPageId: String? + } + + var data: Data { + .init( + isDraft: isDraft, + createdDate: createdDate, + startDate: startDate, + endDate: hasEndDate ? endDate : nil, + tags: tags.map { $0.id }, + german: german.data, + english: english.data, + linkedPageId: linkedPage?.id) + } } diff --git a/CHDataManagement/Model/Settings/AudioPlayerSettings.swift b/CHDataManagement/Model/Settings/AudioPlayerSettings.swift index d6b9ae0..fc0aeb2 100644 --- a/CHDataManagement/Model/Settings/AudioPlayerSettings.swift +++ b/CHDataManagement/Model/Settings/AudioPlayerSettings.swift @@ -1,6 +1,6 @@ import Foundation -final class AudioPlayerSettings: ObservableObject { +final class AudioPlayerSettings: ObservableObject, LocalizedItem { @Published var playlistCoverImageSize: Int @@ -34,24 +34,6 @@ final class AudioPlayerSettings: ObservableObject { self.english = english } - init(file: AudioPlayerSettingsFile, files: [String : FileResource]) { - self.playlistCoverImageSize = file.playlistCoverImageSize - self.smallCoverImageSize = file.smallCoverImageSize - self.audioPlayerJsFile = file.audioPlayerJsFile.map { files[$0] } - self.audioPlayerCssFile = file.audioPlayerCssFile.map { files[$0] } - self.german = .init(file: file.german) - self.english = .init(file: file.english) - } - - var file: AudioPlayerSettingsFile { - .init(playlistCoverImageSize: playlistCoverImageSize, - smallCoverImageSize: smallCoverImageSize, - audioPlayerJsFile: audioPlayerJsFile?.id, - audioPlayerCssFile: audioPlayerCssFile?.id, - german: german.file, - english: english.file) - } - func remove(_ file: FileResource) { if audioPlayerJsFile == file { audioPlayerJsFile = nil @@ -62,17 +44,37 @@ final class AudioPlayerSettings: ObservableObject { } } +// MARK: Storage + extension AudioPlayerSettings { - static let `default`: AudioPlayerSettings = .init( - playlistCoverImageSize: 280, - smallCoverImageSize: 78, - audioPlayerJsFile: nil, - audioPlayerCssFile: nil, - german: .init(playlistText: "Wiedergabeliste"), - english: .init(playlistText: "Playlist")) -} + convenience init(context: LoadingContext, data: Data) { + self.init( + playlistCoverImageSize: data.playlistCoverImageSize, + smallCoverImageSize: data.smallCoverImageSize, + audioPlayerJsFile: data.audioPlayerJsFile.map(context.file), + audioPlayerCssFile: data.audioPlayerCssFile.map(context.file), + german: .init(data: data.german), + english: .init(data: data.english)) + } + + var data: Data { + .init(playlistCoverImageSize: playlistCoverImageSize, + smallCoverImageSize: smallCoverImageSize, + audioPlayerJsFile: audioPlayerJsFile?.id, + audioPlayerCssFile: audioPlayerCssFile?.id, + german: german.data, + english: english.data) + } + + struct Data: Codable { + let playlistCoverImageSize: Int + let smallCoverImageSize: Int + let audioPlayerJsFile: String? + let audioPlayerCssFile: String? + let german: LocalizedAudioPlayerSettings.Data + let english: LocalizedAudioPlayerSettings.Data + } -extension AudioPlayerSettings: LocalizedItem { } diff --git a/CHDataManagement/Model/Settings/LocalizedAudioPlayerSettings.swift b/CHDataManagement/Model/Settings/LocalizedAudioPlayerSettings.swift index 61089d7..a6166e4 100644 --- a/CHDataManagement/Model/Settings/LocalizedAudioPlayerSettings.swift +++ b/CHDataManagement/Model/Settings/LocalizedAudioPlayerSettings.swift @@ -8,12 +8,21 @@ final class LocalizedAudioPlayerSettings: ObservableObject { init(playlistText: String) { self.playlistText = playlistText } +} - init(file: LocalizedAudioPlayerSettingsFile) { - self.playlistText = file.playlistText +// MARK: Storage + +extension LocalizedAudioPlayerSettings { + + convenience init(data: Data) { + self.init(playlistText: data.playlistText) } - var file: LocalizedAudioPlayerSettingsFile { + var data: Data { .init(playlistText: playlistText) } + + struct Data: Codable { + let playlistText: String + } } diff --git a/CHDataManagement/Model/Settings/LocalizedNavigationSettings.swift b/CHDataManagement/Model/Settings/LocalizedNavigationSettings.swift index ffc92c2..bc3a160 100644 --- a/CHDataManagement/Model/Settings/LocalizedNavigationSettings.swift +++ b/CHDataManagement/Model/Settings/LocalizedNavigationSettings.swift @@ -8,12 +8,21 @@ final class LocalizedNavigationSettings: ObservableObject { init(rootUrl: String) { self.rootUrl = rootUrl } +} - init(file: LocalizedNavigationSettingsFile) { - self.rootUrl = file.rootUrl +// MARK: Storage + +extension LocalizedNavigationSettings { + + convenience init(data: Data) { + self.init(rootUrl: data.rootUrl) } - var file: LocalizedNavigationSettingsFile { + struct Data: Codable { + let rootUrl: String + } + + var data: Data { .init(rootUrl: rootUrl) } } diff --git a/CHDataManagement/Model/Settings/LocalizedPageSettings.swift b/CHDataManagement/Model/Settings/LocalizedPageSettings.swift index 1e71a21..96ce355 100644 --- a/CHDataManagement/Model/Settings/LocalizedPageSettings.swift +++ b/CHDataManagement/Model/Settings/LocalizedPageSettings.swift @@ -14,14 +14,25 @@ final class LocalizedPageSettings: ObservableObject { self.emptyPageTitle = emptyPageTitle self.emptyPageText = emptyPageText } +} - init(file: LocalizedPageSettingsFile) { - self.emptyPageTitle = file.emptyPageTitle - self.emptyPageText = file.emptyPageText +// MARK: Storage + +extension LocalizedPageSettings { + + convenience init(data: Data) { + self.init( + emptyPageTitle: data.emptyPageTitle, + emptyPageText: data.emptyPageText) } - var file: LocalizedPageSettingsFile { + var data: Data { .init(emptyPageTitle: emptyPageTitle, emptyPageText: emptyPageText) } + + struct Data: Codable { + let emptyPageTitle: String + let emptyPageText: String + } } diff --git a/CHDataManagement/Model/Settings/LocalizedPostSettings.swift b/CHDataManagement/Model/Settings/LocalizedPostSettings.swift index 4439a8f..71bbf2e 100644 --- a/CHDataManagement/Model/Settings/LocalizedPostSettings.swift +++ b/CHDataManagement/Model/Settings/LocalizedPostSettings.swift @@ -2,15 +2,23 @@ import Foundation final class LocalizedPostSettings: ObservableObject { + /// The page title for the post feed @Published var title: String + /// The page description for the post feed @Published var description: String + /// The path to the feed in the final website, appended with the page number @Published var feedUrlPrefix: String + /** + The text to display when linking to a page + + Each post may define a custom text. + */ @Published var defaultPageLinkText: String @@ -20,21 +28,32 @@ final class LocalizedPostSettings: ObservableObject { self.feedUrlPrefix = feedUrlPrefix self.defaultPageLinkText = defaultPageLinkText } +} - // MARK: Storage +// MARK: Storage - init(file: LocalizedPostSettingsFile) { - self.title = file.feedTitle - self.description = file.feedDescription - self.feedUrlPrefix = file.feedUrlPrefix - self.defaultPageLinkText = file.defaultPageLinkText ?? "View" +extension LocalizedPostSettings { + + convenience init(data: Data) { + self.init( + title: data.feedTitle, + description: data.feedDescription, + feedUrlPrefix: data.feedUrlPrefix, + defaultPageLinkText: data.defaultPageLinkText) } - var file: LocalizedPostSettingsFile { + var data: Data { .init( feedTitle: title, feedDescription: description, feedUrlPrefix: feedUrlPrefix, defaultPageLinkText: defaultPageLinkText) } + + struct Data: Codable { + let feedTitle: String + let feedDescription: String + let feedUrlPrefix: String + let defaultPageLinkText: String + } } diff --git a/CHDataManagement/Model/Settings/NavigationSettings.swift b/CHDataManagement/Model/Settings/NavigationSettings.swift index ff80abc..4813a54 100644 --- a/CHDataManagement/Model/Settings/NavigationSettings.swift +++ b/CHDataManagement/Model/Settings/NavigationSettings.swift @@ -1,6 +1,6 @@ import Foundation -final class NavigationSettings: ObservableObject { +final class NavigationSettings: ObservableObject, LocalizedItem { /// The items to show in the navigation bar @Published @@ -19,23 +19,31 @@ final class NavigationSettings: ObservableObject { self.german = german self.english = english } - - init(file: NavigationSettingsFile, map: (String) -> Item?) { - self.navigationItems = file.navigationItems.compactMap(map) - self.german = LocalizedNavigationSettings(file: file.german) - self.english = LocalizedNavigationSettings(file: file.english) - } - - var file: NavigationSettingsFile { - .init( - navigationItems: navigationItems.map { $0.itemType.id }, - german: german.file, - english: english.file) - } } -extension NavigationSettings: LocalizedItem { - +// MARK: Storage + +extension NavigationSettings { + + convenience init(context: LoadingContext, data: NavigationSettings.Data) { + self.init( + navigationItems: data.navigationItems.compactMap(context.item), + german: LocalizedNavigationSettings(data: data.german), + english: LocalizedNavigationSettings(data: data.english)) + } + + struct Data: Codable { + let navigationItems: [ItemId] + let german: LocalizedNavigationSettings.Data + let english: LocalizedNavigationSettings.Data + } + + var data: Data { + .init( + navigationItems: navigationItems.map { $0.itemId }, + german: german.data, + english: english.data) + } } extension NavigationSettings { diff --git a/CHDataManagement/Model/Settings/PageSettings.swift b/CHDataManagement/Model/Settings/PageSettings.swift index 5f00021..056e122 100644 --- a/CHDataManagement/Model/Settings/PageSettings.swift +++ b/CHDataManagement/Model/Settings/PageSettings.swift @@ -32,30 +32,26 @@ final class PageSettings: ObservableObject { @Published var english: LocalizedPageSettings - init(file: PageSettingsFile, files: [String : FileResource]) { - self.contentWidth = file.contentWidth - self.largeImageWidth = file.largeImageWidth - self.pageLinkImageSize = file.pageLinkImageSize - self.defaultCssFile = file.defaultCssFile.map { files[$0] } - self.codeHighlightingJsFile = file.codeHighlightingJsFile.map { files[$0] } - self.modelViewerJsFile = file.modelViewerJsFile.map { files[$0] } - self.imageCompareCssFile = file.imageCompareCssFile.map { files[$0] } - self.imageCompareJsFile = file.imageCompareJsFile.map { files[$0] } - self.german = .init(file: file.german) - self.english = .init(file: file.english) - } - - var file: PageSettingsFile { - .init(contentWidth: contentWidth, - largeImageWidth: largeImageWidth, - pageLinkImageSize: pageLinkImageSize, - defaultCssFile: defaultCssFile?.id, - codeHighlightingJsFile: codeHighlightingJsFile?.id, - modelViewerJsFile: modelViewerJsFile?.id, - imageCompareJsFile: imageCompareJsFile?.id, - imageCompareCssFile: imageCompareCssFile?.id, - german: german.file, - english: english.file) + init(contentWidth: Int, + largeImageWidth: Int, + pageLinkImageSize: Int, + defaultCssFile: FileResource? = nil, + codeHighlightingJsFile: FileResource? = nil, + modelViewerJsFile: FileResource? = nil, + imageCompareJsFile: FileResource? = nil, + imageCompareCssFile: FileResource? = nil, + german: LocalizedPageSettings, + english: LocalizedPageSettings) { + self.contentWidth = contentWidth + self.largeImageWidth = largeImageWidth + self.pageLinkImageSize = pageLinkImageSize + self.defaultCssFile = defaultCssFile + self.codeHighlightingJsFile = codeHighlightingJsFile + self.modelViewerJsFile = modelViewerJsFile + self.imageCompareJsFile = imageCompareJsFile + self.imageCompareCssFile = imageCompareCssFile + self.german = german + self.english = english } func remove(_ file: FileResource) { @@ -77,6 +73,52 @@ final class PageSettings: ObservableObject { } } +// MARK: Storage + +extension PageSettings { + + convenience init(context: LoadingContext, data: Data) { + self.init( + contentWidth: data.contentWidth, + largeImageWidth: data.largeImageWidth, + pageLinkImageSize: data.pageLinkImageSize, + defaultCssFile: data.defaultCssFile.map(context.file), + codeHighlightingJsFile: data.codeHighlightingJsFile.map(context.file), + modelViewerJsFile: data.modelViewerJsFile.map(context.file), + imageCompareJsFile: data.imageCompareJsFile.map(context.file), + imageCompareCssFile: data.imageCompareCssFile.map(context.file), + german: .init(data: data.german), + english: .init(data: data.english)) + } + + var data: Data { + .init(contentWidth: contentWidth, + largeImageWidth: largeImageWidth, + pageLinkImageSize: pageLinkImageSize, + defaultCssFile: defaultCssFile?.id, + codeHighlightingJsFile: codeHighlightingJsFile?.id, + modelViewerJsFile: modelViewerJsFile?.id, + imageCompareJsFile: imageCompareJsFile?.id, + imageCompareCssFile: imageCompareCssFile?.id, + german: german.data, + english: english.data) + } + + struct Data: Codable { + let contentWidth: Int + let largeImageWidth: Int + let pageLinkImageSize: Int + let defaultCssFile: String? + let codeHighlightingJsFile: String? + let modelViewerJsFile: String? + let imageCompareJsFile: String? + let imageCompareCssFile: String? + let german: LocalizedPageSettings.Data + let english: LocalizedPageSettings.Data + } + +} + extension PageSettings: LocalizedItem { } diff --git a/CHDataManagement/Model/Settings/PathSettings.swift b/CHDataManagement/Model/Settings/PathSettings.swift index 13067f1..141667c 100644 --- a/CHDataManagement/Model/Settings/PathSettings.swift +++ b/CHDataManagement/Model/Settings/PathSettings.swift @@ -23,23 +23,54 @@ final class PathSettings: ObservableObject { @Published var tagsOutputFolderPath: String - init(file: PathSettingsFile) { - self.assetsOutputFolderPath = file.assetsOutputFolderPath - self.pagesOutputFolderPath = file.pagesOutputFolderPath - self.imagesOutputFolderPath = file.imagesOutputFolderPath - self.filesOutputFolderPath = file.filesOutputFolderPath - self.videosOutputFolderPath = file.videosOutputFolderPath - self.tagsOutputFolderPath = file.tagsOutputFolderPath - self.audioOutputFolderPath = file.audioOutputFolderPath - } - - var file: PathSettingsFile { - .init(assetsOutputFolderPath: assetsOutputFolderPath, - pagesOutputFolderPath: pagesOutputFolderPath, - imagesOutputFolderPath: imagesOutputFolderPath, - filesOutputFolderPath: filesOutputFolderPath, - videosOutputFolderPath: videosOutputFolderPath, - tagsOutputFolderPath: tagsOutputFolderPath, - audioOutputFolderPath: audioOutputFolderPath) + init(assetsOutputFolderPath: String, + pagesOutputFolderPath: String, + imagesOutputFolderPath: String, + filesOutputFolderPath: String, + videosOutputFolderPath: String, + audioOutputFolderPath: String, + tagsOutputFolderPath: String) { + self.assetsOutputFolderPath = assetsOutputFolderPath + self.pagesOutputFolderPath = pagesOutputFolderPath + self.imagesOutputFolderPath = imagesOutputFolderPath + self.filesOutputFolderPath = filesOutputFolderPath + self.videosOutputFolderPath = videosOutputFolderPath + self.audioOutputFolderPath = audioOutputFolderPath + self.tagsOutputFolderPath = tagsOutputFolderPath + } +} + +extension PathSettings { + + convenience init(data: Data) { + self.init( + assetsOutputFolderPath: data.assetsOutputFolderPath, + pagesOutputFolderPath: data.pagesOutputFolderPath, + imagesOutputFolderPath: data.imagesOutputFolderPath, + filesOutputFolderPath: data.filesOutputFolderPath, + videosOutputFolderPath: data.videosOutputFolderPath, + audioOutputFolderPath: data.audioOutputFolderPath, + tagsOutputFolderPath: data.tagsOutputFolderPath) + } + + var data: Data { + .init( + assetsOutputFolderPath: assetsOutputFolderPath, + pagesOutputFolderPath: pagesOutputFolderPath, + imagesOutputFolderPath: imagesOutputFolderPath, + filesOutputFolderPath: filesOutputFolderPath, + videosOutputFolderPath: videosOutputFolderPath, + audioOutputFolderPath: audioOutputFolderPath, + tagsOutputFolderPath: tagsOutputFolderPath) + } + + struct Data: Codable { + let assetsOutputFolderPath: String + let pagesOutputFolderPath: String + let imagesOutputFolderPath: String + let filesOutputFolderPath: String + let videosOutputFolderPath: String + let audioOutputFolderPath: String + let tagsOutputFolderPath: String } } diff --git a/CHDataManagement/Model/Settings/PostSettings.swift b/CHDataManagement/Model/Settings/PostSettings.swift index e0aefee..20b3cfb 100644 --- a/CHDataManagement/Model/Settings/PostSettings.swift +++ b/CHDataManagement/Model/Settings/PostSettings.swift @@ -1,6 +1,6 @@ import Foundation -final class PostSettings: ObservableObject { +final class PostSettings: ObservableObject, LocalizedItem { /// The number of posts to show in a single page of the news feed @Published @@ -41,28 +41,6 @@ final class PostSettings: ObservableObject { self.english = english } - // MARK: Storage - - init(file: PostSettingsFile, files: [String : FileResource]) { - self.postsPerPage = file.postsPerPage - self.contentWidth = file.contentWidth - self.swiperCssFile = file.swiperCssFile.map { files[$0] } - self.swiperJsFile = file.swiperJsFile.map { files[$0] } - self.defaultCssFile = file.defaultCssFile.map { files[$0] } - self.german = .init(file: file.german) - self.english = .init(file: file.english) - } - - var file: PostSettingsFile { - .init(postsPerPage: postsPerPage, - contentWidth: contentWidth, - swiperCssFile: swiperCssFile?.id, - swiperJsFile: swiperJsFile?.id, - defaultCssFile: defaultCssFile?.id, - german: german.file, - english: english.file) - } - func remove(_ file: FileResource) { if swiperJsFile == file { swiperJsFile = nil @@ -76,13 +54,38 @@ final class PostSettings: ObservableObject { } } +// MARK: Storage + extension PostSettings { - static var `default`: PostSettings { - .init(file: .default, files: [:]) + convenience init(context: LoadingContext, data: Data) { + self.init( + postsPerPage: data.postsPerPage, + contentWidth: data.contentWidth, + swiperCssFile: data.swiperCssFile.map(context.file), + swiperJsFile: data.swiperJsFile.map(context.file), + defaultCssFile: data.defaultCssFile.map(context.file), + german: .init(data: data.german), + english: .init(data: data.english)) + } + + var data: PostSettings.Data { + .init(postsPerPage: postsPerPage, + contentWidth: contentWidth, + swiperCssFile: swiperCssFile?.id, + swiperJsFile: swiperJsFile?.id, + defaultCssFile: defaultCssFile?.id, + german: german.data, + english: english.data) + } + + struct Data: Codable { + let postsPerPage: Int + let contentWidth: Int + let swiperCssFile: String? + let swiperJsFile: String? + let defaultCssFile: String? + let german: LocalizedPostSettings.Data + let english: LocalizedPostSettings.Data } } - -extension PostSettings: LocalizedItem { - -} diff --git a/CHDataManagement/Model/Settings/Settings.swift b/CHDataManagement/Model/Settings/Settings.swift index 82421f2..d909f2d 100644 --- a/CHDataManagement/Model/Settings/Settings.swift +++ b/CHDataManagement/Model/Settings/Settings.swift @@ -30,25 +30,6 @@ final class Settings: ObservableObject { self.audioPlayer = audioPlayer } - init(file: SettingsFile, files: [String : FileResource], map: (String) -> Item?) { - self.navigation = NavigationSettings(file: file.navigation, map: map) - - self.posts = PostSettings(file: file.posts, files: files) - self.pages = PageSettings(file: file.pages, files: files) - self.paths = PathSettings(file: file.paths) - self.audioPlayer = .init(file: file.audioPlayer, files: files) - } - - func file(tagOverview: TagOverviewPage?) -> SettingsFile { - .init( - paths: paths.file, - navigation: navigation.file, - posts: posts.file, - pages: pages.file, - audioPlayer: audioPlayer.file, - tagOverview: tagOverview?.file) - } - func remove(_ file: FileResource) { pages.remove(file) posts.remove(file) @@ -56,6 +37,39 @@ final class Settings: ObservableObject { } } +// MARK: Storage + +extension Settings { + + convenience init(context: LoadingContext, data: Settings.Data) { + self.init( + paths: .init(data: data.paths), + navigation: .init(context: context, data: data.navigation), + posts: .init(context: context, data: data.posts), + pages: .init(context: context, data: data.pages), + audioPlayer: .init(context: context, data: data.audioPlayer)) + } + + func data(tagOverview: Tag?) -> Data { + .init( + paths: paths.data, + navigation: navigation.data, + posts: posts.data, + pages: pages.data, + audioPlayer: audioPlayer.data, + tagOverview: tagOverview?.data) + } + + struct Data: Codable { + let paths: PathSettings.Data + let navigation: NavigationSettings.Data + let posts: PostSettings.Data + let pages: PageSettings.Data + let audioPlayer: AudioPlayerSettings.Data + let tagOverview: Tag.Data? + } +} + extension Settings { static let `default`: Settings = .init( @@ -65,3 +79,70 @@ extension Settings { pages: .default, audioPlayer: .default) } + +extension AudioPlayerSettings { + + static let `default`: AudioPlayerSettings = .init( + playlistCoverImageSize: 280, + smallCoverImageSize: 78, + audioPlayerJsFile: nil, + audioPlayerCssFile: nil, + german: .init(playlistText: "Wiedergabeliste"), + english: .init(playlistText: "Playlist")) +} + +extension PostSettings { + + static var `default`: PostSettings { + .init(postsPerPage: 25, + contentWidth: 600, + swiperCssFile: nil, + swiperJsFile: nil, + defaultCssFile: nil, + german: .init( + title: "Beiträge", + description: "Alle Beiträge", + feedUrlPrefix: "blog", + defaultPageLinkText: "Anzeigen"), + english: .init( + title: "Blog posts", + description: "All blog posts", + feedUrlPrefix: "blog", + defaultPageLinkText: "View")) + } +} + +extension PathSettings { + + static var `default`: PathSettings { + .init( + assetsOutputFolderPath: "asset", + pagesOutputFolderPath: "page", + imagesOutputFolderPath: "image", + filesOutputFolderPath: "file", + videosOutputFolderPath: "video", + audioOutputFolderPath: "audio", + tagsOutputFolderPath: "tag") + } + +} + +extension PageSettings { + + static var `default`: PageSettings { + .init(contentWidth: 600, + largeImageWidth: 1200, + pageLinkImageSize: 180, + defaultCssFile: nil, + codeHighlightingJsFile: nil, + modelViewerJsFile: nil, + imageCompareJsFile: nil, + imageCompareCssFile: nil, + german: .init( + emptyPageTitle: "Leere Seite", + emptyPageText: "Diese Seite ist leer"), + english: .init( + emptyPageTitle: "Empty page", + emptyPageText: "This page is empty")) + } +} diff --git a/CHDataManagement/Model/Tag.swift b/CHDataManagement/Model/Tag.swift index b5a1ff0..0b60f4d 100644 --- a/CHDataManagement/Model/Tag.swift +++ b/CHDataManagement/Model/Tag.swift @@ -1,6 +1,8 @@ import Foundation -final class Tag: Item { +class Tag: Item, LocalizedItem { + + override var itemType: ItemType { .tag } @Published var isVisible: Bool @@ -59,7 +61,7 @@ final class Tag: Item { localized(in: language).title } - override var itemType: ItemType { + override var itemReference: ItemReference { .tagPage(self) } @@ -68,21 +70,35 @@ final class Tag: Item { } func remove(_ file: FileResource) { - english.remove(linkPreviewImage: file) - german.remove(linkPreviewImage: file) + english.linkPreview.remove(file) + german.linkPreview.remove(file) } } -extension Tag: LocalizedItem { - -} +// MARK: Storage extension Tag { - var file: TagFile { - .init(id: id, - isVisible: isVisible, - german: german.tagFile, - english: english.tagFile) + convenience init(context: LoadingContext, id: String, data: Data) { + self.init( + content: context.content, + id: id, + isVisible: data.isVisible ?? true, + german: .init(context: context, data: data.german), + english: .init(context: context, data: data.english)) + } + + struct Data: Codable { + // Defaults to true if unset + let isVisible: Bool? + let german: LocalizedTag.Data + let english: LocalizedTag.Data + } + + var data: Data { + .init( + isVisible: isVisible ? nil : false, + german: german.data, + english: english.data) } } diff --git a/CHDataManagement/Model/TagOverview.swift b/CHDataManagement/Model/TagOverview.swift new file mode 100644 index 0000000..ef78c14 --- /dev/null +++ b/CHDataManagement/Model/TagOverview.swift @@ -0,0 +1,7 @@ + +final class TagOverview: Tag { + + override var itemId: ItemId { + .init(type: .tagOverview, id: id) + } +} diff --git a/CHDataManagement/Page Elements/ContentElements/Images/PartialSvgImage.swift b/CHDataManagement/Page Elements/ContentElements/Images/PartialSvgImage.swift index edf15af..c8537aa 100644 --- a/CHDataManagement/Page Elements/ContentElements/Images/PartialSvgImage.swift +++ b/CHDataManagement/Page Elements/ContentElements/Images/PartialSvgImage.swift @@ -3,7 +3,7 @@ struct PartialSvgImage: HtmlProducer { let imagePath: String - let altText: String + let altText: String? let x: Int @@ -20,9 +20,16 @@ struct PartialSvgImage: HtmlProducer { return Double(width) / Double(height) } + private var imageAltText: String { + guard let altText else { + return "" + } + return " alt='\(altText.htmlEscaped())'" + } + func populate(_ result: inout String) { result += "" - result += "\(altText)" + result += "" result += "" } } diff --git a/CHDataManagement/Page Elements/ContentElements/Images/SimpleImage.swift b/CHDataManagement/Page Elements/ContentElements/Images/SimpleImage.swift index 20c48a2..ff0ae82 100644 --- a/CHDataManagement/Page Elements/ContentElements/Images/SimpleImage.swift +++ b/CHDataManagement/Page Elements/ContentElements/Images/SimpleImage.swift @@ -4,11 +4,18 @@ struct SimpleImage: HtmlProducer { let imagePath: String - let altText: String + let altText: String? + + private var imageAltText: String { + guard let altText else { + return "" + } + return " alt='\(altText.htmlEscaped())'" + } func populate(_ result: inout String) { result += "
" - result += "\(altText)" + result += "" result += "
" } } diff --git a/CHDataManagement/Page Elements/ContentElements/ModelViewer.swift b/CHDataManagement/Page Elements/ContentElements/ModelViewer.swift index f7e07a3..0a4150a 100644 --- a/CHDataManagement/Page Elements/ContentElements/ModelViewer.swift +++ b/CHDataManagement/Page Elements/ContentElements/ModelViewer.swift @@ -3,9 +3,16 @@ struct ModelViewer { let file: String - let description: String + let description: String? + + private var imageAltText: String { + guard let description else { + return "" + } + return " alt='\(description.htmlEscaped())'" + } var content: String { - "" + "" } } diff --git a/CHDataManagement/Preview Content/Tag+Mock.swift b/CHDataManagement/Preview Content/Tag+Mock.swift index cbc9619..302d02c 100644 --- a/CHDataManagement/Preview Content/Tag+Mock.swift +++ b/CHDataManagement/Preview Content/Tag+Mock.swift @@ -43,15 +43,15 @@ extension LocalizedTag { content: .mock, urlComponent: "electronics", name: "Electronics", - linkPreviewDescription: "Some description of the tag", - linkPreviewImage: FileResource(resourceImage: "image1", type: .jpg), + linkPreview: .init(description: "Some description of the tag", + image: FileResource(resourceImage: "image1", type: .jpg)), originalUrl: "projects/electronics") static let german = LocalizedTag( content: .mock, urlComponent: "elektronik", name: "Elektronik", - linkPreviewDescription: "Eine Beschreibung des Tags", - linkPreviewImage: FileResource(resourceImage: "image2", type: .jpg), + linkPreview: .init(description: "Eine Beschreibung des Tags", + image: FileResource(resourceImage: "image2", type: .jpg)), originalUrl: "projects/electronics") } diff --git a/CHDataManagement/Preview Content/WebsiteData+Mock.swift b/CHDataManagement/Preview Content/WebsiteData+Mock.swift deleted file mode 100644 index 420d4f2..0000000 --- a/CHDataManagement/Preview Content/WebsiteData+Mock.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation - -extension PathSettings { - - static var `default`: PathSettings { - .init(file: .default) - } -} - -extension PageSettings { - - static var `default`: PageSettings { - .init(file: .default, files: [:]) - } -} - -extension LocalizedPostSettings { - - static var german: LocalizedPostSettings { - .init( - title: "Titel", - description: "Beschreibung", - feedUrlPrefix: "blog", - defaultPageLinkText: "Anzeigen") - } - - static var english: LocalizedPostSettings { - .init( - title: "A Title", - description: "Description", - feedUrlPrefix: "feed", - defaultPageLinkText: "View") - } -} diff --git a/CHDataManagement/Storage/Model/FileDescriptions.swift b/CHDataManagement/Storage/Model/FileDescriptions.swift deleted file mode 100644 index d98acef..0000000 --- a/CHDataManagement/Storage/Model/FileDescriptions.swift +++ /dev/null @@ -1,13 +0,0 @@ - -struct FileDescriptions { - - let fileId: String - - let german: String? - - let english: String? -} - -extension FileDescriptions: Codable { - -} diff --git a/CHDataManagement/Storage/Model/FileResourceFile.swift b/CHDataManagement/Storage/Model/FileResourceFile.swift deleted file mode 100644 index 6cca463..0000000 --- a/CHDataManagement/Storage/Model/FileResourceFile.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -/** - This struct holds metadata about a file resource that is stored in the content folder. - */ -struct FileResourceFile { - - /// The file/image description in German - let englishDescription: String? - - /// The file/image description in English - let germanDescription: String? - - /// The list of generated image versions for this image - let generatedImages: [String]? - - /// A custom file path in the output folder where this file is located - let customOutputPath: String? - - /// A version string of this resource, mostly for assets - let version: String? - - /// A URL where the resource was copied/downloaded from - let sourceUrl: String? - - /// The date when the file was added - let addedDate: Date - - /// The date when the file was last modified - let modifiedDate: Date -} - - -extension FileResourceFile: Codable { - -} diff --git a/CHDataManagement/Storage/Model/PageFile.swift b/CHDataManagement/Storage/Model/PageFile.swift deleted file mode 100644 index 7e1eeaf..0000000 --- a/CHDataManagement/Storage/Model/PageFile.swift +++ /dev/null @@ -1,58 +0,0 @@ -import Foundation - -struct PageFile { - - let isDraft: Bool - - let externalLink: String? - - let tags: [String] - - let hideDate: Bool? - - let createdDate: Date - - let startDate: Date - - let endDate: Date? - - let german: LocalizedPageFile - - let english: LocalizedPageFile - - /** - Specifies additional files which should be copied to the destination when generating the content. - - Note: This property defaults to an empty set. - */ - let requiredFiles: [String]? -} - -extension PageFile: Codable { - -} - -/** - The structure to store the metadata of a localized page - */ -struct LocalizedPageFile { - - let url: String - - let title: String - - let linkPreviewImage: String? - - let linkPreviewTitle: String? - - let linkPreviewDescription: String? - - let lastModifiedDate: Date? - - let originalURL: String? - - let hideTitle: Bool? -} - -extension LocalizedPageFile: Codable { - -} diff --git a/CHDataManagement/Storage/Model/PostFile.swift b/CHDataManagement/Storage/Model/PostFile.swift deleted file mode 100644 index 3cb723a..0000000 --- a/CHDataManagement/Storage/Model/PostFile.swift +++ /dev/null @@ -1,50 +0,0 @@ -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: [String] - - let title: String? - - let content: String - - let lastModifiedDate: Date? - - let pageLinkText: String? - - let linkPreviewImage: String? - - let linkPreviewTitle: String? - - let linkPreviewDescription: String? -} - -extension LocalizedPostFile: Codable { - -} diff --git a/CHDataManagement/Storage/Model/Settings/AudioPlayerSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/AudioPlayerSettingsFile.swift deleted file mode 100644 index f0829b7..0000000 --- a/CHDataManagement/Storage/Model/Settings/AudioPlayerSettingsFile.swift +++ /dev/null @@ -1,24 +0,0 @@ - -struct AudioPlayerSettingsFile: Codable { - - let playlistCoverImageSize: Int - - let smallCoverImageSize: Int - - let audioPlayerJsFile: String? - - let audioPlayerCssFile: String? - - let german: LocalizedAudioPlayerSettingsFile - - let english: LocalizedAudioPlayerSettingsFile -} - -struct LocalizedAudioPlayerSettingsFile: Codable { - - let playlistText: String -} - -extension AudioPlayerSettingsFile: LocalizedItem { - -} diff --git a/CHDataManagement/Storage/Model/Settings/LocalizedNavigationSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/LocalizedNavigationSettingsFile.swift deleted file mode 100644 index 13a1035..0000000 --- a/CHDataManagement/Storage/Model/Settings/LocalizedNavigationSettingsFile.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -struct LocalizedNavigationSettingsFile { - - let rootUrl: String -} - -extension LocalizedNavigationSettingsFile: Codable { - -} - -extension LocalizedNavigationSettingsFile { - - static var `default`: LocalizedNavigationSettingsFile { - .init(rootUrl: "/") - } -} diff --git a/CHDataManagement/Storage/Model/Settings/LocalizedPageSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/LocalizedPageSettingsFile.swift deleted file mode 100644 index c08be1a..0000000 --- a/CHDataManagement/Storage/Model/Settings/LocalizedPageSettingsFile.swift +++ /dev/null @@ -1,23 +0,0 @@ - -struct LocalizedPageSettingsFile { - - let emptyPageTitle: String - - let emptyPageText: String - - init(emptyPageTitle: String, emptyPageText: String) { - self.emptyPageTitle = emptyPageTitle - self.emptyPageText = emptyPageText - } -} - -extension LocalizedPageSettingsFile: Codable { - -} - -extension LocalizedPageSettingsFile { - - static var `default`: LocalizedPageSettingsFile { - .init(emptyPageTitle: "Empty Page", emptyPageText: "This page is empty.") - } -} diff --git a/CHDataManagement/Storage/Model/Settings/LocalizedPostSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/LocalizedPostSettingsFile.swift deleted file mode 100644 index cba8577..0000000 --- a/CHDataManagement/Storage/Model/Settings/LocalizedPostSettingsFile.swift +++ /dev/null @@ -1,31 +0,0 @@ - -struct LocalizedPostSettingsFile { - - /// The page title for the post feed - let feedTitle: String - - /// The page description for the post feed - let feedDescription: String - - /// The path to the feed in the final website, appended with the page number - let feedUrlPrefix: String - - /** - The text to display when linking to a page - - Each post may define a custom text. - */ - let defaultPageLinkText: String? -} - -extension LocalizedPostSettingsFile: Codable { } - -extension LocalizedPostSettingsFile { - - static var `default`: LocalizedPostSettingsFile { - .init(feedTitle: "A title", - feedDescription: "A description", - feedUrlPrefix: "blog", - defaultPageLinkText: "View") - } -} diff --git a/CHDataManagement/Storage/Model/Settings/NavigationSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/NavigationSettingsFile.swift deleted file mode 100644 index be5b373..0000000 --- a/CHDataManagement/Storage/Model/Settings/NavigationSettingsFile.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation - -struct NavigationSettingsFile { - - /// The tags to show in the navigation bar - let navigationItems: [String] - - let german: LocalizedNavigationSettingsFile - - let english: LocalizedNavigationSettingsFile -} - -extension NavigationSettingsFile: Codable { - -} - -extension NavigationSettingsFile { - - static var `default`: NavigationSettingsFile { - .init( - navigationItems: [], - german: .default, - english: .default) - } -} diff --git a/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift deleted file mode 100644 index f09da9f..0000000 --- a/CHDataManagement/Storage/Model/Settings/PageSettingsFile.swift +++ /dev/null @@ -1,47 +0,0 @@ - -struct PageSettingsFile { - - let contentWidth: Int - - let largeImageWidth: Int - - let pageLinkImageSize: Int - - let defaultCssFile: String? - - let codeHighlightingJsFile: String? - - let modelViewerJsFile: String? - - let imageCompareJsFile: String? - - let imageCompareCssFile: String? - - let german: LocalizedPageSettingsFile - - let english: LocalizedPageSettingsFile -} - -extension PageSettingsFile: Codable { - -} - -extension PageSettingsFile { - - static var `default`: PageSettingsFile { - .init(contentWidth: 600, - largeImageWidth: 1200, - pageLinkImageSize: 180, - defaultCssFile: nil, - codeHighlightingJsFile: nil, - modelViewerJsFile: nil, - imageCompareJsFile: nil, - imageCompareCssFile: nil, - german: .default, - english: .default) - } -} - -extension PageSettingsFile: LocalizedItem { - -} diff --git a/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift deleted file mode 100644 index 4b241bd..0000000 --- a/CHDataManagement/Storage/Model/Settings/PathSettingsFile.swift +++ /dev/null @@ -1,52 +0,0 @@ - -struct PathSettingsFile { - - let assetsOutputFolderPath: String - - let pagesOutputFolderPath: String - - let imagesOutputFolderPath: String - - let filesOutputFolderPath: String - - let videosOutputFolderPath: String - - let tagsOutputFolderPath: String - - let audioOutputFolderPath: String - - init(assetsOutputFolderPath: String, - pagesOutputFolderPath: String, - imagesOutputFolderPath: String, - filesOutputFolderPath: String, - videosOutputFolderPath: String, - tagsOutputFolderPath: String, - audioOutputFolderPath: String) { - self.assetsOutputFolderPath = assetsOutputFolderPath - self.pagesOutputFolderPath = pagesOutputFolderPath - self.imagesOutputFolderPath = imagesOutputFolderPath - self.filesOutputFolderPath = filesOutputFolderPath - self.videosOutputFolderPath = videosOutputFolderPath - self.tagsOutputFolderPath = tagsOutputFolderPath - self.audioOutputFolderPath = audioOutputFolderPath - } -} - -extension PathSettingsFile: Codable { - -} - -extension PathSettingsFile { - - static var `default`: PathSettingsFile { - PathSettingsFile( - assetsOutputFolderPath: "asset", - pagesOutputFolderPath: "page", - imagesOutputFolderPath: "image", - filesOutputFolderPath: "file", - videosOutputFolderPath: "video", - tagsOutputFolderPath: "tag", - audioOutputFolderPath: "audio") - } - -} diff --git a/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift b/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift deleted file mode 100644 index 5be6a54..0000000 --- a/CHDataManagement/Storage/Model/Settings/PostSettingsFile.swift +++ /dev/null @@ -1,35 +0,0 @@ -import Foundation - -struct PostSettingsFile { - - /// The number of posts to show in a single page of the news feed - let postsPerPage: Int - - /// The maximum width of the main content - let contentWidth: Int - - let swiperCssFile: String? - - let swiperJsFile: String? - - let defaultCssFile: String? - - let german: LocalizedPostSettingsFile - - let english: LocalizedPostSettingsFile -} - -extension PostSettingsFile: Codable { } - -extension PostSettingsFile { - - static var `default`: PostSettingsFile { - .init(postsPerPage: 25, - contentWidth: 600, - swiperCssFile: nil, - swiperJsFile: nil, - defaultCssFile: nil, - german: .default, - english: .default) - } -} diff --git a/CHDataManagement/Storage/Model/Settings/SettingsFile.swift b/CHDataManagement/Storage/Model/Settings/SettingsFile.swift deleted file mode 100644 index cff05e1..0000000 --- a/CHDataManagement/Storage/Model/Settings/SettingsFile.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -struct SettingsFile { - - let paths: PathSettingsFile - - /// The tags to show in the navigation bar - let navigation: NavigationSettingsFile - - let posts: PostSettingsFile - - let pages: PageSettingsFile - - let audioPlayer: AudioPlayerSettingsFile - - let tagOverview: TagOverviewFile? -} - -extension SettingsFile: Codable { } - -extension SettingsFile { - - static var `default`: SettingsFile { - .init( - paths: .default, - navigation: .default, - posts: .default, - pages: .default, - audioPlayer: AudioPlayerSettings.default.file, - tagOverview: nil - ) - } -} diff --git a/CHDataManagement/Storage/Model/Settings/TagOverviewFile.swift b/CHDataManagement/Storage/Model/Settings/TagOverviewFile.swift deleted file mode 100644 index 857408d..0000000 --- a/CHDataManagement/Storage/Model/Settings/TagOverviewFile.swift +++ /dev/null @@ -1,31 +0,0 @@ - -struct TagOverviewFile { - - let german: LocalizedTagOverviewFile - - let english: LocalizedTagOverviewFile -} - -extension TagOverviewFile: Codable { - -} - -/** - The structure to store the metadata of a localized page - */ -struct LocalizedTagOverviewFile { - - let url: String - - let title: String - - let linkPreviewImage: String? - - let linkPreviewTitle: String? - - let linkPreviewDescription: String? -} - -extension LocalizedTagOverviewFile: Codable { - -} diff --git a/CHDataManagement/Storage/Model/TagFile.swift b/CHDataManagement/Storage/Model/TagFile.swift deleted file mode 100644 index 3a12ca5..0000000 --- a/CHDataManagement/Storage/Model/TagFile.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -struct TagFile { - - let id: String - - let isVisible: Bool - - 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 linkPreviewTitle: String? - - let linkPreviewDescription: String? - - /// The image id of the thumbnail - let linkPreviewImage: String? - - /// The original url in the previous site layout - let originalURL: String? - -} - -extension LocalizedTagFile: Codable { - -} diff --git a/CHDataManagement/Storage/SecurityBookmark.swift b/CHDataManagement/Storage/SecurityBookmark.swift index 4b7c0c4..982a0c1 100644 --- a/CHDataManagement/Storage/SecurityBookmark.swift +++ b/CHDataManagement/Storage/SecurityBookmark.swift @@ -312,6 +312,7 @@ struct SecurityBookmark { do { data = try Data(contentsOf: url) } catch { +#warning("Get these errors") print("Storage: Failed to read file \(url.path()): \(error)") return } diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index d52f687..fa9b07d 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -34,6 +34,7 @@ final class Storage: ObservableObject { // MARK: Properties +#warning("Rework to make this non-optional by creating a wrapper class") @Published var contentScope: SecurityBookmark? @@ -72,13 +73,13 @@ final class Storage: ObservableObject { return contentScope.write(pageContent, to: path) } - func save(pageMetadata: PageFile, for pageId: String) -> Bool { + func save(pageMetadata: Page.Data, for pageId: String) -> Bool { guard let contentScope else { return false } let path = pageMetadataPath(page: pageId) return contentScope.encode(pageMetadata, to: path) } - func loadAllPages() -> [String : PageFile]? { + func loadAllPages() -> [String : Page.Data]? { contentScope?.decodeJsonFiles(in: pagesFolderName) } @@ -144,13 +145,13 @@ final class Storage: ObservableObject { postsFolderName + "/" + postFileName(postId) } - func save(post: PostFile, for postId: String) -> Bool { + func save(post: Post.Data, for postId: String) -> Bool { guard let contentScope else { return false } let path = postFilePath(post: postId) return contentScope.encode(post, to: path) } - func loadAllPosts() -> [String : PostFile]? { + func loadAllPosts() -> [String : Post.Data]? { contentScope?.decodeJsonFiles(in: postsFolderName) } @@ -183,13 +184,13 @@ final class Storage: ObservableObject { tagsFolderName + "/" + tagFileName(tagId: tagId) } - func save(tagMetadata: TagFile, for tagId: String) -> Bool { + func save(tagMetadata: Tag.Data, for tagId: String) -> Bool { guard let contentScope else { return false } let path = tagFilePath(tag: tagId) return contentScope.encode(tagMetadata, to: path) } - func loadAllTags() -> [String : TagFile]? { + func loadAllTags() -> [String : Tag.Data]? { contentScope?.decodeJsonFiles(in: tagsFolderName) } @@ -311,9 +312,9 @@ final class Storage: ObservableObject { - Returns: A dictionary with the file ids as keys and the metadata file as a value. */ - func loadAllFiles() -> [String : (data: FileResourceFile, isExternal: Bool)]? { + func loadAllFiles() -> [String : (data: FileResource.Data, isExternal: Bool)]? { guard let contentScope else { return nil } - guard let list: [String : FileResourceFile] = contentScope.decodeJsonFiles(in: fileInfoFolderName) else { + guard let list: [String : FileResource.Data] = contentScope.decodeJsonFiles(in: fileInfoFolderName) else { return nil } guard let existingFiles = contentScope.fileNames(inRelativeFolder: filesFolderName).map(Set.init) else { @@ -326,7 +327,7 @@ final class Storage: ObservableObject { } @discardableResult - func save(fileInfo: FileResourceFile, for fileId: String) -> Bool { + func save(fileInfo: FileResource.Data, for fileId: String) -> Bool { guard let contentScope else { return false } let path = fileInfoPath(file: fileId) return contentScope.encode(fileInfo, to: path) @@ -359,12 +360,12 @@ final class Storage: ObservableObject { // MARK: Settings - func loadSettings() -> SettingsFile? { + func loadSettings() -> Settings.Data? { guard let contentScope else { return nil } return contentScope.decode(at: settingsDataFileName) } - func save(settings: SettingsFile) -> Bool { + func save(settings: Settings.Data) -> Bool { guard let contentScope else { return false } return contentScope.encode(settings, to: settingsDataFileName) } diff --git a/CHDataManagement/Views/Files/FileDetailView.swift b/CHDataManagement/Views/Files/FileDetailView.swift index 2e63944..cd3c402 100644 --- a/CHDataManagement/Views/Files/FileDetailView.swift +++ b/CHDataManagement/Views/Files/FileDetailView.swift @@ -73,12 +73,12 @@ struct FileDetailView: View { switch language { case .english: - StringPropertyView( + OptionalStringPropertyView( title: "Description", text: $file.english, footer: "The description for the file. Descriptions are used for images and to explain the content of a file.") case .german: - StringPropertyView( + OptionalStringPropertyView( title: "Description", text: $file.german, footer: "The description for the file. Descriptions are used for images and to explain the content of a file.") @@ -151,8 +151,7 @@ struct FileDetailView: View { file.determineFileSize() // Force regeneration of images and/or file copying file.removeFileFromOutputFolder() - // Trigger content view update to reload image - file.didChange() + file.modifiedDate = .now } } diff --git a/CHDataManagement/Views/ItemSelectionView.swift b/CHDataManagement/Views/ItemSelectionView.swift index ecbb16b..d3d00e7 100644 --- a/CHDataManagement/Views/ItemSelectionView.swift +++ b/CHDataManagement/Views/ItemSelectionView.swift @@ -47,7 +47,7 @@ struct ItemSelectionView: View { } .contentShape(Rectangle()) .onTapGesture { - if !selectedItems.contains(where: { $0 is TagOverviewPage }) { + if !selectedItems.contains(where: { $0.itemReference == tagOverview.itemReference }) { selectedItems.append(tagOverview) } } diff --git a/CHDataManagement/Views/LinkPreviewDetailView.swift b/CHDataManagement/Views/LinkPreviewDetailView.swift new file mode 100644 index 0000000..49f96a3 --- /dev/null +++ b/CHDataManagement/Views/LinkPreviewDetailView.swift @@ -0,0 +1,29 @@ +import SwiftUI + +struct LinkPreviewDetailView: View { + + @ObservedObject + var linkPreview: LinkPreview + + let fallbackTitle: String? + + var body: some View { + VStack(alignment: .leading) { + OptionalStringPropertyView( + title: "Preview Title", + text: $linkPreview.title, + prompt: fallbackTitle, + footer: "The title to use in a link preview") + + OptionalImagePropertyView( + title: "Preview Image", + selectedImage: $linkPreview.image, + footer: "The image to show in a link preview") + + OptionalTextFieldPropertyView( + title: "Preview Description", + text: $linkPreview.description, + footer: "The description to show in a link preview") + } + } +} diff --git a/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift b/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift index 8866da3..2a733ea 100644 --- a/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift +++ b/CHDataManagement/Views/Pages/LocalizedPageDetailView.swift @@ -30,21 +30,7 @@ struct LocalizedPageDetailView: View { } } - OptionalStringPropertyView( - title: "Preview Title", - text: $page.linkPreviewTitle, - prompt: page.title, - footer: "The title to use for the page when linking to it") - - OptionalImagePropertyView( - title: "Preview Image", - selectedImage: $page.linkPreviewImage, - footer: "The image to show for previews of this page") - - OptionalTextFieldPropertyView( - title: "Preview Description", - text: $page.linkPreviewDescription, - footer: "The description to show in previews of the page") + LinkPreviewDetailView(linkPreview: page.linkPreview, fallbackTitle: page.title) } } } diff --git a/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift b/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift index 06d0637..6b7625a 100644 --- a/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift +++ b/CHDataManagement/Views/Posts/LocalizedPostDetailView.swift @@ -12,21 +12,7 @@ struct LocalizedPostDetailView: View { text: $post.pageLinkText, footer: "The custom text to show for the link to the linked page") - OptionalStringPropertyView( - title: "Preview Title", - text: $post.linkPreviewTitle, - prompt: post.title, - footer: "The title to use for the post when linking to it") - - OptionalImagePropertyView( - title: "Preview Image", - selectedImage: $post.linkPreviewImage, - footer: "The image to show for previews of this post") - - OptionalTextFieldPropertyView( - title: "Preview Description", - text: $post.linkPreviewDescription, - footer: "The description to show in previews of the post") + LinkPreviewDetailView(linkPreview: post.linkPreview, fallbackTitle: post.title) } } } diff --git a/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift b/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift index 08d4d53..1cfd77b 100644 --- a/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift +++ b/CHDataManagement/Views/Settings/LocalizedPostFeedSettingsView.swift @@ -31,6 +31,6 @@ struct LocalizedPostFeedSettingsView: View { } #Preview { - LocalizedPostFeedSettingsView(settings: .english) + LocalizedPostFeedSettingsView(settings: PostSettings.default.english) .padding() } diff --git a/CHDataManagement/Views/Settings/PathSettingsView.swift b/CHDataManagement/Views/Settings/PathSettingsView.swift index 885f3b9..e0f9971 100644 --- a/CHDataManagement/Views/Settings/PathSettingsView.swift +++ b/CHDataManagement/Views/Settings/PathSettingsView.swift @@ -8,6 +8,12 @@ struct PathSettingsView: View { @EnvironmentObject private var content: Content + @State + private var showLoadErrorSheet: Bool = false + + @State + private var loadErrors: [String] = [] + var body: some View { ScrollView { VStack(alignment: .leading) { @@ -19,7 +25,7 @@ struct PathSettingsView: View { title: "Content Folder", folder: $content.storage.contentScope, footer: "The folder where the raw content of the website is stored") { url in - content.update(contentPath: url) + content.update(contentPath: url, callback: showLoadErrors) } FolderOnDiskPropertyView( @@ -65,8 +71,32 @@ struct PathSettingsView: View { footer: "The path in the output folder where assets are stored") } .padding() + .sheet(isPresented: $showLoadErrorSheet) { + VStack { + Text("Failed to load database") + .font(.headline) + List(loadErrors, id: \.self) { error in + HStack { + Text(error) + Spacer() + } + } + .frame(minHeight: 200) + Button("Dismiss", action: { showLoadErrorSheet = false }) + .padding() + } + .padding() + } } } + + private func showLoadErrors(errors: [String]) { + guard !errors.isEmpty else { + return + } + loadErrors = errors + showLoadErrorSheet = true + } } #Preview { diff --git a/CHDataManagement/Views/Settings/TagOverviewDetailView.swift b/CHDataManagement/Views/Settings/TagOverviewDetailView.swift index c753563..eb770b6 100644 --- a/CHDataManagement/Views/Settings/TagOverviewDetailView.swift +++ b/CHDataManagement/Views/Settings/TagOverviewDetailView.swift @@ -15,8 +15,8 @@ struct TagOverviewDetailView: View { title: "Tag Overview", text: "Configure the page showing all tags") - if let page = content.tagOverview?.localized(in: language) { - TagOverviewDetails(page: page) + if let tag = content.tagOverview?.localized(in: language) { + LocalizedTagDetailView(tag: tag) .id(language) } else { Button("Create", action: createTagOverviewPage) @@ -27,50 +27,10 @@ struct TagOverviewDetailView: View { } private func createTagOverviewPage() { - content.tagOverview = TagOverviewPage( + content.tagOverview = Tag( content: content, - german: .init(content: content, title: "Alle Tags", urlString: "alle"), - english: .init(content: content, title: "All tags", urlString: "all")) - } -} - -private struct TagOverviewDetails: View { - - @EnvironmentObject - private var content: Content - - @ObservedObject - var page: LocalizedTagOverviewPage - - var body: some View { - VStack(alignment: .leading) { - StringPropertyView( - title: "Title", - text: $page.title, - footer: "The title of the overview page") - - IdPropertyView( - id: $page.urlComponent, - title: "Page URL String", - footer: "The url component to use for the link to the page", - validation: page.isValid, - update: { page.urlComponent = $0 }) - - OptionalStringPropertyView( - title: "Preview Title", - text: $page.linkPreviewTitle, - prompt: page.title, - footer: "The title to use for the page when linking to it") - - OptionalImagePropertyView( - title: "Preview Image", - selectedImage: $page.linkPreviewImage, - footer: "The image to show for previews of this page") - - OptionalTextFieldPropertyView( - title: "Preview Description", - text: $page.linkPreviewDescription, - footer: "The description to show in previews of the page") - } + id: "all-tags", + german: .init(content: content, urlComponent: "alle", name: "Alle Tags"), + english: .init(content: content, urlComponent: "all", name: "All tags")) } } diff --git a/CHDataManagement/Views/Tags/LocalizedTagDetailView.swift b/CHDataManagement/Views/Tags/LocalizedTagDetailView.swift index a46f932..865aa42 100644 --- a/CHDataManagement/Views/Tags/LocalizedTagDetailView.swift +++ b/CHDataManagement/Views/Tags/LocalizedTagDetailView.swift @@ -31,21 +31,7 @@ struct LocalizedTagDetailView: View { } } - OptionalStringPropertyView( - title: "Preview Title", - text: $tag.linkPreviewTitle, - prompt: tag.name, - footer: "The title to use for the tag in previews and on tag pages") - - OptionalImagePropertyView( - title: "Preview Image", - selectedImage: $tag.linkPreviewImage, - footer: "The image to show for previews of this page") - - OptionalTextFieldPropertyView( - title: "Preview Description", - text: $tag.linkPreviewDescription, - footer: "The description to show in previews of the page") + LinkPreviewDetailView(linkPreview: tag.linkPreview, fallbackTitle: tag.name) } } }