Generate pages, image descriptions
This commit is contained in:
44
CHDataManagement/Model/Content+Generation.swift
Normal file
44
CHDataManagement/Model/Content+Generation.swift
Normal 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 }
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
85
CHDataManagement/Model/DateItem.swift
Normal file
85
CHDataManagement/Model/DateItem.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -84,3 +84,7 @@ extension Page: Hashable {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
extension Page: DateItem {
|
||||
|
||||
}
|
||||
|
@ -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)"
|
||||
}
|
||||
}
|
||||
|
72
CHDataManagement/Model/Types/FileType.swift
Normal file
72
CHDataManagement/Model/Types/FileType.swift
Normal 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
|
||||
}
|
||||
}
|
36
CHDataManagement/Model/Types/VideoType.swift
Normal file
36
CHDataManagement/Model/Types/VideoType.swift
Normal 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 {
|
||||
|
||||
}
|
Reference in New Issue
Block a user