Rework storage structs, link preview
This commit is contained in:
@ -1,158 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
extension Content {
|
||||
|
||||
private func convert(_ tag: LocalizedTagFile, images: [String : FileResource]) -> LocalizedTag {
|
||||
LocalizedTag(
|
||||
content: self,
|
||||
urlComponent: tag.urlComponent,
|
||||
name: tag.name,
|
||||
linkPreviewTitle: tag.linkPreviewTitle,
|
||||
linkPreviewDescription: tag.linkPreviewDescription,
|
||||
linkPreviewImage: tag.linkPreviewImage.map { images[$0] },
|
||||
originalUrl: tag.originalURL)
|
||||
}
|
||||
|
||||
private func convert(_ page: LocalizedPageFile, images: [String : FileResource]) -> LocalizedPage {
|
||||
LocalizedPage(
|
||||
content: self,
|
||||
urlString: page.url,
|
||||
title: page.title,
|
||||
lastModified: page.lastModifiedDate,
|
||||
originalUrl: page.originalURL,
|
||||
linkPreviewImage: page.linkPreviewImage.map { images[$0] },
|
||||
linkPreviewTitle: page.linkPreviewTitle,
|
||||
linkPreviewDescription: page.linkPreviewDescription,
|
||||
hideTitle: page.hideTitle ?? false)
|
||||
}
|
||||
|
||||
func loadFromDisk() throws {
|
||||
guard storage.contentScope != nil else {
|
||||
print("Storage not initialized, not loading content")
|
||||
throw StorageAccessError.noBookmarkData
|
||||
}
|
||||
|
||||
let settings = storage.loadSettings() ?? .default
|
||||
|
||||
guard let tagData = storage.loadAllTags() else {
|
||||
print("Failed to load file tags")
|
||||
return
|
||||
}
|
||||
if tagData.isEmpty { print("No tags loaded") }
|
||||
|
||||
guard let pagesData = storage.loadAllPages() else {
|
||||
print("Failed to load file pages")
|
||||
return
|
||||
}
|
||||
if pagesData.isEmpty { print("No pages loaded") }
|
||||
|
||||
guard let postsData = storage.loadAllPosts() else {
|
||||
print("Failed to load file posts")
|
||||
return
|
||||
}
|
||||
if postsData.isEmpty { print("No posts loaded") }
|
||||
|
||||
guard let fileList = storage.loadAllFiles() else {
|
||||
print("Failed to load file list")
|
||||
return
|
||||
}
|
||||
if fileList.isEmpty { print("No files loaded") }
|
||||
|
||||
print("Loaded data from disk, processing...")
|
||||
// All data loaded from storage, start constructing the data model
|
||||
|
||||
let files: [String : FileResource] = fileList.reduce(into: [:]) { (files, data) in
|
||||
let fileId = data.key
|
||||
let fileData = data.value.data
|
||||
let isExternal = data.value.isExternal
|
||||
files[fileId] = FileResource(content: self, id: fileId, file: fileData, isExternalFile: isExternal)
|
||||
}
|
||||
|
||||
let images = files.filter { $0.value.type.isImage }
|
||||
|
||||
let tags = tagData.reduce(into: [:]) { (tags, data) in
|
||||
tags[data.key] = Tag(
|
||||
content: self,
|
||||
id: data.value.id,
|
||||
isVisible: data.value.isVisible,
|
||||
german: convert(data.value.german, images: images),
|
||||
english: convert(data.value.english, images: images))
|
||||
}
|
||||
|
||||
let pages: [String : Page] = loadPages(pagesData, tags: tags, files: files)
|
||||
|
||||
let posts: [String : Post] = postsData.reduce(into: [:]) { dict, data in
|
||||
let (postId, post) = data
|
||||
let linkedPage = post.linkedPageId.map { pages[$0] }
|
||||
let german = LocalizedPost(content: self, file: post.german, images: images)
|
||||
let english = LocalizedPost(content: self, file: post.english, images: images)
|
||||
|
||||
dict[postId] = Post(
|
||||
content: self,
|
||||
id: postId,
|
||||
isDraft: post.isDraft,
|
||||
createdDate: post.createdDate,
|
||||
startDate: post.startDate,
|
||||
endDate: post.endDate,
|
||||
tags: post.tags.map { tags[$0]! },
|
||||
german: german,
|
||||
english: english,
|
||||
linkedPage: linkedPage)
|
||||
}
|
||||
|
||||
let tagOverview = settings.tagOverview.map { file in
|
||||
TagOverviewPage(
|
||||
content: self,
|
||||
german: .init(content: self, file: file.german, image: file.german.linkPreviewImage.map { files[$0] }),
|
||||
english: .init(content: self, file: file.english, image: file.english.linkPreviewImage.map { files[$0] }))
|
||||
}
|
||||
|
||||
self.tags = tags.values.sorted()
|
||||
self.pages = pages.values.sorted(ascending: false) { $0.startDate }
|
||||
self.files = files.values.sorted { $0.id }
|
||||
self.posts = posts.values.sorted(ascending: false) { $0.startDate }
|
||||
self.tagOverview = tagOverview
|
||||
self.settings = .init(file: settings, files: files) { raw in
|
||||
#warning("Notify about missing links")
|
||||
guard let type = ItemType(rawValue: raw, posts: posts, pages: pages, tags: tags) else {
|
||||
return nil
|
||||
}
|
||||
switch type {
|
||||
case .general:
|
||||
return nil
|
||||
case .post(let post):
|
||||
return post
|
||||
case .feed:
|
||||
return nil // TODO: Provide feed object
|
||||
case .page(let page):
|
||||
return page
|
||||
case .tagPage(let tag):
|
||||
return tag
|
||||
case .tagOverview:
|
||||
return tagOverview
|
||||
}
|
||||
}
|
||||
|
||||
print("Content loaded")
|
||||
}
|
||||
|
||||
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], files: [String : FileResource]) -> [String : Page] {
|
||||
pagesData.reduce(into: [:]) { pages, data in
|
||||
let (pageId, page) = data
|
||||
pages[pageId] = Page(
|
||||
content: self,
|
||||
id: pageId,
|
||||
externalLink: page.externalLink,
|
||||
isDraft: page.isDraft,
|
||||
createdDate: page.createdDate,
|
||||
hideDate: page.hideDate ?? false,
|
||||
startDate: page.startDate,
|
||||
endDate: page.endDate,
|
||||
german: convert(page.german, images: files),
|
||||
english: convert(page.english, images: files),
|
||||
tags: page.tags.compactMap { tags[$0] },
|
||||
requiredFiles: page.requiredFiles?.compactMap { files[$0] } ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -2,22 +2,25 @@ import Foundation
|
||||
|
||||
extension Content {
|
||||
|
||||
func saveToDisk() throws {
|
||||
func saveToDisk() -> Bool {
|
||||
guard didLoadContent else { return false }
|
||||
guard storage.contentScope != nil else {
|
||||
print("Storage not initialized, not saving content")
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
var failedSaves = 0
|
||||
failedSaves += pages.count { !storage.save(pageMetadata: $0.pageFile, for: $0.id) }
|
||||
failedSaves += posts.count { !storage.save(post: $0.postFile, for: $0.id) }
|
||||
failedSaves += tags.count { !storage.save(tagMetadata: $0.file, for: $0.id) }
|
||||
failedSaves.increment(!storage.save(settings: settings.file(tagOverview: tagOverview)))
|
||||
failedSaves += files.count { !storage.save(fileInfo: $0.fileInfo, for: $0.id) }
|
||||
failedSaves += pages.count { !storage.save(pageMetadata: $0.data, for: $0.id) }
|
||||
failedSaves += posts.count { !storage.save(post: $0.data, for: $0.id) }
|
||||
failedSaves += tags.count { !storage.save(tagMetadata: $0.data, for: $0.id) }
|
||||
failedSaves.increment(!storage.save(settings: settings.data(tagOverview: tagOverview)))
|
||||
failedSaves += files.count { !storage.save(fileInfo: $0.data, for: $0.id) }
|
||||
|
||||
if failedSaves > 0 {
|
||||
print("Save partially failed with \(failedSaves) errors")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func removeUnlinkedFiles() -> Bool {
|
||||
@ -37,49 +40,3 @@ extension Content {
|
||||
return success
|
||||
}
|
||||
}
|
||||
|
||||
private extension Page {
|
||||
|
||||
var pageFile: PageFile {
|
||||
.init(isDraft: isDraft,
|
||||
externalLink: externalLink,
|
||||
tags: tags.map { $0.id },
|
||||
hideDate: hideDate ? true : nil,
|
||||
createdDate: createdDate,
|
||||
startDate: startDate,
|
||||
endDate: hasEndDate ? endDate : nil,
|
||||
german: german.pageFile,
|
||||
english: english.pageFile,
|
||||
requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted())
|
||||
}
|
||||
}
|
||||
|
||||
private extension LocalizedPage {
|
||||
|
||||
var pageFile: LocalizedPageFile {
|
||||
.init(url: urlString,
|
||||
title: title,
|
||||
linkPreviewImage: linkPreviewImage?.id,
|
||||
linkPreviewTitle: linkPreviewTitle,
|
||||
linkPreviewDescription: linkPreviewDescription,
|
||||
lastModifiedDate: lastModified,
|
||||
originalURL: originalUrl,
|
||||
hideTitle: hideTitle ? true : nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension Post {
|
||||
|
||||
var postFile: PostFile {
|
||||
.init(
|
||||
isDraft: isDraft,
|
||||
createdDate: createdDate,
|
||||
startDate: startDate,
|
||||
endDate: hasEndDate ? endDate : nil,
|
||||
tags: tags.map { $0.id },
|
||||
german: german.postFile,
|
||||
english: english.postFile,
|
||||
linkedPageId: linkedPage?.id)
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,9 @@ import Combine
|
||||
|
||||
final class Content: ObservableObject {
|
||||
|
||||
@Published
|
||||
var didLoadContent = false
|
||||
|
||||
@ObservedObject
|
||||
var storage: Storage
|
||||
|
||||
@ -23,7 +26,7 @@ final class Content: ObservableObject {
|
||||
var files: [FileResource]
|
||||
|
||||
@Published
|
||||
var tagOverview: TagOverviewPage?
|
||||
var tagOverview: Tag?
|
||||
|
||||
@Published
|
||||
var results: GenerationResults
|
||||
@ -47,7 +50,7 @@ final class Content: ObservableObject {
|
||||
pages: [Page],
|
||||
tags: [Tag],
|
||||
files: [FileResource],
|
||||
tagOverview: TagOverviewPage?) {
|
||||
tagOverview: Tag?) {
|
||||
self.settings = settings
|
||||
self.posts = posts
|
||||
self.pages = pages
|
||||
@ -112,16 +115,11 @@ final class Content: ObservableObject {
|
||||
pages.insert(page, at: 0)
|
||||
}
|
||||
|
||||
func update(contentPath: URL) {
|
||||
func update(contentPath: URL, callback: @escaping ([String]) -> ()) {
|
||||
guard storage.save(contentPath: contentPath) else {
|
||||
return
|
||||
}
|
||||
clear()
|
||||
do {
|
||||
try loadFromDisk()
|
||||
} catch {
|
||||
print("Failed to reload content: \(error)")
|
||||
}
|
||||
loadFromDisk(callback: callback)
|
||||
}
|
||||
|
||||
func remove(_ file: FileResource) {
|
||||
@ -146,4 +144,29 @@ final class Content: ObservableObject {
|
||||
func file(withOutputPath: String) -> FileResource? {
|
||||
files.first { $0.absoluteUrl == withOutputPath }
|
||||
}
|
||||
|
||||
func loadFromDisk(callback: @escaping (_ errors: [String]) -> ()) {
|
||||
DispatchQueue.global().async {
|
||||
let loader = ModelLoader(content: self, storage: self.storage)
|
||||
let result = loader.load()
|
||||
guard result.errors.isEmpty else {
|
||||
DispatchQueue.main.async {
|
||||
self.didLoadContent = false
|
||||
callback(result.errors.sorted())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.files = result.files
|
||||
self.posts = result.posts
|
||||
self.pages = result.pages
|
||||
self.tags = result.tags
|
||||
self.settings = result.settings
|
||||
self.tagOverview = result.tagOverview
|
||||
self.didLoadContent = true
|
||||
callback([])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,43 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class FileResource: Item {
|
||||
final class FileResource: Item, LocalizedItem {
|
||||
|
||||
let type: FileType
|
||||
|
||||
/// Indicate if the file content is stored by the app
|
||||
@Published
|
||||
var isExternallyStored: Bool
|
||||
|
||||
/// The file/image description in German
|
||||
@Published
|
||||
var german: String
|
||||
var german: String?
|
||||
|
||||
/// The file/image description in English
|
||||
@Published
|
||||
var english: String
|
||||
var english: String?
|
||||
|
||||
/// A version string of this resource, mostly for assets
|
||||
@Published
|
||||
var version: String?
|
||||
|
||||
/// A URL where the resource was copied/downloaded from
|
||||
@Published
|
||||
var sourceUrl: String?
|
||||
|
||||
/// The list of generated image versions for this image
|
||||
@Published
|
||||
var generatedImageVersions: Set<String>
|
||||
|
||||
/// A custom file path in the output folder where this file is located
|
||||
@Published
|
||||
var customOutputPath: String?
|
||||
|
||||
/// The date when the file was added
|
||||
@Published
|
||||
var addedDate: Date
|
||||
|
||||
/// The date when the file was last modified
|
||||
@Published
|
||||
var modifiedDate: Date
|
||||
|
||||
@ -53,8 +62,8 @@ final class FileResource: Item {
|
||||
modifiedDate: Date = .now) {
|
||||
self.type = FileType(fileExtension: id.fileExtension)
|
||||
self.isExternallyStored = isExternallyStored
|
||||
self.german = german ?? ""
|
||||
self.english = english ?? ""
|
||||
self.german = german
|
||||
self.english = english
|
||||
self.version = version
|
||||
self.sourceUrl = sourceUrl
|
||||
self.generatedImageVersions = generatedImageVersions
|
||||
@ -64,20 +73,6 @@ final class FileResource: Item {
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
init(content: Content, id: String, file: FileResourceFile, isExternalFile: Bool) {
|
||||
self.type = FileType(fileExtension: id.fileExtension)
|
||||
self.isExternallyStored = isExternalFile
|
||||
self.german = file.germanDescription ?? ""
|
||||
self.english = file.englishDescription ?? ""
|
||||
self.version = file.version
|
||||
self.sourceUrl = file.sourceUrl
|
||||
self.generatedImageVersions = Set(file.generatedImages ?? [])
|
||||
self.customOutputPath = file.customOutputPath
|
||||
self.addedDate = file.addedDate
|
||||
self.modifiedDate = file.modifiedDate
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
/**
|
||||
Only for bundle images
|
||||
*/
|
||||
@ -101,7 +96,7 @@ final class FileResource: Item {
|
||||
content.storage.fileContent(for: id) ?? ""
|
||||
}
|
||||
|
||||
func dataContent() -> Data? {
|
||||
func dataContent() -> Foundation.Data? {
|
||||
content.storage.fileData(for: id)
|
||||
}
|
||||
|
||||
@ -131,7 +126,6 @@ final class FileResource: Item {
|
||||
// Image must have changed, so force regeneration
|
||||
DispatchQueue.main.async {
|
||||
self.imageDimensions = size
|
||||
self.didChange()
|
||||
self.removeGeneratedImages()
|
||||
}
|
||||
}
|
||||
@ -299,12 +293,34 @@ final class FileResource: Item {
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource {
|
||||
|
||||
var fileInfo: FileResourceFile {
|
||||
convenience init(content: Content, id: String, data: FileResource.Data, isExternalFile: Bool) {
|
||||
self.init(
|
||||
content: content,
|
||||
id: id,
|
||||
isExternallyStored: isExternalFile,
|
||||
english: data.englishDescription,
|
||||
german: data.germanDescription,
|
||||
version: data.version,
|
||||
sourceUrl: data.sourceUrl,
|
||||
generatedImageVersions: Set(data.generatedImages ?? []),
|
||||
customOutputPath: data.customOutputPath,
|
||||
addedDate: data.addedDate,
|
||||
modifiedDate: data.modifiedDate)
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(
|
||||
englishDescription: english.nonEmpty,
|
||||
germanDescription: german.nonEmpty,
|
||||
englishDescription: english,
|
||||
germanDescription: german,
|
||||
generatedImages: generatedImageVersions.sorted().nonEmpty,
|
||||
customOutputPath: customOutputPath,
|
||||
version: version,
|
||||
@ -312,15 +328,16 @@ extension FileResource {
|
||||
addedDate: addedDate,
|
||||
modifiedDate: modifiedDate)
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource: LocalizedItem {
|
||||
|
||||
}
|
||||
|
||||
extension FileResource: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
id
|
||||
/// This struct holds metadata about a file resource that is stored in the content folder.
|
||||
struct Data: Codable {
|
||||
let englishDescription: String?
|
||||
let germanDescription: String?
|
||||
let generatedImages: [String]?
|
||||
let customOutputPath: String?
|
||||
let version: String?
|
||||
let sourceUrl: String?
|
||||
let addedDate: Date
|
||||
let modifiedDate: Date
|
||||
}
|
||||
}
|
||||
|
@ -39,12 +39,20 @@ class Item: ObservableObject, Identifiable {
|
||||
var itemType: ItemType {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
var itemReference: ItemReference {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
var itemId: ItemId {
|
||||
.init(type: itemType, id: id)
|
||||
}
|
||||
}
|
||||
|
||||
extension Item: Equatable {
|
||||
|
||||
static func == (lhs: Item, rhs: Item) -> Bool {
|
||||
lhs.id == rhs.id
|
||||
lhs.id == rhs.id && lhs.itemType == rhs.itemType
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,12 +60,13 @@ extension Item: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
hasher.combine(itemType)
|
||||
}
|
||||
}
|
||||
|
||||
extension Item: Comparable {
|
||||
|
||||
static func < (lhs: Item, rhs: Item) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
lhs.id < rhs.id && lhs.itemType < rhs.itemType
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,11 @@
|
||||
|
||||
struct ItemId {
|
||||
|
||||
let language: ContentLanguage
|
||||
let type: ItemType
|
||||
|
||||
let itemType: ItemType
|
||||
let id: String?
|
||||
}
|
||||
|
||||
extension ItemId: Equatable {
|
||||
|
||||
static func == (lhs: ItemId, rhs: ItemId) -> Bool {
|
||||
lhs.language == rhs.language &&
|
||||
lhs.itemType == rhs.itemType
|
||||
}
|
||||
}
|
||||
|
||||
extension ItemId: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(language)
|
||||
hasher.combine(itemType.id)
|
||||
}
|
||||
}
|
||||
|
||||
extension ItemId: Comparable {
|
||||
extension ItemId: Codable {
|
||||
|
||||
static func < (lhs: ItemId, rhs: ItemId) -> Bool {
|
||||
guard lhs.itemType == rhs.itemType else {
|
||||
return lhs.itemType < rhs.itemType
|
||||
}
|
||||
return lhs.language < rhs.language
|
||||
}
|
||||
}
|
||||
|
68
CHDataManagement/Model/Item/ItemReference.swift
Normal file
68
CHDataManagement/Model/Item/ItemReference.swift
Normal file
@ -0,0 +1,68 @@
|
||||
|
||||
enum ItemReference {
|
||||
|
||||
case general
|
||||
|
||||
case post(Post)
|
||||
|
||||
case feed
|
||||
|
||||
case page(Page)
|
||||
|
||||
case tagPage(Tag)
|
||||
|
||||
case tagOverview
|
||||
}
|
||||
|
||||
extension ItemReference: Equatable {
|
||||
|
||||
}
|
||||
|
||||
extension ItemReference: Hashable {
|
||||
|
||||
}
|
||||
|
||||
extension ItemReference: Identifiable {
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .general:
|
||||
return "0-general"
|
||||
case .feed:
|
||||
return "1-feed"
|
||||
case .post(let post):
|
||||
return "2-post-\(post.id)"
|
||||
case .page(let page):
|
||||
return "3-page-\(page.id)"
|
||||
case .tagPage(let tag):
|
||||
return "5-tag-\(tag.id)"
|
||||
case .tagOverview:
|
||||
return "4-tag-overview"
|
||||
}
|
||||
}
|
||||
|
||||
init?(context: LoadingContext, rawValue: String) {
|
||||
if rawValue == "0-general" {
|
||||
self = .general
|
||||
} else if rawValue == "1-feed" {
|
||||
self = .feed
|
||||
} else if rawValue == "4-tag-overview" {
|
||||
self = .tagOverview
|
||||
} else if let id = rawValue.removingPrefix("3-page-"), let page = context.page(id) {
|
||||
self = .page(page)
|
||||
} else if let id = rawValue.removingPrefix("2-post-"), let post = context.post(id) {
|
||||
self = .post(post)
|
||||
} else if let id = rawValue.removingPrefix("5-tag-"), let tag = context.tag(id) {
|
||||
self = .tagPage(tag)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ItemReference: Comparable {
|
||||
|
||||
static func < (lhs: ItemReference, rhs: ItemReference) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
}
|
@ -1,76 +1,21 @@
|
||||
|
||||
enum ItemType {
|
||||
enum ItemType: String, Equatable, Hashable {
|
||||
|
||||
case general
|
||||
case post = "post"
|
||||
|
||||
case post(Post)
|
||||
case page = "page"
|
||||
|
||||
case feed
|
||||
case tag = "tag"
|
||||
|
||||
case page(Page)
|
||||
|
||||
case tagPage(Tag)
|
||||
|
||||
case tagOverview
|
||||
}
|
||||
|
||||
extension ItemType: Equatable {
|
||||
|
||||
}
|
||||
|
||||
extension ItemType: Hashable {
|
||||
|
||||
}
|
||||
|
||||
extension ItemType: Identifiable {
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .general:
|
||||
return "0-general"
|
||||
case .feed:
|
||||
return "1-feed"
|
||||
case .post(let post):
|
||||
return "2-post-\(post.id)"
|
||||
case .page(let page):
|
||||
return "3-page-\(page.id)"
|
||||
case .tagPage(let tag):
|
||||
return "5-tag-\(tag.id)"
|
||||
case .tagOverview:
|
||||
return "4-tag-overview"
|
||||
}
|
||||
}
|
||||
|
||||
init?(rawValue: String, posts: [String : Post], pages: [String : Page], tags: [String : Tag]) {
|
||||
if rawValue == "0-general" {
|
||||
self = .general
|
||||
} else if rawValue == "1-feed" {
|
||||
self = .feed
|
||||
} else if rawValue == "4-tag-overview" {
|
||||
self = .tagOverview
|
||||
} else if let id = rawValue.removingPrefix("3-page-"), let page = pages[id] {
|
||||
self = .page(page)
|
||||
} else if let id = rawValue.removingPrefix("2-post-"), let post = posts[id] {
|
||||
self = .post(post)
|
||||
} else if let id = rawValue.removingPrefix("5-tag-"), let tag = tags[id] {
|
||||
self = .tagPage(tag)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case tagOverview = "tag-overview"
|
||||
}
|
||||
|
||||
extension ItemType: Comparable {
|
||||
|
||||
static func < (lhs: ItemType, rhs: ItemType) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
public static func < (lhs: ItemType, rhs: ItemType) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
func removingPrefix(_ prefix: String) -> String? {
|
||||
guard self.hasPrefix(prefix) else { return nil }
|
||||
return String(self.dropFirst(prefix.count))
|
||||
}
|
||||
extension ItemType: Codable {
|
||||
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
|
||||
protocol LinkPreviewItem: AnyObject {
|
||||
|
||||
var linkPreviewImage: FileResource? { get set }
|
||||
|
||||
var linkPreviewTitle: String? { get }
|
||||
|
||||
var linkPreviewDescription: String? { get }
|
||||
}
|
||||
|
||||
extension LinkPreviewItem {
|
||||
|
||||
func remove(linkPreviewImage file: FileResource) {
|
||||
if linkPreviewImage == file {
|
||||
linkPreviewImage = nil
|
||||
}
|
||||
}
|
||||
}
|
33
CHDataManagement/Model/Item/LocalizedItemId.swift
Normal file
33
CHDataManagement/Model/Item/LocalizedItemId.swift
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
struct LocalizedItemId {
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let itemType: ItemReference
|
||||
}
|
||||
|
||||
extension LocalizedItemId: Equatable {
|
||||
|
||||
static func == (lhs: LocalizedItemId, rhs: LocalizedItemId) -> Bool {
|
||||
lhs.language == rhs.language &&
|
||||
lhs.itemType == rhs.itemType
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedItemId: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(language)
|
||||
hasher.combine(itemType.id)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedItemId: Comparable {
|
||||
|
||||
static func < (lhs: LocalizedItemId, rhs: LocalizedItemId) -> Bool {
|
||||
guard lhs.itemType == rhs.itemType else {
|
||||
return lhs.itemType < rhs.itemType
|
||||
}
|
||||
return lhs.language < rhs.language
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
/*
|
||||
final class TagOverviewPage: Item {
|
||||
|
||||
static let id = "all-tags"
|
||||
@ -105,3 +105,4 @@ final class LocalizedTagOverviewPage: ObservableObject {
|
||||
!content.containsTag(withUrlComponent: urlComponent)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
58
CHDataManagement/Model/LinkPreview.swift
Normal file
58
CHDataManagement/Model/LinkPreview.swift
Normal file
@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
The information to use when constructing the link preview of a page.
|
||||
|
||||
The information will be placed in the `<head>` of the page as `<meta>` tags.
|
||||
*/
|
||||
final class LinkPreview: ObservableObject {
|
||||
|
||||
/// The description to show when linking to a page (contained in the `<head>` of the page)
|
||||
@Published
|
||||
var title: String?
|
||||
|
||||
/// The image id of the thumbnail to attach to the link preview (contained in the `<head>` of the page)
|
||||
@Published
|
||||
var description: String?
|
||||
|
||||
/// The title to show for a link preview (contained in the `<head>` of the page)
|
||||
@Published
|
||||
var image: FileResource?
|
||||
|
||||
init(title: String? = nil, description: String? = nil, image: FileResource? = nil) {
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.image = image
|
||||
}
|
||||
|
||||
/**
|
||||
Remove a file if it is used in the link preview.
|
||||
*/
|
||||
func remove(_ file: FileResource) {
|
||||
if image == file {
|
||||
image = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
var data: Data {
|
||||
.init(title: title, description: description, image: image?.id)
|
||||
}
|
||||
|
||||
init(context: LoadingContext, data: Data) {
|
||||
self.title = data.title
|
||||
self.description = data.description
|
||||
self.image = data.image.map(context.image)
|
||||
}
|
||||
}
|
||||
|
||||
extension LinkPreview {
|
||||
|
||||
/// The object to serialize a link preview for storage
|
||||
struct Data: Codable {
|
||||
let title: String?
|
||||
let description: String?
|
||||
let image: String?
|
||||
}
|
||||
}
|
110
CHDataManagement/Model/Loading/LoadingContext.swift
Normal file
110
CHDataManagement/Model/Loading/LoadingContext.swift
Normal file
@ -0,0 +1,110 @@
|
||||
|
||||
final class LoadingContext {
|
||||
|
||||
let content: Content
|
||||
|
||||
var files: [String: FileResource] = [:]
|
||||
|
||||
var pages: [String : Page] = [:]
|
||||
|
||||
var tags: [String : Tag] = [:]
|
||||
|
||||
var posts: [String : Post] = [:]
|
||||
|
||||
var errors: Set<String> = []
|
||||
|
||||
var tagOverview: TagOverview?
|
||||
|
||||
var settings: Settings?
|
||||
|
||||
init(content: Content) {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
func results() -> LoadingResult {
|
||||
.init(
|
||||
settings: settings ?? .default,
|
||||
posts: posts.values.sorted(ascending: false) { $0.startDate },
|
||||
pages: pages.values.sorted(ascending: false) { $0.startDate },
|
||||
tags: tags.values.sorted(),
|
||||
files: files.values.sorted { $0.id },
|
||||
tagOverview: tagOverview,
|
||||
errors: errors.sorted())
|
||||
}
|
||||
|
||||
func error(_ message: String) {
|
||||
errors.insert(message)
|
||||
}
|
||||
|
||||
func post(_ postId: String) -> Post? {
|
||||
if let post = posts[postId] {
|
||||
return post
|
||||
}
|
||||
error("Missing post \(postId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func tag(_ tagId: String) -> Tag? {
|
||||
if let tag = tags[tagId] {
|
||||
return tag
|
||||
}
|
||||
error("Missing tag \(tagId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func page(_ pageId: String) -> Page? {
|
||||
if let page = pages[pageId] {
|
||||
return page
|
||||
}
|
||||
error("Missing page \(pageId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func file(_ fileId: String) -> FileResource? {
|
||||
if let file = files[fileId] {
|
||||
return file
|
||||
}
|
||||
error("Missing file \(fileId)")
|
||||
return nil
|
||||
}
|
||||
|
||||
func image(_ imageId: String) -> FileResource? {
|
||||
guard let image = file(imageId) else {
|
||||
return nil
|
||||
}
|
||||
if image.type.isImage {
|
||||
return image
|
||||
}
|
||||
error("Image \(imageId) is not an image")
|
||||
return nil
|
||||
}
|
||||
|
||||
func item(itemId: ItemId) -> Item? {
|
||||
switch itemId.type {
|
||||
case .post:
|
||||
guard let id = itemId.id else {
|
||||
error("Missing post id in itemId")
|
||||
return nil
|
||||
}
|
||||
return post(id)
|
||||
case .page:
|
||||
guard let id = itemId.id else {
|
||||
error("Missing page id in itemId")
|
||||
return nil
|
||||
}
|
||||
return page(id)
|
||||
case .tag:
|
||||
guard let id = itemId.id else {
|
||||
error("Missing tag id in itemId")
|
||||
return nil
|
||||
}
|
||||
return tag(id)
|
||||
case .tagOverview:
|
||||
guard let tagOverview else {
|
||||
error("Missing tag overview")
|
||||
return nil
|
||||
}
|
||||
return tagOverview
|
||||
}
|
||||
}
|
||||
}
|
17
CHDataManagement/Model/Loading/LoadingResult.swift
Normal file
17
CHDataManagement/Model/Loading/LoadingResult.swift
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
struct LoadingResult {
|
||||
|
||||
let settings: Settings
|
||||
|
||||
let posts: [Post]
|
||||
|
||||
let pages: [Page]
|
||||
|
||||
let tags: [Tag]
|
||||
|
||||
let files: [FileResource]
|
||||
|
||||
let tagOverview: Tag?
|
||||
|
||||
let errors: [String]
|
||||
}
|
96
CHDataManagement/Model/Loading/ModelLoader.swift
Normal file
96
CHDataManagement/Model/Loading/ModelLoader.swift
Normal file
@ -0,0 +1,96 @@
|
||||
|
||||
final class ModelLoader {
|
||||
|
||||
let content: Content
|
||||
|
||||
let storage: Storage
|
||||
|
||||
let context: LoadingContext
|
||||
|
||||
init(content: Content, storage: Storage) {
|
||||
self.content = content
|
||||
self.storage = storage
|
||||
self.context = .init(content: content)
|
||||
}
|
||||
|
||||
func load() -> LoadingResult {
|
||||
loadInternal()
|
||||
return context.results()
|
||||
}
|
||||
|
||||
private func loadInternal() {
|
||||
guard storage.contentScope != nil else {
|
||||
context.error("Storage not initialized, not loading content")
|
||||
return
|
||||
}
|
||||
|
||||
loadFiles()
|
||||
loadTags()
|
||||
loadPages()
|
||||
loadPosts()
|
||||
loadSettings()
|
||||
}
|
||||
|
||||
private func loadFiles() {
|
||||
guard let files = storage.loadAllFiles() else {
|
||||
context.error("Failed to load file list")
|
||||
return
|
||||
}
|
||||
if files.isEmpty { print("No files loaded") }
|
||||
|
||||
files.forEach { (fileId, data) in
|
||||
let fileData = data.data
|
||||
let isExternal = data.isExternal
|
||||
context.files[fileId] = FileResource(content: content, id: fileId, data: fileData, isExternalFile: isExternal)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadTags() {
|
||||
guard let tags = storage.loadAllTags() else {
|
||||
context.error("Failed to load file tags")
|
||||
return
|
||||
}
|
||||
if tags.isEmpty { print("No tags loaded") }
|
||||
|
||||
tags.forEach { (tagId, data) in
|
||||
context.tags[tagId] = Tag(context: context, id: tagId, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadPages() {
|
||||
guard let pages = storage.loadAllPages() else {
|
||||
context.error("Failed to load file pages")
|
||||
return
|
||||
}
|
||||
if pages.isEmpty { print("No pages loaded") }
|
||||
|
||||
pages.forEach { pageId, data in
|
||||
context.pages[pageId] = Page(context: context, id: pageId, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadPosts() {
|
||||
guard let posts = storage.loadAllPosts() else {
|
||||
context.error("Failed to load file posts")
|
||||
return
|
||||
}
|
||||
if posts.isEmpty { print("No posts loaded") }
|
||||
|
||||
posts.forEach { postId, data in
|
||||
context.posts[postId] = Post(context: context, id: postId, data: data)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSettings() {
|
||||
guard let settings = storage.loadSettings() else {
|
||||
context.error("Failed to load settings")
|
||||
return
|
||||
}
|
||||
|
||||
context.tagOverview = settings.tagOverview.map { data in
|
||||
TagOverview(context: context, id: "all-tags", data: data)
|
||||
}
|
||||
|
||||
context.settings = Settings(context: context, data: settings)
|
||||
}
|
||||
}
|
@ -35,13 +35,7 @@ final class LocalizedPage: ObservableObject {
|
||||
let originalUrl: String?
|
||||
|
||||
@Published
|
||||
var linkPreviewImage: FileResource?
|
||||
|
||||
@Published
|
||||
var linkPreviewTitle: String?
|
||||
|
||||
@Published
|
||||
var linkPreviewDescription: String?
|
||||
var linkPreview: LinkPreview
|
||||
|
||||
@Published
|
||||
var hideTitle: Bool
|
||||
@ -51,18 +45,14 @@ final class LocalizedPage: ObservableObject {
|
||||
title: String,
|
||||
lastModified: Date? = nil,
|
||||
originalUrl: String? = nil,
|
||||
linkPreviewImage: FileResource? = nil,
|
||||
linkPreviewTitle: String? = nil,
|
||||
linkPreviewDescription: String? = nil,
|
||||
linkPreview: LinkPreview = .init(),
|
||||
hideTitle: Bool = false) {
|
||||
self.content = content
|
||||
self.urlString = urlString
|
||||
self.title = title
|
||||
self.lastModified = lastModified
|
||||
self.originalUrl = originalUrl
|
||||
self.linkPreviewImage = linkPreviewImage
|
||||
self.linkPreviewTitle = linkPreviewTitle
|
||||
self.linkPreviewDescription = linkPreviewDescription
|
||||
self.linkPreview = linkPreview
|
||||
self.hideTitle = hideTitle
|
||||
}
|
||||
|
||||
@ -72,6 +62,37 @@ final class LocalizedPage: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedPage: LinkPreviewItem {
|
||||
|
||||
extension LocalizedPage {
|
||||
|
||||
convenience init(context: LoadingContext, data: LocalizedPage.Data) {
|
||||
self.init(
|
||||
content: context.content,
|
||||
urlString: data.url,
|
||||
title: data.title,
|
||||
lastModified: data.lastModifiedDate,
|
||||
originalUrl: data.originalURL,
|
||||
linkPreview: .init(context: context, data: data.linkPreview),
|
||||
hideTitle: data.hideTitle ?? false)
|
||||
}
|
||||
|
||||
/// The structure to store the metadata of a localized page
|
||||
struct Data: Codable {
|
||||
let url: String
|
||||
let title: String
|
||||
let linkPreview: LinkPreview.Data
|
||||
let lastModifiedDate: Date?
|
||||
let originalURL: String?
|
||||
let hideTitle: Bool?
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(
|
||||
url: urlString,
|
||||
title: title,
|
||||
linkPreview: linkPreview.data,
|
||||
lastModifiedDate: lastModified,
|
||||
originalURL: originalUrl,
|
||||
hideTitle: hideTitle ? true : nil)
|
||||
}
|
||||
}
|
||||
|
@ -22,13 +22,7 @@ final class LocalizedPost: ObservableObject {
|
||||
var pageLinkText: String?
|
||||
|
||||
@Published
|
||||
var linkPreviewImage: FileResource?
|
||||
|
||||
@Published
|
||||
var linkPreviewTitle: String?
|
||||
|
||||
@Published
|
||||
var linkPreviewDescription: String?
|
||||
var linkPreview: LinkPreview
|
||||
|
||||
init(content: Content,
|
||||
title: String? = nil,
|
||||
@ -36,41 +30,14 @@ final class LocalizedPost: ObservableObject {
|
||||
lastModified: Date? = nil,
|
||||
images: [FileResource] = [],
|
||||
pageLinkText: String? = nil,
|
||||
linkPreviewImage: FileResource? = nil,
|
||||
linkPreviewTitle: String? = nil,
|
||||
linkPreviewDescription: String? = nil) {
|
||||
linkPreview: LinkPreview = .init()) {
|
||||
self.content = content
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.lastModified = lastModified
|
||||
self.images = images
|
||||
self.pageLinkText = pageLinkText
|
||||
self.linkPreviewImage = linkPreviewImage
|
||||
self.linkPreviewTitle = linkPreviewTitle
|
||||
self.linkPreviewDescription = linkPreviewDescription
|
||||
}
|
||||
|
||||
init(content: Content, file: LocalizedPostFile, images: [String : FileResource]) {
|
||||
self.content = content
|
||||
self.title = file.title
|
||||
self.text = file.content
|
||||
self.lastModified = file.lastModifiedDate
|
||||
self.images = file.images.compactMap { images[$0] }
|
||||
self.pageLinkText = file.pageLinkText
|
||||
self.linkPreviewImage = file.linkPreviewImage.map { images[$0] }
|
||||
self.linkPreviewTitle = file.linkPreviewTitle
|
||||
self.linkPreviewDescription = file.linkPreviewDescription
|
||||
}
|
||||
|
||||
var postFile: LocalizedPostFile {
|
||||
.init(images: images.map { $0.id },
|
||||
title: title,
|
||||
content: text,
|
||||
lastModifiedDate: lastModified,
|
||||
pageLinkText: pageLinkText,
|
||||
linkPreviewImage: linkPreviewImage?.id,
|
||||
linkPreviewTitle: linkPreviewTitle,
|
||||
linkPreviewDescription: linkPreviewDescription)
|
||||
self.linkPreview = linkPreview
|
||||
}
|
||||
|
||||
func contains(_ string: String) -> Bool {
|
||||
@ -84,10 +51,41 @@ final class LocalizedPost: ObservableObject {
|
||||
if images.contains(file) {
|
||||
images.remove(file)
|
||||
}
|
||||
remove(linkPreviewImage: file)
|
||||
linkPreview.remove(file)
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedPost: LinkPreviewItem {
|
||||
// MARK: Storage
|
||||
|
||||
extension LocalizedPost {
|
||||
|
||||
convenience init(context: LoadingContext, data: Data) {
|
||||
self.init(
|
||||
content: context.content,
|
||||
title: data.title,
|
||||
text: data.text,
|
||||
lastModified: data.lastModifiedDate,
|
||||
images: data.images.compactMap(context.image),
|
||||
pageLinkText: data.pageLinkText,
|
||||
linkPreview: .init(context: context, data: data.linkPreview))
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(images: images.map { $0.id },
|
||||
title: title,
|
||||
text: text,
|
||||
lastModifiedDate: lastModified,
|
||||
pageLinkText: pageLinkText,
|
||||
linkPreview: linkPreview.data)
|
||||
}
|
||||
|
||||
/// The structure to store the metadata of a localized post
|
||||
struct Data: Codable {
|
||||
let images: [String]
|
||||
let title: String?
|
||||
let text: String
|
||||
let lastModifiedDate: Date?
|
||||
let pageLinkText: String?
|
||||
let linkPreview: LinkPreview.Data
|
||||
}
|
||||
}
|
||||
|
@ -12,14 +12,7 @@ final class LocalizedTag: ObservableObject {
|
||||
var name: String
|
||||
|
||||
@Published
|
||||
var linkPreviewTitle: String?
|
||||
|
||||
@Published
|
||||
var linkPreviewDescription: String?
|
||||
|
||||
/// The image id of the thumbnail
|
||||
@Published
|
||||
var linkPreviewImage: FileResource?
|
||||
var linkPreview: LinkPreview
|
||||
|
||||
/// The original url in the previous site layout
|
||||
let originalUrl: String?
|
||||
@ -27,42 +20,51 @@ final class LocalizedTag: ObservableObject {
|
||||
init(content: Content,
|
||||
urlComponent: String,
|
||||
name: String,
|
||||
linkPreviewTitle: String? = nil,
|
||||
linkPreviewDescription: String? = nil,
|
||||
linkPreviewImage: FileResource? = nil,
|
||||
linkPreview: LinkPreview = .init(),
|
||||
originalUrl: String? = nil) {
|
||||
self.content = content
|
||||
self.urlComponent = urlComponent
|
||||
self.name = name
|
||||
self.linkPreviewTitle = linkPreviewTitle
|
||||
self.linkPreviewDescription = linkPreviewDescription
|
||||
self.linkPreviewImage = linkPreviewImage
|
||||
self.linkPreview = linkPreview
|
||||
self.originalUrl = originalUrl
|
||||
}
|
||||
|
||||
func isValid(urlComponent: String) -> Bool {
|
||||
!urlComponent.isEmpty &&
|
||||
content.isValidIdForTagOrPageOrPost(urlComponent) &&
|
||||
!content.containsTag(withUrlComponent: urlComponent)
|
||||
}
|
||||
|
||||
/// The title to display when considering multiple items of this tag
|
||||
var title: String {
|
||||
linkPreviewTitle ?? name
|
||||
linkPreview.title ?? name
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedTag: LinkPreviewItem {
|
||||
|
||||
}
|
||||
// MARK: Storage
|
||||
|
||||
extension LocalizedTag {
|
||||
|
||||
var tagFile: LocalizedTagFile {
|
||||
convenience init(context: LoadingContext, data: Data) {
|
||||
self.init(
|
||||
content: context.content,
|
||||
urlComponent: data.urlComponent,
|
||||
name: data.name,
|
||||
linkPreview: .init(context: context, data: data.linkPreview),
|
||||
originalUrl: data.originalUrl)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let urlComponent: String
|
||||
let name: String
|
||||
let linkPreview: LinkPreview.Data
|
||||
let originalUrl: String?
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(urlComponent: urlComponent,
|
||||
name: name,
|
||||
linkPreviewTitle: linkPreviewTitle,
|
||||
linkPreviewDescription: linkPreviewDescription,
|
||||
linkPreviewImage: linkPreviewImage?.id,
|
||||
originalURL: originalUrl)
|
||||
linkPreview: linkPreview.data,
|
||||
originalUrl: originalUrl)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
final class Page: Item {
|
||||
final class Page: Item, DateItem, LocalizedItem {
|
||||
|
||||
override var itemType: ItemType { .page }
|
||||
|
||||
/**
|
||||
The external link this page points to.
|
||||
@ -38,9 +40,7 @@ final class Page: Item {
|
||||
@Published
|
||||
var tags: [Tag]
|
||||
|
||||
/**
|
||||
Additional files to copy, because the page content references them
|
||||
*/
|
||||
/// Additional files to copy, because the page content references them
|
||||
@Published
|
||||
var requiredFiles: [FileResource]
|
||||
|
||||
@ -141,7 +141,7 @@ final class Page: Item {
|
||||
content.settings.paths.pagesOutputFolderPath + "/" + localized(in: language).urlString
|
||||
}
|
||||
|
||||
override var itemType: ItemType {
|
||||
override var itemReference: ItemReference {
|
||||
.page(self)
|
||||
}
|
||||
|
||||
@ -161,15 +161,57 @@ final class Page: Item {
|
||||
if requiredFiles.contains(file) {
|
||||
requiredFiles.remove(file)
|
||||
}
|
||||
english.remove(linkPreviewImage: file)
|
||||
german.remove(linkPreviewImage: file)
|
||||
english.linkPreview.remove(file)
|
||||
german.linkPreview.remove(file)
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: DateItem {
|
||||
// MARK: Storage
|
||||
|
||||
}
|
||||
extension Page {
|
||||
|
||||
extension Page: LocalizedItem {
|
||||
|
||||
convenience init(context: LoadingContext, id: String, data: Data) {
|
||||
self.init(
|
||||
content: context.content,
|
||||
id: id,
|
||||
externalLink: data.externalLink,
|
||||
isDraft: data.isDraft,
|
||||
createdDate: data.createdDate,
|
||||
hideDate: data.hideDate ?? false,
|
||||
startDate: data.startDate,
|
||||
endDate: data.endDate,
|
||||
german: .init(context: context, data: data.german),
|
||||
english: .init(context: context, data: data.english),
|
||||
tags: data.tags.compactMap(context.tag),
|
||||
requiredFiles: data.requiredFiles?.compactMap(context.file) ?? [])
|
||||
}
|
||||
|
||||
/// The structure to store the metadata of a page on disk
|
||||
struct Data: Codable {
|
||||
let isDraft: Bool
|
||||
let externalLink: String?
|
||||
let tags: [String]
|
||||
let hideDate: Bool?
|
||||
let createdDate: Date
|
||||
let startDate: Date
|
||||
let endDate: Date?
|
||||
let german: LocalizedPage.Data
|
||||
let english: LocalizedPage.Data
|
||||
let requiredFiles: [String]?
|
||||
}
|
||||
|
||||
|
||||
var data: Data {
|
||||
.init(
|
||||
isDraft: isDraft,
|
||||
externalLink: externalLink,
|
||||
tags: tags.map { $0.id },
|
||||
hideDate: hideDate ? true : nil,
|
||||
createdDate: createdDate,
|
||||
startDate: startDate,
|
||||
endDate: hasEndDate ? endDate : nil,
|
||||
german: german.data,
|
||||
english: english.data,
|
||||
requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted())
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
final class Post: Item {
|
||||
final class Post: Item, DateItem, LocalizedItem {
|
||||
|
||||
override var itemType: ItemType { .post }
|
||||
|
||||
@Published
|
||||
var isDraft: Bool
|
||||
@ -142,10 +144,42 @@ final class Post: Item {
|
||||
}
|
||||
}
|
||||
|
||||
extension Post: DateItem {
|
||||
|
||||
}
|
||||
|
||||
extension Post: LocalizedItem {
|
||||
extension Post {
|
||||
|
||||
convenience init(context: LoadingContext, id: String, data: Data) {
|
||||
self.init(
|
||||
content: context.content,
|
||||
id: id,
|
||||
isDraft: data.isDraft,
|
||||
createdDate: data.createdDate,
|
||||
startDate: data.startDate,
|
||||
endDate: data.endDate,
|
||||
tags: data.tags.compactMap(context.tag),
|
||||
german: .init(context: context, data: data.german),
|
||||
english: .init(context: context, data: data.english),
|
||||
linkedPage: data.linkedPageId.map(context.page))
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let isDraft: Bool
|
||||
let createdDate: Date
|
||||
let startDate: Date
|
||||
let endDate: Date?
|
||||
let tags: [String]
|
||||
let german: LocalizedPost.Data
|
||||
let english: LocalizedPost.Data
|
||||
let linkedPageId: String?
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(
|
||||
isDraft: isDraft,
|
||||
createdDate: createdDate,
|
||||
startDate: startDate,
|
||||
endDate: hasEndDate ? endDate : nil,
|
||||
tags: tags.map { $0.id },
|
||||
german: german.data,
|
||||
english: english.data,
|
||||
linkedPageId: linkedPage?.id)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
final class AudioPlayerSettings: ObservableObject {
|
||||
final class AudioPlayerSettings: ObservableObject, LocalizedItem {
|
||||
|
||||
@Published
|
||||
var playlistCoverImageSize: Int
|
||||
@ -34,24 +34,6 @@ final class AudioPlayerSettings: ObservableObject {
|
||||
self.english = english
|
||||
}
|
||||
|
||||
init(file: AudioPlayerSettingsFile, files: [String : FileResource]) {
|
||||
self.playlistCoverImageSize = file.playlistCoverImageSize
|
||||
self.smallCoverImageSize = file.smallCoverImageSize
|
||||
self.audioPlayerJsFile = file.audioPlayerJsFile.map { files[$0] }
|
||||
self.audioPlayerCssFile = file.audioPlayerCssFile.map { files[$0] }
|
||||
self.german = .init(file: file.german)
|
||||
self.english = .init(file: file.english)
|
||||
}
|
||||
|
||||
var file: AudioPlayerSettingsFile {
|
||||
.init(playlistCoverImageSize: playlistCoverImageSize,
|
||||
smallCoverImageSize: smallCoverImageSize,
|
||||
audioPlayerJsFile: audioPlayerJsFile?.id,
|
||||
audioPlayerCssFile: audioPlayerCssFile?.id,
|
||||
german: german.file,
|
||||
english: english.file)
|
||||
}
|
||||
|
||||
func remove(_ file: FileResource) {
|
||||
if audioPlayerJsFile == file {
|
||||
audioPlayerJsFile = nil
|
||||
@ -62,17 +44,37 @@ final class AudioPlayerSettings: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
extension AudioPlayerSettings {
|
||||
|
||||
static let `default`: AudioPlayerSettings = .init(
|
||||
playlistCoverImageSize: 280,
|
||||
smallCoverImageSize: 78,
|
||||
audioPlayerJsFile: nil,
|
||||
audioPlayerCssFile: nil,
|
||||
german: .init(playlistText: "Wiedergabeliste"),
|
||||
english: .init(playlistText: "Playlist"))
|
||||
}
|
||||
convenience init(context: LoadingContext, data: Data) {
|
||||
self.init(
|
||||
playlistCoverImageSize: data.playlistCoverImageSize,
|
||||
smallCoverImageSize: data.smallCoverImageSize,
|
||||
audioPlayerJsFile: data.audioPlayerJsFile.map(context.file),
|
||||
audioPlayerCssFile: data.audioPlayerCssFile.map(context.file),
|
||||
german: .init(data: data.german),
|
||||
english: .init(data: data.english))
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(playlistCoverImageSize: playlistCoverImageSize,
|
||||
smallCoverImageSize: smallCoverImageSize,
|
||||
audioPlayerJsFile: audioPlayerJsFile?.id,
|
||||
audioPlayerCssFile: audioPlayerCssFile?.id,
|
||||
german: german.data,
|
||||
english: english.data)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let playlistCoverImageSize: Int
|
||||
let smallCoverImageSize: Int
|
||||
let audioPlayerJsFile: String?
|
||||
let audioPlayerCssFile: String?
|
||||
let german: LocalizedAudioPlayerSettings.Data
|
||||
let english: LocalizedAudioPlayerSettings.Data
|
||||
}
|
||||
|
||||
extension AudioPlayerSettings: LocalizedItem {
|
||||
|
||||
}
|
||||
|
@ -8,12 +8,21 @@ final class LocalizedAudioPlayerSettings: ObservableObject {
|
||||
init(playlistText: String) {
|
||||
self.playlistText = playlistText
|
||||
}
|
||||
}
|
||||
|
||||
init(file: LocalizedAudioPlayerSettingsFile) {
|
||||
self.playlistText = file.playlistText
|
||||
// MARK: Storage
|
||||
|
||||
extension LocalizedAudioPlayerSettings {
|
||||
|
||||
convenience init(data: Data) {
|
||||
self.init(playlistText: data.playlistText)
|
||||
}
|
||||
|
||||
var file: LocalizedAudioPlayerSettingsFile {
|
||||
var data: Data {
|
||||
.init(playlistText: playlistText)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let playlistText: String
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,21 @@ final class LocalizedNavigationSettings: ObservableObject {
|
||||
init(rootUrl: String) {
|
||||
self.rootUrl = rootUrl
|
||||
}
|
||||
}
|
||||
|
||||
init(file: LocalizedNavigationSettingsFile) {
|
||||
self.rootUrl = file.rootUrl
|
||||
// MARK: Storage
|
||||
|
||||
extension LocalizedNavigationSettings {
|
||||
|
||||
convenience init(data: Data) {
|
||||
self.init(rootUrl: data.rootUrl)
|
||||
}
|
||||
|
||||
var file: LocalizedNavigationSettingsFile {
|
||||
struct Data: Codable {
|
||||
let rootUrl: String
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(rootUrl: rootUrl)
|
||||
}
|
||||
}
|
||||
|
@ -14,14 +14,25 @@ final class LocalizedPageSettings: ObservableObject {
|
||||
self.emptyPageTitle = emptyPageTitle
|
||||
self.emptyPageText = emptyPageText
|
||||
}
|
||||
}
|
||||
|
||||
init(file: LocalizedPageSettingsFile) {
|
||||
self.emptyPageTitle = file.emptyPageTitle
|
||||
self.emptyPageText = file.emptyPageText
|
||||
// MARK: Storage
|
||||
|
||||
extension LocalizedPageSettings {
|
||||
|
||||
convenience init(data: Data) {
|
||||
self.init(
|
||||
emptyPageTitle: data.emptyPageTitle,
|
||||
emptyPageText: data.emptyPageText)
|
||||
}
|
||||
|
||||
var file: LocalizedPageSettingsFile {
|
||||
var data: Data {
|
||||
.init(emptyPageTitle: emptyPageTitle,
|
||||
emptyPageText: emptyPageText)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let emptyPageTitle: String
|
||||
let emptyPageText: String
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,23 @@ import Foundation
|
||||
|
||||
final class LocalizedPostSettings: ObservableObject {
|
||||
|
||||
/// The page title for the post feed
|
||||
@Published
|
||||
var title: String
|
||||
|
||||
/// The page description for the post feed
|
||||
@Published
|
||||
var description: String
|
||||
|
||||
/// The path to the feed in the final website, appended with the page number
|
||||
@Published
|
||||
var feedUrlPrefix: String
|
||||
|
||||
/**
|
||||
The text to display when linking to a page
|
||||
|
||||
Each post may define a custom text.
|
||||
*/
|
||||
@Published
|
||||
var defaultPageLinkText: String
|
||||
|
||||
@ -20,21 +28,32 @@ final class LocalizedPostSettings: ObservableObject {
|
||||
self.feedUrlPrefix = feedUrlPrefix
|
||||
self.defaultPageLinkText = defaultPageLinkText
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Storage
|
||||
// MARK: Storage
|
||||
|
||||
init(file: LocalizedPostSettingsFile) {
|
||||
self.title = file.feedTitle
|
||||
self.description = file.feedDescription
|
||||
self.feedUrlPrefix = file.feedUrlPrefix
|
||||
self.defaultPageLinkText = file.defaultPageLinkText ?? "View"
|
||||
extension LocalizedPostSettings {
|
||||
|
||||
convenience init(data: Data) {
|
||||
self.init(
|
||||
title: data.feedTitle,
|
||||
description: data.feedDescription,
|
||||
feedUrlPrefix: data.feedUrlPrefix,
|
||||
defaultPageLinkText: data.defaultPageLinkText)
|
||||
}
|
||||
|
||||
var file: LocalizedPostSettingsFile {
|
||||
var data: Data {
|
||||
.init(
|
||||
feedTitle: title,
|
||||
feedDescription: description,
|
||||
feedUrlPrefix: feedUrlPrefix,
|
||||
defaultPageLinkText: defaultPageLinkText)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let feedTitle: String
|
||||
let feedDescription: String
|
||||
let feedUrlPrefix: String
|
||||
let defaultPageLinkText: String
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
final class NavigationSettings: ObservableObject {
|
||||
final class NavigationSettings: ObservableObject, LocalizedItem {
|
||||
|
||||
/// The items to show in the navigation bar
|
||||
@Published
|
||||
@ -19,23 +19,31 @@ final class NavigationSettings: ObservableObject {
|
||||
self.german = german
|
||||
self.english = english
|
||||
}
|
||||
|
||||
init(file: NavigationSettingsFile, map: (String) -> Item?) {
|
||||
self.navigationItems = file.navigationItems.compactMap(map)
|
||||
self.german = LocalizedNavigationSettings(file: file.german)
|
||||
self.english = LocalizedNavigationSettings(file: file.english)
|
||||
}
|
||||
|
||||
var file: NavigationSettingsFile {
|
||||
.init(
|
||||
navigationItems: navigationItems.map { $0.itemType.id },
|
||||
german: german.file,
|
||||
english: english.file)
|
||||
}
|
||||
}
|
||||
|
||||
extension NavigationSettings: LocalizedItem {
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
extension NavigationSettings {
|
||||
|
||||
convenience init(context: LoadingContext, data: NavigationSettings.Data) {
|
||||
self.init(
|
||||
navigationItems: data.navigationItems.compactMap(context.item),
|
||||
german: LocalizedNavigationSettings(data: data.german),
|
||||
english: LocalizedNavigationSettings(data: data.english))
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let navigationItems: [ItemId]
|
||||
let german: LocalizedNavigationSettings.Data
|
||||
let english: LocalizedNavigationSettings.Data
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(
|
||||
navigationItems: navigationItems.map { $0.itemId },
|
||||
german: german.data,
|
||||
english: english.data)
|
||||
}
|
||||
}
|
||||
|
||||
extension NavigationSettings {
|
||||
|
@ -32,30 +32,26 @@ final class PageSettings: ObservableObject {
|
||||
@Published
|
||||
var english: LocalizedPageSettings
|
||||
|
||||
init(file: PageSettingsFile, files: [String : FileResource]) {
|
||||
self.contentWidth = file.contentWidth
|
||||
self.largeImageWidth = file.largeImageWidth
|
||||
self.pageLinkImageSize = file.pageLinkImageSize
|
||||
self.defaultCssFile = file.defaultCssFile.map { files[$0] }
|
||||
self.codeHighlightingJsFile = file.codeHighlightingJsFile.map { files[$0] }
|
||||
self.modelViewerJsFile = file.modelViewerJsFile.map { files[$0] }
|
||||
self.imageCompareCssFile = file.imageCompareCssFile.map { files[$0] }
|
||||
self.imageCompareJsFile = file.imageCompareJsFile.map { files[$0] }
|
||||
self.german = .init(file: file.german)
|
||||
self.english = .init(file: file.english)
|
||||
}
|
||||
|
||||
var file: PageSettingsFile {
|
||||
.init(contentWidth: contentWidth,
|
||||
largeImageWidth: largeImageWidth,
|
||||
pageLinkImageSize: pageLinkImageSize,
|
||||
defaultCssFile: defaultCssFile?.id,
|
||||
codeHighlightingJsFile: codeHighlightingJsFile?.id,
|
||||
modelViewerJsFile: modelViewerJsFile?.id,
|
||||
imageCompareJsFile: imageCompareJsFile?.id,
|
||||
imageCompareCssFile: imageCompareCssFile?.id,
|
||||
german: german.file,
|
||||
english: english.file)
|
||||
init(contentWidth: Int,
|
||||
largeImageWidth: Int,
|
||||
pageLinkImageSize: Int,
|
||||
defaultCssFile: FileResource? = nil,
|
||||
codeHighlightingJsFile: FileResource? = nil,
|
||||
modelViewerJsFile: FileResource? = nil,
|
||||
imageCompareJsFile: FileResource? = nil,
|
||||
imageCompareCssFile: FileResource? = nil,
|
||||
german: LocalizedPageSettings,
|
||||
english: LocalizedPageSettings) {
|
||||
self.contentWidth = contentWidth
|
||||
self.largeImageWidth = largeImageWidth
|
||||
self.pageLinkImageSize = pageLinkImageSize
|
||||
self.defaultCssFile = defaultCssFile
|
||||
self.codeHighlightingJsFile = codeHighlightingJsFile
|
||||
self.modelViewerJsFile = modelViewerJsFile
|
||||
self.imageCompareJsFile = imageCompareJsFile
|
||||
self.imageCompareCssFile = imageCompareCssFile
|
||||
self.german = german
|
||||
self.english = english
|
||||
}
|
||||
|
||||
func remove(_ file: FileResource) {
|
||||
@ -77,6 +73,52 @@ final class PageSettings: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
extension PageSettings {
|
||||
|
||||
convenience init(context: LoadingContext, data: Data) {
|
||||
self.init(
|
||||
contentWidth: data.contentWidth,
|
||||
largeImageWidth: data.largeImageWidth,
|
||||
pageLinkImageSize: data.pageLinkImageSize,
|
||||
defaultCssFile: data.defaultCssFile.map(context.file),
|
||||
codeHighlightingJsFile: data.codeHighlightingJsFile.map(context.file),
|
||||
modelViewerJsFile: data.modelViewerJsFile.map(context.file),
|
||||
imageCompareJsFile: data.imageCompareJsFile.map(context.file),
|
||||
imageCompareCssFile: data.imageCompareCssFile.map(context.file),
|
||||
german: .init(data: data.german),
|
||||
english: .init(data: data.english))
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(contentWidth: contentWidth,
|
||||
largeImageWidth: largeImageWidth,
|
||||
pageLinkImageSize: pageLinkImageSize,
|
||||
defaultCssFile: defaultCssFile?.id,
|
||||
codeHighlightingJsFile: codeHighlightingJsFile?.id,
|
||||
modelViewerJsFile: modelViewerJsFile?.id,
|
||||
imageCompareJsFile: imageCompareJsFile?.id,
|
||||
imageCompareCssFile: imageCompareCssFile?.id,
|
||||
german: german.data,
|
||||
english: english.data)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let contentWidth: Int
|
||||
let largeImageWidth: Int
|
||||
let pageLinkImageSize: Int
|
||||
let defaultCssFile: String?
|
||||
let codeHighlightingJsFile: String?
|
||||
let modelViewerJsFile: String?
|
||||
let imageCompareJsFile: String?
|
||||
let imageCompareCssFile: String?
|
||||
let german: LocalizedPageSettings.Data
|
||||
let english: LocalizedPageSettings.Data
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PageSettings: LocalizedItem {
|
||||
|
||||
}
|
||||
|
@ -23,23 +23,54 @@ final class PathSettings: ObservableObject {
|
||||
@Published
|
||||
var tagsOutputFolderPath: String
|
||||
|
||||
init(file: PathSettingsFile) {
|
||||
self.assetsOutputFolderPath = file.assetsOutputFolderPath
|
||||
self.pagesOutputFolderPath = file.pagesOutputFolderPath
|
||||
self.imagesOutputFolderPath = file.imagesOutputFolderPath
|
||||
self.filesOutputFolderPath = file.filesOutputFolderPath
|
||||
self.videosOutputFolderPath = file.videosOutputFolderPath
|
||||
self.tagsOutputFolderPath = file.tagsOutputFolderPath
|
||||
self.audioOutputFolderPath = file.audioOutputFolderPath
|
||||
}
|
||||
|
||||
var file: PathSettingsFile {
|
||||
.init(assetsOutputFolderPath: assetsOutputFolderPath,
|
||||
pagesOutputFolderPath: pagesOutputFolderPath,
|
||||
imagesOutputFolderPath: imagesOutputFolderPath,
|
||||
filesOutputFolderPath: filesOutputFolderPath,
|
||||
videosOutputFolderPath: videosOutputFolderPath,
|
||||
tagsOutputFolderPath: tagsOutputFolderPath,
|
||||
audioOutputFolderPath: audioOutputFolderPath)
|
||||
init(assetsOutputFolderPath: String,
|
||||
pagesOutputFolderPath: String,
|
||||
imagesOutputFolderPath: String,
|
||||
filesOutputFolderPath: String,
|
||||
videosOutputFolderPath: String,
|
||||
audioOutputFolderPath: String,
|
||||
tagsOutputFolderPath: String) {
|
||||
self.assetsOutputFolderPath = assetsOutputFolderPath
|
||||
self.pagesOutputFolderPath = pagesOutputFolderPath
|
||||
self.imagesOutputFolderPath = imagesOutputFolderPath
|
||||
self.filesOutputFolderPath = filesOutputFolderPath
|
||||
self.videosOutputFolderPath = videosOutputFolderPath
|
||||
self.audioOutputFolderPath = audioOutputFolderPath
|
||||
self.tagsOutputFolderPath = tagsOutputFolderPath
|
||||
}
|
||||
}
|
||||
|
||||
extension PathSettings {
|
||||
|
||||
convenience init(data: Data) {
|
||||
self.init(
|
||||
assetsOutputFolderPath: data.assetsOutputFolderPath,
|
||||
pagesOutputFolderPath: data.pagesOutputFolderPath,
|
||||
imagesOutputFolderPath: data.imagesOutputFolderPath,
|
||||
filesOutputFolderPath: data.filesOutputFolderPath,
|
||||
videosOutputFolderPath: data.videosOutputFolderPath,
|
||||
audioOutputFolderPath: data.audioOutputFolderPath,
|
||||
tagsOutputFolderPath: data.tagsOutputFolderPath)
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(
|
||||
assetsOutputFolderPath: assetsOutputFolderPath,
|
||||
pagesOutputFolderPath: pagesOutputFolderPath,
|
||||
imagesOutputFolderPath: imagesOutputFolderPath,
|
||||
filesOutputFolderPath: filesOutputFolderPath,
|
||||
videosOutputFolderPath: videosOutputFolderPath,
|
||||
audioOutputFolderPath: audioOutputFolderPath,
|
||||
tagsOutputFolderPath: tagsOutputFolderPath)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let assetsOutputFolderPath: String
|
||||
let pagesOutputFolderPath: String
|
||||
let imagesOutputFolderPath: String
|
||||
let filesOutputFolderPath: String
|
||||
let videosOutputFolderPath: String
|
||||
let audioOutputFolderPath: String
|
||||
let tagsOutputFolderPath: String
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
final class PostSettings: ObservableObject {
|
||||
final class PostSettings: ObservableObject, LocalizedItem {
|
||||
|
||||
/// The number of posts to show in a single page of the news feed
|
||||
@Published
|
||||
@ -41,28 +41,6 @@ final class PostSettings: ObservableObject {
|
||||
self.english = english
|
||||
}
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
init(file: PostSettingsFile, files: [String : FileResource]) {
|
||||
self.postsPerPage = file.postsPerPage
|
||||
self.contentWidth = file.contentWidth
|
||||
self.swiperCssFile = file.swiperCssFile.map { files[$0] }
|
||||
self.swiperJsFile = file.swiperJsFile.map { files[$0] }
|
||||
self.defaultCssFile = file.defaultCssFile.map { files[$0] }
|
||||
self.german = .init(file: file.german)
|
||||
self.english = .init(file: file.english)
|
||||
}
|
||||
|
||||
var file: PostSettingsFile {
|
||||
.init(postsPerPage: postsPerPage,
|
||||
contentWidth: contentWidth,
|
||||
swiperCssFile: swiperCssFile?.id,
|
||||
swiperJsFile: swiperJsFile?.id,
|
||||
defaultCssFile: defaultCssFile?.id,
|
||||
german: german.file,
|
||||
english: english.file)
|
||||
}
|
||||
|
||||
func remove(_ file: FileResource) {
|
||||
if swiperJsFile == file {
|
||||
swiperJsFile = nil
|
||||
@ -76,13 +54,38 @@ final class PostSettings: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
extension PostSettings {
|
||||
|
||||
static var `default`: PostSettings {
|
||||
.init(file: .default, files: [:])
|
||||
convenience init(context: LoadingContext, data: Data) {
|
||||
self.init(
|
||||
postsPerPage: data.postsPerPage,
|
||||
contentWidth: data.contentWidth,
|
||||
swiperCssFile: data.swiperCssFile.map(context.file),
|
||||
swiperJsFile: data.swiperJsFile.map(context.file),
|
||||
defaultCssFile: data.defaultCssFile.map(context.file),
|
||||
german: .init(data: data.german),
|
||||
english: .init(data: data.english))
|
||||
}
|
||||
|
||||
var data: PostSettings.Data {
|
||||
.init(postsPerPage: postsPerPage,
|
||||
contentWidth: contentWidth,
|
||||
swiperCssFile: swiperCssFile?.id,
|
||||
swiperJsFile: swiperJsFile?.id,
|
||||
defaultCssFile: defaultCssFile?.id,
|
||||
german: german.data,
|
||||
english: english.data)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let postsPerPage: Int
|
||||
let contentWidth: Int
|
||||
let swiperCssFile: String?
|
||||
let swiperJsFile: String?
|
||||
let defaultCssFile: String?
|
||||
let german: LocalizedPostSettings.Data
|
||||
let english: LocalizedPostSettings.Data
|
||||
}
|
||||
}
|
||||
|
||||
extension PostSettings: LocalizedItem {
|
||||
|
||||
}
|
||||
|
@ -30,25 +30,6 @@ final class Settings: ObservableObject {
|
||||
self.audioPlayer = audioPlayer
|
||||
}
|
||||
|
||||
init(file: SettingsFile, files: [String : FileResource], map: (String) -> Item?) {
|
||||
self.navigation = NavigationSettings(file: file.navigation, map: map)
|
||||
|
||||
self.posts = PostSettings(file: file.posts, files: files)
|
||||
self.pages = PageSettings(file: file.pages, files: files)
|
||||
self.paths = PathSettings(file: file.paths)
|
||||
self.audioPlayer = .init(file: file.audioPlayer, files: files)
|
||||
}
|
||||
|
||||
func file(tagOverview: TagOverviewPage?) -> SettingsFile {
|
||||
.init(
|
||||
paths: paths.file,
|
||||
navigation: navigation.file,
|
||||
posts: posts.file,
|
||||
pages: pages.file,
|
||||
audioPlayer: audioPlayer.file,
|
||||
tagOverview: tagOverview?.file)
|
||||
}
|
||||
|
||||
func remove(_ file: FileResource) {
|
||||
pages.remove(file)
|
||||
posts.remove(file)
|
||||
@ -56,6 +37,39 @@ final class Settings: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
extension Settings {
|
||||
|
||||
convenience init(context: LoadingContext, data: Settings.Data) {
|
||||
self.init(
|
||||
paths: .init(data: data.paths),
|
||||
navigation: .init(context: context, data: data.navigation),
|
||||
posts: .init(context: context, data: data.posts),
|
||||
pages: .init(context: context, data: data.pages),
|
||||
audioPlayer: .init(context: context, data: data.audioPlayer))
|
||||
}
|
||||
|
||||
func data(tagOverview: Tag?) -> Data {
|
||||
.init(
|
||||
paths: paths.data,
|
||||
navigation: navigation.data,
|
||||
posts: posts.data,
|
||||
pages: pages.data,
|
||||
audioPlayer: audioPlayer.data,
|
||||
tagOverview: tagOverview?.data)
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
let paths: PathSettings.Data
|
||||
let navigation: NavigationSettings.Data
|
||||
let posts: PostSettings.Data
|
||||
let pages: PageSettings.Data
|
||||
let audioPlayer: AudioPlayerSettings.Data
|
||||
let tagOverview: Tag.Data?
|
||||
}
|
||||
}
|
||||
|
||||
extension Settings {
|
||||
|
||||
static let `default`: Settings = .init(
|
||||
@ -65,3 +79,70 @@ extension Settings {
|
||||
pages: .default,
|
||||
audioPlayer: .default)
|
||||
}
|
||||
|
||||
extension AudioPlayerSettings {
|
||||
|
||||
static let `default`: AudioPlayerSettings = .init(
|
||||
playlistCoverImageSize: 280,
|
||||
smallCoverImageSize: 78,
|
||||
audioPlayerJsFile: nil,
|
||||
audioPlayerCssFile: nil,
|
||||
german: .init(playlistText: "Wiedergabeliste"),
|
||||
english: .init(playlistText: "Playlist"))
|
||||
}
|
||||
|
||||
extension PostSettings {
|
||||
|
||||
static var `default`: PostSettings {
|
||||
.init(postsPerPage: 25,
|
||||
contentWidth: 600,
|
||||
swiperCssFile: nil,
|
||||
swiperJsFile: nil,
|
||||
defaultCssFile: nil,
|
||||
german: .init(
|
||||
title: "Beiträge",
|
||||
description: "Alle Beiträge",
|
||||
feedUrlPrefix: "blog",
|
||||
defaultPageLinkText: "Anzeigen"),
|
||||
english: .init(
|
||||
title: "Blog posts",
|
||||
description: "All blog posts",
|
||||
feedUrlPrefix: "blog",
|
||||
defaultPageLinkText: "View"))
|
||||
}
|
||||
}
|
||||
|
||||
extension PathSettings {
|
||||
|
||||
static var `default`: PathSettings {
|
||||
.init(
|
||||
assetsOutputFolderPath: "asset",
|
||||
pagesOutputFolderPath: "page",
|
||||
imagesOutputFolderPath: "image",
|
||||
filesOutputFolderPath: "file",
|
||||
videosOutputFolderPath: "video",
|
||||
audioOutputFolderPath: "audio",
|
||||
tagsOutputFolderPath: "tag")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PageSettings {
|
||||
|
||||
static var `default`: PageSettings {
|
||||
.init(contentWidth: 600,
|
||||
largeImageWidth: 1200,
|
||||
pageLinkImageSize: 180,
|
||||
defaultCssFile: nil,
|
||||
codeHighlightingJsFile: nil,
|
||||
modelViewerJsFile: nil,
|
||||
imageCompareJsFile: nil,
|
||||
imageCompareCssFile: nil,
|
||||
german: .init(
|
||||
emptyPageTitle: "Leere Seite",
|
||||
emptyPageText: "Diese Seite ist leer"),
|
||||
english: .init(
|
||||
emptyPageTitle: "Empty page",
|
||||
emptyPageText: "This page is empty"))
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
final class Tag: Item {
|
||||
class Tag: Item, LocalizedItem {
|
||||
|
||||
override var itemType: ItemType { .tag }
|
||||
|
||||
@Published
|
||||
var isVisible: Bool
|
||||
@ -59,7 +61,7 @@ final class Tag: Item {
|
||||
localized(in: language).title
|
||||
}
|
||||
|
||||
override var itemType: ItemType {
|
||||
override var itemReference: ItemReference {
|
||||
.tagPage(self)
|
||||
}
|
||||
|
||||
@ -68,21 +70,35 @@ final class Tag: Item {
|
||||
}
|
||||
|
||||
func remove(_ file: FileResource) {
|
||||
english.remove(linkPreviewImage: file)
|
||||
german.remove(linkPreviewImage: file)
|
||||
english.linkPreview.remove(file)
|
||||
german.linkPreview.remove(file)
|
||||
}
|
||||
}
|
||||
|
||||
extension Tag: LocalizedItem {
|
||||
|
||||
}
|
||||
// MARK: Storage
|
||||
|
||||
extension Tag {
|
||||
|
||||
var file: TagFile {
|
||||
.init(id: id,
|
||||
isVisible: isVisible,
|
||||
german: german.tagFile,
|
||||
english: english.tagFile)
|
||||
convenience init(context: LoadingContext, id: String, data: Data) {
|
||||
self.init(
|
||||
content: context.content,
|
||||
id: id,
|
||||
isVisible: data.isVisible ?? true,
|
||||
german: .init(context: context, data: data.german),
|
||||
english: .init(context: context, data: data.english))
|
||||
}
|
||||
|
||||
struct Data: Codable {
|
||||
// Defaults to true if unset
|
||||
let isVisible: Bool?
|
||||
let german: LocalizedTag.Data
|
||||
let english: LocalizedTag.Data
|
||||
}
|
||||
|
||||
var data: Data {
|
||||
.init(
|
||||
isVisible: isVisible ? nil : false,
|
||||
german: german.data,
|
||||
english: english.data)
|
||||
}
|
||||
}
|
||||
|
7
CHDataManagement/Model/TagOverview.swift
Normal file
7
CHDataManagement/Model/TagOverview.swift
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
final class TagOverview: Tag {
|
||||
|
||||
override var itemId: ItemId {
|
||||
.init(type: .tagOverview, id: id)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user