Add tag overview, improve assets

This commit is contained in:
Christoph Hagen
2024-12-15 21:20:12 +01:00
parent 8a3a0f1797
commit 1e67a99866
59 changed files with 1301 additions and 480 deletions

View File

@ -51,6 +51,7 @@ extension Content {
let postsData = try storage.loadAllPosts()
let fileList = try storage.loadAllFiles()
let externalFiles = try storage.loadExternalFileList()
let tagOverviewData = try storage.loadTagOverview()
var files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in
let descriptions = imageDescriptions[fileId]
@ -77,6 +78,7 @@ extension Content {
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))
@ -102,28 +104,46 @@ extension Content {
linkedPage: linkedPage)
}
let tagOverview = tagOverviewData.map { file in
TagOverviewPage(
content: self,
german: .init(file: file.german, image: file.german.linkPreviewImage.map { files[$0] }),
english: .init(file: file.english, image: file.english.linkPreviewImage.map { files[$0] }))
}
self.tags = tags.values.sorted()
self.pages = pages.values.sorted(ascending: false) { $0.startDate }
self.files = files.values.sorted { $0.id }
self.posts = posts.sorted(ascending: false) { $0.startDate }
self.settings = makeSettings(settings, tags: tags)
self.tagOverview = tagOverview
self.settings = makeSettings(settings, tags: tags, pages: pages, files: files)
}
private func makeSettings(_ settings: SettingsFile, tags: [String : Tag]) -> Settings {
private func makeSettings(_ settings: SettingsFile, tags: [String : Tag], pages: [String : Page], files: [String : FileResource]) -> Settings {
let navigationTags = settings.navigationTags.map { tags[$0]! }
#warning("Notify about missing links")
let navigationItems: [Item] = settings.navigationItems.compactMap {
switch $0.type {
case .tag:
return tags[$0.id]
case .page:
return pages[$0.id]
case .tagOverview:
return tagOverview
default:
return nil
}
}
let posts = PostSettings(
postsPerPage: settings.posts.postsPerPage,
contentWidth: settings.posts.contentWidth)
let posts = PostSettings(file: settings.posts, files: files)
let pages = PageSettings(file: settings.pages)
let pages = PageSettings(file: settings.pages, files: files)
let paths = PathSettings(file: settings.paths)
return Settings(
paths: paths,
navigationTags: navigationTags,
navigationItems: navigationItems,
posts: posts,
pages: pages,
german: .init(file: settings.german),

View File

@ -18,16 +18,17 @@ extension Content {
try storage.save(settings: settings.file)
let fileDescriptions: [FileDescriptions] = files.sorted().compactMap { file in
guard !file.englishDescription.isEmpty || !file.germanDescription.isEmpty else {
guard !file.english.isEmpty || !file.german.isEmpty else {
return nil
}
return FileDescriptions(
fileId: file.id,
german: file.germanDescription.nonEmpty,
english: file.englishDescription.nonEmpty)
german: file.german.nonEmpty,
english: file.english.nonEmpty)
}
try storage.save(fileDescriptions: fileDescriptions)
try storage.save(tagOverview: tagOverview?.file)
let externalFileList = files.filter { $0.isExternallyStored }.map { $0.id }
try storage.save(externalFileList: externalFileList)
@ -130,7 +131,7 @@ extension Settings {
var file: SettingsFile {
.init(
paths: paths.file,
navigationTags: navigationTags.map { $0.id },
navigationItems: navigationItems.map { .init(type: $0.itemType, id: $0.id) },
posts: posts.file,
pages: pages.file,
german: german.file,
@ -138,34 +139,14 @@ extension Settings {
}
}
private extension PathSettings {
var file: PathSettingsFile {
.init(outputDirectoryPath: outputDirectoryPath,
pagesOutputFolderPath: pagesOutputFolderPath,
imagesOutputFolderPath: imagesOutputFolderPath,
filesOutputFolderPath: filesOutputFolderPath,
videosOutputFolderPath: videosOutputFolderPath,
tagsOutputFolderPath: tagsOutputFolderPath)
}
}
private extension PostSettings {
var file: PostSettingsFile {
.init(postsPerPage: postsPerPage,
contentWidth: contentWidth)
}
}
private extension PageSettings {
var file: PageSettingsFile {
.init(pageUrlPrefix: pageUrlPrefix,
contentWidth: contentWidth,
largeImageWidth: largeImageWidth,
pageLinkImageSize: pageLinkImageSize,
javascriptFilesPath: javascriptFilesPath)
swiperCssFile: swiperCssFile?.id,
swiperJsFile: swiperJsFile?.id,
defaultCssFile: defaultCssFile?.id)
}
}

View File

@ -18,11 +18,16 @@ extension Content {
!posts.contains { $0.id == id }
}
func isValidIdForTagOrTagOrPost(_ id: String) -> Bool {
func isValidIdForTagOrPageOrPost(_ id: String) -> Bool {
id.rangeOfCharacter(from: Content.disallowedCharactersInIds) == nil
}
func isValidIdForFile(_ id: String) -> Bool {
id.rangeOfCharacter(from: Content.disallowedCharactersInFileIds) == nil
}
func containsTag(withUrlComponent urlComponent: String) -> Bool {
(tagOverview?.contains(urlComponent: urlComponent) ?? false) ||
tags.contains { $0.contains(urlComponent: urlComponent) }
}
}

View File

@ -19,6 +19,12 @@ final class Content: ObservableObject {
@Published
var files: [FileResource]
@Published
var tagOverview: TagOverviewPage?
@Published
var results: [ItemId : PageGenerationResults] = [:]
@AppStorage("contentPath")
private var storedContentPath: String = ""
@ -38,12 +44,14 @@ final class Content: ObservableObject {
pages: [Page],
tags: [Tag],
files: [FileResource],
tagOverview: TagOverviewPage?,
storedContentPath: String) {
self.settings = settings
self.posts = posts
self.pages = pages
self.tags = tags
self.files = files
self.tagOverview = tagOverview
self.storedContentPath = storedContentPath
self.contentPath = storedContentPath
self.storage = Storage(baseFolder: URL(filePath: storedContentPath))
@ -64,6 +72,7 @@ final class Content: ObservableObject {
self.pages = []
self.tags = []
self.files = []
self.tagOverview = nil
contentPath = storedContentPath
do {

View File

@ -14,3 +14,21 @@ extension ContentLanguage: Codable {
extension ContentLanguage: CaseIterable {
}
extension ContentLanguage: Hashable {
}
extension ContentLanguage: Identifiable {
var id: String {
rawValue
}
}
extension ContentLanguage: Comparable {
static func < (lhs: ContentLanguage, rhs: ContentLanguage) -> Bool {
lhs.rawValue < rhs.rawValue
}
}

View File

@ -5,29 +5,24 @@ final class FileResource: Item {
let type: FileType
/// Globally unique id
@Published
var id: String
@Published
var isExternallyStored: Bool
@Published
var germanDescription: String
var german: String
@Published
var englishDescription: String
var english: String
@Published
var size: CGSize = .zero
init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) {
self.id = id
self.type = FileType(fileExtension: id.fileExtension)
self.englishDescription = en
self.germanDescription = de
self.english = en
self.german = de
self.isExternallyStored = isExternallyStored
super.init(content: content)
super.init(content: content, id: id)
}
/**
@ -35,18 +30,10 @@ final class FileResource: Item {
*/
init(resourceImage: String, type: ImageFileType) {
self.type = .image(type)
self.id = resourceImage
self.englishDescription = "A test image included in the bundle"
self.germanDescription = "Ein Testbild aus dem Bundle"
self.english = "A test image included in the bundle"
self.german = "Ein Testbild aus dem Bundle"
self.isExternallyStored = true
super.init(content: .mock) // TODO: Add images to mock
}
func getDescription(for language: ContentLanguage) -> String {
switch language {
case .english: return englishDescription
case .german: return germanDescription
}
super.init(content: .mock, id: resourceImage) // TODO: Add images to mock
}
// MARK: Text
@ -108,6 +95,11 @@ final class FileResource: Item {
return makeCleanAbsolutePath(path)
}
var assetUrl: String {
let path = content.settings.paths.assetsOutputFolderPath + "/" + id
return makeCleanAbsolutePath(path)
}
private var pathPrefix: String {
switch type {
@ -135,27 +127,6 @@ final class FileResource: Item {
}
}
extension FileResource: Identifiable {
extension FileResource: LocalizedItem {
}
extension FileResource: Equatable {
static func == (lhs: FileResource, rhs: FileResource) -> Bool {
lhs.id == rhs.id
}
}
extension FileResource: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension FileResource: Comparable {
static func < (lhs: FileResource, rhs: FileResource) -> Bool {
lhs.id < rhs.id
}
}

View File

@ -1,18 +0,0 @@
import Foundation
class Item: ObservableObject {
unowned let content: Content
init(content: Content) {
self.content = content
}
func makeCleanAbsolutePath(_ path: String) -> String {
"/" + makeCleanRelativePath(path)
}
func makeCleanRelativePath(_ path: String) -> String {
path.components(separatedBy: "/").filter { !$0.isEmpty }.joined(separator: "/")
}
}

View File

@ -0,0 +1,55 @@
import Foundation
class Item: ObservableObject, Identifiable {
unowned let content: Content
@Published
var id: String
init(content: Content, id: String) {
self.content = content
self.id = id
}
func makeCleanAbsolutePath(_ path: String) -> String {
"/" + makeCleanRelativePath(path)
}
func makeCleanRelativePath(_ path: String) -> String {
path.components(separatedBy: "/").filter { !$0.isEmpty }.joined(separator: "/")
}
func title(in language: ContentLanguage) -> String {
fatalError()
}
func absoluteUrl(in language: ContentLanguage) -> String {
fatalError()
}
var itemType: ItemType {
fatalError()
}
}
extension Item: Equatable {
static func == (lhs: Item, rhs: Item) -> Bool {
lhs.id == rhs.id
}
}
extension Item: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension Item: Comparable {
static func < (lhs: Item, rhs: Item) -> Bool {
lhs.id < rhs.id
}
}

View File

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

View File

@ -0,0 +1,35 @@
enum ItemType: String, Codable {
case post
case tag
case page
case tagOverview
case file
}
extension ItemType: Equatable {
}
extension ItemType: Hashable {
}
extension ItemType: Identifiable {
var id: String {
rawValue
}
}
extension ItemType: Comparable {
static func < (lhs: ItemType, rhs: ItemType) -> Bool {
lhs.rawValue < rhs.rawValue
}
}

View File

@ -0,0 +1,19 @@
protocol LocalizedItem {
associatedtype Localized
var german: Localized { get }
var english: Localized { get }
}
extension LocalizedItem {
func localized(in language: ContentLanguage) -> Localized {
switch language {
case .german: return german
case .english: return english
}
}
}

View File

@ -0,0 +1,99 @@
import Foundation
final class TagOverviewPage: Item {
static let id = "all-tags"
@Published
var german: LocalizedTagOverviewPage
@Published
var english: LocalizedTagOverviewPage
init(content: Content, german: LocalizedTagOverviewPage, english: LocalizedTagOverviewPage) {
self.german = german
self.english = english
super.init(content: content, id: TagOverviewPage.id)
}
override var itemType: ItemType {
.tagOverview
}
override func title(in language: ContentLanguage) -> String {
localized(in: language).title
}
override func absoluteUrl(in language: ContentLanguage) -> String {
makeCleanAbsolutePath(internalPath(for: language))
}
func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String {
makeCleanRelativePath(internalPath(for: language))
}
private func internalPath(for language: ContentLanguage) -> String {
content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlString
}
func contains(urlComponent: String) -> Bool {
english.urlString == urlComponent || german.urlString == urlComponent
}
var file: TagOverviewFile {
.init(german: german.file,
english: english.file)
}
}
extension TagOverviewPage: LocalizedItem {
}
final class LocalizedTagOverviewPage: ObservableObject {
@Published
var title: String
/**
The string to use when creating the url for the page.
Defaults to ``id`` if unset.
*/
@Published
var urlString: String
@Published
var linkPreviewImage: FileResource?
@Published
var linkPreviewTitle: String?
@Published
var linkPreviewDescription: String?
init(title: String, urlString: String, linkPreviewImage: FileResource? = nil, linkPreviewTitle: String? = nil, linkPreviewDescription: String? = nil) {
self.title = title
self.urlString = urlString
self.linkPreviewImage = linkPreviewImage
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription
}
init(file: LocalizedTagOverviewFile, image: FileResource?) {
self.title = file.title
self.urlString = file.url
self.linkPreviewImage = image
self.linkPreviewTitle = file.linkPreviewTitle
self.linkPreviewDescription = file.linkPreviewDescription
}
var file: LocalizedTagOverviewFile {
.init(url: urlString,
title: title,
linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription)
}
}

View File

@ -2,12 +2,6 @@ import Foundation
final class Page: Item {
/**
The unique id of the entry
*/
@Published
var id: String
/**
The external link this page points to.
@ -59,7 +53,6 @@ final class Page: Item {
german: LocalizedPage,
english: LocalizedPage,
tags: [Tag]) {
self.id = id
self.externalLink = externalLink
self.isDraft = isDraft
self.createdDate = createdDate
@ -70,14 +63,7 @@ final class Page: Item {
self.english = english
self.tags = tags
super.init(content: content)
}
func localized(in language: ContentLanguage) -> LocalizedPage {
switch language {
case .german: return german
case .english: return english
}
super.init(content: content, id: id)
}
func update(id newId: String) -> Bool {
@ -95,7 +81,7 @@ final class Page: Item {
// MARK: Paths
func absoluteUrl(for language: ContentLanguage) -> String {
override func absoluteUrl(in language: ContentLanguage) -> String {
if let url = externalLink {
return url
}
@ -103,40 +89,31 @@ final class Page: Item {
return makeCleanAbsolutePath(internalPath(for: language))
}
override func title(in language: ContentLanguage) -> String {
localized(in: language).title
}
func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String {
makeCleanRelativePath(internalPath(for: language))
}
private func internalPath(for language: ContentLanguage) -> String {
content.settings.pages.pageUrlPrefix + "/" + localized(in: language).urlString
content.settings.paths.pagesOutputFolderPath + "/" + localized(in: language).urlString
}
}
extension Page: Identifiable {
}
extension Page: Equatable {
static func == (lhs: Page, rhs: Page) -> Bool {
lhs.id == rhs.id
override var itemType: ItemType {
.page
}
}
extension Page: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension Page: Comparable {
static func < (lhs: Page, rhs: Page) -> Bool {
lhs.id < rhs.id
func contains(urlComponent: String) -> Bool {
english.urlString == urlComponent || german.urlString == urlComponent
}
}
extension Page: DateItem {
}
extension Page: LocalizedItem {
}

View File

@ -2,11 +2,6 @@ import Foundation
final class PageSettings: ObservableObject {
/// The prefix of the urls for all pages
/// The full path will be `<pagePrefix>/<page-url-component>`
@Published
var pageUrlPrefix: String
@Published
var contentWidth: Int
@ -17,13 +12,39 @@ final class PageSettings: ObservableObject {
var pageLinkImageSize: Int
@Published
var javascriptFilesPath: String
var defaultCssFile: FileResource?
init(file: PageSettingsFile) {
self.pageUrlPrefix = file.pageUrlPrefix
@Published
var codeHighlightingJsFile: FileResource?
@Published
var audioPlayerJsFile: FileResource?
@Published
var audioPlayerCssFile: FileResource?
@Published
var modelViewerJsFile: FileResource?
init(file: PageSettingsFile, files: [String : FileResource]) {
self.contentWidth = file.contentWidth
self.largeImageWidth = file.largeImageWidth
self.pageLinkImageSize = file.pageLinkImageSize
self.javascriptFilesPath = file.javascriptFilesPath
self.defaultCssFile = file.defaultCssFile.map { files[$0] }
self.codeHighlightingJsFile = file.codeHighlightingJsFile.map { files[$0] }
self.audioPlayerJsFile = file.audioPlayerJsFile.map { files[$0] }
self.audioPlayerCssFile = file.audioPlayerCssFile.map { files[$0] }
self.modelViewerJsFile = file.modelViewerJsFile.map { files[$0] }
}
var file: PageSettingsFile {
.init(contentWidth: contentWidth,
largeImageWidth: largeImageWidth,
pageLinkImageSize: pageLinkImageSize,
defaultCssFile: defaultCssFile?.id,
codeHighlightingJsFile: codeHighlightingJsFile?.id,
audioPlayerJsFile: audioPlayerJsFile?.id,
audioPlayerCssFile: audioPlayerJsFile?.id,
modelViewerJsFile: modelViewerJsFile?.id)
}
}

View File

@ -5,6 +5,9 @@ final class PathSettings: ObservableObject {
@Published
var outputDirectoryPath: String
@Published
var assetsOutputFolderPath: String
@Published
var pagesOutputFolderPath: String
@ -21,6 +24,7 @@ final class PathSettings: ObservableObject {
var tagsOutputFolderPath: String
init(file: PathSettingsFile) {
self.assetsOutputFolderPath = file.assetsOutputFolderPath
self.outputDirectoryPath = file.outputDirectoryPath
self.pagesOutputFolderPath = file.pagesOutputFolderPath
self.imagesOutputFolderPath = file.imagesOutputFolderPath
@ -28,4 +32,14 @@ final class PathSettings: ObservableObject {
self.videosOutputFolderPath = file.videosOutputFolderPath
self.tagsOutputFolderPath = file.tagsOutputFolderPath
}
var file: PathSettingsFile {
.init(outputDirectoryPath: outputDirectoryPath,
assetsOutputFolderPath: assetsOutputFolderPath,
pagesOutputFolderPath: pagesOutputFolderPath,
imagesOutputFolderPath: imagesOutputFolderPath,
filesOutputFolderPath: filesOutputFolderPath,
videosOutputFolderPath: videosOutputFolderPath,
tagsOutputFolderPath: tagsOutputFolderPath)
}
}

View File

@ -10,13 +10,32 @@ final class PostSettings: ObservableObject {
@Published
var contentWidth: Int
init(postsPerPage: Int, contentWidth: Int) {
@Published
var swiperCssFile: FileResource?
@Published
var swiperJsFile: FileResource?
@Published
var defaultCssFile: FileResource?
init(postsPerPage: Int,
contentWidth: Int,
swiperCssFile: FileResource?,
swiperJsFile: FileResource?,
defaultCssFile: FileResource?) {
self.postsPerPage = postsPerPage
self.contentWidth = contentWidth
self.swiperCssFile = swiperCssFile
self.swiperJsFile = swiperJsFile
self.defaultCssFile = defaultCssFile
}
init(file: PostSettingsFile) {
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] }
}
}

View File

@ -5,9 +5,9 @@ final class Settings: ObservableObject {
@Published
var paths: PathSettings
/// The tags to show in the navigation bar
/// The items to show in the navigation bar
@Published
var navigationTags: [Tag]
var navigationItems: [Item]
@Published
var posts: PostSettings
@ -21,9 +21,9 @@ final class Settings: ObservableObject {
@Published
var english: LocalizedPostSettings
init(paths: PathSettings, navigationTags: [Tag], posts: PostSettings, pages: PageSettings, german: LocalizedPostSettings, english: LocalizedPostSettings) {
init(paths: PathSettings, navigationItems: [Item], posts: PostSettings, pages: PageSettings, german: LocalizedPostSettings, english: LocalizedPostSettings) {
self.paths = paths
self.navigationTags = navigationTags
self.navigationItems = navigationItems
self.posts = posts
self.pages = pages
self.german = german

View File

@ -2,10 +2,6 @@ import Foundation
final class Tag: Item {
var id: String {
english.urlComponent
}
@Published
var isVisible: Bool
@ -15,19 +11,19 @@ final class Tag: Item {
@Published
var english: LocalizedTag
init(content: Content, id: String) {
override init(content: Content, id: String) {
self.isVisible = true
self.english = .init(urlComponent: id, name: id)
let deId = id + "-" + ContentLanguage.german.rawValue
self.german = .init(urlComponent: deId, name: deId)
super.init(content: content)
super.init(content: content, id: id)
}
init(content: Content, isVisible: Bool = true, german: LocalizedTag, english: LocalizedTag) {
init(content: Content, id: String, isVisible: Bool = true, german: LocalizedTag, english: LocalizedTag) {
self.isVisible = isVisible
self.german = german
self.english = english
super.init(content: content)
super.init(content: content, id: id)
}
var linkName: String {
@ -38,49 +34,33 @@ final class Tag: Item {
"/tags/\(linkName).html"
}
func localized(in language: ContentLanguage) -> LocalizedTag {
switch language {
case .english: return english
case .german: return german
}
}
// MARK: Paths
func absoluteUrl(for language: ContentLanguage) -> String {
makeCleanAbsolutePath(internalPath(for: language))
}
func filePathRelativeToOutputFolder(for language: ContentLanguage) -> String {
makeCleanRelativePath(internalPath(for: language))
}
private func internalPath(for language: ContentLanguage) -> String {
content.settings.pages.pageUrlPrefix + "/" + localized(in: language).urlComponent
content.settings.paths.tagsOutputFolderPath + "/" + localized(in: language).urlComponent
}
override func absoluteUrl(in language: ContentLanguage) -> String {
makeCleanAbsolutePath(internalPath(for: language))
}
override func title(in language: ContentLanguage) -> String {
localized(in: language).name
}
override var itemType: ItemType {
.tag
}
func contains(urlComponent: String) -> Bool {
german.urlComponent == urlComponent || english.urlComponent == urlComponent
}
}
extension Tag: Identifiable {
}
extension Tag: Equatable {
static func == (_ lhs: Tag, _ rhs: Tag) -> Bool {
lhs.id == rhs.id
}
}
extension Tag: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension Tag: Comparable {
static func < (lhs: Tag, rhs: Tag) -> Bool {
lhs.id < rhs.id
}
extension Tag: LocalizedItem {
}