Unified detail views, model

This commit is contained in:
Christoph Hagen 2024-12-16 09:54:21 +01:00
parent 1e67a99866
commit 31d1ecb8bd
57 changed files with 853 additions and 954 deletions

View File

@ -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 */,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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