Rework storage structs, link preview

This commit is contained in:
Christoph Hagen 2025-01-08 14:59:04 +01:00
parent b99c064d10
commit a7197b9628
75 changed files with 1365 additions and 1454 deletions

View File

@ -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 = "<group>"; };
E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = "<group>"; };
E21850322CFAFA200090B18B /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
E21850342CFAFA570090B18B /* SettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFile.swift; sourceTree = "<group>"; };
E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = "<group>"; };
E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Mock.swift"; sourceTree = "<group>"; };
E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostFeedSettingsView.swift; sourceTree = "<group>"; };
E22990142D0E2B74009F8D77 /* ItemSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSelectionView.swift; sourceTree = "<group>"; };
E22990162D0E32F5009F8D77 /* TagOverviewPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewPage.swift; sourceTree = "<group>"; };
E22990182D0E3546009F8D77 /* ItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = "<group>"; };
E22990182D0E3546009F8D77 /* ItemReference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemReference.swift; sourceTree = "<group>"; };
E229901D2D0E4362009F8D77 /* LocalizedItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedItem.swift; sourceTree = "<group>"; };
E229901F2D0ECBD4009F8D77 /* TagOverviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewDetailView.swift; sourceTree = "<group>"; };
E22990212D0ED129009F8D77 /* TagOverviewFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewFile.swift; sourceTree = "<group>"; };
E22990232D0EDBD0009F8D77 /* HeaderElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderElement.swift; sourceTree = "<group>"; };
E22990252D0F5822009F8D77 /* FilePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePropertyView.swift; sourceTree = "<group>"; };
E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerPropertyView.swift; sourceTree = "<group>"; };
E22990292D0F5A10009F8D77 /* DetailTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailTitle.swift; sourceTree = "<group>"; };
E229902B2D0F6FC0009F8D77 /* ItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemId.swift; sourceTree = "<group>"; };
E229902B2D0F6FC0009F8D77 /* LocalizedItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedItemId.swift; sourceTree = "<group>"; };
E229902D2D0F7278009F8D77 /* IdPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdPropertyView.swift; sourceTree = "<group>"; };
E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolPropertyView.swift; sourceTree = "<group>"; };
E22990312D0F7678009F8D77 /* DatePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePropertyView.swift; sourceTree = "<group>"; };
@ -297,7 +284,6 @@
E25DA5082CFD964E00AEF16D /* TagContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentView.swift; sourceTree = "<group>"; };
E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTagAssignmentView.swift; sourceTree = "<group>"; };
E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTagAssignmentView.swift; sourceTree = "<group>"; };
E25DA5142CFF00B900AEF16D /* Content+Load.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Load.swift"; sourceTree = "<group>"; };
E25DA5162CFF00F200AEF16D /* Content+Save.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Save.swift"; sourceTree = "<group>"; };
E25DA5182CFF035200AEF16D /* Array+Split.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Split.swift"; sourceTree = "<group>"; };
E25DA51A2CFF08AF00AEF16D /* PostFeedPageNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFeedPageNavigation.swift; sourceTree = "<group>"; };
@ -306,8 +292,6 @@
E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGenerator.swift; sourceTree = "<group>"; };
E25DA5242CFF73A600AEF16D /* NSSize+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSSize+Scaling.swift"; sourceTree = "<group>"; };
E25DA5262CFF745200AEF16D /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettingsFile.swift; sourceTree = "<group>"; };
E25DA5372D00420D00AEF16D /* LocalizedPostSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettingsFile.swift; sourceTree = "<group>"; };
E25DA5402D00446700AEF16D /* PostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostSettings.swift; sourceTree = "<group>"; };
E25DA5442D00952D00AEF16D /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
E25DA56C2D00EBC900AEF16D /* NavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarSettingsView.swift; sourceTree = "<group>"; };
@ -322,7 +306,6 @@
E25DA58A2D020C9200AEF16D /* PageImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageImage.swift; sourceTree = "<group>"; };
E25DA58E2D02368A00AEF16D /* PageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettings.swift; sourceTree = "<group>"; };
E25DA5902D023A7E00AEF16D /* IntegerField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerField.swift; sourceTree = "<group>"; };
E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsFile.swift; sourceTree = "<group>"; };
E25DA5942D023BCC00AEF16D /* PageSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsDetailView.swift; sourceTree = "<group>"; };
E25DA5982D02401A00AEF16D /* PageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageGenerator.swift; sourceTree = "<group>"; };
E25DA59A2D024A2900AEF16D /* DateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateItem.swift; sourceTree = "<group>"; };
@ -331,7 +314,6 @@
E29D31232D0366820051B7F4 /* TagList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagList.swift; sourceTree = "<group>"; };
E29D31252D0370A50051B7F4 /* VideoCommand+Option.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VideoCommand+Option.swift"; sourceTree = "<group>"; };
E29D31272D0371870051B7F4 /* ContentPageVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentPageVideo.swift; sourceTree = "<group>"; };
E29D31292D039B050051B7F4 /* FileDescriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDescriptions.swift; sourceTree = "<group>"; };
E29D312B2D039DB30051B7F4 /* PageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageDetailView.swift; sourceTree = "<group>"; };
E29D312D2D03A0CF0051B7F4 /* LocalizedPageDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageDetailView.swift; sourceTree = "<group>"; };
E29D312F2D03A2BD0051B7F4 /* DescriptionField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionField.swift; sourceTree = "<group>"; };
@ -369,7 +351,6 @@
E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationAnomaly.swift; sourceTree = "<group>"; };
E29D31932D0B7D250051B7F4 /* SimpleImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleImage.swift; sourceTree = "<group>"; };
E29D31952D0C18690051B7F4 /* PathSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettings.swift; sourceTree = "<group>"; };
E29D31972D0C19300051B7F4 /* PathSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathSettingsFile.swift; sourceTree = "<group>"; };
E29D319A2D0C452B0051B7F4 /* PageIssue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssue.swift; sourceTree = "<group>"; };
E29D319C2D0C45B60051B7F4 /* PageIssueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssueView.swift; sourceTree = "<group>"; };
E29D319E2D0C46290051B7F4 /* PageIssueChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageIssueChecker.swift; sourceTree = "<group>"; };
@ -401,9 +382,6 @@
E2A21C472CBAF8830060935B /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = "<group>"; };
E2A21C502CBBD53C0060935B /* FileResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResource.swift; sourceTree = "<group>"; };
E2A37D0D2CE527040000979F /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
E2A37D102CE537670000979F /* PageFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageFile.swift; sourceTree = "<group>"; };
E2A37D142CE68BEA0000979F /* PostFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostFile.swift; sourceTree = "<group>"; };
E2A37D162CE73F170000979F /* TagFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagFile.swift; sourceTree = "<group>"; };
E2A37D182CEA36A40000979F /* LocalizedTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedTag.swift; sourceTree = "<group>"; };
E2A37D1A2CEA45530000979F /* Tag+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tag+Mock.swift"; sourceTree = "<group>"; };
E2A37D1C2CEA922A0000979F /* LocalizedPost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPost.swift; sourceTree = "<group>"; };
@ -426,6 +404,14 @@
E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
E2E06DFA2CA4A6570019C2AF /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = "<group>"; };
E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Page+Mock.swift"; sourceTree = "<group>"; };
E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = "<group>"; };
E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingContext.swift; sourceTree = "<group>"; };
E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewDetailView.swift; sourceTree = "<group>"; };
E2FD1D1C2D2DE31600B48627 /* ItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemType.swift; sourceTree = "<group>"; };
E2FD1D1E2D2E9CBE00B48627 /* ItemId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemId.swift; sourceTree = "<group>"; };
E2FD1D202D2EB22700B48627 /* ModelLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelLoader.swift; sourceTree = "<group>"; };
E2FD1D222D2EB26C00B48627 /* LoadingResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingResult.swift; sourceTree = "<group>"; };
E2FD1D242D2EBA7C00B48627 /* TagOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverview.swift; sourceTree = "<group>"; };
E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = "<group>"; };
E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = "<group>"; };
E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = "<group>"; };
@ -435,8 +421,6 @@
E2FE0EF72D1D810C002963B7 /* IconCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconCommand.swift; sourceTree = "<group>"; };
E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHeader.swift; sourceTree = "<group>"; };
E2FE0EFB2D266D18002963B7 /* NavigationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettings.swift; sourceTree = "<group>"; };
E2FE0EFD2D266DA1002963B7 /* NavigationSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationSettingsFile.swift; sourceTree = "<group>"; };
E2FE0EFF2D266E0A002963B7 /* LocalizedNavigationSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationSettingsFile.swift; sourceTree = "<group>"; };
E2FE0F012D266FCB002963B7 /* LocalizedNavigationSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationSettings.swift; sourceTree = "<group>"; };
E2FE0F032D2671FC002963B7 /* LocalizedNavigationBarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedNavigationBarSettingsView.swift; sourceTree = "<group>"; };
E2FE0F052D26734E002963B7 /* TextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldPropertyView.swift; sourceTree = "<group>"; };
@ -448,7 +432,6 @@
E2FE0F142D269188002963B7 /* HtmlCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HtmlCommand.swift; sourceTree = "<group>"; };
E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageId.swift; sourceTree = "<group>"; };
E2FE0F182D2723E3002963B7 /* ImageSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSet.swift; sourceTree = "<group>"; };
E2FE0F1A2D274FDA002963B7 /* LinkPreviewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewItem.swift; sourceTree = "<group>"; };
E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewGenerator.swift; sourceTree = "<group>"; };
E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = "<group>"; };
E2FE0F212D2A849B002963B7 /* VideoCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCommand.swift; sourceTree = "<group>"; };
@ -462,7 +445,6 @@
E2FE0F352D2B27F6002963B7 /* BlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockProcessor.swift; sourceTree = "<group>"; };
E2FE0F372D2B32ED002963B7 /* SingleFilePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFilePlayer.swift; sourceTree = "<group>"; };
E2FE0F392D2B3E4E002963B7 /* AudioPlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerSettings.swift; sourceTree = "<group>"; };
E2FE0F3B2D2B3F42002963B7 /* AudioPlayerSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerSettingsFile.swift; sourceTree = "<group>"; };
E2FE0F3D2D2B4225002963B7 /* AudioSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSettingsDetailView.swift; sourceTree = "<group>"; };
E2FE0F3F2D2B45CD002963B7 /* SwiftBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBlock.swift; sourceTree = "<group>"; };
E2FE0F412D2B480B002963B7 /* OtherCodeBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherCodeBlock.swift; sourceTree = "<group>"; };
@ -477,13 +459,11 @@
E2FE0F562D2BCFD4002963B7 /* BlockLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockLineProcessor.swift; sourceTree = "<group>"; };
E2FE0F582D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedKeyBlockProcessor.swift; sourceTree = "<group>"; };
E2FE0F5A2D2BCFF2002963B7 /* KeyedBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedBlockProcessor.swift; sourceTree = "<group>"; };
E2FE0F5D2D2BE18B002963B7 /* FileResourceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResourceFile.swift; sourceTree = "<group>"; };
E2FE0F5F2D2C041E002963B7 /* VideoBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoBlock.swift; sourceTree = "<group>"; };
E2FE0F612D2C0D8D002963B7 /* VersionedVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionedVideo.swift; sourceTree = "<group>"; };
E2FE0F632D2C2F46002963B7 /* ButtonBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonBlock.swift; sourceTree = "<group>"; };
E2FE0F652D2C3B33002963B7 /* LabelsBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelsBlock.swift; sourceTree = "<group>"; };
E2FE0F672D2D2CF0002963B7 /* LocalizedPageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageSettings.swift; sourceTree = "<group>"; };
E2FE0F692D2D2D4F002963B7 /* LocalizedPageSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageSettingsFile.swift; sourceTree = "<group>"; };
E2FE0F6B2D2D3358002963B7 /* LocalizedPageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageSettingsView.swift; sourceTree = "<group>"; };
E2FE0F6D2D2D3685002963B7 /* LocalizedAudioPlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedAudioPlayerSettings.swift; sourceTree = "<group>"; };
E2FE0F6F2D2D5231002963B7 /* DraftIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftIndicator.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
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 = "<group>";
@ -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 = "<group>";
};
E2FD1D262D2EBBA300B48627 /* Loading */ = {
isa = PBXGroup;
children = (
E2FD1D202D2EB22700B48627 /* ModelLoader.swift */,
E2FD1D222D2EB26C00B48627 /* LoadingResult.swift */,
E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */,
);
path = Loading;
sourceTree = "<group>";
};
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 */,
);

