Reorganize saving, generate feed

This commit is contained in:
Christoph Hagen
2024-12-03 13:19:50 +01:00
parent 3c950d47a2
commit dc7b7a0e90
27 changed files with 717 additions and 411 deletions

View File

@ -0,0 +1,9 @@
extension Array {
func split(into size: Int) -> [[Element]] {
guard size > 0 else { return [] }
return stride(from: 0, to: count, by: size).map {
Array(self[$0..<Swift.min($0 + size, count)])
}
}
}

View File

@ -1,35 +0,0 @@
import Foundation
extension Content {
func generateFeed(for language: ContentLanguage, bookmarkKey: String) {
let posts = posts.map { $0.feedEntry(for: language) }
let data = websiteData.localized(in: language)
let navigationItems: [FeedNavigationLink] = websiteData.navigationTags.map {
let localized = $0.localized(in: language)
return .init(text: localized.name, url: localized.urlComponent)
}
DispatchQueue.global(qos: .userInitiated).async {
let feed = Feed(
language: language,
title: data.title,
description: data.description,
iconDescription: data.iconDescription,
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)")
}
}
}
}
}

View File

@ -0,0 +1,128 @@
import Foundation
extension Content {
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()
var images: [String : ImageResource] = [:]
var files: [FileResource] = []
var videos: [String] = []
for (file, url) in filesData {
let ext = file.components(separatedBy: ".").last!.lowercased()
let type = FileType(fileExtension: ext)
switch type {
case .image:
images[file] = ImageResource(uniqueId: file, altText: .init(en: "", de: ""), fileUrl: url)
case .file:
files.append(FileResource(uniqueId: file, description: ""))
case .video:
videos.append(file)
case .resource:
break
}
}
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.videos = videos
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]! })
}
}
}

View File

@ -0,0 +1,133 @@
import Foundation
extension Content {
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)
do {
try storage.deletePostFiles(notIn: posts.map { $0.id })
try storage.deletePageFiles(notIn: pages.map { $0.id })
try storage.deleteTagFiles(notIn: tags.map { $0.id })
let allFiles = files.map { $0.uniqueId } + images.map { $0.id } + videos
try storage.deleteFiles(notIn: allFiles)
} catch {
print("Failed to remove unused files: \(error)")
}
// TODO: Remove all files that are no longer in use (they belong to deleted items)
//print("Finished save")
}
}
private extension Page {
var pageFile: PageFile {
.init(isDraft: isDraft,
tags: tags.map { $0.id },
createdDate: createdDate,
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
german: german.pageFile,
english: english.pageFile)
}
}
private extension LocalizedPage {
var pageFile: LocalizedPageFile {
.init(url: urlString,
files: files.sorted(),
externalFiles: externalFiles.sorted(),
requiredFiles: requiredFiles.sorted(),
title: title,
linkPreviewImage: linkPreviewImage,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription,
lastModifiedDate: lastModified,
originalURL: originalUrl)
}
}
private extension Post {
var postFile: PostFile {
.init(
isDraft: isDraft,
createdDate: createdDate,
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
tags: tags.map { $0.id },
german: german.postFile,
english: english.postFile,
linkedPageId: linkedPage?.id)
}
}
private extension LocalizedPost {
var postFile: LocalizedPostFile {
.init(images: images.map { $0.id },
title: title.nonEmpty,
content: content,
lastModifiedDate: lastModified,
linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription)
}
}
private extension Tag {
var tagFile: TagFile {
.init(id: id,
isVisible: isVisible,
german: german.tagFile,
english: english.tagFile)
}
}
private extension LocalizedTag {
var tagFile: LocalizedTagFile {
.init(urlComponent: urlComponent,
name: name,
subtitle: subtitle,
description: description,
thumbnail: thumbnail?.id,
originalURL: originalUrl)
}
}
private extension WebsiteData {
var dataFile: WebsiteDataFile {
.init(
navigationTags: navigationTags.map { $0.id },
german: german.dataFile,
english: english.dataFile)
}
}
private extension LocalizedWebsiteData {
var dataFile: LocalizedWebsiteDataFile {
.init(title: title,
description: description,
iconDescription: iconDescription)
}
}

View File

