Unified detail views, model
This commit is contained in:
parent
1e67a99866
commit
31d1ecb8bd
@ -13,9 +13,7 @@
|
||||
E21850172CEE55FC0090B18B /* FileType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850162CEE55FB0090B18B /* FileType.swift */; };
|
||||
E21850192CEE561C0090B18B /* PageOnDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850182CEE561B0090B18B /* PageOnDisk.swift */; };
|
||||
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501C2CEE6CB30090B18B /* VerticalCenter.swift */; };
|
||||
E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */; };
|
||||
E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850222CF10C840090B18B /* TagSelectionView.swift */; };
|
||||
E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850242CF38BCE0090B18B /* TextEntrySheet.swift */; };
|
||||
E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850262CF3B42D0090B18B /* PostDetailView.swift */; };
|
||||
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; };
|
||||
E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; };
|
||||
@ -36,6 +34,16 @@
|
||||
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */; };
|
||||
E229902A2D0F5A14009F8D77 /* DetailTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990292D0F5A10009F8D77 /* DetailTitle.swift */; };
|
||||
E229902C2D0F6FC6009F8D77 /* ItemId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902B2D0F6FC0009F8D77 /* ItemId.swift */; };
|
||||
E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902D2D0F7278009F8D77 /* IdPropertyView.swift */; };
|
||||
E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */; };
|
||||
E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990312D0F7678009F8D77 /* DatePropertyView.swift */; };
|
||||
E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990332D0F77E4009F8D77 /* PagePropertyView.swift */; };
|
||||
E22990362D0F79D2009F8D77 /* OptionalStringPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */; };
|
||||
E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */; };
|
||||
E229903A2D0F7E48009F8D77 /* GenericPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */; };
|
||||
E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */; };
|
||||
E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */; };
|
||||
E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */; };
|
||||
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
||||
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; };
|
||||
E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.swift */; };
|
||||
@ -149,7 +157,6 @@
|
||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C072CB17B810060935B /* TagView.swift */; };
|
||||
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0D2CB189D70060935B /* Color+RGB.swift */; };
|
||||
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C0F2CB18B390060935B /* FlowHStack.swift */; };
|
||||
E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C112CB18D520060935B /* DatePickerView.swift */; };
|
||||
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C1F2CB28ED20060935B /* MockImage.swift */; };
|
||||
E2A21C282CB29B2A0060935B /* FeedEntryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C272CB29B290060935B /* FeedEntryData.swift */; };
|
||||
E2A21C2A2CB2AA4F0060935B /* Post+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C292CB2AA4C0060935B /* Post+Mock.swift */; };
|
||||
@ -194,9 +201,7 @@
|
||||
E21850162CEE55FB0090B18B /* FileType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileType.swift; sourceTree = "<group>"; };
|
||||
E21850182CEE561B0090B18B /* PageOnDisk.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageOnDisk.swift; sourceTree = "<group>"; };
|
||||
E218501C2CEE6CB30090B18B /* VerticalCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCenter.swift; sourceTree = "<group>"; };
|
||||
E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerView.swift; sourceTree = "<group>"; };
|
||||
E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = "<group>"; };
|
||||
E21850242CF38BCE0090B18B /* TextEntrySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntrySheet.swift; sourceTree = "<group>"; };
|
||||
E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@ -217,6 +222,16 @@
|
||||
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>"; };
|
||||
E229902D2D0F7278009F8D77 /* IdPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdPropertyView.swift; sourceTree = "<group>"; };
|
||||
E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoolPropertyView.swift; sourceTree = "<group>"; };
|
||||
E22990312D0F7678009F8D77 /* DatePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePropertyView.swift; sourceTree = "<group>"; };
|
||||
E22990332D0F77E4009F8D77 /* PagePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagePropertyView.swift; sourceTree = "<group>"; };
|
||||
E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalStringPropertyView.swift; sourceTree = "<group>"; };
|
||||
E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalImagePropertyView.swift; sourceTree = "<group>"; };
|
||||
E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericPropertyView.swift; sourceTree = "<group>"; };
|
||||
E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextFieldPropertyView.swift; sourceTree = "<group>"; };
|
||||
E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringPropertyView.swift; sourceTree = "<group>"; };
|
||||
E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderOnDiskPropertyView.swift; sourceTree = "<group>"; };
|
||||
E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = "<group>"; };
|
||||
E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = "<group>"; };
|
||||
@ -325,7 +340,6 @@
|
||||
E2A21C072CB17B810060935B /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
||||
E2A21C0D2CB189D70060935B /* Color+RGB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+RGB.swift"; sourceTree = "<group>"; };
|
||||
E2A21C0F2CB18B390060935B /* FlowHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowHStack.swift; sourceTree = "<group>"; };
|
||||
E2A21C112CB18D520060935B /* DatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerView.swift; sourceTree = "<group>"; };
|
||||
E2A21C1F2CB28ED20060935B /* MockImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImage.swift; sourceTree = "<group>"; };
|
||||
E2A21C272CB29B290060935B /* FeedEntryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedEntryData.swift; sourceTree = "<group>"; };
|
||||
E2A21C292CB2AA4C0060935B /* Post+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Post+Mock.swift"; sourceTree = "<group>"; };
|
||||
@ -589,9 +603,19 @@
|
||||
E2A37D2C2CED2EEE0000979F /* OptionalTextField.swift */,
|
||||
E2A21C2F2CB490F90060935B /* HorizontalCenter.swift */,
|
||||
E2A21C0F2CB18B390060935B /* FlowHStack.swift */,
|
||||
E22990292D0F5A10009F8D77 /* DetailTitle.swift */,
|
||||
E22990392D0F7E44009F8D77 /* GenericPropertyView.swift */,
|
||||
E22990272D0F5967009F8D77 /* IntegerPropertyView.swift */,
|
||||
E22990252D0F5822009F8D77 /* FilePropertyView.swift */,
|
||||
E22990292D0F5A10009F8D77 /* DetailTitle.swift */,
|
||||
E229902D2D0F7278009F8D77 /* IdPropertyView.swift */,
|
||||
E229902F2D0F75CF009F8D77 /* BoolPropertyView.swift */,
|
||||
E22990312D0F7678009F8D77 /* DatePropertyView.swift */,
|
||||
E22990332D0F77E4009F8D77 /* PagePropertyView.swift */,
|
||||
E22990352D0F79CC009F8D77 /* OptionalStringPropertyView.swift */,
|
||||
E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */,
|
||||
E22990372D0F7B2C009F8D77 /* OptionalImagePropertyView.swift */,
|
||||
E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */,
|
||||
E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */,
|
||||
);
|
||||
path = Generic;
|
||||
sourceTree = "<group>";
|
||||
@ -707,11 +731,8 @@
|
||||
E21850262CF3B42D0090B18B /* PostDetailView.swift */,
|
||||
E29D313E2D04822C0051B7F4 /* AddPostView.swift */,
|
||||
E21850222CF10C840090B18B /* TagSelectionView.swift */,
|
||||
E218501E2CEE6DAC0090B18B /* ImagePickerView.swift */,
|
||||
E21850082CEE01BF0090B18B /* PagePickerView.swift */,
|
||||
E2A21C112CB18D520060935B /* DatePickerView.swift */,
|
||||
E2A21C072CB17B810060935B /* TagView.swift */,
|
||||
E21850242CF38BCE0090B18B /* TextEntrySheet.swift */,
|
||||
E29D31312D03B5610051B7F4 /* LocalizedPostDetailView.swift */,
|
||||
E218502C2CF791440090B18B /* PostImagesView.swift */,
|
||||
);
|
||||
@ -888,10 +909,10 @@
|
||||
E25DA59B2D024A2B00AEF16D /* DateItem.swift in Sources */,
|
||||
E29D31902D0B34870051B7F4 /* PageContentAnomaly.swift in Sources */,
|
||||
E25DA5172CFF00F500AEF16D /* Content+Save.swift in Sources */,
|
||||
E229902E2D0F7280009F8D77 /* IdPropertyView.swift in Sources */,
|
||||
E29D31AD2D0DA5360051B7F4 /* AudioPlayerIcons.swift in Sources */,
|
||||
E25DA5452D00952E00AEF16D /* SettingsSection.swift in Sources */,
|
||||
E2A21C462CBAE2E60060935B /* FeedEntryContent.swift in Sources */,
|
||||
E21850252CF38BCE0090B18B /* TextEntrySheet.swift in Sources */,
|
||||
E2A37D1D2CEA922D0000979F /* LocalizedPost.swift in Sources */,
|
||||
E21850172CEE55FC0090B18B /* FileType.swift in Sources */,
|
||||
E29D31B12D0DA5510051B7F4 /* ContentIcon.swift in Sources */,
|
||||
@ -913,6 +934,7 @@
|
||||
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */,
|
||||
E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */,
|
||||
E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */,
|
||||
E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */,
|
||||
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */,
|
||||
E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */,
|
||||
E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */,
|
||||
@ -934,6 +956,7 @@
|
||||
E29D31982D0C19340051B7F4 /* PathSettingsFile.swift in Sources */,
|
||||
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
|
||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
||||
E22990302D0F75DE009F8D77 /* BoolPropertyView.swift in Sources */,
|
||||
E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */,
|
||||
E229901E2D0E4364009F8D77 /* LocalizedItem.swift in Sources */,
|
||||
E29D31262D0370A80051B7F4 /* VideoOption.swift in Sources */,
|
||||
@ -952,7 +975,7 @@
|
||||
E29D315D2D06D6EA0051B7F4 /* TextFileType.swift in Sources */,
|
||||
E21850152CEE55D40090B18B /* FileOnDisk.swift in Sources */,
|
||||
E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */,
|
||||
E2A21C122CB18D560060935B /* DatePickerView.swift in Sources */,
|
||||
E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */,
|
||||
E21850332CFAFA2F0090B18B /* Settings.swift in Sources */,
|
||||
E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */,
|
||||
E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */,
|
||||
@ -967,12 +990,14 @@
|
||||
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 */,
|
||||
E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */,
|
||||
E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */,
|
||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
||||
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */,
|
||||
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
|
||||
E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */,
|
||||
E29D31B32D0DA6E80051B7F4 /* ButtonIcons.swift in Sources */,
|
||||
E29D319D2D0C45B90051B7F4 /* PageIssueView.swift in Sources */,
|
||||
E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */,
|
||||
@ -989,11 +1014,11 @@
|
||||
E29D31472D04892E0051B7F4 /* FileListView.swift in Sources */,
|
||||
E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */,
|
||||
E29D31A52D0CD03F0051B7F4 /* FileSelectionView.swift in Sources */,
|
||||
E22990322D0F767B009F8D77 /* DatePropertyView.swift in Sources */,
|
||||
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
|
||||
E25DA5252CFF73AB00AEF16D /* NSSize+Scaling.swift in Sources */,
|
||||
E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */,
|
||||
E2DD04742C276F31003BFF1F /* MainView.swift in Sources */,
|
||||
E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */,
|
||||
E29D31452D0488CB0051B7F4 /* SelectedContentView.swift in Sources */,
|
||||
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
|
||||
E29D319F2D0C46310051B7F4 /* PageIssueChecker.swift in Sources */,
|
||||
@ -1028,10 +1053,13 @@
|
||||
E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */,
|
||||
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */,
|
||||
E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */,
|
||||
E229903A2D0F7E48009F8D77 /* GenericPropertyView.swift in Sources */,
|
||||
E29D31AA2D0CEE3F0051B7F4 /* AudioPlayer.swift in Sources */,
|
||||
E25DA5152CFF00C100AEF16D /* Content+Load.swift in Sources */,
|
||||
E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */,
|
||||
E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */,
|
||||
E22990362D0F79D2009F8D77 /* OptionalStringPropertyView.swift in Sources */,
|
||||
E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */,
|
||||
E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */,
|
||||
E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */,
|
||||
E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */,
|
||||
|
@ -8,13 +8,6 @@ final class FeedPageGenerator {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] {
|
||||
content.settings.navigationItems.map {
|
||||
.init(text: $0.title(in: language),
|
||||
url: $0.absoluteUrl(in: language))
|
||||
}
|
||||
}
|
||||
|
||||
var swiperIncludes: [HeaderElement] {
|
||||
var result = [HeaderElement]()
|
||||
if let swiperCss = content.settings.posts.swiperCssFile {
|
||||
@ -57,7 +50,7 @@ final class FeedPageGenerator {
|
||||
language: language,
|
||||
title: title,
|
||||
description: description,
|
||||
links: navigationBar(in: language),
|
||||
links: content.navigationBar(in: language),
|
||||
headers: headers,
|
||||
additionalFooter: footer) { content in
|
||||
if showTitle {
|
||||
|
@ -22,13 +22,6 @@ final class LocalizedWebsiteGenerator {
|
||||
|
||||
private let imageGenerator: ImageGenerator
|
||||
|
||||
private var navigationBarLinks: [NavigationBar.Link] {
|
||||
content.settings.navigationItems.map {
|
||||
.init(text: $0.title(in: language),
|
||||
url: $0.absoluteUrl(in: language))
|
||||
}
|
||||
}
|
||||
|
||||
init(content: Content, language: ContentLanguage) {
|
||||
self.language = language
|
||||
self.content = content
|
||||
@ -61,7 +54,6 @@ final class LocalizedWebsiteGenerator {
|
||||
language: language,
|
||||
content: content,
|
||||
imageGenerator: imageGenerator,
|
||||
navigationBarLinks: navigationBarLinks,
|
||||
showTitle: false,
|
||||
pageTitle: localizedPostSettings.title,
|
||||
pageDescription: localizedPostSettings.description,
|
||||
@ -82,7 +74,6 @@ final class LocalizedWebsiteGenerator {
|
||||
language: language,
|
||||
content: content,
|
||||
imageGenerator: imageGenerator,
|
||||
navigationBarLinks: navigationBarLinks,
|
||||
showTitle: true,
|
||||
pageTitle: localized.name,
|
||||
pageDescription: localized.description ?? "",
|
||||
@ -115,8 +106,7 @@ final class LocalizedWebsiteGenerator {
|
||||
}
|
||||
let pageGenerator = PageGenerator(
|
||||
content: content,
|
||||
imageGenerator: imageGenerator,
|
||||
navigationBarLinks: navigationBarLinks)
|
||||
imageGenerator: imageGenerator)
|
||||
|
||||
let content: String
|
||||
let results: PageGenerationResults
|
||||
|
@ -4,12 +4,9 @@ final class PageGenerator {
|
||||
|
||||
private let imageGenerator: ImageGenerator
|
||||
|
||||
private let navigationBarLinks: [NavigationBar.Link]
|
||||
|
||||
init(content: Content, imageGenerator: ImageGenerator, navigationBarLinks: [NavigationBar.Link]) {
|
||||
init(content: Content, imageGenerator: ImageGenerator) {
|
||||
self.content = content
|
||||
self.imageGenerator = imageGenerator
|
||||
self.navigationBarLinks = navigationBarLinks
|
||||
}
|
||||
|
||||
func makeHeaders(requiredItems: [HeaderFile]) -> [HeaderElement] {
|
||||
@ -51,7 +48,7 @@ final class PageGenerator {
|
||||
tags: tags,
|
||||
linkTitle: localized.linkPreviewTitle ?? localized.title,
|
||||
description: localized.linkPreviewDescription ?? "",
|
||||
navigationBarLinks: navigationBarLinks,
|
||||
navigationBarLinks: content.navigationBar(in: language),
|
||||
pageContent: pageContent,
|
||||
headers: headers,
|
||||
footers: contentGenerator.results.requiredFooters.sorted(),
|
||||
|
@ -8,9 +8,6 @@ final class PostListPageGenerator {
|
||||
|
||||
private let imageGenerator: ImageGenerator
|
||||
|
||||
#warning("Get from settings")
|
||||
private let navigationBarLinks: [NavigationBar.Link]
|
||||
|
||||
private let showTitle: Bool
|
||||
|
||||
private let pageTitle: String
|
||||
@ -20,11 +17,10 @@ final class PostListPageGenerator {
|
||||
/// The url of the page, excluding the extension
|
||||
private let pageUrlPrefix: String
|
||||
|
||||
init(language: ContentLanguage, content: Content, imageGenerator: ImageGenerator, navigationBarLinks: [NavigationBar.Link], showTitle: Bool, pageTitle: String, pageDescription: String, pageUrlPrefix: String) {
|
||||
init(language: ContentLanguage, content: Content, imageGenerator: ImageGenerator, showTitle: Bool, pageTitle: String, pageDescription: String, pageUrlPrefix: String) {
|
||||
self.language = language
|
||||
self.content = content
|
||||
self.imageGenerator = imageGenerator
|
||||
self.navigationBarLinks = navigationBarLinks
|
||||
self.showTitle = showTitle
|
||||
self.pageTitle = pageTitle
|
||||
self.pageDescription = pageDescription
|
||||
@ -50,14 +46,14 @@ final class PostListPageGenerator {
|
||||
let startIndex = (pageIndex - 1) * postsPerPage
|
||||
let endIndex = min(pageIndex * postsPerPage, totalCount)
|
||||
let postsOnPage = posts[startIndex..<endIndex]
|
||||
guard createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage, bar: navigationBarLinks) else {
|
||||
guard createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage) else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>, bar: [NavigationBar.Link]) -> Bool {
|
||||
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>) -> Bool {
|
||||
let posts: [FeedEntryData] = posts.map { post in
|
||||
let localized: LocalizedPost = post.localized(in: language)
|
||||
|
||||
@ -78,7 +74,7 @@ final class PostListPageGenerator {
|
||||
textAboveTitle: post.dateText(in: language),
|
||||
link: linkUrl,
|
||||
tags: tags,
|
||||
text: [localized.content], // TODO: Convert from markdown to html
|
||||
text: localized.text.components(separatedBy: "\n"),
|
||||
images: localized.images.map(createImageSet))
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
|
||||
#warning("Allow selection of pages as navigation bar items")
|
||||
#warning("Show all warnings on page content")
|
||||
#warning("Button to delete file")
|
||||
#warning("Fix podcast")
|
||||
@ -14,6 +12,10 @@ import SFSafeSymbols
|
||||
#warning("Calculate file sizes")
|
||||
#warning("Specify image aspect ratio to prevent page jumps")
|
||||
#warning("Add version and source url properties to file resources")
|
||||
#warning("Consolidate all errors in Content")
|
||||
#warning("Generate pages for posts")
|
||||
#warning("Clean up mock content")
|
||||
#warning("Show posts linking to a page")
|
||||
|
||||
@main
|
||||
struct MainView: App {
|
||||
|
@ -35,4 +35,11 @@ extension Content {
|
||||
func tag(_ tagId: String) -> Tag? {
|
||||
tags.first { $0.id == tagId }
|
||||
}
|
||||
|
||||
func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] {
|
||||
settings.navigationItems.map {
|
||||
.init(text: $0.title(in: language),
|
||||
url: $0.absoluteUrl(in: language))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ extension Content {
|
||||
|
||||
private func convert(_ tag: LocalizedTagFile, images: [String : FileResource]) -> LocalizedTag {
|
||||
LocalizedTag(
|
||||
content: self,
|
||||
urlComponent: tag.urlComponent,
|
||||
name: tag.name,
|
||||
subtitle: tag.subtitle,
|
||||
@ -14,8 +15,9 @@ extension Content {
|
||||
|
||||
private func convert(_ post: LocalizedPostFile, images: [String : FileResource]) -> LocalizedPost {
|
||||
LocalizedPost(
|
||||
content: self,
|
||||
title: post.title,
|
||||
content: post.content,
|
||||
text: post.content,
|
||||
lastModified: post.lastModifiedDate,
|
||||
images: post.images.compactMap { images[$0] },
|
||||
linkPreviewImage: post.linkPreviewImage.map { images[$0] },
|
||||
@ -107,8 +109,8 @@ extension Content {
|
||||
let tagOverview = tagOverviewData.map { file in
|
||||
TagOverviewPage(
|
||||
content: self,
|
||||
german: .init(file: file.german, image: file.german.linkPreviewImage.map { files[$0] }),
|
||||
english: .init(file: file.english, image: file.english.linkPreviewImage.map { files[$0] }))
|
||||
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()
|
||||
|
@ -95,7 +95,7 @@ private extension LocalizedPost {
|
||||
var postFile: LocalizedPostFile {
|
||||
.init(images: images.map { $0.id },
|
||||
title: title.nonEmpty,
|
||||
content: content,
|
||||
content: text,
|
||||
lastModifiedDate: lastModified,
|
||||
linkPreviewImage: linkPreviewImage?.id,
|
||||
linkPreviewTitle: linkPreviewTitle,
|
||||
|
@ -18,6 +18,10 @@ extension Content {
|
||||
!posts.contains { $0.id == id }
|
||||
}
|
||||
|
||||
func isNewIdForFile(_ id: String) -> Bool {
|
||||
!files.contains { $0.id == id }
|
||||
}
|
||||
|
||||
func isValidIdForTagOrPageOrPost(_ id: String) -> Bool {
|
||||
id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil
|
||||
}
|
||||
@ -26,6 +30,13 @@ extension Content {
|
||||
id.rangeOfCharacter(from: Content.disallowedCharactersInFileIds) == nil
|
||||
}
|
||||
|
||||
func containsPage(withUrlComponent urlComponent: String) -> Bool {
|
||||
pages.contains {
|
||||
$0.german.urlString == urlComponent ||
|
||||
$0.english.urlString == urlComponent
|
||||
}
|
||||
}
|
||||
|
||||
func containsTag(withUrlComponent urlComponent: String) -> Bool {
|
||||
(tagOverview?.contains(urlComponent: urlComponent) ?? false) ||
|
||||
tags.contains { $0.contains(urlComponent: urlComponent) }
|
||||
|
@ -111,6 +111,13 @@ final class FileResource: Item {
|
||||
|
||||
// MARK: File
|
||||
|
||||
func isValid(id: String) -> Bool {
|
||||
!id.isEmpty &&
|
||||
content.isValidIdForFile(id) &&
|
||||
content.isNewIdForFile(id)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func update(id newId: String) -> Bool {
|
||||
guard !isExternallyStored else {
|
||||
id = newId
|
||||
|
@ -34,11 +34,11 @@ final class TagOverviewPage: Item {
|
||||
}
|
||||
|
||||
private func internalPath(for language: ContentLanguage) -> String {
|
||||
content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlString
|
||||
content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlComponent
|
||||
}
|
||||
|
||||
func contains(urlComponent: String) -> Bool {
|
||||
english.urlString == urlComponent || german.urlString == urlComponent
|
||||
english.urlComponent == urlComponent || german.urlComponent == urlComponent
|
||||
}
|
||||
|
||||
var file: TagOverviewFile {
|
||||
@ -53,16 +53,16 @@ extension TagOverviewPage: LocalizedItem {
|
||||
|
||||
final class LocalizedTagOverviewPage: ObservableObject {
|
||||
|
||||
unowned let content: Content
|
||||
|
||||
@Published
|
||||
var title: String
|
||||
|
||||
/**
|
||||
The string to use when creating the url for the page.
|
||||
|
||||
Defaults to ``id`` if unset.
|
||||
*/
|
||||
@Published
|
||||
var urlString: String
|
||||
var urlComponent: String
|
||||
|
||||
@Published
|
||||
var linkPreviewImage: FileResource?
|
||||
@ -73,27 +73,35 @@ final class LocalizedTagOverviewPage: ObservableObject {
|
||||
@Published
|
||||
var linkPreviewDescription: String?
|
||||
|
||||
init(title: String, urlString: String, linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) {
|
||||
init(content: Content, title: String, urlString: String, linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) {
|
||||
self.content = content
|
||||
self.title = title
|
||||
self.urlString = urlString
|
||||
self.urlComponent = urlString
|
||||
self.linkPreviewImage = linkPreviewImage
|
||||
self.linkPreviewTitle = linkPreviewTitle
|
||||
self.linkPreviewDescription = linkPreviewDescription
|
||||
}
|
||||
|
||||
init(file: LocalizedTagOverviewFile, image: FileResource?) {
|
||||
init(content: Content, file: LocalizedTagOverviewFile, image: FileResource?) {
|
||||
self.content = content
|
||||
self.title = file.title
|
||||
self.urlString = file.url
|
||||
self.urlComponent = file.url
|
||||
self.linkPreviewImage = image
|
||||
self.linkPreviewTitle = file.linkPreviewTitle
|
||||
self.linkPreviewDescription = file.linkPreviewDescription
|
||||
}
|
||||
|
||||
var file: LocalizedTagOverviewFile {
|
||||
.init(url: urlString,
|
||||
.init(url: urlComponent,
|
||||
title: title,
|
||||
linkPreviewImage: linkPreviewImage?.id,
|
||||
linkPreviewTitle: linkPreviewTitle,
|
||||
linkPreviewDescription: linkPreviewDescription)
|
||||
}
|
||||
|
||||
func isValid(urlComponent: String) -> Bool {
|
||||
!urlComponent.isEmpty &&
|
||||
content.isValidIdForTagOrPageOrPost(urlComponent) &&
|
||||
!content.containsTag(withUrlComponent: urlComponent)
|
||||
}
|
||||
}
|
||||
|
@ -89,4 +89,9 @@ final class LocalizedPage: ObservableObject {
|
||||
self.linkPreviewTitle = linkPreviewTitle
|
||||
self.linkPreviewDescription = linkPreviewDescription
|
||||
}
|
||||
|
||||
func isValid(urlComponent: String) -> Bool {
|
||||
content.isValidIdForTagOrPageOrPost(urlComponent) &&
|
||||
!content.containsPage(withUrlComponent: urlComponent)
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,13 @@ import SwiftUI
|
||||
|
||||
final class LocalizedPost: ObservableObject {
|
||||
|
||||
unowned let content: Content
|
||||
|
||||
@Published
|
||||
var title: String
|
||||
|
||||
@Published
|
||||
var content: String
|
||||
var text: String
|
||||
|
||||
@Published
|
||||
var lastModified: Date?
|
||||
@ -24,15 +26,17 @@ final class LocalizedPost: ObservableObject {
|
||||
@Published
|
||||
var linkPreviewDescription: String?
|
||||
|
||||
init(title: String? = nil,
|
||||
content: String,
|
||||
init(content: Content,
|
||||
title: String? = nil,
|
||||
text: String,
|
||||
lastModified: Date? = nil,
|
||||
images: [FileResource] = [],
|
||||
linkPreviewImage: FileResource? = nil,
|
||||
linkPreviewTitle: String? = nil,
|
||||
linkPreviewDescription: String? = nil) {
|
||||
self.title = title ?? ""
|
||||
self.content = content
|
||||
self.title = title ?? ""
|
||||
self.text = text
|
||||
self.lastModified = lastModified
|
||||
self.images = images
|
||||
self.linkPreviewImage = linkPreviewImage
|
||||
|
@ -2,6 +2,8 @@ import Foundation
|
||||
|
||||
final class LocalizedTag: ObservableObject {
|
||||
|
||||
unowned let content: Content
|
||||
|
||||
@Published
|
||||
var urlComponent: String
|
||||
|
||||
@ -22,12 +24,14 @@ final class LocalizedTag: ObservableObject {
|
||||
/// The original url in the previous site layout
|
||||
let originalUrl: String?
|
||||
|
||||
init(urlComponent: String,
|
||||
init(content: Content,
|
||||
urlComponent: String,
|
||||
name: String,
|
||||
subtitle: String? = nil,
|
||||
description: String? = nil,
|
||||
thumbnail: FileResource? = nil,
|
||||
originalUrl: String? = nil) {
|
||||
self.content = content
|
||||
self.urlComponent = urlComponent
|
||||
self.name = name
|
||||
self.subtitle = subtitle
|
||||
@ -35,4 +39,9 @@ final class LocalizedTag: ObservableObject {
|
||||
self.linkPreviewImage = thumbnail
|
||||
self.originalUrl = originalUrl
|
||||
}
|
||||
|
||||
func isValid(urlComponent: String) -> Bool {
|
||||
content.isValidIdForTagOrPageOrPost(urlComponent) &&
|
||||
!content.containsTag(withUrlComponent: urlComponent)
|
||||
}
|
||||
}
|
||||
|
@ -66,9 +66,16 @@ final class Page: Item {
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
func isValid(id: String) -> Bool {
|
||||
!id.isEmpty &&
|
||||
content.isValidIdForTagOrPageOrPost(id) &&
|
||||
content.isNewIdForPage(id)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func update(id newId: String) -> Bool {
|
||||
guard content.storage.move(page: id, to: newId) else {
|
||||
print("Failed to move file of page \(id)")
|
||||
print("Failed to move files of page \(id)")
|
||||
return false
|
||||
}
|
||||
id = newId
|
||||
|
@ -65,6 +65,13 @@ final class Post: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func isValid(id: String) -> Bool {
|
||||
!id.isEmpty &&
|
||||
content.isValidIdForTagOrPageOrPost(id) &&
|
||||
content.isNewIdForPost(id)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func update(id newId: String) -> Bool {
|
||||
do {
|
||||
try content.storage.move(post: id, to: newId)
|
||||
|
@ -13,9 +13,9 @@ final class Tag: Item {
|
||||
|
||||
override init(content: Content, id: String) {
|
||||
self.isVisible = true
|
||||
self.english = .init(urlComponent: id, name: id)
|
||||
self.english = .init(content: content, urlComponent: id, name: id)
|
||||
let deId = id + "-" + ContentLanguage.german.rawValue
|
||||
self.german = .init(urlComponent: deId, name: deId)
|
||||
self.german = .init(content: content, urlComponent: deId, name: deId)
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
|
@ -9,8 +9,10 @@ extension Post {
|
||||
startDate: .now,
|
||||
endDate: nil,
|
||||
tags: [],
|
||||
german: .init(content: "Text"),
|
||||
english: .init(content: "Text"),
|
||||
german: .init(content: .mock,
|
||||
text: "Text"),
|
||||
english: .init(content: .mock,
|
||||
text: "Text"),
|
||||
linkedPage: nil)
|
||||
}
|
||||
|
||||
@ -23,8 +25,14 @@ extension Post {
|
||||
startDate: .now,
|
||||
endDate: nil,
|
||||
tags: [.nature, .sports, .hiking],
|
||||
german: .init(title: "Der Titel", content: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend."),
|
||||
english: .init(title: "The title", content: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height")
|
||||
german: .init(
|
||||
content: .mock,
|
||||
title: "Der Titel",
|
||||
text: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend."),
|
||||
english: .init(
|
||||
content: .mock,
|
||||
title: "The title",
|
||||
text: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height")
|
||||
)
|
||||
}
|
||||
|
||||
@ -44,12 +52,14 @@ extension Post {
|
||||
extension LocalizedPost {
|
||||
|
||||
static let german = LocalizedPost(
|
||||
content: .mock,
|
||||
title: "Ein langer Titel",
|
||||
content: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend.",
|
||||
text: "Sehr schöne und einsame Tour von Oberau zum Krottenkopf. Abwechslungsreich und stellenweise fordernd, aufgrund der Länge und der Höhenmeter auch anstrengend.",
|
||||
images: MockImage.images)
|
||||
|
||||
static let english = LocalizedPost(
|
||||
content: .mock,
|
||||
title: "A longer title",
|
||||
content: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
|
||||
text: "Very nice and solitary hike from Oberau to Krottenkopf. Challenging and rewarding, due to the length and height.",
|
||||
images: MockImage.images)
|
||||
}
|
||||
|
@ -11,35 +11,36 @@ extension Tag {
|
||||
static let nature = Tag(
|
||||
content: .mock,
|
||||
id: "nature",
|
||||
german: .init(urlComponent: "natur", name: "Natur"),
|
||||
english: .init(urlComponent: "nature", name: "Nature")
|
||||
german: .init(content: .mock, urlComponent: "natur", name: "Natur"),
|
||||
english: .init(content: .mock, urlComponent: "nature", name: "Nature")
|
||||
)
|
||||
|
||||
static let sports = Tag(
|
||||
content: .mock,
|
||||
id: "sports",
|
||||
german: .init(urlComponent: "sport", name: "Sport"),
|
||||
english: .init(urlComponent: "sports", name: "Sports")
|
||||
german: .init(content: .mock, urlComponent: "sport", name: "Sport"),
|
||||
english: .init(content: .mock, urlComponent: "sports", name: "Sports")
|
||||
)
|
||||
|
||||
static let hiking = Tag(
|
||||
content: .mock,
|
||||
id: "hiking",
|
||||
german: .init(urlComponent: "wandern", name: "Wandern"),
|
||||
english: .init(urlComponent: "hiking", name: "Hiking")
|
||||
german: .init(content: .mock, urlComponent: "wandern", name: "Wandern"),
|
||||
english: .init(content: .mock, urlComponent: "hiking", name: "Hiking")
|
||||
)
|
||||
|
||||
static let mountains = Tag(
|
||||
content: .mock,
|
||||
id: "mountains",
|
||||
german: .init(urlComponent: "berge", name: "Berge"),
|
||||
english: .init(urlComponent: "mountains", name: "Mountains")
|
||||
german: .init(content: .mock, urlComponent: "berge", name: "Berge"),
|
||||
english: .init(content: .mock, urlComponent: "mountains", name: "Mountains")
|
||||
)
|
||||
}
|
||||
|
||||
extension LocalizedTag {
|
||||
|
||||
static let english = LocalizedTag(
|
||||
content: .mock,
|
||||
urlComponent: "electronics",
|
||||
name: "Electronics",
|
||||
subtitle: "Projects with electronics",
|
||||
@ -48,6 +49,7 @@ extension LocalizedTag {
|
||||
originalUrl: "projects/electronics")
|
||||
|
||||
static let german = LocalizedTag(
|
||||
content: .mock,
|
||||
urlComponent: "elektronik",
|
||||
name: "Elektronik",
|
||||
subtitle: "Projekte mit Elektronik",
|
||||
|
@ -435,12 +435,15 @@ final class Storage {
|
||||
|
||||
// MARK: Folder access
|
||||
|
||||
func save(folderUrl url: URL, in bookmark: SecurityScopeBookmark) {
|
||||
@discardableResult
|
||||
func save(folderUrl url: URL, in bookmark: SecurityScopeBookmark) -> Bool {
|
||||
do {
|
||||
let bookmarkData = try url.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)
|
||||
UserDefaults.standard.set(bookmarkData, forKey: bookmark.rawValue)
|
||||
return true
|
||||
} catch {
|
||||
print("Failed to create security-scoped bookmark: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,44 +5,29 @@ struct FileDetailView: View {
|
||||
@ObservedObject
|
||||
var file: FileResource
|
||||
|
||||
@State
|
||||
private var newId: String
|
||||
|
||||
init(file: FileResource) {
|
||||
self.file = file
|
||||
self.newId = file.id
|
||||
}
|
||||
|
||||
private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-.")).inverted
|
||||
|
||||
private var idExists: Bool {
|
||||
file.content.files.contains { $0.id == newId }
|
||||
}
|
||||
|
||||
private var containsInvalidCharacters: Bool {
|
||||
newId.rangeOfCharacter(from: allowedCharactersInPostId) != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("File Name")
|
||||
.font(.headline)
|
||||
HStack {
|
||||
TextField("", text: $newId)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button(action: setNewId) {
|
||||
Text("Update")
|
||||
}
|
||||
.disabled(newId.isEmpty || containsInvalidCharacters || idExists)
|
||||
}
|
||||
Text("German Description")
|
||||
.font(.headline)
|
||||
TextField("", text: $file.german)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("English Description")
|
||||
.font(.headline)
|
||||
TextField("", text: $file.english)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
DetailTitle(
|
||||
title: "File",
|
||||
text: "A file that can be used in a post or page")
|
||||
|
||||
IdPropertyView(
|
||||
id: $file.id,
|
||||
title: "Name",
|
||||
footer: "The unique name of the file, which is also used to reference it in posts and pages.",
|
||||
validation: file.isValid,
|
||||
update: { file.update(id: $0) })
|
||||
|
||||
StringPropertyView(
|
||||
title: "German Description",
|
||||
text: $file.german,
|
||||
footer: "The description for the file in German. Descriptions are used for images and to explain the content of a file.")
|
||||
|
||||
StringPropertyView(
|
||||
title: "English Description",
|
||||
text: $file.english,
|
||||
footer: "The description for the file in English. Descriptions are used for images and to explain the content of a file.")
|
||||
|
||||
if file.type.isImage {
|
||||
Text("Image size")
|
||||
.font(.headline)
|
||||
@ -53,12 +38,6 @@ struct FileDetailView: View {
|
||||
Spacer()
|
||||
}.padding()
|
||||
}
|
||||
|
||||
private func setNewId() {
|
||||
if !file.update(id: newId) {
|
||||
newId = file.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FileDetailView: MainContentView {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import SwiftUI
|
||||
|
||||
private enum FileFilterType: String, Hashable, CaseIterable, Identifiable {
|
||||
enum FileFilterType: String, Hashable, CaseIterable, Identifiable {
|
||||
case images
|
||||
case text
|
||||
case videos
|
||||
@ -38,11 +38,19 @@ struct FileListView: View {
|
||||
var selectedFile: FileResource?
|
||||
|
||||
@State
|
||||
private var selectedFileType: FileFilterType = .images
|
||||
private var selectedFileType: FileFilterType
|
||||
|
||||
@State
|
||||
private var searchString = ""
|
||||
|
||||
let allowedType: FileFilterType?
|
||||
|
||||
init(selectedFile: Binding<FileResource?>, allowedType: FileFilterType? = nil) {
|
||||
self._selectedFile = selectedFile
|
||||
self.allowedType = allowedType
|
||||
self.selectedFileType = allowedType ?? .images
|
||||
}
|
||||
|
||||
var filesBySelectedType: [FileResource] {
|
||||
content.files.filter { selectedFileType.matches($0.type) }
|
||||
}
|
||||
@ -63,6 +71,7 @@ struct FileListView: View {
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.trailing, 7)
|
||||
.disabled(allowedType != nil)
|
||||
TextField("", text: $searchString, prompt: Text("Search"))
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.horizontal, 8)
|
||||
|
@ -2,15 +2,18 @@ import SwiftUI
|
||||
|
||||
struct FileSelectionView: View {
|
||||
|
||||
@Binding
|
||||
private var selectedFile: FileResource?
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss
|
||||
|
||||
init(selectedFile: Binding<FileResource?>) {
|
||||
@Binding
|
||||
private var selectedFile: FileResource?
|
||||
|
||||
let allowedType: FileFilterType?
|
||||
|
||||
init(selectedFile: Binding<FileResource?>, allowedType: FileFilterType? = nil) {
|
||||
self._selectedFile = selectedFile
|
||||
self.newSelection = selectedFile.wrappedValue
|
||||
self.allowedType = allowedType
|
||||
}
|
||||
|
||||
@State
|
||||
@ -18,7 +21,7 @@ struct FileSelectionView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
FileListView(selectedFile: $newSelection)
|
||||
FileListView(selectedFile: $newSelection, allowedType: allowedType)
|
||||
.frame(minHeight: 500, idealHeight: 600)
|
||||
HStack {
|
||||
Button("Cancel") {
|
||||
|
26
CHDataManagement/Views/Generic/BoolPropertyView.swift
Normal file
26
CHDataManagement/Views/Generic/BoolPropertyView.swift
Normal file
@ -0,0 +1,26 @@
|
||||
import SwiftUI
|
||||
|
||||
struct BoolPropertyView: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var value: Bool
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Toggle("", isOn: $value)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
Text(footer)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
50
CHDataManagement/Views/Generic/DatePropertyView.swift
Normal file
50
CHDataManagement/Views/Generic/DatePropertyView.swift
Normal file
@ -0,0 +1,50 @@
|
||||
import SwiftUI
|
||||
|
||||
struct DatePropertyView: View {
|
||||
|
||||
let title: String
|
||||
|
||||
@Binding
|
||||
var value: Date
|
||||
|
||||
let footer: String
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
DatePicker("", selection: $value, displayedComponents: .date)
|
||||
.datePickerStyle(.compact)
|
||||
Text(footer)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OptionalDatePropertyView: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var isEnabled: Bool
|
||||
|
||||
@Binding
|
||||
var date: Date
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Toggle("", isOn: $isEnabled)
|
||||
.toggleStyle(.switch)
|
||||
DatePicker("", selection: $date, displayedComponents: .date)
|
||||
.datePickerStyle(.compact)
|
||||
.padding(.bottom)
|
||||
.disabled(!isEnabled)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -2,9 +2,9 @@ import SwiftUI
|
||||
|
||||
struct FilePropertyView: View {
|
||||
|
||||
let title: String
|
||||
let title: LocalizedStringKey
|
||||
|
||||
let description: String
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var selectedFile: FileResource?
|
||||
@ -13,9 +13,7 @@ struct FilePropertyView: View {
|
||||
private var showFileSelectionSheet = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
HStack {
|
||||
Text(selectedFile?.id ?? "No file selected")
|
||||
Spacer()
|
||||
@ -23,9 +21,6 @@ struct FilePropertyView: View {
|
||||
showFileSelectionSheet = true
|
||||
}
|
||||
}
|
||||
Text(description)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.sheet(isPresented: $showFileSelectionSheet) {
|
||||
FileSelectionView(selectedFile: $selectedFile)
|
||||
|
@ -0,0 +1,57 @@
|
||||
import SwiftUI
|
||||
|
||||
struct FolderOnDiskPropertyView: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var folder: String
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
let update: (URL) -> Void
|
||||
|
||||
init(title: LocalizedStringKey, folder: Binding<String>, footer: LocalizedStringKey, update: @escaping (URL) -> Void) {
|
||||
self.title = title
|
||||
self._folder = folder
|
||||
self.footer = footer
|
||||
self.update = update
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(folder)
|
||||
Spacer()
|
||||
Button("Select") {
|
||||
guard let url = openFolderSelectionPanel() else {
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
update(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func openFolderSelectionPanel() -> URL? {
|
||||
let panel = NSOpenPanel()
|
||||
// Sets up so user can only select a single directory
|
||||
panel.canChooseFiles = false
|
||||
panel.canChooseDirectories = true
|
||||
panel.allowsMultipleSelection = false
|
||||
panel.showsHiddenFiles = false
|
||||
panel.title = "Select directory"
|
||||
//panel.prompt = "Select Directory"
|
||||
|
||||
let response = panel.runModal()
|
||||
guard response == .OK else {
|
||||
return nil
|
||||
}
|
||||
guard let url = panel.url else {
|
||||
return nil
|
||||
}
|
||||
return url
|
||||
}
|
||||
}
|
27
CHDataManagement/Views/Generic/GenericPropertyView.swift
Normal file
27
CHDataManagement/Views/Generic/GenericPropertyView.swift
Normal file
@ -0,0 +1,27 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GenericPropertyView<Content>: View where Content: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
let content: Content
|
||||
|
||||
public init(title: LocalizedStringKey, footer: LocalizedStringKey, @ViewBuilder content: () -> Content) {
|
||||
self.title = title
|
||||
self.footer = footer
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
content
|
||||
Text(footer)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
56
CHDataManagement/Views/Generic/IdPropertyView.swift
Normal file
56
CHDataManagement/Views/Generic/IdPropertyView.swift
Normal file
@ -0,0 +1,56 @@
|
||||
import SwiftUI
|
||||
|
||||
struct IdPropertyView: View {
|
||||
|
||||
@Binding
|
||||
var id: String
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
let validation: (String) -> Bool
|
||||
|
||||
let update: (String) -> Void
|
||||
|
||||
@State
|
||||
private var newId: String
|
||||
|
||||
init(id: Binding<String>,
|
||||
title: LocalizedStringKey = "ID",
|
||||
footer: LocalizedStringKey,
|
||||
validation: @escaping (String) -> Bool = { _ in true },
|
||||
update: @escaping (String) -> Void) {
|
||||
self._id = id
|
||||
self.title = title
|
||||
self.footer = footer
|
||||
self.validation = validation
|
||||
self.update = update
|
||||
self.newId = id.wrappedValue
|
||||
}
|
||||
|
||||
private var isValid: Bool {
|
||||
validation(id)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
HStack {
|
||||
TextField("", text: $newId)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Spacer()
|
||||
Button("Update", action: setNewId)
|
||||
.disabled(!isValid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setNewId() {
|
||||
update(newId)
|
||||
// In case of failure, resets the id
|
||||
// In case of update, sets to potentially modified id
|
||||
DispatchQueue.main.async {
|
||||
newId = id
|
||||
}
|
||||
}
|
||||
}
|
@ -2,22 +2,17 @@ import SwiftUI
|
||||
|
||||
struct IntegerPropertyView: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var value: Int
|
||||
|
||||
let title: String
|
||||
|
||||
let footer: String
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
IntegerField("", number: $value)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text(footer)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OptionalImagePropertyView: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var selectedImage: FileResource?
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
@State
|
||||
private var showSelectionSheet = false
|
||||
|
||||
var body: some View {
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
if let image = selectedImage {
|
||||
image.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxHeight: 300)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
HStack {
|
||||
Text(selectedImage?.id ?? "No file selected")
|
||||
Spacer()
|
||||
Button("Select") {
|
||||
showSelectionSheet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSelectionSheet) {
|
||||
FileSelectionView(selectedFile: $selectedImage, allowedType: .images)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OptionalStringPropertyView: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var text: String?
|
||||
|
||||
let prompt: String?
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
init(title: LocalizedStringKey, text: Binding<String?>, prompt: String? = nil, footer: LocalizedStringKey) {
|
||||
self.title = title
|
||||
self._text = text
|
||||
self.prompt = prompt
|
||||
self.footer = footer
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
OptionalTextField(title, text: $text, prompt: prompt)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import SwiftUI
|
||||
|
||||
struct OptionalTextFieldPropertyView: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var text: String?
|
||||
|
||||
let prompt: String?
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
init(title: LocalizedStringKey, text: Binding<String?>, prompt: String? = nil, footer: LocalizedStringKey) {
|
||||
self.title = title
|
||||
self._text = text
|
||||
self.prompt = prompt
|
||||
self.footer = footer
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
OptionalDescriptionField(text: $text)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
}
|
||||
}
|
30
CHDataManagement/Views/Generic/PagePropertyView.swift
Normal file
30
CHDataManagement/Views/Generic/PagePropertyView.swift
Normal file
@ -0,0 +1,30 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PagePropertyView: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var selectedPage: Page?
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
@State
|
||||
private var showPageSelectionSheet = false
|
||||
|
||||
var body: some View {
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
HStack {
|
||||
Text(selectedPage?.id ?? "No page selected")
|
||||
Spacer()
|
||||
Button("Select") {
|
||||
showPageSelectionSheet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showPageSelectionSheet) {
|
||||
PagePickerView(selectedPage: $selectedPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
27
CHDataManagement/Views/Generic/StringPropertyView.swift
Normal file
27
CHDataManagement/Views/Generic/StringPropertyView.swift
Normal file
@ -0,0 +1,27 @@
|
||||
import SwiftUI
|
||||
|
||||
struct StringPropertyView: View {
|
||||
|
||||
let title: LocalizedStringKey
|
||||
|
||||
@Binding
|
||||
var text: String
|
||||
|
||||
let prompt: String?
|
||||
|
||||
let footer: LocalizedStringKey
|
||||
|
||||
init(title: LocalizedStringKey, text: Binding<String>, prompt: String? = nil, footer: LocalizedStringKey) {
|
||||
self.title = title
|
||||
self._text = text
|
||||
self.prompt = prompt
|
||||
self.footer = footer
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
GenericPropertyView(title: title, footer: footer) {
|
||||
TextField(title, text: $text, prompt: prompt.map(Text.init))
|
||||
.textFieldStyle(.roundedBorder)
|
||||
}
|
||||
}
|
||||
}
|
@ -3,101 +3,41 @@ import SFSafeSymbols
|
||||
|
||||
struct LocalizedPageDetailView: View {
|
||||
|
||||
let isExternalPage: Bool
|
||||
|
||||
@ObservedObject
|
||||
private var page: LocalizedPage
|
||||
|
||||
init(page: LocalizedPage, showImagePicker: Bool = false) {
|
||||
self.page = page
|
||||
self.showImagePicker = showImagePicker
|
||||
self.newUrlString = page.urlString
|
||||
}
|
||||
|
||||
@State
|
||||
private var showImagePicker = false
|
||||
|
||||
@State
|
||||
private var newUrlString: String
|
||||
|
||||
private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted
|
||||
|
||||
private var idExists: Bool {
|
||||
page.content.pages.contains {
|
||||
$0.german.urlString == newUrlString
|
||||
|| $0.english.urlString == newUrlString
|
||||
}
|
||||
}
|
||||
|
||||
private var containsInvalidCharacters: Bool {
|
||||
newUrlString.rangeOfCharacter(from: allowedCharactersInPostId) != nil
|
||||
}
|
||||
var page: LocalizedPage
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text("Page URL String")
|
||||
.font(.headline)
|
||||
TextField("", text: $newUrlString)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button("Update", action: setNewId)
|
||||
.disabled(newUrlString.isEmpty || containsInvalidCharacters || idExists)
|
||||
}
|
||||
.padding(.bottom)
|
||||
IdPropertyView(
|
||||
id: $page.urlString,
|
||||
title: "Page URL String",
|
||||
footer: "The url component to use for the link to the page",
|
||||
validation: page.isValid,
|
||||
update: { page.urlString = $0 })
|
||||
.disabled(isExternalPage)
|
||||
|
||||
Text("Link Preview Title")
|
||||
.font(.headline)
|
||||
OptionalTextField("", text: $page.linkPreviewTitle,
|
||||
prompt: page.title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
OptionalStringPropertyView(
|
||||
title: "Preview Title",
|
||||
text: $page.linkPreviewTitle,
|
||||
prompt: page.title,
|
||||
footer: "The title to use for the page when linking to it")
|
||||
|
||||
HStack {
|
||||
Text("Link Preview Image")
|
||||
.font(.headline)
|
||||
IconButton(symbol: .squareAndPencilCircleFill,
|
||||
size: 22,
|
||||
color: .blue) {
|
||||
showImagePicker = true
|
||||
}
|
||||
OptionalImagePropertyView(
|
||||
title: "Preview Image",
|
||||
selectedImage: $page.linkPreviewImage,
|
||||
footer: "The image to show for previews of this page")
|
||||
|
||||
IconButton(symbol: .trashCircleFill,
|
||||
size: 22,
|
||||
color: .red) {
|
||||
page.linkPreviewImage = nil
|
||||
}.disabled(page.linkPreviewImage == nil)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
.buttonStyle(.plain)
|
||||
if let image = page.linkPreviewImage {
|
||||
image.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400, maxHeight: 300)
|
||||
.cornerRadius(8)
|
||||
Text(image.id)
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
Text("Link Preview Description")
|
||||
.font(.headline)
|
||||
.padding(.top)
|
||||
OptionalDescriptionField(text: $page.linkPreviewDescription)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
OptionalTextFieldPropertyView(
|
||||
title: "Preview Description",
|
||||
text: $page.linkPreviewDescription,
|
||||
footer: "The description to show in previews of the page")
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(showImagePicker: $showImagePicker) { image in
|
||||
page.linkPreviewImage = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setNewId() {
|
||||
page.urlString = newUrlString
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LocalizedPageDetailView(page: .english)
|
||||
LocalizedPageDetailView(isExternalPage: false, page: .english)
|
||||
.environmentObject(Content.mock)
|
||||
}
|
||||
|
@ -15,30 +15,19 @@ struct PageDetailView: View {
|
||||
@State
|
||||
private var isGeneratingWebsite = false
|
||||
|
||||
@State
|
||||
private var newId: String
|
||||
|
||||
@State
|
||||
private var didGenerateWebsite: Bool?
|
||||
|
||||
init(page: Page) {
|
||||
self.page = page
|
||||
self.newId = page.id
|
||||
}
|
||||
|
||||
private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted
|
||||
|
||||
private var idExists: Bool {
|
||||
page.content.pages.contains { $0.id == newId }
|
||||
}
|
||||
|
||||
private var containsInvalidCharacters: Bool {
|
||||
newId.rangeOfCharacter(from: allowedCharactersInPostId) != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
DetailTitle(
|
||||
title: "Page",
|
||||
text: "A page contains longer content")
|
||||
HStack {
|
||||
Button(action: generate) {
|
||||
Text("Generate")
|
||||
@ -54,62 +43,40 @@ struct PageDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
TextField("", text: $newId)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button("Update", action: setNewId)
|
||||
.disabled(newId.isEmpty || containsInvalidCharacters || idExists)
|
||||
}
|
||||
.padding(.bottom)
|
||||
IdPropertyView(
|
||||
id: $page.id,
|
||||
footer: "The page id is used to link to it internally.",
|
||||
validation: page.isValid,
|
||||
update: { page.update(id: $0) })
|
||||
|
||||
Text("External url")
|
||||
.font(.headline)
|
||||
OptionalTextField("", text: $page.externalLink,
|
||||
prompt: "External url")
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
OptionalStringPropertyView(
|
||||
title: "External url",
|
||||
text: $page.externalLink,
|
||||
footer: "Set an external url to mark this page as external. It will not be generated, and links will be created using the provided url")
|
||||
|
||||
HStack {
|
||||
Text("Draft")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Toggle("", isOn: $page.isDraft)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
.padding(.bottom)
|
||||
BoolPropertyView(
|
||||
title: "Draft",
|
||||
value: $page.isDraft,
|
||||
footer: "Indicate a page as a draft to hide it from the website")
|
||||
.disabled(page.isExternalUrl)
|
||||
|
||||
HStack {
|
||||
Text("Start")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
DatePicker("", selection: $page.startDate, displayedComponents: .date)
|
||||
.datePickerStyle(.compact)
|
||||
.padding(.bottom)
|
||||
}
|
||||
DatePropertyView(
|
||||
title: "Start date",
|
||||
value: $page.startDate,
|
||||
footer: "The date when the page content started")
|
||||
.disabled(page.isExternalUrl)
|
||||
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text("Has end date")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Toggle("", isOn: $page.hasEndDate)
|
||||
.toggleStyle(.switch)
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
if page.hasEndDate {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text("End date")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
DatePicker("", selection: $page.endDate, displayedComponents: .date)
|
||||
.datePickerStyle(.compact)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
LocalizedPageDetailView(page: page.localized(in: language))
|
||||
.id(page.id + language.rawValue)
|
||||
OptionalDatePropertyView(
|
||||
title: "End date",
|
||||
isEnabled: $page.hasEndDate,
|
||||
date: $page.endDate,
|
||||
footer: "The date when the page content ended")
|
||||
.disabled(page.isExternalUrl)
|
||||
|
||||
LocalizedPageDetailView(
|
||||
isExternalPage: page.isExternalUrl,
|
||||
page: page.localized(in: language))
|
||||
.id(page.id + language.rawValue)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@ -144,14 +111,6 @@ struct PageDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setNewId() {
|
||||
guard page.update(id: newId) else {
|
||||
newId = page.id
|
||||
return
|
||||
}
|
||||
page.id = newId
|
||||
}
|
||||
}
|
||||
|
||||
extension PageDetailView: MainContentView {
|
||||
|
@ -74,8 +74,8 @@ struct AddPostView: View {
|
||||
startDate: .now,
|
||||
endDate: nil,
|
||||
tags: [],
|
||||
german: .init(title: "Titel", content: "Text"),
|
||||
english: .init(title: "Title", content: "Text"))
|
||||
german: .init(content: content, title: "Titel", text: "Text"),
|
||||
english: .init(content: content, title: "Title", text: "Text"))
|
||||
content.posts.insert(post, at: 0)
|
||||
selectedPost = post
|
||||
dismissSheet()
|
||||
|
@ -1,49 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct DatePickerView: View {
|
||||
|
||||
@ObservedObject
|
||||
var post: Post
|
||||
|
||||
@Binding var showDatePicker: Bool
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack {
|
||||
HStack(alignment: .top) {
|
||||
VStack {
|
||||
Text("Start date")
|
||||
.font(.headline)
|
||||
.padding(.vertical, 3)
|
||||
DatePicker("", selection: $post.startDate, displayedComponents: .date)
|
||||
.datePickerStyle(GraphicalDatePickerStyle())
|
||||
.labelsHidden()
|
||||
.padding()
|
||||
|
||||
}
|
||||
|
||||
VStack {
|
||||
Toggle("End date", isOn: $post.hasEndDate)
|
||||
.toggleStyle(.switch)
|
||||
.font(.headline)
|
||||
DatePicker("Select a date", selection: $post.startDate, displayedComponents: .date)
|
||||
.datePickerStyle(GraphicalDatePickerStyle())
|
||||
.labelsHidden()
|
||||
.padding()
|
||||
.disabled(!post.hasEndDate)
|
||||
}
|
||||
}
|
||||
Button("Done") {
|
||||
showDatePicker = false
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("Pick a Date")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
DatePickerView(post: .mock, showDatePicker: .constant(true))
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ImagePickerView: View {
|
||||
|
||||
@Binding
|
||||
var showImagePicker: Bool
|
||||
|
||||
private let selected: (FileResource) -> Void
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
init(showImagePicker: Binding<Bool>, selected: @escaping (FileResource) -> Void) {
|
||||
self._showImagePicker = showImagePicker
|
||||
self.selected = selected
|
||||
}
|
||||
|
||||
@State
|
||||
private var selectedImage: FileResource?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Select the image to add")
|
||||
List(content.images, selection: $selectedImage) { image in
|
||||
Text("\(image.id)")
|
||||
.tag(image)
|
||||
}
|
||||
.frame(minHeight: 300)
|
||||
HStack {
|
||||
Button("Add") {
|
||||
DispatchQueue.main.async {
|
||||
if let selectedImage {
|
||||
print("Added image")
|
||||
selected(selectedImage)
|
||||
} else {
|
||||
print("No image to add")
|
||||
}
|
||||
}
|
||||
showImagePicker = false
|
||||
}
|
||||
.disabled(selectedImage == nil)
|
||||
Button("Cancel", role: .cancel) {
|
||||
showImagePicker = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Pick an image")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ImagePickerView(showImagePicker: .constant(true)) { _ in
|
||||
}
|
||||
.environmentObject(Content.mock)
|
||||
}
|
@ -3,64 +3,25 @@ import SwiftUI
|
||||
struct LocalizedPostDetailView: View {
|
||||
|
||||
@ObservedObject
|
||||
private var item: LocalizedPost
|
||||
|
||||
init(post: LocalizedPost, showImagePicker: Bool = false) {
|
||||
self.item = post
|
||||
self.showImagePicker = showImagePicker
|
||||
}
|
||||
|
||||
@State
|
||||
private var showImagePicker = false
|
||||
var post: LocalizedPost
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Link Preview Title")
|
||||
.font(.headline)
|
||||
OptionalTextField("", text: $item.linkPreviewTitle,
|
||||
prompt: item.title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
OptionalStringPropertyView(
|
||||
title: "Preview Title",
|
||||
text: $post.linkPreviewTitle,
|
||||
prompt: post.title,
|
||||
footer: "The title to use for the post when linking to it")
|
||||
|
||||
HStack {
|
||||
Text("Link Preview Image")
|
||||
.font(.headline)
|
||||
IconButton(symbol: .squareAndPencilCircleFill,
|
||||
size: 22,
|
||||
color: .blue) {
|
||||
showImagePicker = true
|
||||
}.padding(.bottom)
|
||||
OptionalImagePropertyView(
|
||||
title: "Preview Image",
|
||||
selectedImage: $post.linkPreviewImage,
|
||||
footer: "The image to show for previews of this post")
|
||||
|
||||
IconButton(symbol: .trashCircleFill,
|
||||
size: 22,
|
||||
color: .red) {
|
||||
item.linkPreviewImage = nil
|
||||
}.disabled(item.linkPreviewImage == nil)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
.buttonStyle(.plain)
|
||||
if let image = item.linkPreviewImage {
|
||||
image.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400, maxHeight: 300)
|
||||
.cornerRadius(8)
|
||||
Text(image.id)
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
Text("Link Preview Description")
|
||||
.font(.headline)
|
||||
.padding(.top)
|
||||
OptionalDescriptionField(text: $item.linkPreviewDescription)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(showImagePicker: $showImagePicker) { image in
|
||||
item.linkPreviewImage = image
|
||||
}
|
||||
OptionalTextFieldPropertyView(
|
||||
title: "Preview Description",
|
||||
text: $post.linkPreviewDescription,
|
||||
footer: "The description to show in previews of the post")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,21 +2,21 @@ import SwiftUI
|
||||
|
||||
struct PagePickerView: View {
|
||||
|
||||
@Binding var showPagePicker: Bool
|
||||
|
||||
@Binding var selectedPage: Page?
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@Environment(\.dismiss)
|
||||
var dismiss
|
||||
|
||||
@Binding var selectedPage: Page?
|
||||
|
||||
@State
|
||||
private var newSelection: Page?
|
||||
|
||||
init(showPagePicker: Binding<Bool>, selectedPage: Binding<Page?>) {
|
||||
self._showPagePicker = showPagePicker
|
||||
init(selectedPage: Binding<Page?>) {
|
||||
self._selectedPage = selectedPage
|
||||
self.newSelection = selectedPage.wrappedValue
|
||||
// TODO: Fix assignment not working
|
||||
@ -35,17 +35,17 @@ struct PagePickerView: View {
|
||||
Button("Use selection") {
|
||||
DispatchQueue.main.async {
|
||||
self.selectedPage = self.newSelection
|
||||
dismiss()
|
||||
}
|
||||
showPagePicker = false
|
||||
}
|
||||
Button("Remove page", role: .destructive) {
|
||||
DispatchQueue.main.async {
|
||||
self.selectedPage = nil
|
||||
dismiss()
|
||||
}
|
||||
showPagePicker = false
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
showPagePicker = false
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,7 +55,6 @@ struct PagePickerView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PagePickerView(showPagePicker: .constant(true),
|
||||
selectedPage: .constant(nil))
|
||||
.environmentObject(Content.mock)
|
||||
PagePickerView(selectedPage: .constant(nil))
|
||||
.environmentObject(Content.mock)
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ private struct LocalizedContentEditor: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TextEditor(text: $post.content)
|
||||
TextEditor(text: $post.text)
|
||||
.font(.body)
|
||||
.frame(minHeight: 150)
|
||||
.textEditorStyle(.plain)
|
||||
|
@ -36,109 +36,51 @@ struct PostDetailView: View {
|
||||
@ObservedObject
|
||||
private var post: Post
|
||||
|
||||
@State
|
||||
private var newId: String
|
||||
|
||||
@State
|
||||
private var showLinkedPagePicker = false
|
||||
|
||||
init(post: Post) {
|
||||
self.post = post
|
||||
self.newId = post.id
|
||||
}
|
||||
|
||||
private let allowedCharactersInPostId = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-")).inverted
|
||||
|
||||
private var idExists: Bool {
|
||||
post.content.posts.contains { $0.id == newId }
|
||||
}
|
||||
|
||||
private var containsInvalidCharacters: Bool {
|
||||
newId.rangeOfCharacter(from: allowedCharactersInPostId) != nil
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("ID")
|
||||
.font(.headline)
|
||||
HStack {
|
||||
TextField("", text: $newId)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button("Update", action: setNewId)
|
||||
.disabled(newId.isEmpty || containsInvalidCharacters || idExists)
|
||||
}
|
||||
.padding(.bottom)
|
||||
DetailTitle(
|
||||
title: "Post",
|
||||
text: "Posts capture quick updates and can link to pages")
|
||||
|
||||
HStack {
|
||||
Text("Draft")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Toggle("", isOn: $post.isDraft)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
.padding(.bottom)
|
||||
IdPropertyView(
|
||||
id: $post.id,
|
||||
footer: "The id is used to link to post and store them",
|
||||
validation: post.isValid,
|
||||
update: { post.update(id: $0) })
|
||||
|
||||
HStack {
|
||||
Text("Start")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
DatePicker("", selection: $post.startDate, displayedComponents: .date)
|
||||
.datePickerStyle(.compact)
|
||||
.padding(.bottom)
|
||||
}
|
||||
BoolPropertyView(
|
||||
title: "Draft",
|
||||
value: $post.isDraft,
|
||||
footer: "Indicate a post as a draft to hide it from the website")
|
||||
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text("Has end date")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Toggle("", isOn: $post.hasEndDate)
|
||||
.toggleStyle(.switch)
|
||||
.padding(.bottom)
|
||||
}
|
||||
DatePropertyView(
|
||||
title: "Start date",
|
||||
value: $post.startDate,
|
||||
footer: "The date when the post content started")
|
||||
|
||||
if post.hasEndDate {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Text("End date")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
DatePicker("", selection: $post.endDate, displayedComponents: .date)
|
||||
.datePickerStyle(.compact)
|
||||
.padding(.bottom)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Linked page")
|
||||
.font(.headline)
|
||||
IconButton(symbol: .squareAndPencilCircleFill,
|
||||
size: 22,
|
||||
color: .blue) {
|
||||
showLinkedPagePicker = true
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
Text(post.linkedPage?.localized(in: language).title ?? "No page linked")
|
||||
OptionalDatePropertyView(
|
||||
title: "End date",
|
||||
isEnabled: $post.hasEndDate,
|
||||
date: $post.endDate,
|
||||
footer: "The date when the post content ended")
|
||||
|
||||
PagePropertyView(
|
||||
title: "Linked page",
|
||||
selectedPage: $post.linkedPage,
|
||||
footer: "The page to open when clicking on the post")
|
||||
|
||||
LocalizedPostDetailView(post: post.localized(in: language))
|
||||
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $showLinkedPagePicker) {
|
||||
PagePickerView(
|
||||
showPagePicker: $showLinkedPagePicker,
|
||||
selectedPage: $post.linkedPage)
|
||||
}
|
||||
}
|
||||
|
||||
private func setNewId() {
|
||||
guard post.update(id: newId) else {
|
||||
newId = post.id
|
||||
return
|
||||
}
|
||||
post.id = newId
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,11 +50,6 @@ struct PostImagesView: View {
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(showImagePicker: $showImagePicker) { image in
|
||||
post.images.append(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shiftLeft(_ image: FileResource) {
|
||||
|
@ -1,60 +0,0 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TextEntrySheet: View {
|
||||
|
||||
let title: String
|
||||
|
||||
@Binding
|
||||
var text: String
|
||||
|
||||
@Binding
|
||||
var isValid: Bool
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(title)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("Text", text: $text)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.overlay {
|
||||
if isValid {
|
||||
EmptyView()
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.strokeBorder(lineWidth: 3)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 300)
|
||||
HStack {
|
||||
Button(action: submit) {
|
||||
Text("Submit")
|
||||
}
|
||||
.disabled(!isValid)
|
||||
Button(role: .cancel, action: cancel) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
private func submit() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private func cancel() {
|
||||
text = ""
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TextEntrySheet(
|
||||
title: "Enter the id for the new post",
|
||||
text: .constant("new"),
|
||||
isValid: .constant(false))
|
||||
}
|
@ -120,9 +120,7 @@ struct PageIssueView: View {
|
||||
didSelect(page: page)
|
||||
}
|
||||
} content: {
|
||||
PagePickerView(
|
||||
showPagePicker: $showPagePicker,
|
||||
selectedPage: $selectedPage)
|
||||
PagePickerView(selectedPage: $selectedPage)
|
||||
}
|
||||
.sheet(isPresented: $showFilePicker) {
|
||||
if let file = selectedFile {
|
||||
|
@ -5,25 +5,18 @@ struct GenerationDetailView: View {
|
||||
let section: SettingsSection
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch section {
|
||||
//case .generation:
|
||||
// GenerationSettingsView()
|
||||
case .folders:
|
||||
PathSettingsView()
|
||||
case .navigationBar:
|
||||
NavigationBarSettingsView()
|
||||
case .postFeed:
|
||||
PostFeedSettingsView()
|
||||
case .pages:
|
||||
PageSettingsDetailView()
|
||||
case .tagOverview:
|
||||
TagOverviewDetailView()
|
||||
}
|
||||
switch section {
|
||||
case .folders:
|
||||
PathSettingsView()
|
||||
case .navigationBar:
|
||||
NavigationBarSettingsView()
|
||||
case .postFeed:
|
||||
PostFeedSettingsView()
|
||||
case .pages:
|
||||
PageSettingsDetailView()
|
||||
case .tagOverview:
|
||||
TagOverviewDetailView()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.padding()
|
||||
.navigationTitle("")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,12 +15,9 @@ struct NavigationBarSettingsView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Navigation Bar")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Customize the navigation bar for all pages at the top of the website")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 30)
|
||||
DetailTitle(
|
||||
title: "Navigation Bar",
|
||||
text: "Customize the navigation bar for all pages at the top of the website")
|
||||
|
||||
HStack {
|
||||
Text("Links")
|
||||
|
@ -16,43 +16,43 @@ struct PageSettingsDetailView: View {
|
||||
text: "Change the way pages are displayed")
|
||||
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.pages.contentWidth,
|
||||
title: "Content Width",
|
||||
value: $content.settings.pages.contentWidth,
|
||||
footer: "The maximum width of the content in pages (in pixels)")
|
||||
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.pages.largeImageWidth,
|
||||
title: "Fullscreen Image Width",
|
||||
value: $content.settings.pages.largeImageWidth,
|
||||
footer: "The maximum width of images that are diplayed fullscreen")
|
||||
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.pages.pageLinkImageSize,
|
||||
title: "Page Link Image Width",
|
||||
value: $content.settings.pages.pageLinkImageSize,
|
||||
footer: "The maximum width of images diplayed as thumbnails on page links")
|
||||
|
||||
FilePropertyView(
|
||||
title: "Default CSS File",
|
||||
description: "The CSS file containing the styling of all pages",
|
||||
footer: "The CSS file containing the styling of all pages",
|
||||
selectedFile: $content.settings.pages.defaultCssFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Code Highlighting File",
|
||||
description: "The JavaScript file to provide syntax highlighting of code blocks",
|
||||
footer: "The JavaScript file to provide syntax highlighting of code blocks",
|
||||
selectedFile: $content.settings.pages.codeHighlightingJsFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Audio Player CSS File",
|
||||
description: "The CSS file to provide the style for the audio player",
|
||||
footer: "The CSS file to provide the style for the audio player",
|
||||
selectedFile: $content.settings.pages.audioPlayerCssFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Audio Player JavaScript File",
|
||||
description: "The CSS file to provide the functionality for the audio player",
|
||||
footer: "The CSS file to provide the functionality for the audio player",
|
||||
selectedFile: $content.settings.pages.audioPlayerJsFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "3D Model Viewer File",
|
||||
description: "The JavaScript file to provide the functionality for the 3D model viewer",
|
||||
footer: "The JavaScript file to provide the functionality for the 3D model viewer",
|
||||
selectedFile: $content.settings.pages.modelViewerJsFile)
|
||||
}
|
||||
}
|
||||
|
@ -11,131 +11,66 @@ struct PathSettingsView: View {
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var folderSelection: SecurityScopeBookmark = .contentPath
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Folder Settings")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Select the folders for the app to work.")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 30)
|
||||
DetailTitle(
|
||||
title: "Folder Settings",
|
||||
text: "Select the folders for the app to work.")
|
||||
|
||||
Text("Content Folder")
|
||||
.font(.headline)
|
||||
.padding(.bottom, 1)
|
||||
Text(contentPath)
|
||||
Button(action: selectContentFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
Text("The folder where the raw content of the website is stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
FolderOnDiskPropertyView(
|
||||
title: "Content Folder",
|
||||
folder: $contentPath,
|
||||
footer: "The folder where the raw content of the website is stored") { url in
|
||||
guard content.storage.save(folderUrl: url, in: .contentPath) else {
|
||||
return
|
||||
}
|
||||
contentPath = url.path()
|
||||
}
|
||||
|
||||
Text("Output Folder")
|
||||
.font(.headline)
|
||||
.padding(.bottom, 1)
|
||||
Text(content.settings.paths.outputDirectoryPath)
|
||||
Button(action: selectOutputFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
Text("The folder where the generated website is stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
FolderOnDiskPropertyView(
|
||||
title: "Output Folder",
|
||||
folder: $content.settings.paths.outputDirectoryPath,
|
||||
footer: "The folder where the generated website is stored") { url in
|
||||
guard content.storage.save(folderUrl: url, in: .outputPath) else {
|
||||
return
|
||||
}
|
||||
content.settings.paths.outputDirectoryPath = url.path()
|
||||
}
|
||||
|
||||
Text("Pages output folder")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.paths.pagesOutputFolderPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The path in the output folder where the generated pages are stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
StringPropertyView(
|
||||
title: "Pages output folder",
|
||||
text: $content.settings.paths.pagesOutputFolderPath,
|
||||
footer: "The path in the output folder where the generated pages are stored")
|
||||
|
||||
Text("Tags output folder")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.paths.tagsOutputFolderPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The path in the output folder where the generated tag pages are stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
StringPropertyView(
|
||||
title: "Tags output folder",
|
||||
text: $content.settings.paths.tagsOutputFolderPath,
|
||||
footer: "The path in the output folder where the generated tag pages are stored")
|
||||
|
||||
Text("Files output folder")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.paths.filesOutputFolderPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The path in the output folder where the copied files are stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
StringPropertyView(
|
||||
title: "Files output folder",
|
||||
text: $content.settings.paths.filesOutputFolderPath,
|
||||
footer: "The path in the output folder where the copied files are stored")
|
||||
|
||||
Text("Images output folder")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.paths.imagesOutputFolderPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The path in the output folder where the generated images are stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
StringPropertyView(
|
||||
title: "Images output folder",
|
||||
text: $content.settings.paths.imagesOutputFolderPath,
|
||||
footer: "The path in the output folder where the generated images are stored")
|
||||
|
||||
Text("Videos output folder")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.paths.videosOutputFolderPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The path in the output folder where the generated videos are stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
StringPropertyView(
|
||||
title: "Videos output folder",
|
||||
text: $content.settings.paths.videosOutputFolderPath,
|
||||
footer: "The path in the output folder where the generated videos are stored")
|
||||
|
||||
Text("Assets output folder")
|
||||
.font(.headline)
|
||||
TextField("", text: $content.settings.paths.assetsOutputFolderPath)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Text("The path in the output folder where assets are stored")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom)
|
||||
StringPropertyView(
|
||||
title: "Assets output folder",
|
||||
text: $content.settings.paths.assetsOutputFolderPath,
|
||||
footer: "The path in the output folder where assets are stored")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Folder selection
|
||||
|
||||
private func selectContentFolder() {
|
||||
folderSelection = .contentPath
|
||||
guard let url = savePanelUsingOpenPanel() else {
|
||||
return
|
||||
}
|
||||
self.contentPath = url.path()
|
||||
}
|
||||
|
||||
private func selectOutputFolder() {
|
||||
folderSelection = .outputPath
|
||||
guard let url = savePanelUsingOpenPanel() else {
|
||||
return
|
||||
}
|
||||
content.settings.paths.outputDirectoryPath = url.path()
|
||||
}
|
||||
|
||||
private func savePanelUsingOpenPanel() -> URL? {
|
||||
let panel = NSOpenPanel()
|
||||
// Sets up so user can only select a single directory
|
||||
panel.canChooseFiles = false
|
||||
panel.canChooseDirectories = true
|
||||
panel.allowsMultipleSelection = false
|
||||
panel.showsHiddenFiles = false
|
||||
panel.title = "Select Save Directory"
|
||||
panel.prompt = "Select Save Directory"
|
||||
|
||||
let response = panel.runModal()
|
||||
guard response == .OK else {
|
||||
|
||||
return nil
|
||||
}
|
||||
guard let url = panel.url else {
|
||||
return nil
|
||||
}
|
||||
content.storage.save(folderUrl: url, in: folderSelection)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
@ -15,28 +15,28 @@ struct PostFeedSettingsView: View {
|
||||
text: "Change the way the posts are displayed")
|
||||
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.posts.contentWidth,
|
||||
title: "Content Width",
|
||||
value: $content.settings.posts.contentWidth,
|
||||
footer: "The maximum width of the content the post feed (in pixels)")
|
||||
|
||||
IntegerPropertyView(
|
||||
value: $content.settings.posts.postsPerPage,
|
||||
title: "Posts Per Page",
|
||||
value: $content.settings.posts.postsPerPage,
|
||||
footer: "The maximum number of posts displayed on a single page")
|
||||
|
||||
FilePropertyView(
|
||||
title: "Default CSS File",
|
||||
description: "The CSS file containing the styling of all post pages",
|
||||
footer: "The CSS file containing the styling of all post pages",
|
||||
selectedFile: $content.settings.posts.defaultCssFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Swiper CSS File",
|
||||
description: "The CSS file containing the styling of image galleries in post feeds",
|
||||
footer: "The CSS file containing the styling of image galleries in post feeds",
|
||||
selectedFile: $content.settings.posts.swiperCssFile)
|
||||
|
||||
FilePropertyView(
|
||||
title: "Swiper JavaScript File",
|
||||
description: "The JavaScript file to load the image gallery code in post feeds",
|
||||
footer: "The JavaScript file to load the image gallery code in post feeds",
|
||||
selectedFile: $content.settings.posts.swiperJsFile)
|
||||
|
||||
LocalizedPostFeedSettingsView(
|
||||
|
@ -11,12 +11,9 @@ struct TagOverviewDetailView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Tag Overview")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
Text("Configure the page showing all tags")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 30)
|
||||
DetailTitle(
|
||||
title: "Tag Overview",
|
||||
text: "Configure the page showing all tags")
|
||||
|
||||
if let page = content.tagOverview?.localized(in: language) {
|
||||
TagOverviewDetails(page: page)
|
||||
@ -30,101 +27,48 @@ struct TagOverviewDetailView: View {
|
||||
private func createTagOverviewPage() {
|
||||
content.tagOverview = TagOverviewPage(
|
||||
content: content,
|
||||
german: .init(title: "Alle Tags", urlString: "alle"),
|
||||
english: .init(title: "All tags", urlString: "all"))
|
||||
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
|
||||
|
||||
@EnvironmentObject
|
||||
var content: Content
|
||||
|
||||
@State
|
||||
private var showImagePicker = false
|
||||
|
||||
@State
|
||||
private var newUrlString: String = ""
|
||||
|
||||
init(page: LocalizedTagOverviewPage) {
|
||||
self.page = page
|
||||
}
|
||||
|
||||
private var newUrlCanBeUpdated: Bool {
|
||||
guard !newUrlString.isEmpty else { return false }
|
||||
guard content.isValidIdForTagOrPageOrPost(newUrlString) else { return false }
|
||||
return !content.containsTag(withUrlComponent: newUrlString)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Title")
|
||||
.font(.headline)
|
||||
TextField("", text: $page.title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
StringPropertyView(
|
||||
title: "Title",
|
||||
text: $page.title,
|
||||
footer: "The title of the overview page")
|
||||
|
||||
HStack {
|
||||
Text("Page URL String")
|
||||
.font(.headline)
|
||||
TextField("", text: $newUrlString)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
Button("Update", action: setNewId)
|
||||
.disabled(!newUrlCanBeUpdated)
|
||||
}
|
||||
.padding(.bottom)
|
||||
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 })
|
||||
|
||||
Text("Link Preview Title")
|
||||
.font(.headline)
|
||||
OptionalTextField("", text: $page.linkPreviewTitle,
|
||||
prompt: page.title)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
OptionalStringPropertyView(
|
||||
title: "Preview Title",
|
||||
text: $page.linkPreviewTitle,
|
||||
prompt: page.title,
|
||||
footer: "The title to use for the page when linking to it")
|
||||
|
||||
HStack {
|
||||
Text("Link Preview Image")
|
||||
.font(.headline)
|
||||
IconButton(symbol: .squareAndPencilCircleFill,
|
||||
size: 22,
|
||||
color: .blue) {
|
||||
showImagePicker = true
|
||||
}
|
||||
OptionalImagePropertyView(
|
||||
title: "Preview Image",
|
||||
selectedImage: $page.linkPreviewImage,
|
||||
footer: "The image to show for previews of this page")
|
||||
|
||||
IconButton(symbol: .trashCircleFill,
|
||||
size: 22,
|
||||
color: .red) {
|
||||
page.linkPreviewImage = nil
|
||||
}.disabled(page.linkPreviewImage == nil)
|
||||
}
|
||||
|
||||
.buttonStyle(.plain)
|
||||
if let image = page.linkPreviewImage {
|
||||
image.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400, maxHeight: 300)
|
||||
.cornerRadius(8)
|
||||
Text(image.id)
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
Text("Link Preview Description")
|
||||
.font(.headline)
|
||||
.padding(.top)
|
||||
OptionalDescriptionField(text: $page.linkPreviewDescription)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
OptionalTextFieldPropertyView(
|
||||
title: "Preview Description",
|
||||
text: $page.linkPreviewDescription,
|
||||
footer: "The description to show in previews of the page")
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(showImagePicker: $showImagePicker) { image in
|
||||
page.linkPreviewImage = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setNewId() {
|
||||
page.urlString = newUrlString
|
||||
}
|
||||
}
|
||||
|
@ -28,8 +28,8 @@ struct AddTagView: View {
|
||||
content: content,
|
||||
id: "tag",
|
||||
isVisible: true,
|
||||
german: .init(urlComponent: "tag", name: "Neuer Tag"),
|
||||
english: .init(urlComponent: "tag-en", name: "New Tag"))
|
||||
german: .init(content: .mock, urlComponent: "tag", name: "Neuer Tag"),
|
||||
english: .init(content: .mock, urlComponent: "tag-en", name: "New Tag"))
|
||||
// Add to top of the list, and resort when changing the name
|
||||
content.tags.insert(newTag, at: 0)
|
||||
dismiss()
|
||||
|
@ -1,96 +1,55 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LocalizedTagDetailView: View {
|
||||
|
||||
@Binding
|
||||
var tagIsVisible: Bool
|
||||
|
||||
|
||||
@ObservedObject
|
||||
var tag: LocalizedTag
|
||||
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
|
||||
@State
|
||||
private var showImagePicker = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle("Appears in overviews", isOn: $tagIsVisible)
|
||||
.toggleStyle(.switch)
|
||||
.font(.headline)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Name")
|
||||
.font(.headline)
|
||||
TextField("", text: $tag.name)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("URL String")
|
||||
.font(.headline)
|
||||
TextField("", text: $tag.urlComponent)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Original url")
|
||||
.font(.headline)
|
||||
Text(tag.originalUrl ?? "-")
|
||||
.padding(.top, 1)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Subtitle")
|
||||
.font(.headline)
|
||||
OptionalTextField("", text: $tag.subtitle)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Link Preview Description")
|
||||
.font(.headline)
|
||||
.padding(.top)
|
||||
OptionalDescriptionField(text: $tag.description)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.bottom)
|
||||
|
||||
HStack {
|
||||
Text("Link Preview Image")
|
||||
.font(.headline)
|
||||
IconButton(symbol: .squareAndPencilCircleFill,
|
||||
size: 22,
|
||||
color: .blue) {
|
||||
showImagePicker = true
|
||||
}
|
||||
|
||||
IconButton(symbol: .trashCircleFill,
|
||||
size: 22,
|
||||
color: .red) {
|
||||
tag.linkPreviewImage = nil
|
||||
}.disabled(tag.linkPreviewImage == nil)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
.buttonStyle(.plain)
|
||||
if let image = tag.linkPreviewImage {
|
||||
image.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 400, maxHeight: 300)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(showImagePicker: $showImagePicker) { image in
|
||||
tag.linkPreviewImage = image
|
||||
}
|
||||
VStack(alignment: .leading) {
|
||||
StringPropertyView(
|
||||
title: "Name",
|
||||
text: $tag.name,
|
||||
footer: "The displayed name of the tag")
|
||||
|
||||
IdPropertyView(
|
||||
id: $tag.urlComponent,
|
||||
title: "Page URL String",
|
||||
footer: "The url component to use in the url for this tag",
|
||||
validation: tag.isValid,
|
||||
update: { tag.urlComponent = $0 })
|
||||
|
||||
Text("Original url")
|
||||
.font(.headline)
|
||||
Text(tag.originalUrl ?? "-")
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.top, 1)
|
||||
.padding(.bottom)
|
||||
|
||||
OptionalStringPropertyView(
|
||||
title: "Subtitle",
|
||||
text: $tag.subtitle,
|
||||
footer: "The subtitle/tagline to use")
|
||||
|
||||
OptionalImagePropertyView(
|
||||
title: "Preview Image",
|
||||
selectedImage: $tag.linkPreviewImage,
|
||||
footer: "The image to show for previews of this page")
|
||||
|
||||
OptionalTextFieldPropertyView(
|
||||
title: "Preview Description",
|
||||
text: $tag.description,
|
||||
footer: "The description to show in previews of the page")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LocalizedTagDetailView(
|
||||
tagIsVisible: .constant(true),
|
||||
tag: Tag.mock.english)
|
||||
LocalizedTagDetailView(tag: Tag.mock.english)
|
||||
}
|
||||
|
@ -10,9 +10,22 @@ struct TagDetailView: View {
|
||||
var tag: Tag
|
||||
|
||||
var body: some View {
|
||||
LocalizedTagDetailView(
|
||||
tagIsVisible: $tag.isVisible,
|
||||
tag: tag.localized(in: language))
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
DetailTitle(
|
||||
title: "Tag",
|
||||
text: "A tag groups posts and pages together based on a common theme.")
|
||||
|
||||
BoolPropertyView(
|
||||
title: "Appears in overviews",
|
||||
value: $tag.isVisible,
|
||||
footer: "Indicate if the tag should appear in the tag list of posts and pages. If the tag is not visible, then it can still be used as a filter.")
|
||||
|
||||
LocalizedTagDetailView(
|
||||
tag: tag.localized(in: language))
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user