View File

@ -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 {

View File

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

View File

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

View File

@ -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 += "<picture>"
result += "<source type='image/avif' srcset='\(prefix1x).avif 1x, \(prefix2x).avif 2x'/>"
result += "<source type='image/webp' srcset='\(prefix1x).webp 1x, \(prefix1x).webp 2x'/>"
result += "<img srcset='\(prefix2x)\(fileExtension) 2x' src='\(prefix1x)\(fileExtension)' loading='lazy' alt='\(description.htmlEscaped())'\(extraAttributes)/>"
result += "<img srcset='\(prefix2x)\(fileExtension) 2x' src='\(prefix1x)\(fileExtension)' loading='lazy'\(imageAltText)\(extraAttributes)/>"
result += "</picture>"
}
}

View File

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

View File

@ -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 += "<h1 class='separated-headline'>\(header.title)</h1>"
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
}
}
}

View File

@ -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 {

View File

@ -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<ItemId> {
private extension Dictionary where Value == Set<LocalizedItemId> {
mutating func remove<S>(keys: S, of item: ItemId) where S: Sequence, S.Element == Key {
mutating func remove<S>(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)

View File

@ -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<String>
/// The files that could not be saved to the output folder
private(set) var unsavedOutputFiles: [String: Set<ItemType>] = [:]
private(set) var unsavedOutputFiles: [String: Set<ItemReference>] = [:]
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)
}

View File

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

View File

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

View File

@ -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] } ?? [])
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 `<head>` of the page as `<meta>` tags.
*/
final class LinkPreview: ObservableObject {
/// The description to show when linking to a page (contained in the `<head>` of the page)
@Published
var title: String?
/// The image id of the thumbnail to attach to the link preview (contained in the `<head>` of the page)
@Published
var description: String?
/// The title to show for a link preview (contained in the `<head>` 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?
}
}