@ -97,159 +97,6 @@ final class Content: ObservableObject {
.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()
var images: [String : ImageResource] = [:]
var files: [FileResource] = []
var videos: [String] = []
for (file, url) in filesData {
let ext = file.components(separatedBy: ".").last!.lowercased()
let type = FileType(fileExtension: ext)
switch type {
case .image:
images[file] = ImageResource(uniqueId: file, altText: .init(en: "", de: ""), fileUrl: url)
case .file:
files.append(FileResource(uniqueId: file, description: ""))
case .video:
videos.append(file)
case .resource:
break
}
}
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.videos = videos
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)
do {
try storage.deletePostFiles(notIn: posts.map { $0.id })
try storage.deletePageFiles(notIn: pages.map { $0.id })
try storage.deleteTagFiles(notIn: tags.map { $0.id })
let allFiles = files.map { $0.uniqueId } + images.map { $0.id } + videos
try storage.deleteFiles(notIn: allFiles)
} catch {
print("Failed to remove unused files: \(error)")
}
// 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) {

View File

@ -1,30 +0,0 @@
import Foundation
extension Page {
var pageFile: PageFile {
.init(isDraft: isDraft,
tags: tags.map { $0.id },
createdDate: createdDate,
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
german: german.pageFile,
english: english.pageFile)
}
}
extension LocalizedPage {
var pageFile: LocalizedPageFile {
.init(url: urlString,
files: files.sorted(),
externalFiles: externalFiles.sorted(),
requiredFiles: requiredFiles.sorted(),
title: title,
linkPreviewImage: linkPreviewImage,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription,
lastModifiedDate: lastModified,
originalURL: originalUrl)
}
}

View File

@ -1,29 +0,0 @@
import Foundation
extension Post {
var postFile: PostFile {
.init(
isDraft: isDraft,
createdDate: createdDate,
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
tags: tags.map { $0.id },
german: german.postFile,
english: english.postFile,
linkedPageId: linkedPage?.id)
}
}
extension LocalizedPost {
var postFile: LocalizedPostFile {
.init(images: images.map { $0.id },
title: title.nonEmpty,
content: content,
lastModifiedDate: lastModified,
linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle,
linkPreviewDescription: linkPreviewDescription)
}
}

View File

@ -1,23 +0,0 @@
import Foundation
extension Tag {
var tagFile: TagFile {
.init(id: id,
isVisible: isVisible,
german: german.tagFile,
english: english.tagFile)
}
}
extension LocalizedTag {
var tagFile: LocalizedTagFile {
.init(urlComponent: urlComponent,
name: name,
subtitle: subtitle,
description: description,
thumbnail: thumbnail?.id,
originalURL: originalUrl)
}
}

View File

@ -1,20 +0,0 @@
import Foundation
extension WebsiteData {
var dataFile: WebsiteDataFile {
.init(
navigationTags: navigationTags.map { $0.id },
german: german.dataFile,
english: english.dataFile)
}
}
extension LocalizedWebsiteData {
var dataFile: LocalizedWebsiteDataFile {
.init(title: title,
description: description,
iconDescription: iconDescription)
}
}

View File

@ -0,0 +1,102 @@
import Foundation
struct WebsiteGeneratorConfiguration {
let language: ContentLanguage
let postsPerPage: Int
let postFeedTitle: String
let postFeedDescription: String
let postFeedUrlPrefix: String
}
final class WebsiteGenerator {
let language: ContentLanguage
let content: Content
let postsPerPage: Int
let postFeedTitle: String
let postFeedDescription: String
let postFeedUrlPrefix: String
init(content: Content, configuration: WebsiteGeneratorConfiguration) {
self.content = content
self.language = configuration.language
self.postsPerPage = configuration.postsPerPage
self.postFeedTitle = configuration.postFeedTitle
self.postFeedDescription = configuration.postFeedDescription
self.postFeedUrlPrefix = configuration.postFeedUrlPrefix
}
func generateWebsite() {
createPostFeedPages()
}
private func createPostFeedPages() {
let totalCount = content.posts.count
guard totalCount > 0 else { return }
let navBarData = createNavigationBarData()
let numberOfPages = (totalCount + postsPerPage - 1) / postsPerPage // Round up
for pageIndex in 1...numberOfPages {
let startIndex = (pageIndex - 1) * postsPerPage
let endIndex = min(pageIndex * postsPerPage, totalCount)
let postsOnPage = content.posts[startIndex..<endIndex]
createPostFeedPage(pageIndex, pageCount: numberOfPages, posts: postsOnPage, bar: navBarData)
}
}
private func createNavigationBarData() -> NavigationBarData {
let data = content.websiteData.localized(in: language)
let navigationItems: [NavigationBarLink] = content.websiteData.navigationTags.map {
let localized = $0.localized(in: language)
return .init(text: localized.name, url: localized.urlComponent)
}
return NavigationBarData(
navigationIconPath: "/assets/icons/ch.svg",
iconDescription: data.iconDescription,
navigationItems: navigationItems)
}
private func createPostFeedPage(_ pageIndex: Int, pageCount: Int, posts: ArraySlice<Post>, bar: NavigationBarData) {
let posts = posts.map { $0.feedEntry(for: language) }
let feed = PageInFeed(
language: language,
title: postFeedTitle,
description: postFeedDescription,
navigationBarData: bar,
pageNumber: pageIndex,
totalPages: pageCount,
posts: posts)
let fileContent = feed.content
if pageIndex == 1 {
save(fileContent, to: "\(postFeedUrlPrefix).html")
} else {
save(fileContent, to: "\(postFeedUrlPrefix)-\(pageIndex).html")
}
}
private func save(_ content: String, to relativePath: String) {
Content.accessFolderFromBookmark(key: Storage.outputPathBookmarkKey) { folder in
let outputFile = folder.appendingPathComponent(relativePath, isDirectory: false)
do {
try content
.data(using: .utf8)!
.write(to: outputFile)
} catch {
print("Failed to save: \(error)")
}
}
}
}

