import Foundation import SwiftUI final class Content: ObservableObject { @Published var posts: [Post] = [] @Published var pages: [Page] = [] @Published var tags: [Tag] = [] @Published var images: [ImageResource] = [] @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 { 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 storage = Storage(baseFolder: URL(filePath: "/Users/ch/Downloads/Content")) do { try storage.createFolderStructure() } catch { print(error) return } 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) { 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)") } } }