View File

@ -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<String> = []
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
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
final class TagOverview: Tag {
override var itemId: ItemId {
.init(type: .tagOverview, id: id)
}
}

View File

@ -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 += "<span class='content-image svg-image'>"
result += "<img src='\(imagePath)#svgView(viewBox(\(x), \(y), \(width), \(height)))' loading='lazy' style='aspect-ratio:\(aspectRatio)' alt='\(altText)'/>"
result += "<img src='\(imagePath)#svgView(viewBox(\(x), \(y), \(width), \(height)))' loading='lazy' style='aspect-ratio:\(aspectRatio)'\(imageAltText)/>"
result += "</span>"
}
}

View File

@ -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 += "<div class='content-image svg-image'>"
result += "<img src='\(imagePath)' loading='lazy' alt='\(altText)'/>"
result += "<img src='\(imagePath)' loading='lazy'\(imageAltText)/>"
result += "</div>"
}
}

View File

@ -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 {
"<model-viewer alt='\(description)' src='\(file)' ar shadow-intensity='1' camera-controls touch-action='pan-y'></model-viewer>"
"<model-viewer\(imageAltText) src='\(file)' ar shadow-intensity='1' camera-controls touch-action='pan-y'></model-viewer>"
}
}

View File

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

View File

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

View File

@ -1,13 +0,0 @@
struct FileDescriptions {
let fileId: String
let german: String?
let english: String?
}
extension FileDescriptions: Codable {
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +0,0 @@
import Foundation
struct LocalizedNavigationSettingsFile {
let rootUrl: String
}
extension LocalizedNavigationSettingsFile: Codable {
}
extension LocalizedNavigationSettingsFile {
static var `default`: LocalizedNavigationSettingsFile {
.init(rootUrl: "/")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,6 +31,6 @@ struct LocalizedPostFeedSettingsView: View {
}
#Preview {
LocalizedPostFeedSettingsView(settings: .english)
LocalizedPostFeedSettingsView(settings: PostSettings.default.english)
.padding()
}

View File

@ -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 {

View File

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

View File

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