Generate pages, image descriptions

This commit is contained in:
Christoph Hagen
2024-12-06 21:59:36 +01:00
parent 18eb64f289
commit 5fb689ac7c
42 changed files with 1653 additions and 273 deletions

View File

@ -0,0 +1,44 @@
extension Content {
func pageLink(_ page: Page, language: ContentLanguage) -> String {
// TODO: Record link to trace connections between pages
var prefix = settings.pages.pageUrlPrefix
if !prefix.hasPrefix("/") {
prefix = "/" + prefix
}
if !prefix.hasSuffix("/") {
prefix.append("/")
}
return prefix + page.localized(in: language).urlString
}
func pageLink(pageId: String, language: ContentLanguage) -> String? {
guard let page = pages.first(where: { $0.id == pageId }) else {
// TODO: Note missing link
print("Missing page \(pageId) linked")
return nil
}
return pageLink(page, language: language)
}
func pathToFile(_ fileId: String) -> String? {
guard let file = file(id: fileId) else {
return nil
}
#warning("Add files path to settings")
return "/files/\(file.uniqueId)"
}
func image(_ imageId: String) -> ImageResource? {
images.first { $0.id == imageId }
}
func imageLink(imageId: String) {
}
func file(id: String) -> FileResource? {
files.first { $0.id == id }
}
}

View File

@ -49,6 +49,9 @@ extension Content {
let storage = Storage(baseFolder: URL(filePath: contentPath))
let settings = try storage.loadSettings()
let imageDescriptions = storage.loadImageDescriptions().reduce(into: [:]) { descriptions, description in
descriptions[description.imageId] = description
}
let tagData = try storage.loadAllTags()
let pagesData = try storage.loadAllPages()
@ -57,20 +60,20 @@ extension Content {
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
if case .image(let type) = type {
let descriptions = imageDescriptions[file]
images[file] = ImageResource(
type: type,
uniqueId: file,
en: descriptions?.english ?? "",
de: descriptions?.german ?? "",
fileUrl: url)
} else {
files.append(FileResource(type: type, uniqueId: file, description: ""))
}
}
@ -104,7 +107,6 @@ extension Content {
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.settings = makeSettings(settings, tags: tags)
}
@ -119,10 +121,15 @@ extension Content {
postsPerPage: settings.posts.postsPerPage,
contentWidth: settings.posts.contentWidth)
let pages = PageSettings(
pageUrlPrefix: settings.pages.pageUrlPrefix,
contentWidth: settings.pages.contentWidth)
return Settings(
outputDirectoryPath: settings.outputDirectoryPath,
navigationBar: navigationBar,
posts: posts,
pages: pages,
german: convert(settings.german),
english: convert(settings.english))
}

View File

