Rework storage structs, link preview

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

View File

@ -1,158 +0,0 @@
import Foundation
extension Content {
private func convert(_ tag: LocalizedTagFile, images: [String : FileResource]) -> LocalizedTag {
LocalizedTag(
content: self,
urlComponent: tag.urlComponent,
name: tag.name,
linkPreviewTitle: tag.linkPreviewTitle,
linkPreviewDescription: tag.linkPreviewDescription,
linkPreviewImage: tag.linkPreviewImage.map { images[$0] },
originalUrl: tag.originalURL)
}
private func convert(_ page: LocalizedPageFile, images: [String : FileResource]) -> LocalizedPage {
LocalizedPage(
content: self,
urlString: page.url,
title: page.title,
lastModified: page.lastModifiedDate,
originalUrl: page.originalURL,
linkPreviewImage: page.linkPreviewImage.map { images[$0] },
linkPreviewTitle: page.linkPreviewTitle,
linkPreviewDescription: page.linkPreviewDescription,
hideTitle: page.hideTitle ?? false)
}
func loadFromDisk() throws {
guard storage.contentScope != nil else {
print("Storage not initialized, not loading content")
throw StorageAccessError.noBookmarkData
}
let settings = storage.loadSettings() ?? .default
guard let tagData = storage.loadAllTags() else {
print("Failed to load file tags")
return
}
if tagData.isEmpty { print("No tags loaded") }
guard let pagesData = storage.loadAllPages() else {
print("Failed to load file pages")
return
}
if pagesData.isEmpty { print("No pages loaded") }
guard let postsData = storage.loadAllPosts() else {
print("Failed to load file posts")
return
}
if postsData.isEmpty { print("No posts loaded") }
guard let fileList = storage.loadAllFiles() else {
print("Failed to load file list")
return
}
if fileList.isEmpty { print("No files loaded") }
print("Loaded data from disk, processing...")
// All data loaded from storage, start constructing the data model
let files: [String : FileResource] = fileList.reduce(into: [:]) { (files, data) in
let fileId = data.key
let fileData = data.value.data
let isExternal = data.value.isExternal
files[fileId] = FileResource(content: self, id: fileId, file: fileData, isExternalFile: isExternal)
}
let images = files.filter { $0.value.type.isImage }
let tags = tagData.reduce(into: [:]) { (tags, data) in
tags[data.key] = Tag(
content: self,
id: data.value.id,
isVisible: data.value.isVisible,
german: convert(data.value.german, images: images),
english: convert(data.value.english, images: images))
}
let pages: [String : Page] = loadPages(pagesData, tags: tags, files: files)
let posts: [String : Post] = postsData.reduce(into: [:]) { dict, data in
let (postId, post) = data
let linkedPage = post.linkedPageId.map { pages[$0] }
let german = LocalizedPost(content: self, file: post.german, images: images)
let english = LocalizedPost(content: self, file: post.english, images: images)
dict[postId] = Post(
content: self,
id: postId,
isDraft: post.isDraft,
createdDate: post.createdDate,
startDate: post.startDate,
endDate: post.endDate,
tags: post.tags.map { tags[$0]! },
german: german,
english: english,
linkedPage: linkedPage)
}
let tagOverview = settings.tagOverview.map { file in
TagOverviewPage(
content: self,
german: .init(content: self, file: file.german, image: file.german.linkPreviewImage.map { files[$0] }),
english: .init(content: self, file: file.english, image: file.english.linkPreviewImage.map { files[$0] }))
}
self.tags = tags.values.sorted()
self.pages = pages.values.sorted(ascending: false) { $0.startDate }
self.files = files.values.sorted { $0.id }
self.posts = posts.values.sorted(ascending: false) { $0.startDate }
self.tagOverview = tagOverview
self.settings = .init(file: settings, files: files) { raw in
#warning("Notify about missing links")
guard let type = ItemType(rawValue: raw, posts: posts, pages: pages, tags: tags) else {
return nil
}
switch type {
case .general:
return nil
case .post(let post):
return post
case .feed:
return nil // TODO: Provide feed object
case .page(let page):
return page
case .tagPage(let tag):
return tag
case .tagOverview:
return tagOverview
}
}
print("Content loaded")
}
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], files: [String : FileResource]) -> [String : Page] {
pagesData.reduce(into: [:]) { pages, data in
let (pageId, page) = data
pages[pageId] = Page(
content: self,
id: pageId,
externalLink: page.externalLink,
isDraft: page.isDraft,
createdDate: page.createdDate,
hideDate: page.hideDate ?? false,
startDate: page.startDate,
endDate: page.endDate,
german: convert(page.german, images: files),
english: convert(page.english, images: files),
tags: page.tags.compactMap { tags[$0] },
requiredFiles: page.requiredFiles?.compactMap { files[$0] } ?? [])
}
}
}

View File

