Import old content, load from disk
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class Content: ObservableObject {
|
||||
|
||||
@ -17,6 +18,9 @@ final class Content: ObservableObject {
|
||||
@Published
|
||||
var files: [FileResources] = []
|
||||
|
||||
@AppStorage("contentPath")
|
||||
var contentPath: String = ""
|
||||
|
||||
func generateFeed(for language: ContentLanguage, bookmarkKey: String) {
|
||||
let posts = posts.map { $0.feedEntry(for: language) }
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
@ -56,19 +60,178 @@ final class Content: ObservableObject {
|
||||
}
|
||||
|
||||
func importOldContent() {
|
||||
let importer = Importer()
|
||||
let storage = Storage(baseFolder: URL(filePath: "/Users/ch/Downloads/Content"))
|
||||
do {
|
||||
try importer.importOldContent()
|
||||
try storage.createFolderStructure()
|
||||
} catch {
|
||||
print(error)
|
||||
return
|
||||
}
|
||||
self.posts = importer.posts
|
||||
self.tags = importer.tags
|
||||
#warning("TODO: Copy page sources to data folder")
|
||||
self.pages = importer.pages
|
||||
self.images = importer.images
|
||||
#warning("TODO: Copy images to data folder")
|
||||
|
||||
let importer = Importer()
|
||||
do {
|
||||
try importer.importContent()
|
||||
} catch {
|
||||
print(error)
|
||||
return
|
||||
}
|
||||
for (_, file) in importer.files.sorted(by: { $0.key < $1.key }) {
|
||||
storage.copyFile(at: file.url, fileId: file.name)
|
||||
// TODO: Store alt text for image and videos
|
||||
}
|
||||
var missingPages: [String] = []
|
||||
for (pageId, page) in importer.pages.sorted(by: { $0.key < $1.key }) {
|
||||
storage.save(pageMetadata: page.page, for: pageId)
|
||||
|
||||
if FileManager.default.fileExists(atPath: page.deContentUrl.path()) {
|
||||
storage.copyPageContent(from: page.deContentUrl, for: pageId, language: .german)
|
||||
} else {
|
||||
missingPages.append(pageId + " (DE)")
|
||||
}
|
||||
|
||||
if FileManager.default.fileExists(atPath: page.enContentUrl.path()) {
|
||||
storage.copyPageContent(from: page.enContentUrl, for: pageId, language: .english)
|
||||
} else {
|
||||
missingPages.append(pageId + " (EN)")
|
||||
}
|
||||
}
|
||||
|
||||
for (tagId, tag) in importer.tags {
|
||||
storage.save(tagMetadata: tag, for: tagId)
|
||||
}
|
||||
|
||||
for (postId, post) in importer.posts {
|
||||
storage.save(post: post, for: postId)
|
||||
}
|
||||
|
||||
let ignoredFiles = importer.ignoredFiles
|
||||
.map { $0.path() }
|
||||
.sorted()
|
||||
|
||||
print("Ignored files:")
|
||||
for file in ignoredFiles {
|
||||
print(file)
|
||||
}
|
||||
|
||||
print("Missing pages:")
|
||||
for page in missingPages {
|
||||
print(page)
|
||||
}
|
||||
|
||||
do {
|
||||
try loadFromDisk()
|
||||
} catch {
|
||||
print("Failed to load from disk: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func convert(_ tag: LocalizedTagFile) -> LocalizedTag {
|
||||
LocalizedTag(
|
||||
urlComponent: tag.urlComponent,
|
||||
name: tag.name,
|
||||
subtitle: tag.subtitle,
|
||||
description: tag.description,
|
||||
thumbnail: tag.thumbnail,
|
||||
originalUrl: tag.originalURL)
|
||||
}
|
||||
|
||||
func loadFromDisk() throws {
|
||||
let storage = Storage(baseFolder: URL(filePath: contentPath))
|
||||
|
||||
let tagData = try storage.loadAllTags()
|
||||
let pagesData = try storage.loadAllPages()
|
||||
let postsData = try storage.loadAllPosts()
|
||||
let filesData = try storage.loadAllFiles()
|
||||
|
||||
let tags = tagData.reduce(into: [:]) { (tags, data) in
|
||||
tags[data.key] = Tag(german: convert(data.value.german),
|
||||
english: convert(data.value.english))
|
||||
}
|
||||
|
||||
let pages: [String : Page] = loadPages(pagesData, tags: tags)
|
||||
|
||||
let images: [String : ImageResource] = filesData.reduce(into: [:]) { dict, item in
|
||||
let (file, url) = item
|
||||
let ext = file.components(separatedBy: ".").last!.lowercased()
|
||||
let type = FileType(fileExtension: ext)
|
||||
guard type == .image else { return }
|
||||
dict[file] = ImageResource(uniqueId: file, altText: .init(en: "", de: ""), fileUrl: url)
|
||||
}
|
||||
|
||||
let files: [FileResources] = filesData.compactMap { file, url in
|
||||
let ext = file.components(separatedBy: ".").last!.lowercased()
|
||||
let type = FileType(fileExtension: ext)
|
||||
guard type == .file else {
|
||||
return nil
|
||||
}
|
||||
return FileResources(uniqueId: file, description: "")
|
||||
}
|
||||
|
||||
let posts = postsData.map { postId, post in
|
||||
let linkedPage = post.linkedPageId.map { pages[$0] }
|
||||
|
||||
let german = LocalizedPost(
|
||||
title: post.german.title,
|
||||
content: post.german.content,
|
||||
lastModified: post.german.lastModifiedDate,
|
||||
images: post.german.images.compactMap { images[$0] })
|
||||
|
||||
let english = LocalizedPost(
|
||||
title: post.english.title,
|
||||
content: post.english.content,
|
||||
lastModified: post.english.lastModifiedDate,
|
||||
images: post.english.images.compactMap { images[$0] })
|
||||
|
||||
return Post(
|
||||
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)
|
||||
}
|
||||
|
||||
self.tags = tags.values.sorted()
|
||||
self.pages = pages.values.sorted(ascending: false) { $0.startDate }
|
||||
self.files = files.sorted { $0.uniqueId }
|
||||
self.images = images.values.sorted { $0.id }
|
||||
self.posts = posts.sorted(ascending: false) { $0.startDate }
|
||||
}
|
||||
|
||||
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag]) -> [String : Page] {
|
||||
pagesData.reduce(into: [:]) { pages, data in
|
||||
let (pageId, page) = data
|
||||
let germanPage = LocalizedPage(
|
||||
urlString: page.german.url,
|
||||
title: page.german.title,
|
||||
lastModified: page.german.lastModifiedDate,
|
||||
originalUrl: page.german.originalURL,
|
||||
files: page.german.files,
|
||||
externalFiles: page.german.externalFiles,
|
||||
requiredFiles: page.german.requiredFiles)
|
||||
|
||||
let englishPage = LocalizedPage(
|
||||
urlString: page.english.url,
|
||||
title: page.english.title,
|
||||
lastModified: page.english.lastModifiedDate,
|
||||
originalUrl: page.english.originalURL,
|
||||
files: page.english.files,
|
||||
externalFiles: page.english.externalFiles,
|
||||
requiredFiles: page.english.requiredFiles)
|
||||
|
||||
pages[pageId] = Page(
|
||||
id: pageId,
|
||||
isDraft: page.isDraft,
|
||||
createdDate: page.createdDate,
|
||||
startDate: page.startDate,
|
||||
endDate: page.endDate,
|
||||
german: germanPage,
|
||||
english: englishPage,
|
||||
tags: page.tags.map { tags[$0]! })
|
||||
}
|
||||
}
|
||||
|
||||
static func accessFolderFromBookmark(key: String, operation: (URL) -> Void) {
|
||||
@ -100,5 +263,4 @@ final class Content: ObservableObject {
|
||||
print("Failed to access folder: \(folderURL.path)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,3 +6,7 @@ enum ContentLanguage: String {
|
||||
|
||||
case german = "de"
|
||||
}
|
||||
|
||||
extension ContentLanguage: Codable {
|
||||
|
||||
}
|
||||
|
72
CHDataManagement/Model/LocalizedPage.swift
Normal file
72
CHDataManagement/Model/LocalizedPage.swift
Normal file
@ -0,0 +1,72 @@
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A localized page contains the page content of a single language,
|
||||
including the title, url path and required resources
|
||||
|
||||
*/
|
||||
final class LocalizedPage: ObservableObject {
|
||||
|
||||
/**
|
||||
The string to use when creating the url for the page.
|
||||
|
||||
Defaults to ``id`` if unset.
|
||||
*/
|
||||
@Published
|
||||
var urlString: String
|
||||
|
||||
/**
|
||||
The headline to use when showing the entry on it's own page
|
||||
*/
|
||||
@Published
|
||||
var title: String
|
||||
|
||||
@Published
|
||||
var lastModified: Date?
|
||||
|
||||
/**
|
||||
The url used on the old version of the website.
|
||||
|
||||
Needed to redirect links to their new locations.
|
||||
*/
|
||||
let originalUrl: String?
|
||||
|
||||
/**
|
||||
All files which occur in the content and are stored.
|
||||
- Note: This property defaults to an empty set.
|
||||
*/
|
||||
@Published
|
||||
var files: Set<String> = []
|
||||
|
||||
/**
|
||||
All files which may occur in the content but are stored externally.
|
||||
|
||||
Missing files which would otherwise produce a warning are ignored when included here.
|
||||
- Note: This property defaults to an empty set.
|
||||
*/
|
||||
@Published
|
||||
var externalFiles: Set<String> = []
|
||||
|
||||
/**
|
||||
Specifies additional files which should be copied to the destination when generating the content.
|
||||
- Note: This property defaults to an empty set.
|
||||
*/
|
||||
@Published
|
||||
var requiredFiles: Set<String> = []
|
||||
|
||||
init(urlString: String,
|
||||
title: String,
|
||||
lastModified: Date? = nil,
|
||||
originalUrl: String? = nil,
|
||||
files: Set<String> = [],
|
||||
externalFiles: Set<String> = [],
|
||||
requiredFiles: Set<String> = []) {
|
||||
self.urlString = urlString
|
||||
self.title = title
|
||||
self.lastModified = lastModified
|
||||
self.originalUrl = originalUrl
|
||||
self.files = files
|
||||
self.externalFiles = externalFiles
|
||||
self.requiredFiles = requiredFiles
|
||||
}
|
||||
}
|
54
CHDataManagement/Model/LocalizedPost.swift
Normal file
54
CHDataManagement/Model/LocalizedPost.swift
Normal file
@ -0,0 +1,54 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class LocalizedPost: ObservableObject {
|
||||
|
||||
@Published
|
||||
var title: String
|
||||
|
||||
@Published
|
||||
var content: String
|
||||
|
||||
@Published
|
||||
var lastModified: Date?
|
||||
|
||||
@Published
|
||||
var images: [ImageResource]
|
||||
|
||||
init(title: String? = nil,
|
||||
content: String,
|
||||
lastModified: Date? = nil,
|
||||
images: [ImageResource] = []) {
|
||||
self.title = title ?? ""
|
||||
self.content = content
|
||||
self.lastModified = lastModified
|
||||
self.images = images
|
||||
}
|
||||
|
||||
var displayImages: [Image] {
|
||||
images.map { $0.imageToDisplay }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func editableTitle() -> Binding<String> {
|
||||
Binding(
|
||||
get: {
|
||||
self.title
|
||||
},
|
||||
set: { newValue in
|
||||
self.title = newValue
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func editableContent() -> Binding<String> {
|
||||
Binding(
|
||||
get: {
|
||||
self.content
|
||||
},
|
||||
set: { newValue in
|
||||
self.content = newValue
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
48
CHDataManagement/Model/LocalizedTag.swift
Normal file
48
CHDataManagement/Model/LocalizedTag.swift
Normal file
@ -0,0 +1,48 @@
|
||||
import Foundation
|
||||
|
||||
final class LocalizedTag: ObservableObject {
|
||||
|
||||
@Published
|
||||
var urlComponent: String
|
||||
|
||||
/// A custom name, different from the tag id
|
||||
@Published
|
||||
var name: String
|
||||
|
||||
@Published
|
||||
var subtitle: String?
|
||||
|
||||
@Published
|
||||
var description: String?
|
||||
|
||||
/// The image id of the thumbnail
|
||||
@Published
|
||||
var thumbnail: String?
|
||||
|
||||
/// The original url in the previous site layout
|
||||
let originalUrl: String?
|
||||
|
||||
init(urlComponent: String,
|
||||
name: String,
|
||||
subtitle: String? = nil,
|
||||
description: String? = nil,
|
||||
thumbnail: String? = nil,
|
||||
originalUrl: String? = nil) {
|
||||
self.urlComponent = urlComponent
|
||||
self.name = name
|
||||
self.subtitle = subtitle
|
||||
self.description = description
|
||||
self.thumbnail = thumbnail
|
||||
self.originalUrl = originalUrl
|
||||
}
|
||||
}
|
||||
|
||||
extension LocalizedTag {
|
||||
|
||||
func data() -> FeedEntryData.Tag {
|
||||
.init(
|
||||
name: name,
|
||||
url: "tags/\(urlComponent).html"
|
||||
)
|
||||
}
|
||||
}
|
@ -12,23 +12,25 @@ final class Page: ObservableObject {
|
||||
var isDraft: Bool
|
||||
|
||||
@Published
|
||||
var metadata: [LocalizedPage]
|
||||
var createdDate: Date
|
||||
|
||||
/**
|
||||
All files which may occur in content but is stored externally.
|
||||
|
||||
Missing files which would otherwise produce a warning are ignored when included here.
|
||||
- Note: This property defaults to an empty set.
|
||||
*/
|
||||
@Published
|
||||
var externalFiles: Set<String> = []
|
||||
var startDate: Date
|
||||
|
||||
/**
|
||||
Specifies additional files which should be copied to the destination when generating the content.
|
||||
- Note: This property defaults to an empty set.
|
||||
*/
|
||||
@Published
|
||||
var requiredFiles: Set<String> = []
|
||||
var hasEndDate: Bool
|
||||
|
||||
@Published
|
||||
var endDate: Date
|
||||
|
||||
@Published
|
||||
var german: LocalizedPage
|
||||
|
||||
@Published
|
||||
var english: LocalizedPage
|
||||
|
||||
@Published
|
||||
var tags: [Tag]
|
||||
|
||||
/**
|
||||
Additional images required by the element.
|
||||
@ -38,36 +40,31 @@ final class Page: ObservableObject {
|
||||
@Published
|
||||
var images: Set<String> = []
|
||||
|
||||
init(id: String, isDraft: Bool, metadata: [LocalizedPage], externalFiles: Set<String> = [], requiredFiles: Set<String> = [], images: Set<String> = []) {
|
||||
init(id: String,
|
||||
isDraft: Bool,
|
||||
createdDate: Date,
|
||||
startDate: Date,
|
||||
endDate: Date?,
|
||||
german: LocalizedPage,
|
||||
english: LocalizedPage,
|
||||
tags: [Tag]) {
|
||||
self.id = id
|
||||
self.isDraft = isDraft
|
||||
self.metadata = metadata
|
||||
self.externalFiles = externalFiles
|
||||
self.requiredFiles = requiredFiles
|
||||
self.images = images
|
||||
self.createdDate = createdDate
|
||||
self.startDate = startDate
|
||||
self.hasEndDate = endDate != nil
|
||||
self.endDate = endDate ?? startDate
|
||||
self.german = german
|
||||
self.english = english
|
||||
self.tags = tags
|
||||
}
|
||||
|
||||
func metadata(for language: ContentLanguage) -> LocalizedPage? {
|
||||
metadata.first { $0.language == language }
|
||||
switch language {
|
||||
case .german: return german
|
||||
case .english: return english
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct LocalizedPage {
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
/**
|
||||
The string to use when creating the url for the page.
|
||||
|
||||
Defaults to ``id`` if unset.
|
||||
*/
|
||||
var urlString: String?
|
||||
|
||||
/**
|
||||
The headline to use when showing the entry on it's own page
|
||||
*/
|
||||
var headline: String
|
||||
}
|
||||
|
||||
extension Page: Identifiable {
|
||||
|
@ -1,12 +1,15 @@
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
final class Post: ObservableObject {
|
||||
|
||||
let id: Int
|
||||
let id: String
|
||||
|
||||
@Published
|
||||
var isDraft: Bool
|
||||
|
||||
@Published
|
||||
var createdDate: Date
|
||||
|
||||
@Published
|
||||
var startDate: Date
|
||||
|
||||
@ -19,33 +22,42 @@ final class Post: ObservableObject {
|
||||
@Published
|
||||
var tags: [Tag]
|
||||
|
||||
let title: LocalizedText
|
||||
@Published
|
||||
var german: LocalizedPost
|
||||
|
||||
let text: LocalizedText
|
||||
|
||||
var images: [ImageResource]
|
||||
@Published
|
||||
var english: LocalizedPost
|
||||
|
||||
/// The page linked to by this post
|
||||
@Published
|
||||
var linkedPage: Page?
|
||||
|
||||
init(id: Int,
|
||||
isDraft: Bool = false,
|
||||
init(id: String,
|
||||
isDraft: Bool,
|
||||
createdDate: Date,
|
||||
startDate: Date,
|
||||
endDate: Date? = nil,
|
||||
title: LocalizedText,
|
||||
text: LocalizedText,
|
||||
endDate: Date?,
|
||||
tags: [Tag],
|
||||
images: [ImageResource]) {
|
||||
german: LocalizedPost,
|
||||
english: LocalizedPost,
|
||||
linkedPage: Page? = nil) {
|
||||
self.id = id
|
||||
self.isDraft = isDraft
|
||||
self.createdDate = createdDate
|
||||
self.startDate = startDate
|
||||
self.hasEndDate = endDate != nil
|
||||
self.endDate = endDate ?? startDate
|
||||
self.title = title
|
||||
self.text = text
|
||||
self.tags = tags
|
||||
self.images = images
|
||||
self.german = german
|
||||
self.english = english
|
||||
self.linkedPage = linkedPage
|
||||
}
|
||||
|
||||
func localized(in language: ContentLanguage) -> LocalizedPost {
|
||||
switch language {
|
||||
case .english: return english
|
||||
case .german: return german
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,8 +154,7 @@ extension Post {
|
||||
}
|
||||
|
||||
private func paragraphs(in language: ContentLanguage) -> [String] {
|
||||
text
|
||||
.getText(for: language)
|
||||
localized(in: language).content
|
||||
.components(separatedBy: "\n")
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { $0 != "" }
|
||||
@ -154,17 +165,16 @@ extension Post {
|
||||
}
|
||||
|
||||
func feedEntry(for language: ContentLanguage) -> FeedEntryData {
|
||||
.init(
|
||||
let post = localized(in: language)
|
||||
return .init(
|
||||
entryId: "\(id)",
|
||||
title: title.getText(for: language),
|
||||
title: post.title,
|
||||
textAboveTitle: dateText(in: language),
|
||||
link: linkToPageInFeed(for: language),
|
||||
tags: tags.map { $0.data(in: language) },
|
||||
text: paragraphs(in: language),
|
||||
images: images.map { $0.feedEntryImage(for: language) })
|
||||
}
|
||||
|
||||
var displayImages: [Image] {
|
||||
images.map { $0.imageToDisplay }
|
||||
images: post.images.map {
|
||||
$0.feedEntryImage(for: language)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,18 @@ import Foundation
|
||||
final class Tag: ObservableObject {
|
||||
|
||||
var id: String {
|
||||
name.getText(for: .english).lowercased().replacingOccurrences(of: " ", with: "-")
|
||||
english.urlComponent
|
||||
}
|
||||
|
||||
@Published
|
||||
var name: LocalizedText
|
||||
var german: LocalizedTag
|
||||
|
||||
init(en: String, de: String) {
|
||||
self.name = .init(en: en, de: de)
|
||||
@Published
|
||||
var english: LocalizedTag
|
||||
|
||||
init(german: LocalizedTag, english: LocalizedTag) {
|
||||
self.german = german
|
||||
self.english = english
|
||||
}
|
||||
|
||||
var linkName: String {
|
||||
@ -24,22 +28,13 @@ final class Tag: ObservableObject {
|
||||
|
||||
extension Tag {
|
||||
|
||||
func getUrl(for language: ContentLanguage) -> String {
|
||||
"/\(language.rawValue)/tags/\(id).html"
|
||||
}
|
||||
|
||||
func data(in language: ContentLanguage) -> FeedEntryData.Tag {
|
||||
.init(
|
||||
name: name.getText(for: language),
|
||||
url: getUrl(for: language)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Tag: ExpressibleByStringLiteral {
|
||||
|
||||
convenience init(stringLiteral value: StringLiteralType) {
|
||||
self.init(en: value.capitalized, de: value.capitalized)
|
||||
switch language {
|
||||
case .english:
|
||||
return english.data()
|
||||
case .german:
|
||||
return german.data()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user