@ -17,11 +17,23 @@ extension Content {
}
storage.save(settings: settings.file)
let imageDescriptions: [ImageDescriptions] = images.sorted().compactMap { image in
guard !image.englishDescription.isEmpty || !image.germanDescription.isEmpty else {
return nil
}
return ImageDescriptions(
imageId: image.id,
german: image.germanDescription.nonEmpty,
english: image.englishDescription.nonEmpty)
}
storage.save(imageDescriptions: imageDescriptions)
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
let allFiles = files.map { $0.uniqueId } + images.map { $0.id }
try storage.deleteFiles(notIn: allFiles)
} catch {
print("Failed to remove unused files: \(error)")
@ -128,6 +140,7 @@ extension Settings {
outputDirectoryPath: outputDirectoryPath,
navigationBar: navigationBar.file,
posts: posts.file,
pages: pages.file,
german: german.file,
english: english.file)
}
@ -141,6 +154,14 @@ private extension PostSettings {
}
}
private extension PageSettings {
var file: PageSettingsFile {
.init(pageUrlPrefix: pageUrlPrefix,
contentWidth: contentWidth)
}
}
private extension LocalizedSettings {
var file: LocalizedSettingsFile {

View File

@ -19,9 +19,6 @@ final class Content: ObservableObject {
@Published
var images: [ImageResource]
@Published
var videos: [String]
@Published
var files: [FileResource]
@ -45,7 +42,6 @@ final class Content: ObservableObject {
tags: [Tag],
images: [ImageResource],
files: [FileResource],
videos: [String],
storedContentPath: String) {
self.settings = settings
self.posts = posts
@ -53,7 +49,6 @@ final class Content: ObservableObject {
self.tags = tags
self.images = images
self.files = files
self.videos = videos
self.storedContentPath = storedContentPath
self.contentPath = storedContentPath
self.storage = Storage(baseFolder: URL(filePath: storedContentPath))
@ -75,7 +70,6 @@ final class Content: ObservableObject {
self.tags = []
self.images = []
self.files = []
self.videos = []
contentPath = storedContentPath
do {

View File

@ -0,0 +1,85 @@
import Foundation
protocol DateItem {
var startDate: Date { get }
var hasEndDate: Bool { get }
var endDate: Date { get }
}
extension DateItem {
private func datePrefixString(in language: ContentLanguage) -> String {
guard Calendar.current.isDate(startDate, equalTo: endDate, toGranularity: .year) else {
// Different year, return full string
return startDate.formatted(date: .long, time: .omitted)
}
guard Calendar.current.isDate(startDate, equalTo: endDate, toGranularity: .month) else {
// Different month
return DateItemStorage.dayAndMonth(of: startDate, in: language)
}
return DateItemStorage.day.string(from: startDate)
}
func dateText(in language: ContentLanguage) -> String {
guard hasEndDate else {
return DateItemStorage.dateString(for: startDate, in: language)
}
let endText = DateItemStorage.dateString(for: endDate, in: language)
return "\(datePrefixString(in: language)) - \(endText)"
}
}
private enum DateItemStorage {
static let englishDate: DateFormatter = {
let df = DateFormatter()
df.locale = .init(identifier: "en")
df.dateFormat = "d. MMMM yyyy"
return df
}()
static let germanDate: DateFormatter = {
let df = DateFormatter()
df.locale = .init(identifier: "de")
df.dateFormat = "d. MMMM yyyy"
return df
}()
static let englishDayAndMonth: DateFormatter = {
let df = DateFormatter()
df.locale = .init(identifier: "en")
df.dateFormat = "d. MMMM"
return df
}()
static let germanDayAndMonth: DateFormatter = {
let df = DateFormatter()
df.locale = .init(identifier: "de")
df.dateFormat = "d. MMMM"
return df
}()
static let day: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "d."
return df
}()
static func dayAndMonth(of date: Date, in language: ContentLanguage) -> String {
switch language {
case .english: return englishDayAndMonth.string(from: date)
case .german: return germanDayAndMonth.string(from: date)
}
}
static func dateString(for date: Date, in language: ContentLanguage) -> String {
switch language {
case .english: return englishDate.string(from: date)
case .german: return germanDate.string(from: date)
}
}
}

View File

@ -2,6 +2,8 @@ import Foundation
final class FileResource: ObservableObject {
let type: FileType
/// Globally unique id
@Published
var uniqueId: String
@ -10,6 +12,13 @@ final class FileResource: ObservableObject {
var description: String
init(uniqueId: String, description: String) {
self.type = FileType(fileExtension: uniqueId.fileExtension)
self.uniqueId = uniqueId
self.description = description
}
init(type: FileType, uniqueId: String, description: String) {
self.type = type
self.uniqueId = uniqueId
self.description = description
}

View File

@ -2,12 +2,18 @@ import SwiftUI
final class ImageResource: ObservableObject {
@Published
var type: ImageType
/// Globally unique id
@Published
var id: String
@Published
var altText: LocalizedText
var germanDescription: String
@Published
var englishDescription: String
@Published
var size: CGSize = .zero
@ -21,28 +27,46 @@ final class ImageResource: ObservableObject {
private let source: ImageSource
init(uniqueId: String, altText: LocalizedText, fileUrl: URL) {
init(type: ImageType, uniqueId: String, en: String, de: String, fileUrl: URL) {
self.type = type
self.id = uniqueId
self.source = .file(fileUrl)
self.altText = altText
self.englishDescription = en
self.germanDescription = de
}
init(resourceName: String) {
self.type = ImageType(fileExtension: resourceName.fileExtension!)!
self.id = resourceName
self.source = .resource(resourceName)
self.altText = .init(en: "A test image included in the bundle", de: "Ein Test-Image aus dem Bundle")
self.englishDescription = "A test image included in the bundle"
self.germanDescription = "Ein Test-Image aus dem Bundle"
}
private enum ImageSource {
case file(URL)
case resource(String)
}
func getDescription(for language: ContentLanguage) -> String {
switch language {
case .english: return englishDescription
case .german: return germanDescription
}
}
}
extension ImageResource: Identifiable {
}
extension ImageResource: Comparable {
static func < (lhs: ImageResource, rhs: ImageResource) -> Bool {
lhs.id < rhs.id
}
}
extension ImageResource: Equatable {
static func == (lhs: ImageResource, rhs: ImageResource) -> Bool {

View File

@ -85,20 +85,4 @@ final class LocalizedPage: ObservableObject {
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription
}
@MainActor
func editableTitle() -> Binding<String> {
Binding(
get: {
self.title
},
set: { newValue in
self.title = newValue
}
)
}
var relativeUrl: String {
"/page/\(urlString)"
}
}

View File

@ -39,28 +39,4 @@ final class LocalizedPost: ObservableObject {
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription
}
@MainActor
func editableTitle() -> Binding<String> {
Binding(
get: {
self.title
},
set: { newValue in
self.title = newValue
}
)
}
@MainActor
func editableContent() -> Binding<String> {
Binding(
get: {
self.content
},
set: { newValue in
self.content = newValue
}
)
}
}

View File

@ -84,3 +84,7 @@ extension Page: Hashable {
hasher.combine(id)
}
}
extension Page: DateItem {
}

View File

@ -80,77 +80,6 @@ extension Post: Hashable {
}
}
extension Post: DateItem {
// MARK: Feed entry
extension Post {
private static let englishDate: DateFormatter = {
let df = DateFormatter()
df.locale = .init(identifier: "en")
df.dateFormat = "d. MMMM yyyy"
return df
}()
private static let germanDate: DateFormatter = {
let df = DateFormatter()
df.locale = .init(identifier: "de")
df.dateFormat = "d. MMMM yyyy"
return df
}()
private static let englishDayAndMonth: DateFormatter = {
let df = DateFormatter()
df.locale = .init(identifier: "en")
df.dateFormat = "d. MMMM"
return df
}()
private static let germanDayAndMonth: DateFormatter = {
let df = DateFormatter()
df.locale = .init(identifier: "de")
df.dateFormat = "d. MMMM"
return df
}()
private static let day: DateFormatter = {
let df = DateFormatter()
df.dateFormat = "d."
return df
}()
private static func dayAndMonth(of date: Date, in language: ContentLanguage) -> String {
switch language {
case .english: return englishDayAndMonth.string(from: date)
case .german: return germanDayAndMonth.string(from: date)
}
}
private static func dateString(for date: Date, in language: ContentLanguage) -> String {
switch language {
case .english: return englishDate.string(from: date)
case .german: return germanDate.string(from: date)
}
}
private func datePrefixString(in language: ContentLanguage) -> String {
guard Calendar.current.isDate(startDate, equalTo: endDate, toGranularity: .year) else {
// Different year, return full string
return startDate.formatted(date: .long, time: .omitted)
}
guard Calendar.current.isDate(startDate, equalTo: endDate, toGranularity: .month) else {
// Different month
return Post.dayAndMonth(of: startDate, in: language)
}
return Post.day.string(from: startDate)
}
func dateText(in language: ContentLanguage) -> String {
guard hasEndDate else {
return Post.dateString(for: startDate, in: language)
}
let endText = Post.dateString(for: endDate, in: language)
return "\(datePrefixString(in: language)) - \(endText)"
}
}

View File

@ -0,0 +1,72 @@
import Foundation
enum FileType {
case image(ImageType)
case file(String)
case video(VideoType)
case resource(String)
init(fileExtension: String?) {
guard let ext = fileExtension?.lowercased() else {
self = .file("")
return
}
switch ext {
case "jpg", "jpeg":
self = .image(.jpg)
case "png":
self = .image(.png)
case "avif":
self = .image(.avif)
case "webp":
self = .image(.webp)
case "gif":
self = .image(.gif)
case "html", "stl", "f3d", "step", "f3z", "zip", "json", "conf", "css", "js", "cpp", "cddx", "svg", "glb", "mp3", "pdf", "swift":
self = .file(ext)
case "mp4":
self = .video(.mp4)
case "m4v":
self = .video(.m4v)
case "webm":
self = .video(.webm)
case "key", "psd":
self = .resource(ext)
default:
print("Unhandled file type: \(ext)")
self = .resource(ext)
}
}
var fileExtension: String {
switch self {
case .image(let imageType): return imageType.fileExtension
case .video(let videoType): return videoType.fileExtension
default:
return "" // TODO: Fix
}
}
var isImage: Bool {
if case .image = self {
return true
}
return false
}
var isVideo: Bool {
if case .video = self {
return true
}
return false
}
var videoType: VideoType? {
if case .video(let videoType) = self {
return videoType
}
return nil
}
}

View File

@ -0,0 +1,36 @@
enum VideoType: String {
case mp4
case m4v
case webm
}
extension VideoType {
var fileExtension: String {
switch self {
case .mp4:
return "mp4"
case .m4v:
return "m4v"
case .webm:
return "webm"
}
}
var htmlType: String {
switch self {
case .mp4, .m4v:
return "video/mp4"
case .webm:
return "video/webm"
}
}
}
extension VideoType: CaseIterable {
}