@ -2,22 +2,25 @@ import Foundation
extension Content {
func saveToDisk() throws {
func saveToDisk() -> Bool {
guard didLoadContent else { return false }
guard storage.contentScope != nil else {
print("Storage not initialized, not saving content")
return
return false
}
var failedSaves = 0
failedSaves += pages.count { !storage.save(pageMetadata: $0.pageFile, for: $0.id) }
failedSaves += posts.count { !storage.save(post: $0.postFile, for: $0.id) }
failedSaves += tags.count { !storage.save(tagMetadata: $0.file, for: $0.id) }
failedSaves.increment(!storage.save(settings: settings.file(tagOverview: tagOverview)))
failedSaves += files.count { !storage.save(fileInfo: $0.fileInfo, for: $0.id) }
failedSaves += pages.count { !storage.save(pageMetadata: $0.data, for: $0.id) }
failedSaves += posts.count { !storage.save(post: $0.data, for: $0.id) }
failedSaves += tags.count { !storage.save(tagMetadata: $0.data, for: $0.id) }
failedSaves.increment(!storage.save(settings: settings.data(tagOverview: tagOverview)))
failedSaves += files.count { !storage.save(fileInfo: $0.data, for: $0.id) }
if failedSaves > 0 {
print("Save partially failed with \(failedSaves) errors")
return false
}
return true
}
func removeUnlinkedFiles() -> Bool {
@ -37,49 +40,3 @@ extension Content {
return success
}
}
private extension Page {
var pageFile: PageFile {
.init(isDraft: isDraft,
externalLink: externalLink,
tags: tags.map { $0.id },
hideDate: hideDate ? true : nil,
createdDate: createdDate,
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
german: german.pageFile,
english: english.pageFile,
requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted())
}
}
private extension LocalizedPage {
var pageFile: LocalizedPageFile {
.init(url: urlString,
title: title,
linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription,
lastModifiedDate: lastModified,
originalURL: originalUrl,
hideTitle: hideTitle ? true : nil)
}
}
private extension Post {
var postFile: PostFile {
.init(
isDraft: isDraft,
createdDate: createdDate,
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
tags: tags.map { $0.id },
german: german.postFile,
english: english.postFile,
linkedPageId: linkedPage?.id)
}
}

View File

@ -4,6 +4,9 @@ import Combine
final class Content: ObservableObject {
@Published
var didLoadContent = false
@ObservedObject
var storage: Storage
@ -23,7 +26,7 @@ final class Content: ObservableObject {
var files: [FileResource]
@Published
var tagOverview: TagOverviewPage?
var tagOverview: Tag?
@Published
var results: GenerationResults
@ -47,7 +50,7 @@ final class Content: ObservableObject {
pages: [Page],
tags: [Tag],
files: [FileResource],
tagOverview: TagOverviewPage?) {
tagOverview: Tag?) {
self.settings = settings
self.posts = posts
self.pages = pages
@ -112,16 +115,11 @@ final class Content: ObservableObject {
pages.insert(page, at: 0)
}
func update(contentPath: URL) {
func update(contentPath: URL, callback: @escaping ([String]) -> ()) {
guard storage.save(contentPath: contentPath) else {
return
}
clear()
do {
try loadFromDisk()
} catch {
print("Failed to reload content: \(error)")
}
loadFromDisk(callback: callback)
}
func remove(_ file: FileResource) {
@ -146,4 +144,29 @@ final class Content: ObservableObject {
func file(withOutputPath: String) -> FileResource? {
files.first { $0.absoluteUrl == withOutputPath }
}
func loadFromDisk(callback: @escaping (_ errors: [String]) -> ()) {
DispatchQueue.global().async {
let loader = ModelLoader(content: self, storage: self.storage)
let result = loader.load()
guard result.errors.isEmpty else {
DispatchQueue.main.async {
self.didLoadContent = false
callback(result.errors.sorted())
}
return
}
DispatchQueue.main.async {
self.files = result.files
self.posts = result.posts
self.pages = result.pages
self.tags = result.tags
self.settings = result.settings
self.tagOverview = result.tagOverview
self.didLoadContent = true
callback([])
}
}
}
}

View File

@ -1,34 +1,43 @@
import Foundation
import SwiftUI
final class FileResource: Item {
final class FileResource: Item, LocalizedItem {
let type: FileType
/// Indicate if the file content is stored by the app
@Published
var isExternallyStored: Bool
/// The file/image description in German
@Published
var german: String
var german: String?
/// The file/image description in English
@Published
var english: String
var english: String?
/// A version string of this resource, mostly for assets
@Published
var version: String?
/// A URL where the resource was copied/downloaded from
@Published
var sourceUrl: String?
/// The list of generated image versions for this image
@Published
var generatedImageVersions: Set<String>
/// A custom file path in the output folder where this file is located
@Published
var customOutputPath: String?
/// The date when the file was added
@Published
var addedDate: Date
/// The date when the file was last modified
@Published
var modifiedDate: Date
@ -53,8 +62,8 @@ final class FileResource: Item {
modifiedDate: Date = .now) {
self.type = FileType(fileExtension: id.fileExtension)
self.isExternallyStored = isExternallyStored
self.german = german ?? ""
self.english = english ?? ""
self.german = german
self.english = english
self.version = version
self.sourceUrl = sourceUrl
self.generatedImageVersions = generatedImageVersions
@ -64,20 +73,6 @@ final class FileResource: Item {
super.init(content: content, id: id)
}
init(content: Content, id: String, file: FileResourceFile, isExternalFile: Bool) {
self.type = FileType(fileExtension: id.fileExtension)
self.isExternallyStored = isExternalFile
self.german = file.germanDescription ?? ""
self.english = file.englishDescription ?? ""
self.version = file.version
self.sourceUrl = file.sourceUrl
self.generatedImageVersions = Set(file.generatedImages ?? [])
self.customOutputPath = file.customOutputPath
self.addedDate = file.addedDate
self.modifiedDate = file.modifiedDate
super.init(content: content, id: id)
}
/**
Only for bundle images
*/
@ -101,7 +96,7 @@ final class FileResource: Item {
content.storage.fileContent(for: id) ?? ""
}
func dataContent() -> Data? {
func dataContent() -> Foundation.Data? {
content.storage.fileData(for: id)
}
@ -131,7 +126,6 @@ final class FileResource: Item {
// Image must have changed, so force regeneration
DispatchQueue.main.async {
self.imageDimensions = size
self.didChange()
self.removeGeneratedImages()
}
}
@ -299,12 +293,34 @@ final class FileResource: Item {
}
}
extension FileResource: CustomStringConvertible {
var description: String {
id
}
}
extension FileResource {
var fileInfo: FileResourceFile {
convenience init(content: Content, id: String, data: FileResource.Data, isExternalFile: Bool) {
self.init(
content: content,
id: id,
isExternallyStored: isExternalFile,
english: data.englishDescription,
german: data.germanDescription,
version: data.version,
sourceUrl: data.sourceUrl,
generatedImageVersions: Set(data.generatedImages ?? []),
customOutputPath: data.customOutputPath,
addedDate: data.addedDate,
modifiedDate: data.modifiedDate)
}
var data: Data {
.init(
englishDescription: english.nonEmpty,
germanDescription: german.nonEmpty,
englishDescription: english,
germanDescription: german,
generatedImages: generatedImageVersions.sorted().nonEmpty,
customOutputPath: customOutputPath,
version: version,
@ -312,15 +328,16 @@ extension FileResource {
addedDate: addedDate,
modifiedDate: modifiedDate)
}
}
extension FileResource: LocalizedItem {
}
extension FileResource: CustomStringConvertible {
var description: String {
id
/// This struct holds metadata about a file resource that is stored in the content folder.
struct Data: Codable {
let englishDescription: String?
let germanDescription: String?
let generatedImages: [String]?
let customOutputPath: String?
let version: String?
let sourceUrl: String?
let addedDate: Date
let modifiedDate: Date
}
}

View File

@ -39,12 +39,20 @@ class Item: ObservableObject, Identifiable {
var itemType: ItemType {
fatalError()
}
var itemReference: ItemReference {
fatalError()
}
var itemId: ItemId {
.init(type: itemType, id: id)
}
}
extension Item: Equatable {
static func == (lhs: Item, rhs: Item) -> Bool {
lhs.id == rhs.id
lhs.id == rhs.id && lhs.itemType == rhs.itemType
}
}
@ -52,12 +60,13 @@ extension Item: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(itemType)
}
}
extension Item: Comparable {
static func < (lhs: Item, rhs: Item) -> Bool {
lhs.id < rhs.id
lhs.id < rhs.id && lhs.itemType < rhs.itemType
}
}

View File

@ -1,33 +1,11 @@
struct ItemId {
let language: ContentLanguage
let type: ItemType
let itemType: ItemType
let id: String?
}
extension ItemId: Equatable {
static func == (lhs: ItemId, rhs: ItemId) -> Bool {
lhs.language == rhs.language &&
lhs.itemType == rhs.itemType
}
}
extension ItemId: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(language)
hasher.combine(itemType.id)
}
}
extension ItemId: Comparable {
extension ItemId: Codable {
static func < (lhs: ItemId, rhs: ItemId) -> Bool {
guard lhs.itemType == rhs.itemType else {
return lhs.itemType < rhs.itemType
}
return lhs.language < rhs.language
}
}

View File

@ -0,0 +1,68 @@
enum ItemReference {
case general
case post(Post)
case feed
case page(Page)
case tagPage(Tag)
case tagOverview
}
extension ItemReference: Equatable {
}
extension ItemReference: Hashable {
}
extension ItemReference: Identifiable {
var id: String {
switch self {
case .general:
return "0-general"
case .feed:
return "1-feed"
case .post(let post):
return "2-post-\(post.id)"
case .page(let page):
return "3-page-\(page.id)"
case .tagPage(let tag):
return "5-tag-\(tag.id)"
case .tagOverview:
return "4-tag-overview"
}
}
init?(context: LoadingContext, rawValue: String) {
if rawValue == "0-general" {
self = .general
} else if rawValue == "1-feed" {
self = .feed
} else if rawValue == "4-tag-overview" {
self = .tagOverview
} else if let id = rawValue.removingPrefix("3-page-"), let page = context.page(id) {
self = .page(page)
} else if let id = rawValue.removingPrefix("2-post-"), let post = context.post(id) {
self = .post(post)
} else if let id = rawValue.removingPrefix("5-tag-"), let tag = context.tag(id) {
self = .tagPage(tag)
} else {
return nil
}
}
}
extension ItemReference: Comparable {
static func < (lhs: ItemReference, rhs: ItemReference) -> Bool {
lhs.id < rhs.id
}
}

View File

@ -1,76 +1,21 @@
enum ItemType {
enum ItemType: String, Equatable, Hashable {
case general
case post = "post"
case post(Post)
case page = "page"
case feed
case tag = "tag"
case page(Page)
case tagPage(Tag)
case tagOverview
}
extension ItemType: Equatable {
}
extension ItemType: Hashable {
}
extension ItemType: Identifiable {
var id: String {
switch self {
case .general:
return "0-general"
case .feed:
return "1-feed"
case .post(let post):
return "2-post-\(post.id)"
case .page(let page):
return "3-page-\(page.id)"
case .tagPage(let tag):
return "5-tag-\(tag.id)"
case .tagOverview:
return "4-tag-overview"
}
}
init?(rawValue: String, posts: [String : Post], pages: [String : Page], tags: [String : Tag]) {
if rawValue == "0-general" {
self = .general
} else if rawValue == "1-feed" {
self = .feed
} else if rawValue == "4-tag-overview" {
self = .tagOverview
} else if let id = rawValue.removingPrefix("3-page-"), let page = pages[id] {
self = .page(page)
} else if let id = rawValue.removingPrefix("2-post-"), let post = posts[id] {
self = .post(post)
} else if let id = rawValue.removingPrefix("5-tag-"), let tag = tags[id] {
self = .tagPage(tag)
} else {
return nil
}
}
case tagOverview = "tag-overview"
}
extension ItemType: Comparable {
static func < (lhs: ItemType, rhs: ItemType) -> Bool {
lhs.id < rhs.id
public static func < (lhs: ItemType, rhs: ItemType) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
extension String {
func removingPrefix(_ prefix: String) -> String? {
guard self.hasPrefix(prefix) else { return nil }
return String(self.dropFirst(prefix.count))
}
extension ItemType: Codable {
}

View File

@ -1,18 +0,0 @@
protocol LinkPreviewItem: AnyObject {
var linkPreviewImage: FileResource? { get set }
var linkPreviewTitle: String? { get }
var linkPreviewDescription: String? { get }
}
extension LinkPreviewItem {
func remove(linkPreviewImage file: FileResource) {
if linkPreviewImage == file {
linkPreviewImage = nil
}
}
}

View File

@ -0,0 +1,33 @@
struct LocalizedItemId {
let language: ContentLanguage
let itemType: ItemReference
}
extension LocalizedItemId: Equatable {
static func == (lhs: LocalizedItemId, rhs: LocalizedItemId) -> Bool {
lhs.language == rhs.language &&
lhs.itemType == rhs.itemType
}
}
extension LocalizedItemId: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(language)
hasher.combine(itemType.id)
}
}
extension LocalizedItemId: Comparable {
static func < (lhs: LocalizedItemId, rhs: LocalizedItemId) -> Bool {
guard lhs.itemType == rhs.itemType else {
return lhs.itemType < rhs.itemType
}
return lhs.language < rhs.language
}
}

View File

@ -1,5 +1,5 @@
import Foundation
/*
final class TagOverviewPage: Item {
static let id = "all-tags"
@ -105,3 +105,4 @@ final class LocalizedTagOverviewPage: ObservableObject {
!content.containsTag(withUrlComponent: urlComponent)
}
}
*/

View File

@ -0,0 +1,58 @@
import Foundation
/**
The information to use when constructing the link preview of a page.
The information will be placed in the `<head>` of the page as `<meta>` tags.
*/
final class LinkPreview: ObservableObject {
/// The description to show when linking to a page (contained in the `<head>` of the page)
@Published
var title: String?
/// The image id of the thumbnail to attach to the link preview (contained in the `<head>` of the page)
@Published
var description: String?
/// The title to show for a link preview (contained in the `<head>` of the page)
@Published
var image: FileResource?
init(title: String? = nil, description: String? = nil, image: FileResource? = nil) {
self.title = title
self.description = description
self.image = image
}
/**
Remove a file if it is used in the link preview.
*/
func remove(_ file: FileResource) {
if image == file {
image = nil
}
}
// MARK: Storage
var data: Data {
.init(title: title, description: description, image: image?.id)
}
init(context: LoadingContext, data: Data) {
self.title = data.title
self.description = data.description
self.image = data.image.map(context.image)
}
}
extension LinkPreview {
/// The object to serialize a link preview for storage
struct Data: Codable {
let title: String?
let description: String?
let image: String?
}
}

View File

@ -0,0 +1,110 @@
final class LoadingContext {
let content: Content
var files: [String: FileResource] = [:]
var pages: [String : Page] = [:]
var tags: [String : Tag] = [:]
var posts: [String : Post] = [:]
var errors: Set<String> = []
var tagOverview: TagOverview?
var settings: Settings?
init(content: Content) {
self.content = content
}
func results() -> LoadingResult {
.init(
settings: settings ?? .default,
posts: posts.values.sorted(ascending: false) { $0.startDate },
pages: pages.values.sorted(ascending: false) { $0.startDate },
tags: tags.values.sorted(),
files: files.values.sorted { $0.id },
tagOverview: tagOverview,
errors: errors.sorted())
}
func error(_ message: String) {
errors.insert(message)
}
func post(_ postId: String) -> Post? {
if let post = posts[postId] {
return post
}
error("Missing post \(postId)")
return nil
}
func tag(_ tagId: String) -> Tag? {
if let tag = tags[tagId] {
return tag
}
error("Missing tag \(tagId)")
return nil
}
func page(_ pageId: String) -> Page? {
if let page = pages[pageId] {
return page
}
error("Missing page \(pageId)")
return nil
}
func file(_ fileId: String) -> FileResource? {
if let file = files[fileId] {
return file
}
error("Missing file \(fileId)")
return nil
}
func image(_ imageId: String) -> FileResource? {
guard let image = file(imageId) else {
return nil
}
if image.type.isImage {
return image
}
error("Image \(imageId) is not an image")
return nil
}
func item(itemId: ItemId) -> Item? {
switch itemId.type {
case .post:
guard let id = itemId.id else {
error("Missing post id in itemId")
return nil
}
return post(id)
case .page:
guard let id = itemId.id else {
error("Missing page id in itemId")
return nil
}
return page(id)
case .tag:
guard let id = itemId.id else {
error("Missing tag id in itemId")
return nil
}
return tag(id)
case .tagOverview:
guard let tagOverview else {
error("Missing tag overview")
return nil
}
return tagOverview
}
}
}

View File

@ -0,0 +1,17 @@
struct LoadingResult {
let settings: Settings
let posts: [Post]
let pages: [Page]
let tags: [Tag]
let files: [FileResource]
let tagOverview: Tag?
let errors: [String]
}

View File

@ -0,0 +1,96 @@
final class ModelLoader {
let content: Content
let storage: Storage
let context: LoadingContext
init(content: Content, storage: Storage) {
self.content = content
self.storage = storage
self.context = .init(content: content)
}
func load() -> LoadingResult {
loadInternal()
return context.results()
}
private func loadInternal() {
guard storage.contentScope != nil else {
context.error("Storage not initialized, not loading content")
return
}
loadFiles()
loadTags()
loadPages()
loadPosts()
loadSettings()
}
private func loadFiles() {
guard let files = storage.loadAllFiles() else {
context.error("Failed to load file list")
return
}
if files.isEmpty { print("No files loaded") }
files.forEach { (fileId, data) in
let fileData = data.data
let isExternal = data.isExternal
context.files[fileId] = FileResource(content: content, id: fileId, data: fileData, isExternalFile: isExternal)
}
}
private func loadTags() {
guard let tags = storage.loadAllTags() else {
context.error("Failed to load file tags")
return
}
if tags.isEmpty { print("No tags loaded") }
tags.forEach { (tagId, data) in
context.tags[tagId] = Tag(context: context, id: tagId, data: data)
}
}
private func loadPages() {
guard let pages = storage.loadAllPages() else {
context.error("Failed to load file pages")
return
}
if pages.isEmpty { print("No pages loaded") }
pages.forEach { pageId, data in
context.pages[pageId] = Page(context: context, id: pageId, data: data)
}
}
private func loadPosts() {
guard let posts = storage.loadAllPosts() else {
context.error("Failed to load file posts")
return
}
if posts.isEmpty { print("No posts loaded") }
posts.forEach { postId, data in
context.posts[postId] = Post(context: context, id: postId, data: data)
}
}
private func loadSettings() {
guard let settings = storage.loadSettings() else {
context.error("Failed to load settings")
return
}
context.tagOverview = settings.tagOverview.map { data in
TagOverview(context: context, id: "all-tags", data: data)
}
context.settings = Settings(context: context, data: settings)
}
}

View File

@ -35,13 +35,7 @@ final class LocalizedPage: ObservableObject {
let originalUrl: String?
@Published
var linkPreviewImage: FileResource?
@Published
var linkPreviewTitle: String?
@Published
var linkPreviewDescription: String?
var linkPreview: LinkPreview
@Published
var hideTitle: Bool
@ -51,18 +45,14 @@ final class LocalizedPage: ObservableObject {
title: String,
lastModified: Date? = nil,
originalUrl: String? = nil,
linkPreviewImage: FileResource? = nil,
linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil,
linkPreview: LinkPreview = .init(),
hideTitle: Bool = false) {
self.content = content
self.urlString = urlString
self.title = title
self.lastModified = lastModified
self.originalUrl = originalUrl
self.linkPreviewImage = linkPreviewImage
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription
self.linkPreview = linkPreview
self.hideTitle = hideTitle
}
@ -72,6 +62,37 @@ final class LocalizedPage: ObservableObject {
}
}
extension LocalizedPage: LinkPreviewItem {
extension LocalizedPage {
convenience init(context: LoadingContext, data: LocalizedPage.Data) {
self.init(
content: context.content,
urlString: data.url,
title: data.title,
lastModified: data.lastModifiedDate,
originalUrl: data.originalURL,
linkPreview: .init(context: context, data: data.linkPreview),
hideTitle: data.hideTitle ?? false)
}
/// The structure to store the metadata of a localized page
struct Data: Codable {
let url: String
let title: String
let linkPreview: LinkPreview.Data
let lastModifiedDate: Date?
let originalURL: String?
let hideTitle: Bool?
}
var data: Data {
.init(
url: urlString,
title: title,
linkPreview: linkPreview.data,
lastModifiedDate: lastModified,
originalURL: originalUrl,
hideTitle: hideTitle ? true : nil)
}
}

View File

@ -22,13 +22,7 @@ final class LocalizedPost: ObservableObject {
var pageLinkText: String?
@Published
var linkPreviewImage: FileResource?
@Published
var linkPreviewTitle: String?
@Published
var linkPreviewDescription: String?
var linkPreview: LinkPreview
init(content: Content,
title: String? = nil,
@ -36,41 +30,14 @@ final class LocalizedPost: ObservableObject {
lastModified: Date? = nil,
images: [FileResource] = [],
pageLinkText: String? = nil,
linkPreviewImage: FileResource? = nil,
linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil) {
linkPreview: LinkPreview = .init()) {
self.content = content
self.title = title
self.text = text
self.lastModified = lastModified
self.images = images
self.pageLinkText = pageLinkText
self.linkPreviewImage = linkPreviewImage
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription
}
init(content: Content, file: LocalizedPostFile, images: [String : FileResource]) {
self.content = content
self.title = file.title
self.text = file.content
self.lastModified = file.lastModifiedDate
self.images = file.images.compactMap { images[$0] }
self.pageLinkText = file.pageLinkText
self.linkPreviewImage = file.linkPreviewImage.map { images[$0] }
self.linkPreviewTitle = file.linkPreviewTitle
self.linkPreviewDescription = file.linkPreviewDescription
}
var postFile: LocalizedPostFile {
.init(images: images.map { $0.id },
title: title,
content: text,
lastModifiedDate: lastModified,
pageLinkText: pageLinkText,
linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription)
self.linkPreview = linkPreview
}
func contains(_ string: String) -> Bool {
@ -84,10 +51,41 @@ final class LocalizedPost: ObservableObject {
if images.contains(file) {
images.remove(file)
}
remove(linkPreviewImage: file)
linkPreview.remove(file)
}
}
extension LocalizedPost: LinkPreviewItem {
// MARK: Storage
extension LocalizedPost {
convenience init(context: LoadingContext, data: Data) {
self.init(
content: context.content,
title: data.title,
text: data.text,
lastModified: data.lastModifiedDate,
images: data.images.compactMap(context.image),
pageLinkText: data.pageLinkText,
linkPreview: .init(context: context, data: data.linkPreview))
}
var data: Data {
.init(images: images.map { $0.id },
title: title,
text: text,
lastModifiedDate: lastModified,
pageLinkText: pageLinkText,
linkPreview: linkPreview.data)
}
/// The structure to store the metadata of a localized post
struct Data: Codable {
let images: [String]
let title: String?
let text: String
let lastModifiedDate: Date?
let pageLinkText: String?
let linkPreview: LinkPreview.Data
}
}

View File

@ -12,14 +12,7 @@ final class LocalizedTag: ObservableObject {
var name: String
@Published
var linkPreviewTitle: String?
@Published
var linkPreviewDescription: String?
/// The image id of the thumbnail
@Published
var linkPreviewImage: FileResource?
var linkPreview: LinkPreview
/// The original url in the previous site layout
let originalUrl: String?
@ -27,42 +20,51 @@ final class LocalizedTag: ObservableObject {
init(content: Content,
urlComponent: String,
name: String,
linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil,
linkPreviewImage: FileResource? = nil,
linkPreview: LinkPreview = .init(),
originalUrl: String? = nil) {
self.content = content
self.urlComponent = urlComponent
self.name = name
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription
self.linkPreviewImage = linkPreviewImage
self.linkPreview = linkPreview
self.originalUrl = originalUrl
}
func isValid(urlComponent: String) -> Bool {
!urlComponent.isEmpty &&
content.isValidIdForTagOrPageOrPost(urlComponent) &&
!content.containsTag(withUrlComponent: urlComponent)
}
/// The title to display when considering multiple items of this tag
var title: String {
linkPreviewTitle ?? name
linkPreview.title ?? name
}
}
extension LocalizedTag: LinkPreviewItem {
}
// MARK: Storage
extension LocalizedTag {
var tagFile: LocalizedTagFile {
convenience init(context: LoadingContext, data: Data) {
self.init(
content: context.content,
urlComponent: data.urlComponent,
name: data.name,
linkPreview: .init(context: context, data: data.linkPreview),
originalUrl: data.originalUrl)
}
struct Data: Codable {
let urlComponent: String
let name: String
let linkPreview: LinkPreview.Data
let originalUrl: String?
}
var data: Data {
.init(urlComponent: urlComponent,
name: name,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription,
linkPreviewImage: linkPreviewImage?.id,
originalURL: originalUrl)
linkPreview: linkPreview.data,
originalUrl: originalUrl)
}
}

View File

@ -1,6 +1,8 @@
import Foundation
final class Page: Item {
final class Page: Item, DateItem, LocalizedItem {
override var itemType: ItemType { .page }
/**
The external link this page points to.
@ -38,9 +40,7 @@ final class Page: Item {
@Published
var tags: [Tag]
/**
Additional files to copy, because the page content references them
*/
/// Additional files to copy, because the page content references them
@Published
var requiredFiles: [FileResource]
@ -141,7 +141,7 @@ final class Page: Item {
content.settings.paths.pagesOutputFolderPath + "/" + localized(in: language).urlString
}
override var itemType: ItemType {
override var itemReference: ItemReference {
.page(self)
}
@ -161,15 +161,57 @@ final class Page: Item {
if requiredFiles.contains(file) {
requiredFiles.remove(file)
}
english.remove(linkPreviewImage: file)
german.remove(linkPreviewImage: file)
english.linkPreview.remove(file)
german.linkPreview.remove(file)
}
}
extension Page: DateItem {
// MARK: Storage
}
extension Page {
extension Page: LocalizedItem {
convenience init(context: LoadingContext, id: String, data: Data) {
self.init(
content: context.content,
id: id,
externalLink: data.externalLink,
isDraft: data.isDraft,
createdDate: data.createdDate,
hideDate: data.hideDate ?? false,
startDate: data.startDate,
endDate: data.endDate,
german: .init(context: context, data: data.german),
english: .init(context: context, data: data.english),
tags: data.tags.compactMap(context.tag),
requiredFiles: data.requiredFiles?.compactMap(context.file) ?? [])
}
/// The structure to store the metadata of a page on disk
struct Data: Codable {
let isDraft: Bool
let externalLink: String?
let tags: [String]
let hideDate: Bool?
let createdDate: Date
let startDate: Date
let endDate: Date?
let german: LocalizedPage.Data
let english: LocalizedPage.Data
let requiredFiles: [String]?
}
var data: Data {
.init(
isDraft: isDraft,
externalLink: externalLink,
tags: tags.map { $0.id },
hideDate: hideDate ? true : nil,
createdDate: createdDate,
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
german: german.data,
english: english.data,
requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted())
}
}

View File

@ -1,6 +1,8 @@
import Foundation
final class Post: Item {
final class Post: Item, DateItem, LocalizedItem {
override var itemType: ItemType { .post }
@Published
var isDraft: Bool
@ -142,10 +144,42 @@ final class Post: Item {
}
}
extension Post: DateItem {
}
extension Post: LocalizedItem {
extension Post {
convenience init(context: LoadingContext, id: String, data: Data) {
self.init(
content: context.content,
id: id,
isDraft: data.isDraft,
createdDate: data.createdDate,
startDate: data.startDate,
endDate: data.endDate,
tags: data.tags.compactMap(context.tag),
german: .init(context: context, data: data.german),
english: .init(context: context, data: data.english),
linkedPage: data.linkedPageId.map(context.page))
}
struct Data: Codable {
let isDraft: Bool
let createdDate: Date
let startDate: Date
let endDate: Date?
let tags: [String]
let german: LocalizedPost.Data
let english: LocalizedPost.Data
let linkedPageId: String?
}
var data: Data {
.init(
isDraft: isDraft,
createdDate: createdDate,
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
tags: tags.map { $0.id },
german: german.data,
english: english.data,
linkedPageId: linkedPage?.id)
}
}

View File

@ -1,6 +1,6 @@
import Foundation
final class AudioPlayerSettings: ObservableObject {
final class AudioPlayerSettings: ObservableObject, LocalizedItem {
@Published
var playlistCoverImageSize: Int
@ -34,24 +34,6 @@ final class AudioPlayerSettings: ObservableObject {
self.english = english
}
init(file: AudioPlayerSettingsFile, files: [String : FileResource]) {
self.playlistCoverImageSize = file.playlistCoverImageSize
self.smallCoverImageSize = file.smallCoverImageSize
self.audioPlayerJsFile = file.audioPlayerJsFile.map { files[$0] }
self.audioPlayerCssFile = file.audioPlayerCssFile.map { files[$0] }
self.german = .init(file: file.german)
self.english = .init(file: file.english)
}
var file: AudioPlayerSettingsFile {
.init(playlistCoverImageSize: playlistCoverImageSize,
smallCoverImageSize: smallCoverImageSize,
audioPlayerJsFile: audioPlayerJsFile?.id,
audioPlayerCssFile: audioPlayerCssFile?.id,
german: german.file,
english: english.file)
}
func remove(_ file: FileResource) {
if audioPlayerJsFile == file {
audioPlayerJsFile = nil
@ -62,17 +44,37 @@ final class AudioPlayerSettings: ObservableObject {
}
}
// MARK: Storage
extension AudioPlayerSettings {
static let `default`: AudioPlayerSettings = .init(
playlistCoverImageSize: 280,
smallCoverImageSize: 78,
audioPlayerJsFile: nil,
audioPlayerCssFile: nil,
german: .init(playlistText: "Wiedergabeliste"),
english: .init(playlistText: "Playlist"))
}
convenience init(context: LoadingContext, data: Data) {
self.init(
playlistCoverImageSize: data.playlistCoverImageSize,
smallCoverImageSize: data.smallCoverImageSize,
audioPlayerJsFile: data.audioPlayerJsFile.map(context.file),
audioPlayerCssFile: data.audioPlayerCssFile.map(context.file),
german: .init(data: data.german),
english: .init(data: data.english))
}
var data: Data {
.init(playlistCoverImageSize: playlistCoverImageSize,
smallCoverImageSize: smallCoverImageSize,
audioPlayerJsFile: audioPlayerJsFile?.id,
audioPlayerCssFile: audioPlayerCssFile?.id,
german: german.data,
english: english.data)
}
struct Data: Codable {
let playlistCoverImageSize: Int
let smallCoverImageSize: Int
let audioPlayerJsFile: String?
let audioPlayerCssFile: String?
let german: LocalizedAudioPlayerSettings.Data
let english: LocalizedAudioPlayerSettings.Data
}
extension AudioPlayerSettings: LocalizedItem {
}

View File

@ -8,12 +8,21 @@ final class LocalizedAudioPlayerSettings: ObservableObject {
init(playlistText: String) {
self.playlistText = playlistText
}
}
init(file: LocalizedAudioPlayerSettingsFile) {
self.playlistText = file.playlistText
// MARK: Storage
extension LocalizedAudioPlayerSettings {
convenience init(data: Data) {
self.init(playlistText: data.playlistText)
}
var file: LocalizedAudioPlayerSettingsFile {
var data: Data {
.init(playlistText: playlistText)
}
struct Data: Codable {
let playlistText: String
}
}

View File

@ -8,12 +8,21 @@ final class LocalizedNavigationSettings: ObservableObject {
init(rootUrl: String) {
self.rootUrl = rootUrl
}
}
init(file: LocalizedNavigationSettingsFile) {
self.rootUrl = file.rootUrl
// MARK: Storage
extension LocalizedNavigationSettings {
convenience init(data: Data) {
self.init(rootUrl: data.rootUrl)
}
var file: LocalizedNavigationSettingsFile {
struct Data: Codable {
let rootUrl: String
}
var data: Data {
.init(rootUrl: rootUrl)
}
}

View File

@ -14,14 +14,25 @@ final class LocalizedPageSettings: ObservableObject {
self.emptyPageTitle = emptyPageTitle
self.emptyPageText = emptyPageText
}
}
init(file: LocalizedPageSettingsFile) {
self.emptyPageTitle = file.emptyPageTitle
self.emptyPageText = file.emptyPageText
// MARK: Storage
extension LocalizedPageSettings {
convenience init(data: Data) {
self.init(
emptyPageTitle: data.emptyPageTitle,
emptyPageText: data.emptyPageText)
}
var file: LocalizedPageSettingsFile {
var data: Data {
.init(emptyPageTitle: emptyPageTitle,
emptyPageText: emptyPageText)
}
struct Data: Codable {
let emptyPageTitle: String
let emptyPageText: String
}
}

View File

@ -2,15 +2,23 @@ import Foundation
final class LocalizedPostSettings: ObservableObject {
/// The page title for the post feed
@Published
var title: String
/// The page description for the post feed
@Published
var description: String
/// The path to the feed in the final website, appended with the page number
@Published
var feedUrlPrefix: String
/**
The text to display when linking to a page
Each post may define a custom text.
*/
@Published
var defaultPageLinkText: String
@ -20,21 +28,32 @@ final class LocalizedPostSettings: ObservableObject {
self.feedUrlPrefix = feedUrlPrefix
self.defaultPageLinkText = defaultPageLinkText
}
}
// MARK: Storage
// MARK: Storage
init(file: LocalizedPostSettingsFile) {
self.title = file.feedTitle
self.description = file.feedDescription
self.feedUrlPrefix = file.feedUrlPrefix
self.defaultPageLinkText = file.defaultPageLinkText ?? "View"
extension LocalizedPostSettings {
convenience init(data: Data) {
self.init(
title: data.feedTitle,
description: data.feedDescription,
feedUrlPrefix: data.feedUrlPrefix,
defaultPageLinkText: data.defaultPageLinkText)
}
var file: LocalizedPostSettingsFile {
var data: Data {
.init(
feedTitle: title,
feedDescription: description,
feedUrlPrefix: feedUrlPrefix,
defaultPageLinkText: defaultPageLinkText)
}
struct Data: Codable {
let feedTitle: String
let feedDescription: String
let feedUrlPrefix: String
let defaultPageLinkText: String
}
}

View File

@ -1,6 +1,6 @@
import Foundation
final class NavigationSettings: ObservableObject {
final class NavigationSettings: ObservableObject, LocalizedItem {
/// The items to show in the navigation bar
@Published
@ -19,23 +19,31 @@ final class NavigationSettings: ObservableObject {
self.german = german
self.english = english
}
init(file: NavigationSettingsFile, map: (String) -> Item?) {
self.navigationItems = file.navigationItems.compactMap(map)
self.german = LocalizedNavigationSettings(file: file.german)
self.english = LocalizedNavigationSettings(file: file.english)
}
var file: NavigationSettingsFile {
.init(
navigationItems: navigationItems.map { $0.itemType.id },
german: german.file,
english: english.file)
}
}
extension NavigationSettings: LocalizedItem {
// MARK: Storage
extension NavigationSettings {
convenience init(context: LoadingContext, data: NavigationSettings.Data) {
self.init(
navigationItems: data.navigationItems.compactMap(context.item),
german: LocalizedNavigationSettings(data: data.german),
english: LocalizedNavigationSettings(data: data.english))
}
struct Data: Codable {
let navigationItems: [ItemId]
let german: LocalizedNavigationSettings.Data
let english: LocalizedNavigationSettings.Data
}
var data: Data {
.init(
navigationItems: navigationItems.map { $0.itemId },
german: german.data,
english: english.data)
}
}
extension NavigationSettings {

View File

@ -32,30 +32,26 @@ final class PageSettings: ObservableObject {
@Published
var english: LocalizedPageSettings
init(file: PageSettingsFile, files: [String : FileResource]) {
self.contentWidth = file.contentWidth
self.largeImageWidth = file.largeImageWidth
self.pageLinkImageSize = file.pageLinkImageSize
self.defaultCssFile = file.defaultCssFile.map { files[$0] }
self.codeHighlightingJsFile = file.codeHighlightingJsFile.map { files[$0] }
self.modelViewerJsFile = file.modelViewerJsFile.map { files[$0] }
self.imageCompareCssFile = file.imageCompareCssFile.map { files[$0] }
self.imageCompareJsFile = file.imageCompareJsFile.map { files[$0] }
self.german = .init(file: file.german)
self.english = .init(file: file.english)
}
var file: PageSettingsFile {
.init(contentWidth: contentWidth,
largeImageWidth: largeImageWidth,
pageLinkImageSize: pageLinkImageSize,
defaultCssFile: defaultCssFile?.id,
codeHighlightingJsFile: codeHighlightingJsFile?.id,
modelViewerJsFile: modelViewerJsFile?.id,
imageCompareJsFile: imageCompareJsFile?.id,
imageCompareCssFile: imageCompareCssFile?.id,
german: german.file,
english: english.file)
init(contentWidth: Int,
largeImageWidth: Int,
pageLinkImageSize: Int,
defaultCssFile: FileResource? = nil,
codeHighlightingJsFile: FileResource? = nil,
modelViewerJsFile: FileResource? = nil,
imageCompareJsFile: FileResource? = nil,
imageCompareCssFile: FileResource? = nil,
german: LocalizedPageSettings,
english: LocalizedPageSettings) {
self.contentWidth = contentWidth
self.largeImageWidth = largeImageWidth
self.pageLinkImageSize = pageLinkImageSize
self.defaultCssFile = defaultCssFile
self.codeHighlightingJsFile = codeHighlightingJsFile
self.modelViewerJsFile = modelViewerJsFile
self.imageCompareJsFile = imageCompareJsFile
self.imageCompareCssFile = imageCompareCssFile
self.german = german
self.english = english
}
func remove(_ file: FileResource) {
@ -77,6 +73,52 @@ final class PageSettings: ObservableObject {
}
}
// MARK: Storage
extension PageSettings {
convenience init(context: LoadingContext, data: Data) {
self.init(
contentWidth: data.contentWidth,
largeImageWidth: data.largeImageWidth,
pageLinkImageSize: data.pageLinkImageSize,
defaultCssFile: data.defaultCssFile.map(context.file),
codeHighlightingJsFile: data.codeHighlightingJsFile.map(context.file),
modelViewerJsFile: data.modelViewerJsFile.map(context.file),
imageCompareJsFile: data.imageCompareJsFile.map(context.file),
imageCompareCssFile: data.imageCompareCssFile.map(context.file),
german: .init(data: data.german),
english: .init(data: data.english))
}
var data: Data {
.init(contentWidth: contentWidth,
largeImageWidth: largeImageWidth,
pageLinkImageSize: pageLinkImageSize,
defaultCssFile: defaultCssFile?.id,
codeHighlightingJsFile: codeHighlightingJsFile?.id,
modelViewerJsFile: modelViewerJsFile?.id,
imageCompareJsFile: imageCompareJsFile?.id,
imageCompareCssFile: imageCompareCssFile?.id,
german: german.data,
english: english.data)
}
struct Data: Codable {
let contentWidth: Int
let largeImageWidth: Int
let pageLinkImageSize: Int
let defaultCssFile: String?
let codeHighlightingJsFile: String?
let modelViewerJsFile: String?
let imageCompareJsFile: String?
let imageCompareCssFile: String?
let german: LocalizedPageSettings.Data
let english: LocalizedPageSettings.Data
}
}
extension PageSettings: LocalizedItem {
}

View File

@ -23,23 +23,54 @@ final class PathSettings: ObservableObject {
@Published
var tagsOutputFolderPath: String
init(file: PathSettingsFile) {
self.assetsOutputFolderPath = file.assetsOutputFolderPath
self.pagesOutputFolderPath = file.pagesOutputFolderPath
self.imagesOutputFolderPath = file.imagesOutputFolderPath
self.filesOutputFolderPath = file.filesOutputFolderPath
self.videosOutputFolderPath = file.videosOutputFolderPath
self.tagsOutputFolderPath = file.tagsOutputFolderPath
self.audioOutputFolderPath = file.audioOutputFolderPath
}
var file: PathSettingsFile {
.init(assetsOutputFolderPath: assetsOutputFolderPath,
pagesOutputFolderPath: pagesOutputFolderPath,
imagesOutputFolderPath: imagesOutputFolderPath,
filesOutputFolderPath: filesOutputFolderPath,
videosOutputFolderPath: videosOutputFolderPath,
tagsOutputFolderPath: tagsOutputFolderPath,
audioOutputFolderPath: audioOutputFolderPath)
init(assetsOutputFolderPath: String,
pagesOutputFolderPath: String,
imagesOutputFolderPath: String,
filesOutputFolderPath: String,
videosOutputFolderPath: String,
audioOutputFolderPath: String,
tagsOutputFolderPath: String) {
self.assetsOutputFolderPath = assetsOutputFolderPath
self.pagesOutputFolderPath = pagesOutputFolderPath
self.imagesOutputFolderPath = imagesOutputFolderPath
self.filesOutputFolderPath = filesOutputFolderPath
self.videosOutputFolderPath = videosOutputFolderPath
self.audioOutputFolderPath = audioOutputFolderPath
self.tagsOutputFolderPath = tagsOutputFolderPath
}
}
extension PathSettings {
convenience init(data: Data) {
self.init(
assetsOutputFolderPath: data.assetsOutputFolderPath,
pagesOutputFolderPath: data.pagesOutputFolderPath,
imagesOutputFolderPath: data.imagesOutputFolderPath,
filesOutputFolderPath: data.filesOutputFolderPath,
videosOutputFolderPath: data.videosOutputFolderPath,
audioOutputFolderPath: data.audioOutputFolderPath,
tagsOutputFolderPath: data.tagsOutputFolderPath)
}
var data: Data {
.init(
assetsOutputFolderPath: assetsOutputFolderPath,
pagesOutputFolderPath: pagesOutputFolderPath,
imagesOutputFolderPath: imagesOutputFolderPath,
filesOutputFolderPath: filesOutputFolderPath,
videosOutputFolderPath: videosOutputFolderPath,
audioOutputFolderPath: audioOutputFolderPath,
tagsOutputFolderPath: tagsOutputFolderPath)
}
struct Data: Codable {
let assetsOutputFolderPath: String
let pagesOutputFolderPath: String
let imagesOutputFolderPath: String
let filesOutputFolderPath: String
let videosOutputFolderPath: String
let audioOutputFolderPath: String
let tagsOutputFolderPath: String
}
}

View File

@ -1,6 +1,6 @@
import Foundation
final class PostSettings: ObservableObject {
final class PostSettings: ObservableObject, LocalizedItem {
/// The number of posts to show in a single page of the news feed
@Published
@ -41,28 +41,6 @@ final class PostSettings: ObservableObject {
self.english = english
}
// MARK: Storage
init(file: PostSettingsFile, files: [String : FileResource]) {
self.postsPerPage = file.postsPerPage
self.contentWidth = file.contentWidth
self.swiperCssFile = file.swiperCssFile.map { files[$0] }
self.swiperJsFile = file.swiperJsFile.map { files[$0] }
self.defaultCssFile = file.defaultCssFile.map { files[$0] }
self.german = .init(file: file.german)
self.english = .init(file: file.english)
}
var file: PostSettingsFile {
.init(postsPerPage: postsPerPage,
contentWidth: contentWidth,
swiperCssFile: swiperCssFile?.id,
swiperJsFile: swiperJsFile?.id,
defaultCssFile: defaultCssFile?.id,
german: german.file,
english: english.file)
}
func remove(_ file: FileResource) {
if swiperJsFile == file {
swiperJsFile = nil
@ -76,13 +54,38 @@ final class PostSettings: ObservableObject {
}
}
// MARK: Storage
extension PostSettings {
static var `default`: PostSettings {
.init(file: .default, files: [:])
convenience init(context: LoadingContext, data: Data) {
self.init(
postsPerPage: data.postsPerPage,
contentWidth: data.contentWidth,
swiperCssFile: data.swiperCssFile.map(context.file),
swiperJsFile: data.swiperJsFile.map(context.file),
defaultCssFile: data.defaultCssFile.map(context.file),
german: .init(data: data.german),
english: .init(data: data.english))
}
var data: PostSettings.Data {
.init(postsPerPage: postsPerPage,
contentWidth: contentWidth,
swiperCssFile: swiperCssFile?.id,
swiperJsFile: swiperJsFile?.id,
defaultCssFile: defaultCssFile?.id,
german: german.data,
english: english.data)
}
struct Data: Codable {
let postsPerPage: Int
let contentWidth: Int
let swiperCssFile: String?
let swiperJsFile: String?
let defaultCssFile: String?
let german: LocalizedPostSettings.Data
let english: LocalizedPostSettings.Data
}
}
extension PostSettings: LocalizedItem {
}

View File

@ -30,25 +30,6 @@ final class Settings: ObservableObject {
self.audioPlayer = audioPlayer
}
init(file: SettingsFile, files: [String : FileResource], map: (String) -> Item?) {
self.navigation = NavigationSettings(file: file.navigation, map: map)
self.posts = PostSettings(file: file.posts, files: files)
self.pages = PageSettings(file: file.pages, files: files)
self.paths = PathSettings(file: file.paths)
self.audioPlayer = .init(file: file.audioPlayer, files: files)
}
func file(tagOverview: TagOverviewPage?) -> SettingsFile {
.init(
paths: paths.file,
navigation: navigation.file,
posts: posts.file,
pages: pages.file,
audioPlayer: audioPlayer.file,
tagOverview: tagOverview?.file)
}
func remove(_ file: FileResource) {
pages.remove(file)
posts.remove(file)
@ -56,6 +37,39 @@ final class Settings: ObservableObject {
}
}
// MARK: Storage
extension Settings {
convenience init(context: LoadingContext, data: Settings.Data) {
self.init(
paths: .init(data: data.paths),
navigation: .init(context: context, data: data.navigation),
posts: .init(context: context, data: data.posts),
pages: .init(context: context, data: data.pages),
audioPlayer: .init(context: context, data: data.audioPlayer))
}
func data(tagOverview: Tag?) -> Data {
.init(
paths: paths.data,
navigation: navigation.data,
posts: posts.data,
pages: pages.data,
audioPlayer: audioPlayer.data,
tagOverview: tagOverview?.data)
}
struct Data: Codable {
let paths: PathSettings.Data
let navigation: NavigationSettings.Data
let posts: PostSettings.Data
let pages: PageSettings.Data
let audioPlayer: AudioPlayerSettings.Data
let tagOverview: Tag.Data?
}
}
extension Settings {
static let `default`: Settings = .init(
@ -65,3 +79,70 @@ extension Settings {
pages: .default,
audioPlayer: .default)
}
extension AudioPlayerSettings {
static let `default`: AudioPlayerSettings = .init(
playlistCoverImageSize: 280,
smallCoverImageSize: 78,
audioPlayerJsFile: nil,
audioPlayerCssFile: nil,
german: .init(playlistText: "Wiedergabeliste"),
english: .init(playlistText: "Playlist"))
}
extension PostSettings {
static var `default`: PostSettings {
.init(postsPerPage: 25,
contentWidth: 600,
swiperCssFile: nil,
swiperJsFile: nil,
defaultCssFile: nil,
german: .init(
title: "Beiträge",
description: "Alle Beiträge",
feedUrlPrefix: "blog",
defaultPageLinkText: "Anzeigen"),
english: .init(
title: "Blog posts",
description: "All blog posts",
feedUrlPrefix: "blog",
defaultPageLinkText: "View"))
}
}
extension PathSettings {
static var `default`: PathSettings {
.init(
assetsOutputFolderPath: "asset",
pagesOutputFolderPath: "page",
imagesOutputFolderPath: "image",
filesOutputFolderPath: "file",
videosOutputFolderPath: "video",
audioOutputFolderPath: "audio",
tagsOutputFolderPath: "tag")
}
}
extension PageSettings {
static var `default`: PageSettings {
.init(contentWidth: 600,
largeImageWidth: 1200,
pageLinkImageSize: 180,
defaultCssFile: nil,
codeHighlightingJsFile: nil,
modelViewerJsFile: nil,
imageCompareJsFile: nil,
imageCompareCssFile: nil,
german: .init(
emptyPageTitle: "Leere Seite",
emptyPageText: "Diese Seite ist leer"),
english: .init(
emptyPageTitle: "Empty page",
emptyPageText: "This page is empty"))
}
}

View File

@ -1,6 +1,8 @@
import Foundation
final class Tag: Item {
class Tag: Item, LocalizedItem {
override var itemType: ItemType { .tag }
@Published
var isVisible: Bool
@ -59,7 +61,7 @@ final class Tag: Item {
localized(in: language).title
}
override var itemType: ItemType {
override var itemReference: ItemReference {
.tagPage(self)
}
@ -68,21 +70,35 @@ final class Tag: Item {
}
func remove(_ file: FileResource) {
english.remove(linkPreviewImage: file)
german.remove(linkPreviewImage: file)
english.linkPreview.remove(file)
german.linkPreview.remove(file)
}
}
extension Tag: LocalizedItem {
}
// MARK: Storage
extension Tag {
var file: TagFile {
.init(id: id,
isVisible: isVisible,
german: german.tagFile,
english: english.tagFile)
convenience init(context: LoadingContext, id: String, data: Data) {
self.init(
content: context.content,
id: id,
isVisible: data.isVisible ?? true,
german: .init(context: context, data: data.german),
english: .init(context: context, data: data.english))
}
struct Data: Codable {
// Defaults to true if unset
let isVisible: Bool?
let german: LocalizedTag.Data
let english: LocalizedTag.Data
}
var data: Data {
.init(
isVisible: isVisible ? nil : false,
german: german.data,
english: english.data)
}
}

View File

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