Simplify images, tag overview
This commit is contained in:
@ -26,7 +26,7 @@ final class GenerationResults: ObservableObject {
|
||||
var requiredFiles: Set<FileResource> = []
|
||||
|
||||
@Published
|
||||
var imagesToGenerate: Set<ImageGenerationJob> = []
|
||||
var imagesToGenerate: Set<ImageVersion> = []
|
||||
|
||||
@Published
|
||||
var invalidCommands: Set<String> = []
|
||||
@ -38,7 +38,7 @@ final class GenerationResults: ObservableObject {
|
||||
var unsavedOutputFiles: Set<String> = []
|
||||
|
||||
@Published
|
||||
var failedImages: Set<ImageGenerationJob> = []
|
||||
var failedImages: Set<ImageVersion> = []
|
||||
|
||||
@Published
|
||||
var emptyPages: Set<LocalizedPageId> = []
|
||||
@ -151,11 +151,11 @@ final class GenerationResults: ObservableObject {
|
||||
update { self.requiredFiles.formUnion(files) }
|
||||
}
|
||||
|
||||
func generate(_ image: ImageGenerationJob) {
|
||||
func generate(_ image: ImageVersion) {
|
||||
update { self.imagesToGenerate.insert(image) }
|
||||
}
|
||||
|
||||
func generate<S>(_ images: S) where S: Sequence, S.Element == ImageGenerationJob {
|
||||
func generate<S>(_ images: S) where S: Sequence, S.Element == ImageVersion {
|
||||
update { self.imagesToGenerate.formUnion(images) }
|
||||
}
|
||||
|
||||
@ -167,7 +167,7 @@ final class GenerationResults: ObservableObject {
|
||||
update { self.warnings.insert(warning) }
|
||||
}
|
||||
|
||||
func failed(image: ImageGenerationJob) {
|
||||
func failed(image: ImageVersion) {
|
||||
update { self.failedImages.insert(image) }
|
||||
}
|
||||
|
||||
|
@ -15,20 +15,33 @@ final class ImageGenerator {
|
||||
self.storage = storage
|
||||
self.settings = settings
|
||||
self.generatedImages = storage.loadListOfGeneratedImages() ?? [:]
|
||||
print("ImageGenerator: Loaded list of \(totalImageCount) already generated images")
|
||||
}
|
||||
|
||||
private var outputFolder: String {
|
||||
settings.paths.imagesOutputFolderPath
|
||||
}
|
||||
|
||||
private var totalImageCount: Int {
|
||||
generatedImages.values.reduce(0) { $0 + $1.count }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func save() -> Bool {
|
||||
guard storage.save(listOfGeneratedImages: generatedImages) else {
|
||||
print("Failed to save list of generated images")
|
||||
print("ImageGenerator: Failed to save list of generated images")
|
||||
return false
|
||||
}
|
||||
print("ImageGenerator: Saved list of \(totalImageCount) images")
|
||||
return true
|
||||
}
|
||||
|
||||
private var avifCommands: Set<String> = []
|
||||
|
||||
func printAvifCommands() {
|
||||
avifCommands.sorted().forEach { print($0) }
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all versions of an image, so that they will be recreated on the next run.
|
||||
|
||||
@ -44,26 +57,27 @@ final class ImageGenerator {
|
||||
print("Image generator: \(generatedImages.count)/\(images.count) images (\(versionCount) versions)")
|
||||
}
|
||||
|
||||
private func needsToGenerate(version: String, for image: String) -> Bool {
|
||||
if exists(version) {
|
||||
private func hasPreviouslyGenerated(_ version: ImageVersion) -> Bool {
|
||||
guard let versions = generatedImages[version.image.id] else {
|
||||
return false
|
||||
}
|
||||
guard let versions = generatedImages[image] else {
|
||||
return true
|
||||
}
|
||||
guard versions.contains(version) else {
|
||||
return true
|
||||
}
|
||||
return !exists(version)
|
||||
return versions.contains(version.versionId)
|
||||
}
|
||||
|
||||
private func hasNowGenerated(version: String, for image: String) {
|
||||
guard var versions = generatedImages[image] else {
|
||||
generatedImages[image] = [version]
|
||||
return
|
||||
private func needsToGenerate(_ version: ImageVersion) -> Bool {
|
||||
if hasPreviouslyGenerated(version) {
|
||||
return false
|
||||
}
|
||||
versions.insert(version)
|
||||
generatedImages[image] = versions
|
||||
if exists(version) {
|
||||
// Mark as already generated
|
||||
hasNowGenerated(version)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func hasNowGenerated(_ version: ImageVersion) {
|
||||
generatedImages[version.image.id, default: []].insert(version.versionId)
|
||||
}
|
||||
|
||||
private func removeVersions(for image: String) {
|
||||
@ -72,53 +86,59 @@ final class ImageGenerator {
|
||||
|
||||
// MARK: Files
|
||||
|
||||
private func exists(_ image: String) -> Bool {
|
||||
storage.hasFileInOutputFolder(relativePath(for: image))
|
||||
private func exists(_ version: ImageVersion) -> Bool {
|
||||
storage.hasFileInOutputFolder(version.outputPath)
|
||||
}
|
||||
|
||||
private func relativePath(for image: String) -> String {
|
||||
outputFolder + "/" + image
|
||||
}
|
||||
|
||||
private func write(imageData data: Data, version: String) -> Bool {
|
||||
return storage.write(data, to: relativePath(for: version))
|
||||
private func write(imageData data: Data, of version: ImageVersion) -> Bool {
|
||||
return storage.write(data, to: version.outputPath)
|
||||
}
|
||||
|
||||
// MARK: Image operations
|
||||
|
||||
func generate(job: ImageGenerationJob) -> Bool {
|
||||
guard needsToGenerate(version: job.version, for: job.image) else {
|
||||
func generate(version: ImageVersion) -> Bool {
|
||||
guard needsToGenerate(version) else {
|
||||
return true
|
||||
}
|
||||
|
||||
guard let data = storage.fileData(for: job.image) else {
|
||||
print("Failed to load image \(job.image)")
|
||||
guard let data = version.image.dataContent() else {
|
||||
print("ImageGenerator: Failed to load data for image \(version.image.id)")
|
||||
return false
|
||||
}
|
||||
|
||||
guard let originalImage = NSImage(data: data) else {
|
||||
print("Failed to load image")
|
||||
print("ImageGenerator: Failed to load image \(version.image.id)")
|
||||
return false
|
||||
}
|
||||
|
||||
let representation = create(image: originalImage, width: CGFloat(job.maximumWidth), height: CGFloat(job.maximumHeight))
|
||||
let representation = create(image: originalImage, width: CGFloat(version.maximumWidth), height: CGFloat(version.maximumHeight))
|
||||
|
||||
guard let data = create(image: representation, type: job.type, quality: job.quality) else {
|
||||
print("Failed to get data for type \(job.type)")
|
||||
guard let data = create(image: representation, type: version.type, quality: version.quality) else {
|
||||
print("ImageGenerator: Failed to get data for type \(version.type) of image \(version.image.id)")
|
||||
return false
|
||||
}
|
||||
|
||||
if job.type == .avif {
|
||||
let input = job.version.fileNameAndExtension.fileName + "." + job.image.fileExtension!
|
||||
print("avifenc -q 70 \(input) \(job.version)")
|
||||
hasNowGenerated(version: job.version, for: job.image)
|
||||
if version.type == .avif {
|
||||
// AVIF conversion is very slow, so we save bash commands
|
||||
// for the conversion instead
|
||||
let baseVersion = ImageVersion(
|
||||
image: version.image,
|
||||
type: version.image.type,
|
||||
maximumWidth: version.maximumWidth,
|
||||
maximumHeight: version.maximumHeight)
|
||||
let originalImagePath = storage.outputPath(to: baseVersion.outputPath)!.path()
|
||||
let generatedImagePath = storage.outputPath(to: version.outputPath)!.path()
|
||||
let quality = Int(version.quality * 100)
|
||||
|
||||
avifCommands.insert("avifenc -q \(quality) '\(originalImagePath)' '\(generatedImagePath)'")
|
||||
// hasNowGenerated(version)
|
||||
return true
|
||||
}
|
||||
|
||||
guard write(imageData: data, version: job.version) else {
|
||||
guard write(imageData: data, of: version) else {
|
||||
return false
|
||||
}
|
||||
hasNowGenerated(version: job.version, for: job.image)
|
||||
hasNowGenerated(version)
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -1,73 +0,0 @@
|
||||
import Foundation
|
||||
|
||||
struct ImageGenerationJob {
|
||||
|
||||
let image: String
|
||||
|
||||
let type: FileType
|
||||
|
||||
let maximumWidth: Int
|
||||
|
||||
let maximumHeight: Int
|
||||
|
||||
let quality: CGFloat
|
||||
|
||||
init(image: String, type: FileType, maximumWidth: CGFloat, maximumHeight: CGFloat, quality: CGFloat = 0.7) {
|
||||
self.image = image
|
||||
self.type = type
|
||||
self.maximumWidth = Int(maximumWidth)
|
||||
self.maximumHeight = Int(maximumHeight)
|
||||
self.quality = quality
|
||||
}
|
||||
|
||||
init(image: String, type: FileType, maximumWidth: Int, maximumHeight: Int, quality: CGFloat = 0.7) {
|
||||
self.image = image
|
||||
self.type = type
|
||||
self.maximumWidth = maximumWidth
|
||||
self.maximumHeight = maximumHeight
|
||||
self.quality = quality
|
||||
}
|
||||
|
||||
var version: String {
|
||||
let fileName = image.fileNameAndExtension.fileName
|
||||
let prefix = "\(fileName)@\(maximumWidth)x\(maximumHeight)"
|
||||
return "\(prefix).\(type.fileExtension)"
|
||||
}
|
||||
|
||||
static func imageSet(for image: String, maxWidth: Int, maxHeight: Int, quality: CGFloat = 0.7) -> [ImageGenerationJob] {
|
||||
let type = FileType(fileExtension: image.fileExtension)
|
||||
|
||||
let width2x = maxWidth * 2
|
||||
let height2x = maxHeight * 2
|
||||
|
||||
return [
|
||||
.init(image: image, type: .avif, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
|
||||
.init(image: image, type: .avif, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
|
||||
.init(image: image, type: .webp, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
|
||||
.init(image: image, type: .webp, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
|
||||
.init(image: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
|
||||
.init(image: image, type: type, maximumWidth: width2x, maximumHeight: height2x, quality: quality)
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageGenerationJob: Equatable {
|
||||
|
||||
static func == (lhs: ImageGenerationJob, rhs: ImageGenerationJob) -> Bool {
|
||||
lhs.version == rhs.version
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageGenerationJob: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(version)
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageGenerationJob: Comparable {
|
||||
|
||||
static func < (lhs: ImageGenerationJob, rhs: ImageGenerationJob) -> Bool {
|
||||
lhs.version < rhs.version
|
||||
}
|
||||
}
|
52
CHDataManagement/Generator/ImageSet.swift
Normal file
52
CHDataManagement/Generator/ImageSet.swift
Normal file
@ -0,0 +1,52 @@
|
||||
import Foundation
|
||||
|
||||
struct ImageSet {
|
||||
|
||||
let image: FileResource
|
||||
|
||||
let maxWidth: Int
|
||||
|
||||
let maxHeight: Int
|
||||
|
||||
let quality: CGFloat
|
||||
|
||||
let description: String
|
||||
|
||||
init(image: FileResource, maxWidth: Int, maxHeight: Int, description: String, quality: CGFloat = 0.7) {
|
||||
self.image = image
|
||||
self.maxWidth = maxWidth
|
||||
self.maxHeight = maxHeight
|
||||
self.description = description
|
||||
self.quality = quality
|
||||
}
|
||||
|
||||
var jobs: [ImageVersion] {
|
||||
let type = image.type
|
||||
|
||||
let width2x = maxWidth * 2
|
||||
let height2x = maxHeight * 2
|
||||
|
||||
return [
|
||||
.init(image: image, type: .avif, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
|
||||
.init(image: image, type: .avif, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
|
||||
.init(image: image, type: .webp, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
|
||||
.init(image: image, type: .webp, maximumWidth: width2x, maximumHeight: height2x, quality: quality),
|
||||
.init(image: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight, quality: quality),
|
||||
.init(image: image, type: type, maximumWidth: width2x, maximumHeight: height2x, quality: quality)
|
||||
]
|
||||
}
|
||||
|
||||
var content: String {
|
||||
let fileExtension = image.type.fileExtension.map { "." + $0 } ?? ""
|
||||
|
||||
let prefix1x = "/\(image.outputImageFolder)/\(maxWidth)x\(maxHeight)"
|
||||
let prefix2x = "/\(image.outputImageFolder)/\(maxWidth*2)x\(maxHeight*2)"
|
||||
|
||||
var result = "<picture>"
|
||||
result += "<source type='image/avif' srcset='\(prefix1x).avif 1x, \(prefix2x).avif 2x'/>"
|
||||
result += "<source type='image/webp' srcset='\(prefix1x).webm 1x, \(prefix1x).webm 2x'/>"
|
||||
result += "<img srcset='\(prefix2x)\(fileExtension) 2x' src='\(prefix1x)\(fileExtension)' loading='lazy' alt='\(description.htmlEscaped())'/>"
|
||||
result += "</picture>"
|
||||
return result
|
||||
}
|
||||
}
|
78
CHDataManagement/Generator/ImageVersion.swift
Normal file
78
CHDataManagement/Generator/ImageVersion.swift
Normal file
@ -0,0 +1,78 @@
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A version of an image with a specific size and possible different image type.
|
||||
*/
|
||||
struct ImageVersion {
|
||||
|
||||
/// The name of the image file to convert
|
||||
let image: FileResource
|
||||
|
||||
let type: FileType
|
||||
|
||||
let maximumWidth: Int
|
||||
|
||||
let maximumHeight: Int
|
||||
|
||||
let quality: CGFloat
|
||||
|
||||
init(image: FileResource, type: FileType, maximumWidth: CGFloat, maximumHeight: CGFloat, quality: CGFloat = 0.7) {
|
||||
self.image = image
|
||||
self.type = type
|
||||
self.maximumWidth = Int(maximumWidth)
|
||||
self.maximumHeight = Int(maximumHeight)
|
||||
self.quality = quality
|
||||
}
|
||||
|
||||
init(image: FileResource, type: FileType, maximumWidth: Int, maximumHeight: Int, quality: CGFloat = 0.7) {
|
||||
self.image = image
|
||||
self.type = type
|
||||
self.maximumWidth = maximumWidth
|
||||
self.maximumHeight = maximumHeight
|
||||
self.quality = quality
|
||||
}
|
||||
|
||||
/// A unique id of the version for this image (not unique across images)
|
||||
var versionId: String {
|
||||
"\(maximumWidth)-\(maximumHeight)-\(type.fileExtension!)"
|
||||
}
|
||||
|
||||
/// The path of the generated image version in the output folder (without leading slash)
|
||||
var outputPath: String {
|
||||
image.outputPath(width: maximumWidth, height: maximumHeight, type: type)
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageVersion: Identifiable {
|
||||
|
||||
var id: String {
|
||||
image.id + "-" + versionId
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageVersion: Equatable {
|
||||
|
||||
static func == (lhs: ImageVersion, rhs: ImageVersion) -> Bool {
|
||||
lhs.image.id == rhs.image.id &&
|
||||
lhs.maximumWidth == rhs.maximumWidth &&
|
||||
lhs.maximumHeight == rhs.maximumHeight &&
|
||||
lhs.type == rhs.type
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageVersion: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(image.id)
|
||||
hasher.combine(maximumWidth)
|
||||
hasher.combine(maximumHeight)
|
||||
hasher.combine(type)
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageVersion: Comparable {
|
||||
|
||||
static func < (lhs: ImageVersion, rhs: ImageVersion) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
}
|
@ -39,8 +39,6 @@ extension KnownHeaderElement: Comparable {
|
||||
static func < (lhs: KnownHeaderElement, rhs: KnownHeaderElement) -> Bool {
|
||||
lhs.rawValue < rhs.rawValue
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension KnownHeaderElement: CustomStringConvertible {
|
||||
|
@ -50,12 +50,19 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
|
||||
results.missing(file: song.cover, containedIn: file)
|
||||
continue
|
||||
}
|
||||
guard image.type.isImage else {
|
||||
results.warning("Cover '\(song.cover)' in file \(fileId) is not an image file")
|
||||
continue
|
||||
}
|
||||
|
||||
guard let audioFile = content.file(song.file) else {
|
||||
results.missing(file: song.cover, containedIn: file)
|
||||
continue
|
||||
}
|
||||
#warning("Check if file is audio")
|
||||
guard audioFile.type.isAudio else {
|
||||
results.warning("Song '\(song.file)' in file \(fileId) is not an audio file")
|
||||
continue
|
||||
}
|
||||
let coverUrl = image.absoluteUrl
|
||||
|
||||
let playlistItem = AudioPlayer.PlaylistItem(
|
||||
|
@ -106,7 +106,8 @@ struct PageHtmlProcessor: CommandProcessor {
|
||||
results.warning("Failed to find <source> elements in inline HTML: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
checkSourceSetAttributes(sources: sources)
|
||||
checkSourceAttributes(sources: sources)
|
||||
}
|
||||
|
||||
private func checkSourceSetAttributes(sources: [Element]) {
|
||||
|
@ -29,7 +29,8 @@ final class FeedPageGenerator {
|
||||
showTitle: Bool,
|
||||
pageNumber: Int,
|
||||
totalPages: Int,
|
||||
languageButtonUrl: String) -> String {
|
||||
languageButtonUrl: String,
|
||||
linkPrefix: String) -> String {
|
||||
var headers = content.defaultPageHeaders
|
||||
var footer = ""
|
||||
if posts.contains(where: { $0.images.count > 1 }) {
|
||||
@ -63,7 +64,10 @@ final class FeedPageGenerator {
|
||||
content += FeedEntry(data: post).content
|
||||
}
|
||||
if totalPages > 1 {
|
||||
content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content
|
||||
content += PostFeedPageNavigation(
|
||||
linkPrefix: linkPrefix,
|
||||
currentPage: pageNumber,
|
||||
numberOfPages: totalPages).content
|
||||
}
|
||||
}
|
||||
return page.content
|
@ -6,12 +6,11 @@ final class PageGenerator {
|
||||
self.content = content
|
||||
}
|
||||
|
||||
private func makeHeaders(requiredItems: Set<KnownHeaderElement>) -> Set<HeaderElement> {
|
||||
private func makeHeaders(requiredItems: Set<KnownHeaderElement>, results: PageGenerationResults) -> Set<HeaderElement> {
|
||||
var result = content.defaultPageHeaders
|
||||
for item in requiredItems {
|
||||
guard let header = item.header(content: content) else {
|
||||
print("Missing header \(item)")
|
||||
#warning("Add warning on missing file assignment")
|
||||
results.warning("Header \(item) not configured in settings")
|
||||
continue
|
||||
}
|
||||
result.insert(header)
|
||||
@ -20,6 +19,7 @@ final class PageGenerator {
|
||||
}
|
||||
|
||||
private func makeEmptyPageContent(in language: ContentLanguage) -> String {
|
||||
#warning("Configure empty page text in settings")
|
||||
switch language {
|
||||
case .english:
|
||||
return ContentBox(
|
||||
@ -56,7 +56,7 @@ final class PageGenerator {
|
||||
url: tag.absoluteUrl(in: language))
|
||||
}
|
||||
|
||||
let headers = makeHeaders(requiredItems: results.requiredHeaders)
|
||||
let headers = makeHeaders(requiredItems: results.requiredHeaders, results: results)
|
||||
results.require(files: headers.compactMap { $0.file })
|
||||
|
||||
let iconUrl = content.settings.navigation.localized(in: language).rootUrl
|
@ -0,0 +1,74 @@
|
||||
|
||||
final class TagOverviewGenerator {
|
||||
|
||||
let content: Content
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
init(content: Content, language: ContentLanguage, results: PageGenerationResults) {
|
||||
self.content = content
|
||||
self.language = language
|
||||
self.results = results
|
||||
}
|
||||
|
||||
func generatePage(tags: [Tag], overview: TagOverviewPage) {
|
||||
let iconUrl = content.settings.navigation.localized(in: language).rootUrl
|
||||
let languageUrl = overview.absoluteUrl(in: language.next)
|
||||
let languageButton = NavigationBar.Link(
|
||||
text: language.next.rawValue,
|
||||
url: languageUrl)
|
||||
|
||||
let localized = overview.localized(in: language)
|
||||
|
||||
let pageHeader = PageHeader(
|
||||
language: language,
|
||||
title: localized.linkPreviewTitle ?? localized.title,
|
||||
description: localized.linkPreviewDescription,
|
||||
iconUrl: iconUrl,
|
||||
languageButton: languageButton,
|
||||
links: content.navigationBar(in: language),
|
||||
headers: content.defaultPageHeaders,
|
||||
icons: [])
|
||||
|
||||
let page = GenericPage(
|
||||
header: pageHeader,
|
||||
additionalFooter: "") { content in
|
||||
content += "<h1>\(localized.title)</h1>"
|
||||
for tag in tags {
|
||||
let localized = tag.localized(in: self.language)
|
||||
let url = tag.absoluteUrl(in: self.language)
|
||||
let title = localized.name
|
||||
let description = localized.description ?? ""
|
||||
let image = self.makePageImage(item: localized)
|
||||
|
||||
content += RelatedPageLink(
|
||||
title: title,
|
||||
description: description,
|
||||
url: url,
|
||||
image: image)
|
||||
.content
|
||||
}
|
||||
// if totalPages > 1 {
|
||||
// content += PostFeedPageNavigation(currentPage: pageNumber, numberOfPages: totalPages, language: language).content
|
||||
// }
|
||||
}
|
||||
let fileContent = page.content
|
||||
let url = overview.absoluteUrl(in: language) + ".html"
|
||||
|
||||
guard content.storage.write(fileContent, to: url) else {
|
||||
results.unsavedOutput(url, source: .tagOverview)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func makePageImage(item: LinkPreviewItem) -> ImageSet? {
|
||||
item.linkPreviewImage.map { image in
|
||||
let size = content.settings.pages.pageLinkImageSize
|
||||
let imageSet = image.imageSet(width: size, height: size, language: language)
|
||||
results.require(imageSet: imageSet)
|
||||
return imageSet
|
||||
}
|
||||
}
|
||||
}
|
@ -160,20 +160,11 @@ final class PageContentParser {
|
||||
guard !image.type.isSvg else {
|
||||
return SvgImage(imagePath: path, altText: altText).content
|
||||
}
|
||||
let thumbnail = image.imageSet(width: thumbnailWidth, height: thumbnailWidth, language: language)
|
||||
results.require(imageSet: thumbnail)
|
||||
|
||||
let thumbnail = FeedEntryData.Image(
|
||||
rawImagePath: path,
|
||||
width: thumbnailWidth,
|
||||
height: thumbnailWidth,
|
||||
altText: altText)
|
||||
results.requireImageSet(for: image, size: thumbnailWidth)
|
||||
|
||||
let largeImage = FeedEntryData.Image(
|
||||
rawImagePath: path,
|
||||
width: largeImageWidth,
|
||||
height: largeImageWidth,
|
||||
altText: altText)
|
||||
results.requireImageSet(for: image, size: largeImageWidth)
|
||||
let largeImage = image.imageSet(width: largeImageWidth, height: largeImageWidth, language: language)
|
||||
results.require(imageSet: largeImage)
|
||||
|
||||
return PageImage(
|
||||
imageId: imageId.replacingOccurrences(of: ".", with: "-"),
|
||||
@ -221,18 +212,18 @@ final class PageContentParser {
|
||||
results.invalid(command: .video, markdown)
|
||||
return nil
|
||||
}
|
||||
if case let .poster(imageId) = option {
|
||||
switch option {
|
||||
case .poster(let imageId):
|
||||
if let image = content.image(imageId) {
|
||||
results.used(file: image)
|
||||
let width = 2*thumbnailWidth
|
||||
let fullLink = WebsiteImage.imagePath(source: image.absoluteUrl, width: width, height: width)
|
||||
return .poster(image: fullLink)
|
||||
let version = image.imageVersion(width: width, height: width, type: .jpg)
|
||||
results.require(image: version)
|
||||
return .poster(image: version.outputPath)
|
||||
} else {
|
||||
results.missing(file: imageId, source: "Video command poster")
|
||||
return nil // Image file not present, so skip the option
|
||||
}
|
||||
}
|
||||
if case let .src(videoId) = option {
|
||||
case .src(let videoId):
|
||||
if let video = content.video(videoId) {
|
||||
results.used(file: video)
|
||||
let link = video.absoluteUrl
|
||||
@ -241,8 +232,9 @@ final class PageContentParser {
|
||||
results.missing(file: videoId, source: "Video command source")
|
||||
return nil // Video file not present, so skip the option
|
||||
}
|
||||
default:
|
||||
return option
|
||||
}
|
||||
return option
|
||||
}
|
||||
|
||||
/**
|
||||
@ -270,17 +262,7 @@ final class PageContentParser {
|
||||
let url = page.absoluteUrl(in: language)
|
||||
let title = localized.linkPreviewTitle ?? localized.title
|
||||
let description = localized.linkPreviewDescription ?? ""
|
||||
|
||||
let image = localized.linkPreviewImage.map { image in
|
||||
let size = content.settings.pages.pageLinkImageSize
|
||||
results.used(file: image)
|
||||
results.requireImageSet(for: image, size: size)
|
||||
|
||||
return RelatedPageLink.Image(
|
||||
url: image.absoluteUrl,
|
||||
description: image.localized(in: language),
|
||||
size: size)
|
||||
}
|
||||
let image = makePageImage(item: localized)
|
||||
|
||||
return RelatedPageLink(
|
||||
title: title,
|
||||
@ -309,16 +291,7 @@ final class PageContentParser {
|
||||
let url = tag.absoluteUrl(in: language)
|
||||
let title = localized.name
|
||||
let description = localized.description ?? ""
|
||||
|
||||
let image = localized.linkPreviewImage.map { image in
|
||||
let size = content.settings.pages.pageLinkImageSize
|
||||
results.requireImageSet(for: image, size: size)
|
||||
|
||||
return RelatedPageLink.Image(
|
||||
url: image.absoluteUrl,
|
||||
description: image.localized(in: language),
|
||||
size: size)
|
||||
}
|
||||
let image = makePageImage(item: localized)
|
||||
|
||||
return RelatedPageLink(
|
||||
title: title,
|
||||
@ -328,6 +301,15 @@ final class PageContentParser {
|
||||
.content
|
||||
}
|
||||
|
||||
private func makePageImage(item: LinkPreviewItem) -> ImageSet? {
|
||||
item.linkPreviewImage.map { image in
|
||||
let size = content.settings.pages.pageLinkImageSize
|
||||
let imageSet = image.imageSet(width: size, height: size, language: language)
|
||||
results.require(imageSet: imageSet)
|
||||
return imageSet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Format: ``
|
||||
*/
|
||||
|
@ -64,7 +64,7 @@ final class PageGenerationResults: ObservableObject {
|
||||
private(set) var requiredFiles: Set<FileResource>
|
||||
|
||||
/// The image versions required for this page
|
||||
private(set) var imagesToGenerate: Set<ImageGenerationJob>
|
||||
private(set) var imagesToGenerate: Set<ImageVersion>
|
||||
|
||||
private(set) var invalidCommands: [(command: ShorthandMarkdownKey?, markdown: String)] = []
|
||||
|
||||
@ -127,10 +127,16 @@ final class PageGenerationResults: ObservableObject {
|
||||
delegate.missing(file: file)
|
||||
}
|
||||
|
||||
func requireImageSet(for image: FileResource, size: Int) {
|
||||
let jobs = ImageGenerationJob.imageSet(for: image.id, maxWidth: size, maxHeight: size)
|
||||
func require(image: ImageVersion) {
|
||||
imagesToGenerate.insert(image)
|
||||
used(file: image.image)
|
||||
delegate.generate(image)
|
||||
}
|
||||
|
||||
func require(imageSet: ImageSet) {
|
||||
let jobs = imageSet.jobs
|
||||
imagesToGenerate.formUnion(jobs)
|
||||
used(file: image)
|
||||
used(file: imageSet.image)
|
||||
delegate.generate(jobs)
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,10 @@ struct FeedGeneratorSource: PostListPageGeneratorSource {
|
||||
false
|
||||
}
|
||||
|
||||
var postsPerPage: Int {
|
||||
content.settings.posts.postsPerPage
|
||||
}
|
||||
|
||||
var pageTitle: String {
|
||||
content.settings.localized(in: language).title
|
||||
}
|
||||
|
@ -16,12 +16,8 @@ final class PostListPageGenerator {
|
||||
source.content.settings.posts.contentWidth
|
||||
}
|
||||
|
||||
private var postsPerPage: Int {
|
||||
source.content.settings.posts.postsPerPage
|
||||
}
|
||||
|
||||
private func pageUrl(in language: ContentLanguage, pageNumber: Int) -> String {
|
||||
"\(source.pageUrlPrefix(for: language))/\(pageNumber).html"
|
||||
"\(source.pageUrlPrefix(for: language))/\(pageNumber)"
|
||||
}
|
||||
|
||||
func createPages(for posts: [Post]) {
|
||||
@ -29,6 +25,7 @@ final class PostListPageGenerator {
|
||||
guard totalCount > 0 else {
|
||||
return
|
||||
}
|
||||
let postsPerPage = source.postsPerPage
|
||||
|
||||
let numberOfPages = (totalCount + postsPerPage - 1) / postsPerPage // Round up
|
||||
for pageIndex in 1...numberOfPages {
|
||||
@ -43,10 +40,11 @@ final class PostListPageGenerator {
|
||||
let posts: [FeedEntryData] = posts.map { post in
|
||||
let localized: LocalizedPost = post.localized(in: language)
|
||||
|
||||
#warning("Add post link text to settings or to each post")
|
||||
let linkUrl = post.linkedPage.map {
|
||||
FeedEntryData.Link(
|
||||
url: $0.absoluteUrl(in: language),
|
||||
text: language == .english ? "View" : "Anzeigen") // TODO: Add to settings
|
||||
text: language == .english ? "View" : "Anzeigen")
|
||||
}
|
||||
|
||||
let tags: [FeedEntryData.Tag] = post.tags.filter { $0.isVisible }.map { tag in
|
||||
@ -54,7 +52,10 @@ final class PostListPageGenerator {
|
||||
url: tag.absoluteUrl(in: language))
|
||||
}
|
||||
|
||||
let images = localized.images.map(createFeedImage)
|
||||
let images = localized.images.map { image in
|
||||
image.imageSet(width: mainContentMaximumWidth, height: mainContentMaximumWidth, language: language)
|
||||
}
|
||||
images.forEach(source.results.require)
|
||||
|
||||
return FeedEntryData(
|
||||
entryId: post.id,
|
||||
@ -68,7 +69,7 @@ final class PostListPageGenerator {
|
||||
|
||||
let feedPageGenerator = FeedPageGenerator(content: source.content, results: source.results)
|
||||
|
||||
let languageButtonUrl = pageUrl(in: language.next, pageNumber: pageIndex)
|
||||
let languageButtonUrl = "/" + pageUrl(in: language.next, pageNumber: pageIndex)
|
||||
|
||||
let fileContent = feedPageGenerator.generatePage(
|
||||
language: language,
|
||||
@ -78,23 +79,15 @@ final class PostListPageGenerator {
|
||||
showTitle: source.showTitle,
|
||||
pageNumber: pageIndex,
|
||||
totalPages: pageCount,
|
||||
languageButtonUrl: languageButtonUrl)
|
||||
let filePath = pageUrl(in: language, pageNumber: pageIndex)
|
||||
languageButtonUrl: languageButtonUrl,
|
||||
linkPrefix: "/" + source.pageUrlPrefix(for: language) + "/")
|
||||
let filePath = pageUrl(in: language, pageNumber: pageIndex) + ".html"
|
||||
guard save(fileContent, to: filePath) else {
|
||||
source.results.unsavedOutput(filePath, source: .feed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func createFeedImage(for image: FileResource) -> FeedEntryData.Image {
|
||||
source.results.requireImageSet(for: image, size: mainContentMaximumWidth)
|
||||
return .init(
|
||||
rawImagePath: image.absoluteUrl,
|
||||
width: mainContentMaximumWidth,
|
||||
height: mainContentMaximumWidth,
|
||||
altText: image.localized(in: language))
|
||||
}
|
||||
|
||||
private func save(_ content: String, to relativePath: String) -> Bool {
|
||||
source.content.storage.write(content, to: relativePath)
|
||||
}
|
||||
|
@ -14,4 +14,6 @@ protocol PostListPageGeneratorSource {
|
||||
var pageDescription: String { get }
|
||||
|
||||
func pageUrlPrefix(for language: ContentLanguage) -> String
|
||||
|
||||
var postsPerPage: Int { get }
|
||||
}
|
||||
|
@ -13,6 +13,10 @@ struct TagPageGeneratorSource: PostListPageGeneratorSource {
|
||||
true
|
||||
}
|
||||
|
||||
var postsPerPage: Int {
|
||||
content.settings.posts.postsPerPage
|
||||
}
|
||||
|
||||
var pageTitle: String {
|
||||
tag.localized(in: language).name
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ enum VideoOption {
|
||||
case .height(let height): return "height='\(height)'"
|
||||
case .width(let width): return "width='\(width)'"
|
||||
case .preload(let option): return "preload='\(option)'"
|
||||
case .poster(let image): return "poster='\(image)'"
|
||||
case .poster(let image): return "poster='/\(image)'"
|
||||
case .src(let url): return "src='\(url)'"
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user