View File

@ -0,0 +1,52 @@
import Foundation
struct NavigationBarLink {
let text: String
let url: String
}
struct NavigationBarData {
let navigationIconPath: String
let iconDescription: String
let navigationItems: [NavigationBarLink]
}
struct NavigationBar {
let data: NavigationBarData
init(data: NavigationBarData) {
self.data = data
}
private var items: [NavigationBarLink] {
data.navigationItems
}
var content: String {
var result = "<nav class=\"navbar\"><div class=\"navbar-fade\"></div><div class=\"nav-center\">"
let middleIndex = items.count / 2
let leftNavigationItems = items[..<middleIndex]
let rightNavigationItems = items[middleIndex...]
for item in leftNavigationItems {
result += "<a class=\"nav-animate\" href=\"\(item.url)\">\(item.text)</a>"
}
result += "<a id=\"nav-image\" href=\"/\">"
result += "<img class=\"navbar-icon\" src=\"\(data.navigationIconPath)\" alt=\"\(data.iconDescription)\">"
for item in rightNavigationItems {
result += "<a class=\"nav-animate\" href=\"\(item.url)\">\(item.text)</a>"
}
result += "</div></nav>" // Close nav-center, navbar
return result
}
}

View File

@ -7,6 +7,8 @@ struct PageHead {
let description: String
let additionalHeaders: String
var content: String {
"""
<head>
@ -14,7 +16,7 @@ struct PageHead {
<title>\(title)</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
<meta name="description" content="\(description)">
<link rel="stylesheet" href="/assets/swiper/swiper.css" />
\(additionalHeaders)
<link rel="stylesheet" href="/assets/css/style.css" />
</head>
"""

View File

@ -0,0 +1,88 @@
import Foundation
struct PostFeedPageNavigation {
let language: ContentLanguage
let currentPage: Int
let numberOfPages: Int
init(currentPage: Int, numberOfPages: Int, language: ContentLanguage) {
self.currentPage = currentPage
self.numberOfPages = numberOfPages
self.language = language
}
private func pageLink(_ page: Int) -> String {
guard page > 1 else { return "href='/feed'" }
return "href='/feed-\(page)'"
}
private func previousText() -> String {
switch language {
case .english:
return "Previous"
case .german:
return "Zurück"
}
}
private func addPreviousButton(to result: inout String) {
if currentPage == 1 {
// Disable the previous button if we are on the first page
result += "<a class='tag prev disabled' href='' aria-label='Previous'>"
} else {
let link = pageLink(currentPage - 1)
result += "<a class='tag prev' \(link) aria-label='Previous'>"
}
result += "<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'>"
result += "<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15 19l-7-7 7-7' />"
result += "</svg></a>"
}
private func addNextButton(to result: inout String) {
if currentPage == numberOfPages {
// Disable the previous button if we are on the first page
result += "<a class='tag next disabled' href='' aria-label='Next'>"
} else {
let link = pageLink(currentPage + 1)
result += "<a class='tag next' \(link) aria-label='Next'>"
}
result += "<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'>"
result += "<path stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M9 5l7 7-7 7' />"
result += "</svg></a>"
}
private func addLink(page: Int, to result: inout String) {
result += "<a class='tag' \(pageLink(page))>\(page)</a>"
}
var content: String {
var result = "<div class='pagination'>"
addPreviousButton(to: &result)
// Add a maximum of two buttons before the current page
if currentPage > 2 {
addLink(page: currentPage - 2, to: &result)
}
if currentPage > 1 {
addLink(page: currentPage - 1, to: &result)
}
// Add the current page
result += "<span class='tag current'>\(currentPage)</span>"
// Add a maximum of two buttons after the current page
if currentPage < numberOfPages {
addLink(page: currentPage + 1, to: &result)
}
if currentPage + 1 < numberOfPages {
addLink(page: currentPage + 2, to: &result)
}
addNextButton(to: &result)
result += "</div>" // Close pagination
return result
}
}

View File

@ -1,79 +0,0 @@
import Foundation
struct FeedNavigationLink {
let text: String
let url: String
}
struct Feed {
private let navigationIconPath = "/assets/icons/ch.svg"
let language: ContentLanguage
let title: String
let description: String
let iconDescription: String
let navigationItems: [FeedNavigationLink]
let posts: [FeedEntryData]
var content: String {
#warning("TODO: Split feed into multiple pages")
var result = ""
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
let head = PageHead(
title: title,
description: description)
result += head.content
result += "<body>"
addNavbar(to: &result)
result += "<div class=\"content\"><div style=\"height: 70px;\"></div>"
for post in posts {
FeedEntry(data: post)
.addContent(to: &result)
}
addSwiperInits(to: &result)
result += "</div></body></html>" // Close content
return result
}
#warning("TODO: Set correct navigation links and texts")
private func addNavbar(to result: inout String) {
result += "<nav class=\"navbar\"><div class=\"navbar-fade\"></div><div class=\"nav-center\">"
let middleIndex = navigationItems.count / 2
let leftNavigationItems = navigationItems[..<middleIndex]
let rightNavigationItems = navigationItems[middleIndex...]
for item in leftNavigationItems {
result += "<a class=\"nav-animate\" href=\"\(item.url)\">\(item.text)</a>"
}
result += "<a id=\"nav-image\" href=\"/\">"
result += "<img class=\"navbar-icon\" src=\"\(navigationIconPath)\" alt=\"\(iconDescription)\">"
for item in rightNavigationItems {
result += "<a class=\"nav-animate\" href=\"\(item.url)\">\(item.text)</a>"
}
result += "</div></nav>" // Close nav-center, navbar
}
private func addSwiperInits(to result: inout String) {
if posts.contains(where: { $0.images.count > 1 }) {
result += "<script src=\"/assets/swiper/swiper.min.js\"></script><script>"
for post in posts {
guard post.images.count > 1 else {
continue
}
result += ImageGallery.swiperInit(id: post.entryId)
}
result += "</script>"
}
}
}

View File

@ -0,0 +1,36 @@
import Foundation
struct GenericPage {
let language: ContentLanguage
let title: String
let description: String
let data: NavigationBarData
let additionalHeaders: String
let insertedContent: (inout String) -> Void
init(language: ContentLanguage, title: String, description: String, data: NavigationBarData, additionalHeaders: String, insertedContent: @escaping (inout String) -> Void) {
self.language = language
self.title = title
self.description = description
self.data = data
self.additionalHeaders = additionalHeaders
self.insertedContent = insertedContent
}
var content: String {
var result = ""
result += "<!DOCTYPE html><html lang=\"\(language.rawValue)\">"
result += PageHead(title: title, description: description, additionalHeaders: additionalHeaders).content
result += "<body>"
result += NavigationBar(data: data).content
result += "<div class=\"content\"><div style=\"height: 70px;\"></div>"
insertedContent(&result)
result += "</div></body></html>" // Close content
return result
}
}

View File

@ -0,0 +1,58 @@
import Foundation
struct PageInFeed {
private let swiperStyleSheetPath = "/assets/swiper/swiper-bundle.min.css"
private let swiperJsPath = "/assets/swiper/swiper-bundle.min.js"
let language: ContentLanguage
let title: String
let description: String
let navigationBarData: NavigationBarData
let pageNumber: Int
let totalPages: Int
let posts: [FeedEntryData]
private var swiperHeader: String {
"<link rel='stylesheet' href='\(swiperStyleSheetPath)' />"
}
private var swiperIsNeeded: Bool {
posts.contains(where: { $0.images.count > 1 })
}
private var headers: String {
swiperIsNeeded ? swiperHeader : ""
}
var content: String {
GenericPage(language: language, title: title, description: description, data: navigationBarData, additionalHeaders: headers) { content in
for post in posts {
FeedEntry(data: post)
.addContent(to: &content)
}
content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content
if swiperIsNeeded {
addSwiperInits(to: &content)
}
}.content
}
private func addSwiperInits(to result: inout String) {
result += "<script src='\(swiperJsPath)'></script><script>"
for post in posts {
guard post.images.count > 1 else {
continue
}
result += ImageGallery.swiperInit(id: post.entryId)
}
result += "</script>"
}
}

View File

@ -0,0 +1,19 @@
import Foundation
extension WebsiteGeneratorConfiguration {
static let english = WebsiteGeneratorConfiguration(
language: .english,
postsPerPage: 20,
postFeedTitle: "Posts",
postFeedDescription: "The most recent posts on christophhagen.de",
postFeedUrlPrefix: "feed")
static let german = WebsiteGeneratorConfiguration(
language: .german,
postsPerPage: 20,
postFeedTitle: "Beiträge",
postFeedDescription: "Die neusten Beiträge auf christophhagen.de",
postFeedUrlPrefix: "beiträge")
}

View File

@ -13,6 +13,9 @@ import Foundation
*/
final class Storage {
static let outputPathBookmarkKey = "outputPathBookmark"
static let contentPathBookmarkKey = "contentPathBookmark"
private(set) var baseFolder: URL
private let encoder = JSONEncoder()
@ -252,7 +255,7 @@ final class Storage {
private func deleteFiles(in folder: URL, notIn fileSet: Set<String>) throws {
let filesToDelete = try files(in: folder)
.filter { !fileSet.contains($0.lastPathComponent) }
for file in filesToDelete {
try fm.removeItem(at: file)
print("Deleted \(file.path())")

View File

@ -23,6 +23,9 @@ struct SettingsView: View {
@State
private var showTagPicker = false
@State
private var isGeneratingWebsite = false
var body: some View {
ScrollView {
VStack(alignment: .leading) {
@ -63,8 +66,16 @@ struct SettingsView: View {
LocalizedSettingsView(settings: content.websiteData.localized(in: language))
Text("Feed")
.font(.headline)
Button(action: generateFeed) {
Text("Generate")
HStack {
Button(action: generateFeed) {
Text("Generate")
}
if isGeneratingWebsite {
ProgressView()
.progressViewStyle(.circular)
.frame(height: 25)
}
Spacer()
}
}
.padding()
@ -86,7 +97,7 @@ struct SettingsView: View {
private func selectContentFolder() {
isSelectingContentFolder = true
//showFileImporter = true
guard let url = savePanelUsingOpenPanel(key: "contentPathBookmark") else {
guard let url = savePanelUsingOpenPanel(key: Storage.contentPathBookmarkKey) else {
return
}
self.contentPath = url.path()
@ -94,8 +105,7 @@ struct SettingsView: View {
private func selectOutputFolder() {
isSelectingContentFolder = false
//showFileImporter = true
guard let url = savePanelUsingOpenPanel(key: "outputPathBookmark") else {
guard let url = savePanelUsingOpenPanel(key: Storage.outputPathBookmarkKey) else {
return
}
self.outputPath = url.path()
@ -115,10 +125,10 @@ struct SettingsView: View {
.replacingOccurrences(of: "file://", with: "")
if isSelectingContentFolder {
self.contentPath = path
saveSecurityScopedBookmark(folder, key: "contentPathBookmark")
saveSecurityScopedBookmark(folder, key: Storage.contentPathBookmarkKey)
} else {
self.outputPath = path
saveSecurityScopedBookmark(folder, key: "outputPathBookmark")
saveSecurityScopedBookmark(folder, key: Storage.outputPathBookmarkKey)
}
}
@ -135,8 +145,23 @@ struct SettingsView: View {
print("Missing output folder")
return
}
isGeneratingWebsite = true
DispatchQueue.global(qos: .userInitiated).async {
let generator = WebsiteGenerator(
content: content,
configuration: configuration)
generator.generateWebsite()
DispatchQueue.main.async {
isGeneratingWebsite = false
}
}
}
content.generateFeed(for: language, bookmarkKey: "outputPathBookmark")
private var configuration: WebsiteGeneratorConfiguration {
switch language {
case .english: return .english
case .german: return .german
}
}
func savePanelUsingOpenPanel(key: String) -> URL? {