import Foundation import SwiftUI import Combine final class Content: ObservableObject { @Published var websiteData: WebsiteData @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() init(websiteData: WebsiteData, posts: [Post], pages: [Page], tags: [Tag], images: [ImageResource], files: [FileResource], storedContentPath: String) { self.websiteData = websiteData 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: "")) self.websiteData = .mock self.posts = [] self.pages = [] self.tags = [] self.images = [] self.files = [] 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) } private func convert(_ tag: LocalizedTagFile, images: [String : ImageResource]) -> LocalizedTag { LocalizedTag( urlComponent: tag.urlComponent, name: tag.name, subtitle: tag.subtitle, description: tag.description, thumbnail: tag.thumbnail.map { images[$0] }, originalUrl: tag.originalURL) } private func convert(_ post: LocalizedPostFile, images: [String : ImageResource]) -> LocalizedPost { LocalizedPost( title: post.title, content: post.content, lastModified: post.lastModifiedDate, images: post.images.compactMap { images[$0] }, linkPreviewImage: post.linkPreviewImage.map { images[$0] }, linkPreviewTitle: post.linkPreviewTitle, linkPreviewDescription: post.linkPreviewDescription) } private func convert(_ page: LocalizedPageFile) -> LocalizedPage { LocalizedPage( urlString: page.url, title: page.title, lastModified: page.lastModifiedDate, originalUrl: page.originalURL, files: Set(page.files), externalFiles: Set(page.externalFiles), requiredFiles: Set(page.requiredFiles), linkPreviewImage: page.linkPreviewImage, linkPreviewTitle: page.linkPreviewTitle, linkPreviewDescription: page.linkPreviewDescription) } private func convert(_ websiteData: LocalizedWebsiteDataFile) -> LocalizedWebsiteData { .init(title: websiteData.title, description: websiteData.description, iconDescription: websiteData.iconDescription) } func loadFromDisk() throws { let storage = Storage(baseFolder: URL(filePath: contentPath)) let websiteData = try storage.loadWebsiteData() let tagData = try storage.loadAllTags() let pagesData = try storage.loadAllPages() let postsData = try storage.loadAllPosts() let filesData = try storage.loadAllFiles() 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 tags = tagData.reduce(into: [:]) { (tags, data) in tags[data.key] = Tag( 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) let posts = postsData.map { postId, post in let linkedPage = post.linkedPageId.map { pages[$0] } let german = convert(post.german, images: images) let english = convert(post.english, images: images) 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 } self.websiteData = WebsiteData( navigationTags: websiteData.navigationTags.map { tags[$0]! }, german: convert(websiteData.german), english: convert(websiteData.english)) } private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag]) -> [String : Page] { pagesData.reduce(into: [:]) { pages, data in let (pageId, page) = data pages[pageId] = Page( id: pageId, isDraft: page.isDraft, createdDate: page.createdDate, startDate: page.startDate, endDate: page.endDate, german: convert(page.german), english: convert(page.english), 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) } storage.save(websiteData: websiteData.dataFile) // 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)") } } }