356 lines
12 KiB
Swift
356 lines
12 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
import Combine
|
|
|
|
final class Content: ObservableObject {
|
|
|
|
@Published
|
|
var posts: [Post] = []
|
|
|
|
@Published
|
|
var pages: [Page] = []
|
|
|
|
@Published
|
|
var tags: [Tag] = []
|
|
|
|
@Published
|
|
var images: [ImageResource] = []
|
|
|
|
@Published
|
|
var files: [FileResource] = []
|
|
|
|
@AppStorage("contentPath")
|
|
private var storedContentPath: String = ""
|
|
|
|
@Published
|
|
var contentPath: String = "" {
|
|
didSet {
|
|
storedContentPath = contentPath
|
|
}
|
|
}
|
|
|
|
let storage: Storage
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
init(posts: [Post] = [],
|
|
pages: [Page] = [],
|
|
tags: [Tag] = [],
|
|
images: [ImageResource] = [],
|
|
files: [FileResource] = [],
|
|
storedContentPath: String) {
|
|
self.posts = posts
|
|
self.pages = pages
|
|
self.tags = tags
|
|
self.images = images
|
|
self.files = files
|
|
self.storedContentPath = storedContentPath
|
|
self.contentPath = storedContentPath
|
|
self.storage = Storage(baseFolder: URL(filePath: storedContentPath))
|
|
do {
|
|
try storage.createFolderStructure()
|
|
} catch {
|
|
print(error)
|
|
return
|
|
}
|
|
observeContentPath()
|
|
}
|
|
|
|
init() {
|
|
self.storage = Storage(baseFolder: URL(filePath: ""))
|
|
|
|
contentPath = storedContentPath
|
|
do {
|
|
try storage.createFolderStructure()
|
|
} catch {
|
|
print(error)
|
|
return
|
|
}
|
|
|
|
try? storage.update(baseFolder: URL(filePath: contentPath), moveContent: false)
|
|
observeContentPath()
|
|
}
|
|
|
|
private func observeContentPath() {
|
|
$contentPath.sink { newValue in
|
|
let url = URL(filePath: newValue)
|
|
try? self.storage.update(baseFolder: url, moveContent: true)
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
|
|
func generateFeed(for language: ContentLanguage, bookmarkKey: String) {
|
|
let posts = posts.map { $0.feedEntry(for: language) }
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
|
|
let navigationItems: [FeedNavigationLink] = [
|
|
.init(text: .init(en: "Projects", de: "Projekte"),
|
|
url: .init(en: "/projects", de: "/projekte")),
|
|
.init(text: .init(en: "Adventures", de: "Abenteuer"),
|
|
url: .init(en: "/adventures", de: "/abenteuer")),
|
|
.init(text: .init(en: "Services", de: "Dienste"),
|
|
url: .init(en: "/services", de: "/dienste")),
|
|
.init(text: .init(en: "Tags", de: "Kategorien"),
|
|
url: .init(en: "/tags", de: "/kategorien")),
|
|
]
|
|
|
|
let feed = Feed(
|
|
language: language,
|
|
title: .init(en: "Blog | CH", de: "Blog | CH"),
|
|
description: .init(en: "The latests posts, projects and adventures",
|
|
de: "Die neusten Beiträge, Projekte und Abenteuer"),
|
|
iconDescription: .init(en: "An icon consisting of the letters C and H in blue and orange",
|
|
de: "Ein Logo aus den Buchstaben C und H in Blau und Orange"),
|
|
navigationItems: navigationItems,
|
|
posts: posts)
|
|
let fileContent = feed.content
|
|
Content.accessFolderFromBookmark(key: bookmarkKey) { folder in
|
|
let outputFile = folder.appendingPathComponent("feed.html", isDirectory: false)
|
|
do {
|
|
try fileContent
|
|
.data(using: .utf8)!
|
|
.write(to: outputFile)
|
|
} catch {
|
|
print("Failed to save: \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func importOldContent() {
|
|
|
|
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: [FileResource] = filesData.compactMap { file, url in
|
|
let ext = file.components(separatedBy: ".").last!.lowercased()
|
|
let type = FileType(fileExtension: ext)
|
|
guard type == .file else {
|
|
return nil
|
|
}
|
|
return FileResource(uniqueId: file, description: "")
|
|
}
|
|
|
|
let posts = postsData.map { postId, post in
|
|
let linkedPage = post.linkedPageId.map { pages[$0] }
|
|
|
|
|
|
let germanData = post.german
|
|
let german = LocalizedPost(
|
|
title: germanData.title,
|
|
content: germanData.content,
|
|
lastModified: germanData.lastModifiedDate,
|
|
images: germanData.images.compactMap { images[$0] },
|
|
linkPreviewImage: germanData.linkPreviewImage.map { images[$0] },
|
|
linkPreviewTitle: germanData.linkPreviewTitle,
|
|
linkPreviewDescription: germanData.linkPreviewDescription)
|
|
|
|
let englishData = post.english
|
|
let english = LocalizedPost(
|
|
title: englishData.title,
|
|
content: englishData.content,
|
|
lastModified: englishData.lastModifiedDate,
|
|
images: englishData.images.compactMap { images[$0] },
|
|
linkPreviewImage: englishData.linkPreviewImage.map { images[$0] },
|
|
linkPreviewTitle: englishData.linkPreviewTitle,
|
|
linkPreviewDescription: englishData.linkPreviewDescription)
|
|
|
|
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 german = page.german
|
|
let germanPage = LocalizedPage(
|
|
urlString: german.url,
|
|
title: german.title,
|
|
lastModified: german.lastModifiedDate,
|
|
originalUrl: german.originalURL,
|
|
files: Set(german.files),
|
|
externalFiles: Set(german.externalFiles),
|
|
requiredFiles: Set(german.requiredFiles),
|
|
linkPreviewImage: german.linkPreviewImage,
|
|
linkPreviewTitle: german.linkPreviewTitle,
|
|
linkPreviewDescription: german.linkPreviewDescription)
|
|
|
|
let english = page.english
|
|
let englishPage = LocalizedPage(
|
|
urlString: english.url,
|
|
title: english.title,
|
|
lastModified: english.lastModifiedDate,
|
|
originalUrl: english.originalURL,
|
|
files: Set(english.files),
|
|
externalFiles: Set(english.externalFiles),
|
|
requiredFiles: Set(english.requiredFiles),
|
|
linkPreviewImage: english.linkPreviewImage,
|
|
linkPreviewTitle: english.linkPreviewTitle,
|
|
linkPreviewDescription: english.linkPreviewDescription)
|
|
|
|
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]! })
|
|
}
|
|
}
|
|
|
|
// MARK: Saving
|
|
|
|
func saveToDisk() {
|
|
//print("Starting save")
|
|
for page in pages {
|
|
storage.save(pageMetadata: page.pageFile, for: page.id)
|
|
}
|
|
|
|
for post in posts {
|
|
storage.save(post: post.postFile, for: post.id)
|
|
}
|
|
|
|
for tag in tags {
|
|
storage.save(tagMetadata: tag.tagFile, for: tag.id)
|
|
}
|
|
// TODO: Remove all files that are no longer in use (they belong to deleted items)
|
|
//print("Finished save")
|
|
}
|
|
|
|
// MARK: Folder access
|
|
|
|
static func accessFolderFromBookmark(key: String, operation: (URL) -> Void) {
|
|
guard let bookmarkData = UserDefaults.standard.data(forKey: key) else {
|
|
print("No bookmark data to access folder")
|
|
return
|
|
}
|
|
var isStale = false
|
|
let folderURL: URL
|
|
do {
|
|
// Resolve the bookmark to get the folder URL
|
|
folderURL = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale)
|
|
} catch {
|
|
print("Failed to resolve bookmark: \(error)")
|
|
return
|
|
}
|
|
|
|
if isStale {
|
|
print("Bookmark is stale, consider saving a new bookmark.")
|
|
}
|
|
|
|
// Start accessing the security-scoped resource
|
|
if folderURL.startAccessingSecurityScopedResource() {
|
|
print("Accessing folder: \(folderURL.path)")
|
|
|
|
operation(folderURL)
|
|
folderURL.stopAccessingSecurityScopedResource()
|
|
} else {
|
|
print("Failed to access folder: \(folderURL.path)")
|
|
}
|
|
}
|
|
}
|