Improve tag and images view, save website settings
This commit is contained in:
parent
1261ea534b
commit
4440b2ce0d
@ -27,6 +27,10 @@
|
||||
E21850312CFAF8880090B18B /* Content+Import.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850302CFAF8840090B18B /* Content+Import.swift */; };
|
||||
E21850332CFAFA2F0090B18B /* WebsiteData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850322CFAFA200090B18B /* WebsiteData.swift */; };
|
||||
E21850352CFAFA5A0090B18B /* WebsiteDataFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850342CFAFA570090B18B /* WebsiteDataFile.swift */; };
|
||||
E21850372CFCA55F0090B18B /* LocalizedWebsiteData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedWebsiteData.swift */; };
|
||||
E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */; };
|
||||
E218503B2CFCFBE70090B18B /* WebsiteData+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503A2CFCFBDE0090B18B /* WebsiteData+Storage.swift */; };
|
||||
E218503D2CFCFD910090B18B /* LocalizedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503C2CFCFD8C0090B18B /* LocalizedSettingsView.swift */; };
|
||||
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
||||
E24252032C5163CF0029FF16 /* Importer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252022C5163CF0029FF16 /* Importer.swift */; };
|
||||
E24252062C51684E0029FF16 /* GenericMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252052C51684E0029FF16 /* GenericMetadata.swift */; };
|
||||
@ -34,6 +38,10 @@
|
||||
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; };
|
||||
E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.swift */; };
|
||||
E2581DF12C7523F400F1F079 /* ImportableTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DF02C7523F400F1F079 /* ImportableTag.swift */; };
|
||||
E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5082CFD964E00AEF16D /* TagContentView.swift */; };
|
||||
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */; };
|
||||
E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */; };
|
||||
E25DA50F2CFDD76B00AEF16D /* ImagesContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA50E2CFDD76B00AEF16D /* ImagesContentView.swift */; };
|
||||
E2A21C012CB16A820060935B /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C002CB16A820060935B /* PostView.swift */; };
|
||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C022CB16C220060935B /* Environment+Language.swift */; };
|
||||
E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A21C042CB176670060935B /* LocalizedText.swift */; };
|
||||
@ -107,6 +115,10 @@
|
||||
E21850302CFAF8840090B18B /* Content+Import.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Import.swift"; sourceTree = "<group>"; };
|
||||
E21850322CFAFA200090B18B /* WebsiteData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteData.swift; sourceTree = "<group>"; };
|
||||
E21850342CFAFA570090B18B /* WebsiteDataFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteDataFile.swift; sourceTree = "<group>"; };
|
||||
E21850362CFCA5580090B18B /* LocalizedWebsiteData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedWebsiteData.swift; sourceTree = "<group>"; };
|
||||
E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Mock.swift"; sourceTree = "<group>"; };
|
||||
E218503A2CFCFBDE0090B18B /* WebsiteData+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebsiteData+Storage.swift"; sourceTree = "<group>"; };
|
||||
E218503C2CFCFD8C0090B18B /* LocalizedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedSettingsView.swift; sourceTree = "<group>"; };
|
||||
E24252022C5163CF0029FF16 /* Importer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Importer.swift; sourceTree = "<group>"; };
|
||||
E24252052C51684E0029FF16 /* GenericMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericMetadata.swift; sourceTree = "<group>"; };
|
||||
E24252072C5168750029FF16 /* GenericMetadata+Localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GenericMetadata+Localized.swift"; sourceTree = "<group>"; };
|
||||
@ -114,6 +126,10 @@
|
||||
E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
E2581DF02C7523F400F1F079 /* ImportableTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportableTag.swift; sourceTree = "<group>"; };
|
||||
E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = "<group>"; };
|
||||
E25DA5082CFD964E00AEF16D /* TagContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagContentView.swift; sourceTree = "<group>"; };
|
||||
E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTagAssignmentView.swift; sourceTree = "<group>"; };
|
||||
E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTagAssignmentView.swift; sourceTree = "<group>"; };
|
||||
E25DA50E2CFDD76B00AEF16D /* ImagesContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesContentView.swift; sourceTree = "<group>"; };
|
||||
E2A21C002CB16A820060935B /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
||||
E2A21C022CB16C220060935B /* Environment+Language.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+Language.swift"; sourceTree = "<group>"; };
|
||||
E2A21C042CB176670060935B /* LocalizedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedText.swift; sourceTree = "<group>"; };
|
||||
@ -202,6 +218,7 @@
|
||||
E2A21C342CB9A3CA0060935B /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E218503C2CFCFD8C0090B18B /* LocalizedSettingsView.swift */,
|
||||
E2A21C352CB9A3D70060935B /* SettingsView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
@ -222,6 +239,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2A21C4C2CBB16B50060935B /* ImagesView.swift */,
|
||||
E25DA50E2CFDD76B00AEF16D /* ImagesContentView.swift */,
|
||||
E2A21C4E2CBB29E50060935B /* ImageDetailsView.swift */,
|
||||
E2A21C552CBBF9880060935B /* FlexibleColumnView.swift */,
|
||||
);
|
||||
@ -254,8 +272,11 @@
|
||||
E2A9CB7F2C7E686C005C89CC /* Tags */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E25DA50C2CFD9B9C00AEF16D /* PostTagAssignmentView.swift */,
|
||||
E2A37D282CED2C6A0000979F /* TagsListView.swift */,
|
||||
E2A37D2A2CED2CC30000979F /* TagDetailView.swift */,
|
||||
E25DA5082CFD964E00AEF16D /* TagContentView.swift */,
|
||||
E25DA50A2CFD988100AEF16D /* PageTagAssignmentView.swift */,
|
||||
);
|
||||
path = Tags;
|
||||
sourceTree = "<group>";
|
||||
@ -264,6 +285,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E21850322CFAFA200090B18B /* WebsiteData.swift */,
|
||||
E218503A2CFCFBDE0090B18B /* WebsiteData+Storage.swift */,
|
||||
E21850362CFCA5580090B18B /* LocalizedWebsiteData.swift */,
|
||||
E2E06DFA2CA4A6570019C2AF /* Content.swift */,
|
||||
E218502E2CFAF6990090B18B /* Content+Generate.swift */,
|
||||
E21850302CFAF8840090B18B /* Content+Import.swift */,
|
||||
@ -388,6 +411,7 @@
|
||||
E2DD047C2C276F32003BFF1F /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E21850382CFCA6BA0090B18B /* WebsiteData+Mock.swift */,
|
||||
E218500A2CEE02FA0090B18B /* Content+Mock.swift */,
|
||||
E2A37D1A2CEA45530000979F /* Tag+Mock.swift */,
|
||||
E2A21C1F2CB28ED20060935B /* MockImage.swift */,
|
||||
@ -508,6 +532,7 @@
|
||||
E2A37D152CE68BEC0000979F /* PostFile.swift in Sources */,
|
||||
E218501B2CEE59EC0090B18B /* Tag+Storage.swift in Sources */,
|
||||
E2A21C032CB16C290060935B /* Environment+Language.swift in Sources */,
|
||||
E21850392CFCA6C00090B18B /* WebsiteData+Mock.swift in Sources */,
|
||||
E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */,
|
||||
E2A37D172CE73F1A0000979F /* TagFile.swift in Sources */,
|
||||
E2A37D292CED2C6A0000979F /* TagsListView.swift in Sources */,
|
||||
@ -522,8 +547,10 @@
|
||||
E2A21C4D2CBB16B50060935B /* ImagesView.swift in Sources */,
|
||||
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
|
||||
E2A21C2C2CB2BB250060935B /* PostList.swift in Sources */,
|
||||
E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */,
|
||||
E21850112CEE17070090B18B /* Page+Storage.swift in Sources */,
|
||||
E218500D2CEE07180090B18B /* ColorPalette.swift in Sources */,
|
||||
E218503B2CFCFBE70090B18B /* WebsiteData+Storage.swift in Sources */,
|
||||
E21850352CFAFA5A0090B18B /* WebsiteDataFile.swift in Sources */,
|
||||
E21850132CEE541D0090B18B /* Post+Storage.swift in Sources */,
|
||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
||||
@ -532,10 +559,13 @@
|
||||
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */,
|
||||
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */,
|
||||
E2A21C3B2CB9D9A60060935B /* ImageResource.swift in Sources */,
|
||||
E218503D2CFCFD910090B18B /* LocalizedSettingsView.swift in Sources */,
|
||||
E2A21C302CB490F90060935B /* HorizontalCenter.swift in Sources */,
|
||||
E21850372CFCA55F0090B18B /* LocalizedWebsiteData.swift in Sources */,
|
||||
E2DD04742C276F31003BFF1F /* CHDataManagementApp.swift in Sources */,
|
||||
E218501F2CEE6DAC0090B18B /* ImagePickerView.swift in Sources */,
|
||||
E2A37D1B2CEA45560000979F /* Tag+Mock.swift in Sources */,
|
||||
E25DA50F2CFDD76B00AEF16D /* ImagesContentView.swift in Sources */,
|
||||
E2A21C482CBAF88B0060935B /* String+Extensions.swift in Sources */,
|
||||
E2A37D1F2CEA94370000979F /* Optional+Extensions.swift in Sources */,
|
||||
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
|
||||
@ -545,7 +575,9 @@
|
||||
E218502D2CF791440090B18B /* PostImagesView.swift in Sources */,
|
||||
E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */,
|
||||
E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */,
|
||||
E25DA5092CFD964E00AEF16D /* TagContentView.swift in Sources */,
|
||||
E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */,
|
||||
E25DA50D2CFD9BA200AEF16D /* PostTagAssignmentView.swift in Sources */,
|
||||
E2A21C362CB9A3D70060935B /* SettingsView.swift in Sources */,
|
||||
E2A21C012CB16A820060935B /* PostView.swift in Sources */,
|
||||
E2A21C052CB1766C0060935B /* LocalizedText.swift in Sources */,
|
||||
|
@ -4,26 +4,19 @@ extension Content {
|
||||
|
||||
func generateFeed(for language: ContentLanguage, bookmarkKey: String) {
|
||||
let posts = posts.map { $0.feedEntry(for: language) }
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let data = websiteData.localized(in: language)
|
||||
let navigationItems: [FeedNavigationLink] = websiteData.navigationTags.map {
|
||||
let localized = $0.localized(in: language)
|
||||
return .init(text: localized.name, url: localized.urlComponent)
|
||||
}
|
||||
|
||||
let navigationItems: [FeedNavigationLink] = [
|
||||
.init(text: .init(en: "Projects", de: "Projekte"),
|
||||
url: .init(en: "/projects", de: "/projekte")),
|
||||
.init(text: .init(en: "Adventures", de: "Abenteuer"),
|
||||
url: .init(en: "/adventures", de: "/abenteuer")),
|
||||
.init(text: .init(en: "Services", de: "Dienste"),
|
||||
url: .init(en: "/services", de: "/dienste")),
|
||||
.init(text: .init(en: "Tags", de: "Kategorien"),
|
||||
url: .init(en: "/tags", de: "/kategorien")),
|
||||
]
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
|
||||
let feed = Feed(
|
||||
language: language,
|
||||
title: .init(en: "Blog | CH", de: "Blog | CH"),
|
||||
description: .init(en: "The latests posts, projects and adventures",
|
||||
de: "Die neusten Beiträge, Projekte und Abenteuer"),
|
||||
iconDescription: .init(en: "An icon consisting of the letters C and H in blue and orange",
|
||||
de: "Ein Logo aus den Buchstaben C und H in Blau und Orange"),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
iconDescription: data.iconDescription,
|
||||
navigationItems: navigationItems,
|
||||
posts: posts)
|
||||
let fileContent = feed.content
|
||||
|
@ -5,19 +5,22 @@ import Combine
|
||||
final class Content: ObservableObject {
|
||||
|
||||
@Published
|
||||
var posts: [Post] = []
|
||||
var websiteData: WebsiteData
|
||||
|
||||
@Published
|
||||
var pages: [Page] = []
|
||||
var posts: [Post]
|
||||
|
||||
@Published
|
||||
var tags: [Tag] = []
|
||||
var pages: [Page]
|
||||
|
||||
@Published
|
||||
var images: [ImageResource] = []
|
||||
var tags: [Tag]
|
||||
|
||||
@Published
|
||||
var files: [FileResource] = []
|
||||
var images: [ImageResource]
|
||||
|
||||
@Published
|
||||
var files: [FileResource]
|
||||
|
||||
@AppStorage("contentPath")
|
||||
private var storedContentPath: String = ""
|
||||
@ -33,12 +36,14 @@ final class Content: ObservableObject {
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(posts: [Post] = [],
|
||||
pages: [Page] = [],
|
||||
tags: [Tag] = [],
|
||||
images: [ImageResource] = [],
|
||||
files: [FileResource] = [],
|
||||
init(websiteData: WebsiteData,
|
||||
posts: [Post],
|
||||
pages: [Page],
|
||||
tags: [Tag],
|
||||
images: [ImageResource],
|
||||
files: [FileResource],
|
||||
storedContentPath: String) {
|
||||
self.websiteData = websiteData
|
||||
self.posts = posts
|
||||
self.pages = pages
|
||||
self.tags = tags
|
||||
@ -59,6 +64,13 @@ final class Content: ObservableObject {
|
||||
init() {
|
||||
self.storage = Storage(baseFolder: URL(filePath: ""))
|
||||
|
||||
self.websiteData = .mock
|
||||
self.posts = []
|
||||
self.pages = []
|
||||
self.tags = []
|
||||
self.images = []
|
||||
self.files = []
|
||||
|
||||
contentPath = storedContentPath
|
||||
do {
|
||||
try storage.createFolderStructure()
|
||||
@ -114,9 +126,17 @@ final class Content: ObservableObject {
|
||||
linkPreviewDescription: page.linkPreviewDescription)
|
||||
}
|
||||
|
||||
private func convert(_ websiteData: LocalizedWebsiteDataFile) -> LocalizedWebsiteData {
|
||||
.init(title: websiteData.title,
|
||||
description: websiteData.description,
|
||||
iconDescription: websiteData.iconDescription)
|
||||
}
|
||||
|
||||
func loadFromDisk() throws {
|
||||
let storage = Storage(baseFolder: URL(filePath: contentPath))
|
||||
|
||||
let websiteData = try storage.loadWebsiteData()
|
||||
|
||||
let tagData = try storage.loadAllTags()
|
||||
let pagesData = try storage.loadAllPages()
|
||||
let postsData = try storage.loadAllPosts()
|
||||
@ -170,6 +190,10 @@ final class Content: ObservableObject {
|
||||
self.files = files.sorted { $0.uniqueId }
|
||||
self.images = images.values.sorted { $0.id }
|
||||
self.posts = posts.sorted(ascending: false) { $0.startDate }
|
||||
self.websiteData = WebsiteData(
|
||||
navigationTags: websiteData.navigationTags.map { tags[$0]! },
|
||||
german: convert(websiteData.german),
|
||||
english: convert(websiteData.english))
|
||||
}
|
||||
|
||||
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag]) -> [String : Page] {
|
||||
@ -202,6 +226,8 @@ final class Content: ObservableObject {
|
||||
for tag in tags {
|
||||
storage.save(tagMetadata: tag.tagFile, for: tag.id)
|
||||
}
|
||||
storage.save(websiteData: websiteData.dataFile)
|
||||
|
||||
// TODO: Remove all files that are no longer in use (they belong to deleted items)
|
||||
//print("Finished save")
|
||||
}
|
||||
|
@ -80,7 +80,11 @@ extension ImageResource {
|
||||
print("Failed to create image from \(url.path)")
|
||||
return failureImage
|
||||
}
|
||||
self.size = loadedImage.size
|
||||
if self.size == .zero && loadedImage.size != .zero {
|
||||
DispatchQueue.main.async {
|
||||
self.size = loadedImage.size
|
||||
}
|
||||
}
|
||||
return .init(nsImage: loadedImage)
|
||||
}
|
||||
|
||||
|
19
CHDataManagement/Model/LocalizedWebsiteData.swift
Normal file
19
CHDataManagement/Model/LocalizedWebsiteData.swift
Normal file
@ -0,0 +1,19 @@
|
||||
import Foundation
|
||||
|
||||
final class LocalizedWebsiteData: ObservableObject {
|
||||
|
||||
@Published
|
||||
var title: String
|
||||
|
||||
@Published
|
||||
var description: String
|
||||
|
||||
@Published
|
||||
var iconDescription: String
|
||||
|
||||
init(title: String, description: String, iconDescription: String) {
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.iconDescription = iconDescription
|
||||
}
|
||||
}
|
20
CHDataManagement/Model/WebsiteData+Storage.swift
Normal file
20
CHDataManagement/Model/WebsiteData+Storage.swift
Normal file
@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
|
||||
extension WebsiteData {
|
||||
|
||||
var dataFile: WebsiteDataFile {
|
||||
.init(
|
||||
navigationTags: navigationTags.map { $0.id },
|
||||
german: german.dataFile,
|
||||
english: english.dataFile)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedWebsiteData {
|
||||
|
||||
var dataFile: LocalizedWebsiteDataFile {
|
||||
.init(title: title,
|
||||
description: description,
|
||||
iconDescription: iconDescription)
|
||||
}
|
||||
}
|
@ -1,5 +1,26 @@
|
||||
import Foundation
|
||||
|
||||
final class WebsiteData: ObservableObject {
|
||||
|
||||
|
||||
@Published
|
||||
var navigationTags: [Tag]
|
||||
|
||||
@Published
|
||||
var german: LocalizedWebsiteData
|
||||
|
||||
@Published
|
||||
var english: LocalizedWebsiteData
|
||||
|
||||
init(navigationTags: [Tag] = [], german: LocalizedWebsiteData, english: LocalizedWebsiteData) {
|
||||
self.navigationTags = navigationTags
|
||||
self.german = german
|
||||
self.english = english
|
||||
}
|
||||
|
||||
func localized(in language: ContentLanguage) -> LocalizedWebsiteData {
|
||||
switch language {
|
||||
case .english: return english
|
||||
case .german: return german
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ import Foundation
|
||||
|
||||
struct FeedNavigationLink {
|
||||
|
||||
let text: LocalizedText
|
||||
let text: String
|
||||
|
||||
let url: LocalizedText
|
||||
let url: String
|
||||
}
|
||||
|
||||
struct Feed {
|
||||
@ -13,11 +13,11 @@ struct Feed {
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let title: LocalizedText
|
||||
let title: String
|
||||
|
||||
let description: LocalizedText
|
||||
let description: String
|
||||
|
||||
let iconDescription: LocalizedText
|
||||
let iconDescription: String
|
||||
|
||||
let navigationItems: [FeedNavigationLink]
|
||||
|
||||
@ -28,8 +28,8 @@ struct Feed {
|
||||
var result = ""
|
||||
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
|
||||
let head = PageHead(
|
||||
title: title.getText(for: language),
|
||||
description: description.getText(for: language))
|
||||
title: title,
|
||||
description: description)
|
||||
result += head.content
|
||||
result += "<body>"
|
||||
addNavbar(to: &result)
|
||||
@ -52,14 +52,14 @@ struct Feed {
|
||||
let rightNavigationItems = navigationItems[middleIndex...]
|
||||
|
||||
for item in leftNavigationItems {
|
||||
result += "<a class=\"nav-animate\" href=\"\(item.url.getText(for: language))\">\(item.text.getText(for: language))</a>"
|
||||
result += "<a class=\"nav-animate\" href=\"\(item.url)\">\(item.text)</a>"
|
||||
}
|
||||
|
||||
result += "<a id=\"nav-image\" href=\"/\">"
|
||||
result += "<img class=\"navbar-icon\" src=\"\(navigationIconPath)\" alt=\"\(iconDescription.getText(for: language))\">"
|
||||
result += "<img class=\"navbar-icon\" src=\"\(navigationIconPath)\" alt=\"\(iconDescription)\">"
|
||||
|
||||
for item in rightNavigationItems {
|
||||
result += "<a class=\"nav-animate\" href=\"\(item.url.getText(for: language))\">\(item.text.getText(for: language))</a>"
|
||||
result += "<a class=\"nav-animate\" href=\"\(item.url)\">\(item.text)</a>"
|
||||
}
|
||||
result += "</div></nav>" // Close nav-center, navbar
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ extension Content {
|
||||
private static let dbPath = FileManager.default.documentDirectory.appendingPathComponent("db").path()
|
||||
|
||||
static let mock: Content = Content(
|
||||
websiteData: .mock,
|
||||
posts: [.empty, .mock, .fullMock],
|
||||
pages: [.empty],
|
||||
tags: [.hiking, .mountains, .nature, .sports],
|
||||
|
25
CHDataManagement/Preview Content/WebsiteData+Mock.swift
Normal file
25
CHDataManagement/Preview Content/WebsiteData+Mock.swift
Normal file
@ -0,0 +1,25 @@
|
||||
import Foundation
|
||||
|
||||
extension WebsiteData {
|
||||
|
||||
static let mock: WebsiteData = .init(
|
||||
german: .german,
|
||||
english: .english)
|
||||
}
|
||||
|
||||
extension LocalizedWebsiteData {
|
||||
|
||||
static var german: LocalizedWebsiteData {
|
||||
.init(
|
||||
title: "Titel",
|
||||
description: "Beschreibung",
|
||||
iconDescription: "Icon")
|
||||
}
|
||||
|
||||
static var english: LocalizedWebsiteData {
|
||||
.init(
|
||||
title: "A Title",
|
||||
description: "Description",
|
||||
iconDescription: "Icon")
|
||||
}
|
||||
}
|
@ -242,8 +242,26 @@ final class Storage {
|
||||
try fileNames(in: videosFolder)
|
||||
}
|
||||
|
||||
// MARK: Website data
|
||||
|
||||
private var websiteDataUrl: URL {
|
||||
baseFolder.appending(path: "website-data.json", directoryHint: .notDirectory)
|
||||
}
|
||||
|
||||
func loadWebsiteData() throws -> WebsiteDataFile {
|
||||
try read(at: websiteDataUrl)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func save(websiteData: WebsiteDataFile) -> Bool {
|
||||
write(websiteData, type: "Website Data", id: "-", to: websiteDataUrl)
|
||||
}
|
||||
|
||||
// MARK: Writing files
|
||||
|
||||
/**
|
||||
Encode a value and write it to a file, if the content changed
|
||||
*/
|
||||
private func write<T>(_ value: T, type: String, id: String, to file: URL) -> Bool where T: Encodable {
|
||||
let content: Data
|
||||
do {
|
||||
@ -255,6 +273,9 @@ final class Storage {
|
||||
return write(data: content, type: type, id: id, to: file)
|
||||
}
|
||||
|
||||
/**
|
||||
Write data to a file if the content changed
|
||||
*/
|
||||
private func write(data: Data, type: String, id: String, to file: URL) -> Bool {
|
||||
if fm.fileExists(atPath: file.path()) {
|
||||
// Check if content is the same, to prevent unnecessary writes
|
||||
|
@ -2,6 +2,8 @@ import Foundation
|
||||
|
||||
struct WebsiteDataFile {
|
||||
|
||||
let navigationTags: [String]
|
||||
|
||||
let german: LocalizedWebsiteDataFile
|
||||
|
||||
let english: LocalizedWebsiteDataFile
|
||||
@ -24,26 +26,3 @@ struct LocalizedWebsiteDataFile {
|
||||
extension LocalizedWebsiteDataFile: Codable {
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
let navigationItems: [FeedNavigationLink] = [
|
||||
.init(text: .init(en: "Projects", de: "Projekte"),
|
||||
url: .init(en: "/projects", de: "/projekte")),
|
||||
.init(text: .init(en: "Adventures", de: "Abenteuer"),
|
||||
url: .init(en: "/adventures", de: "/abenteuer")),
|
||||
.init(text: .init(en: "Services", de: "Dienste"),
|
||||
url: .init(en: "/services", de: "/dienste")),
|
||||
.init(text: .init(en: "Tags", de: "Kategorien"),
|
||||
url: .init(en: "/tags", de: "/kategorien")),
|
||||
]
|
||||
|
||||
let feed = Feed(
|
||||
language: language,
|
||||
title: .init(en: "Blog | CH", de: "Blog | CH"),
|
||||
description: .init(en: "The latests posts, projects and adventures",
|
||||
de: "Die neusten Beiträge, Projekte und Abenteuer"),
|
||||
iconDescription: .init(en: "An icon consisting of the letters C and H in blue and orange",
|
||||
de: "Ein Logo aus den Buchstaben C und H in Blau und Orange"),
|
||||
navigationItems: navigationItems,
|
||||
posts: posts)
|
||||
*/
|
||||
|
@ -5,7 +5,8 @@ struct ImageDetailsView: View {
|
||||
@Environment(\.language)
|
||||
var language
|
||||
|
||||
let image: ImageResource
|
||||
@ObservedObject
|
||||
var image: ImageResource
|
||||
|
||||
@State
|
||||
private var newId: String
|
||||
|
17
CHDataManagement/Views/Images/ImagesContentView.swift
Normal file
17
CHDataManagement/Views/Images/ImagesContentView.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ImagesContentView: View {
|
||||
|
||||
@ObservedObject
|
||||
var image: ImageResource
|
||||
|
||||
var body: some View {
|
||||
image.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ImagesContentView(image: .init(resourceName: "image1"))
|
||||
}
|
@ -19,38 +19,26 @@ struct ImagesView: View {
|
||||
private var showImageDetails = false
|
||||
|
||||
var body: some View {
|
||||
FlexibleColumnView(items: $content.images) { image, width in
|
||||
let isSelected = selectedImage == image
|
||||
let borderColor: Color = isSelected ? .accentColor : .clear
|
||||
return image.imageToDisplay
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.border(borderColor, width: 5)
|
||||
.frame(width: width)
|
||||
.onTapGesture { didTap(image: image) }
|
||||
}
|
||||
.inspector(isPresented: $showImageDetails) {
|
||||
NavigationSplitView {
|
||||
List(content.images, selection: $selectedImage) { image in
|
||||
Text(image.id)
|
||||
.tag(image)
|
||||
}
|
||||
} content: {
|
||||
if let selectedImage {
|
||||
ImagesContentView(image: selectedImage)
|
||||
.layoutPriority(1)
|
||||
} else {
|
||||
Text("Select an image in the sidebar")
|
||||
}
|
||||
} detail: {
|
||||
if let selectedImage {
|
||||
ImageDetailsView(image: selectedImage)
|
||||
.frame(maxWidth: 350)
|
||||
} else {
|
||||
Text("Select an image to show its details")
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(action: { showImageDetails.toggle() }) {
|
||||
Label("Details", systemSymbol: .infoCircle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func didTap(image: ImageResource) {
|
||||
if selectedImage == image {
|
||||
selectedImage = nil
|
||||
} else {
|
||||
selectedImage = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
25
CHDataManagement/Views/Settings/LocalizedSettingsView.swift
Normal file
25
CHDataManagement/Views/Settings/LocalizedSettingsView.swift
Normal file
@ -0,0 +1,25 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LocalizedSettingsView: View {
|
||||
|
||||
@ObservedObject
|
||||
var settings: LocalizedWebsiteData
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Title")
|
||||
.font(.headline)
|
||||
TextField("", text: $settings.title)
|
||||
Text("Description")
|
||||
.font(.headline)
|
||||
TextField("", text: $settings.description)
|
||||
Text("Icon description")
|
||||
.font(.headline)
|
||||
TextField("", text: $settings.iconDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LocalizedSettingsView(settings: .english)
|
||||
}
|
@ -20,31 +20,65 @@ struct SettingsView: View {
|
||||
@State
|
||||
private var showFileImporter = false
|
||||
|
||||
@State
|
||||
private var showTagPicker = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Content Folder")
|
||||
.font(.headline)
|
||||
TextField("Content Folder", text: $contentPath)
|
||||
Button(action: selectContentFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
Text("Output Folder")
|
||||
.font(.headline)
|
||||
TextField("Output Folder", text: $outputPath)
|
||||
Button(action: selectOutputFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
Text("Feed")
|
||||
.font(.headline)
|
||||
Button(action: generateFeed) {
|
||||
Text("Generate")
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Content Folder")
|
||||
.font(.headline)
|
||||
TextField("Content Folder", text: $contentPath)
|
||||
Button(action: selectContentFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
Text("Output Folder")
|
||||
.font(.headline)
|
||||
TextField("Output Folder", text: $outputPath)
|
||||
Button(action: selectOutputFolder) {
|
||||
Text("Select folder")
|
||||
}
|
||||
Text("Navigation Bar Items")
|
||||
.font(.headline)
|
||||
FlowHStack {
|
||||
ForEach(content.websiteData.navigationTags, id: \.id) { tag in
|
||||
TagView(tag: .init(
|
||||
en: tag.english.name,
|
||||
de: tag.german.name)
|
||||
)
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
Button(action: { showTagPicker = true }) {
|
||||
Image(systemSymbol: .squareAndPencilCircleFill)
|
||||
.resizable()
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
.frame(height: 22)
|
||||
.foregroundColor(Color.gray)
|
||||
.background(Circle()
|
||||
.fill(Color.white)
|
||||
.padding(1))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
LocalizedSettingsView(settings: content.websiteData.localized(in: language))
|
||||
Text("Feed")
|
||||
.font(.headline)
|
||||
Button(action: generateFeed) {
|
||||
Text("Generate")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
.fileImporter(
|
||||
isPresented: $showFileImporter,
|
||||
allowedContentTypes: [.folder],
|
||||
onCompletion: didSelectContentFolder)
|
||||
.sheet(isPresented: $showTagPicker) {
|
||||
TagSelectionView(
|
||||
presented: $showTagPicker,
|
||||
selected: $content.websiteData.navigationTags,
|
||||
tags: $content.tags)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Folder selection
|
||||
@ -140,4 +174,5 @@ struct SettingsView: View {
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
.environmentObject(Content.mock)
|
||||
}
|
||||
|
68
CHDataManagement/Views/Tags/PageTagAssignmentView.swift
Normal file
68
CHDataManagement/Views/Tags/PageTagAssignmentView.swift
Normal file
@ -0,0 +1,68 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
private struct PageSelectionView: View {
|
||||
|
||||
@ObservedObject
|
||||
var tag: Tag
|
||||
|
||||
@ObservedObject
|
||||
var page: Page
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
let isSelected = page.tags.contains(tag)
|
||||
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
|
||||
.foregroundStyle(Color.blue)
|
||||
Text(page.localized(in: language).title)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
toggleTagAssignment()
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleTagAssignment() {
|
||||
guard let index = page.tags.firstIndex(of: tag) else {
|
||||
page.tags.append(tag)
|
||||
return
|
||||
}
|
||||
page.tags.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
struct PageTagAssignmentView: View {
|
||||
|
||||
@ObservedObject
|
||||
var tag: Tag
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
List {
|
||||
ForEach(content.pages) { page in
|
||||
PageSelectionView(tag: tag, page: page)
|
||||
}
|
||||
}.frame(minHeight: 400)
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PageTagAssignmentView(tag: .hiking)
|
||||
.environmentObject(Content.mock)
|
||||
}
|
68
CHDataManagement/Views/Tags/PostTagAssignmentView.swift
Normal file
68
CHDataManagement/Views/Tags/PostTagAssignmentView.swift
Normal file
@ -0,0 +1,68 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
private struct PostSelectionView: View {
|
||||
|
||||
@ObservedObject
|
||||
var tag: Tag
|
||||
|
||||
@ObservedObject
|
||||
var post: Post
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
let isSelected = post.tags.contains(tag)
|
||||
Image(systemSymbol: isSelected ? .checkmarkCircleFill : .circle)
|
||||
.foregroundStyle(Color.blue)
|
||||
Text(post.localized(in: language).title)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
toggleTagAssignment()
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleTagAssignment() {
|
||||
guard let index = post.tags.firstIndex(of: tag) else {
|
||||
post.tags.append(tag)
|
||||
return
|
||||
}
|
||||
post.tags.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
struct PostTagAssignmentView: View {
|
||||
|
||||
@ObservedObject
|
||||
var tag: Tag
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@Environment(\.dismiss)
|
||||
private var dismiss: DismissAction
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
List {
|
||||
ForEach(content.posts) { post in
|
||||
PostSelectionView(tag: tag, post: post)
|
||||
}
|
||||
}.frame(minHeight: 400)
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}.padding(.bottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PostTagAssignmentView(tag: .hiking)
|
||||
.environmentObject(Content.mock)
|
||||
}
|
63
CHDataManagement/Views/Tags/TagContentView.swift
Normal file
63
CHDataManagement/Views/Tags/TagContentView.swift
Normal file
@ -0,0 +1,63 @@
|
||||
import SwiftUI
|
||||
|
||||
struct TagContentView: View {
|
||||
|
||||
@ObservedObject
|
||||
var tag: Tag
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@State
|
||||
private var showPageSelection = false
|
||||
|
||||
@State
|
||||
private var showPostSelection = false
|
||||
|
||||
var selectedPages: [Page] {
|
||||
content.pages.filter { $0.tags.contains(tag) }
|
||||
}
|
||||
|
||||
var selectedPosts: [Post] {
|
||||
content.posts.filter { $0.tags.contains(tag) }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section("Pages") {
|
||||
ForEach(selectedPages) { page in
|
||||
Text(page.localized(in: language).title)
|
||||
}
|
||||
Button(action: { showPageSelection = true }) {
|
||||
Text("Select pages")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(Color.blue)
|
||||
}
|
||||
Section("Posts") {
|
||||
ForEach(selectedPosts) { post in
|
||||
Text(post.localized(in: language).title)
|
||||
}
|
||||
Button(action: { showPostSelection = true }) {
|
||||
Text("Select posts")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(Color.blue)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showPageSelection) {
|
||||
PageTagAssignmentView(tag: tag)
|
||||
}
|
||||
.sheet(isPresented: $showPostSelection) {
|
||||
PostTagAssignmentView(tag: tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
TagContentView(tag: .hiking)
|
||||
.environmentObject(Content.mock)
|
||||
}
|
@ -15,49 +15,51 @@ struct TagDetailView: View {
|
||||
private var showImagePicker = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle("Appears in overviews", isOn: $tagIsVisible)
|
||||
.toggleStyle(.switch)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle("Appears in overviews", isOn: $tagIsVisible)
|
||||
.toggleStyle(.switch)
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Text("Name")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("", text: $tag.name)
|
||||
Text("Name")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("", text: $tag.name)
|
||||
|
||||
Text("URL String")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("", text: $tag.urlComponent)
|
||||
Text("URL String")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
TextField("", text: $tag.urlComponent)
|
||||
|
||||
Text("Original url")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(tag.originalUrl ?? "-")
|
||||
.padding(.top, 1)
|
||||
.padding(.bottom)
|
||||
Text("Original url")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(tag.originalUrl ?? "-")
|
||||
.padding(.top, 1)
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Subtitle")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
OptionalTextField("", text: $tag.subtitle)
|
||||
Text("Subtitle")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
OptionalTextField("", text: $tag.subtitle)
|
||||
|
||||
Text("Description")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
OptionalTextField("", text: $tag.description)
|
||||
Text("Description")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
OptionalTextField("", text: $tag.description)
|
||||
|
||||
Text("Thumbnail")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
Button(action: { showImagePicker = true }) {
|
||||
Text(tag.thumbnail?.id ?? "Select")
|
||||
Text("Thumbnail")
|
||||
.font(.callout)
|
||||
.foregroundStyle(.secondary)
|
||||
Button(action: { showImagePicker = true }) {
|
||||
Text(tag.thumbnail?.id ?? "Select")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.blue)
|
||||
.padding()
|
||||
}
|
||||
.padding()
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
ImagePickerView(showImagePicker: $showImagePicker) { image in
|
||||
tag.thumbnail = image
|
||||
|
@ -26,6 +26,7 @@ struct TagsListView: View {
|
||||
@State
|
||||
var selectedTag: Tag?
|
||||
|
||||
#warning("TODO: Resort tag list when name changes")
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List(content.tags, selection: $selectedTag) { tag in
|
||||
@ -33,20 +34,43 @@ struct TagsListView: View {
|
||||
.tag(tag)
|
||||
|
||||
}
|
||||
} detail: {
|
||||
} content: {
|
||||
if let selectedTag {
|
||||
SelectedTagView(tag: selectedTag)
|
||||
TagContentView(tag: selectedTag)
|
||||
.layoutPriority(1)
|
||||
} else {
|
||||
Text("Select a tag to show the details")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
} detail: {
|
||||
if let selectedTag {
|
||||
SelectedTagView(tag: selectedTag)
|
||||
.frame(maxWidth: 350)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if selectedTag == nil {
|
||||
selectedTag = content.tags.first
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(action: addNewTag) {
|
||||
Label("New tag", systemSymbol: .plus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addNewTag() {
|
||||
let newTag = Tag(isVisible: true,
|
||||
german: .init(urlComponent: "tag", name: "Neuer Tag"),
|
||||
english: .init(urlComponent: "tag-en", name: "New Tag"))
|
||||
// Add to top of the list, and resort when changing the name
|
||||
content.tags.insert(newTag, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user