Consolidate images and files

This commit is contained in:
Christoph Hagen
2024-12-09 12:18:55 +01:00
parent 394cf7a2e4
commit 4f08526978
77 changed files with 1970 additions and 1619 deletions

View File

@ -27,11 +27,11 @@ extension Content {
return nil
}
#warning("Add files path to settings")
return "/files/\(file.uniqueId)"
return "/files/\(file.id)"
}
func image(_ imageId: String) -> ImageResource? {
images.first { $0.id == imageId }
func image(_ imageId: String) -> FileResource? {
files.first { $0.id == imageId }
}
func imageLink(imageId: String) {

View File

@ -2,7 +2,7 @@ import Foundation
extension Content {
private func convert(_ tag: LocalizedTagFile, images: [String : ImageResource]) -> LocalizedTag {
private func convert(_ tag: LocalizedTagFile, images: [String : FileResource]) -> LocalizedTag {
LocalizedTag(
urlComponent: tag.urlComponent,
name: tag.name,
@ -12,7 +12,7 @@ extension Content {
originalUrl: tag.originalURL)
}
private func convert(_ post: LocalizedPostFile, images: [String : ImageResource]) -> LocalizedPost {
private func convert(_ post: LocalizedPostFile, images: [String : FileResource]) -> LocalizedPost {
LocalizedPost(
title: post.title,
content: post.content,
@ -23,7 +23,7 @@ extension Content {
linkPreviewDescription: post.linkPreviewDescription)
}
private func convert(_ page: LocalizedPageFile, images: [String : ImageResource]) -> LocalizedPage {
private func convert(_ page: LocalizedPageFile, images: [String : FileResource]) -> LocalizedPage {
LocalizedPage(
urlString: page.url,
title: page.title,
@ -49,34 +49,26 @@ 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 imageDescriptions = storage.loadFileDescriptions().reduce(into: [:]) { descriptions, description in
descriptions[description.fileId] = description
}
let tagData = try storage.loadAllTags()
let pagesData = try storage.loadAllPages()
let postsData = try storage.loadAllPosts()
let filesData = try storage.loadAllFiles()
let fileList = try storage.loadAllFiles()
var images: [String : ImageResource] = [:]
var files: [FileResource] = []
for (file, url) in filesData {
let ext = file.components(separatedBy: ".").last!.lowercased()
let type = FileType(fileExtension: ext)
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: ""))
}
let files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in
let descriptions = imageDescriptions[fileId]
files[fileId] = FileResource(
content: self,
id: fileId,
en: descriptions?.english ?? "",
de: descriptions?.german ?? "")
}
let images = files.filter { $0.value.type.isImage }
let tags = tagData.reduce(into: [:]) { (tags, data) in
tags[data.key] = Tag(
isVisible: data.value.isVisible,
@ -105,8 +97,7 @@ extension Content {
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.files = files.values.sorted { $0.id }
self.posts = posts.sorted(ascending: false) { $0.startDate }
self.settings = makeSettings(settings, tags: tags)
}
@ -134,7 +125,7 @@ extension Content {
english: convert(settings.english))
}
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], images: [String : ImageResource]) -> [String : Page] {
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], images: [String : FileResource]) -> [String : Page] {
pagesData.reduce(into: [:]) { pages, data in
let (pageId, page) = data
pages[pageId] = Page(

View File

@ -17,24 +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 {
let fileDescriptions: [FileDescriptions] = files.sorted().compactMap { file in
guard !file.englishDescription.isEmpty || !file.germanDescription.isEmpty else {
return nil
}
return ImageDescriptions(
imageId: image.id,
german: image.germanDescription.nonEmpty,
english: image.englishDescription.nonEmpty)
return FileDescriptions(
fileId: file.id,
german: file.germanDescription.nonEmpty,
english: file.englishDescription.nonEmpty)
}
storage.save(imageDescriptions: imageDescriptions)
storage.save(fileDescriptions: fileDescriptions)
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 }
try storage.deleteFiles(notIn: allFiles)
try storage.deleteFiles(notIn: files.map { $0.id })
} catch {
print("Failed to remove unused files: \(error)")
}

View File

@ -16,9 +16,6 @@ final class Content: ObservableObject {
@Published
var tags: [Tag]
@Published
var images: [ImageResource]
@Published
var files: [FileResource]
@ -40,14 +37,12 @@ final class Content: ObservableObject {
posts: [Post],
pages: [Page],
tags: [Tag],
images: [ImageResource],
files: [FileResource],
storedContentPath: String) {
self.settings = settings
self.posts = posts
self.pages = pages
self.tags = tags
self.images = images
self.files = files
self.storedContentPath = storedContentPath
self.contentPath = storedContentPath
@ -68,7 +63,6 @@ final class Content: ObservableObject {
self.posts = []
self.pages = []
self.tags = []
self.images = []
self.files = []
contentPath = storedContentPath
@ -95,4 +89,8 @@ final class Content: ObservableObject {
}
.store(in: &cancellables)
}
var images: [FileResource] {
files.filter { $0.type.isImage }
}
}

View File

@ -1,44 +1,117 @@
import Foundation
import SwiftUI
final class FileResource: ObservableObject {
unowned let content: Content
let type: FileType
/// Globally unique id
@Published
var uniqueId: String
var id: String
@Published
var description: String
var germanDescription: String
init(uniqueId: String, description: String) {
self.type = FileType(fileExtension: uniqueId.fileExtension)
self.uniqueId = uniqueId
self.description = description
@Published
var englishDescription: String
@Published
var size: CGSize = .zero
init(content: Content, id: String, en: String, de: String) {
self.content = content
self.id = id
self.type = FileType(fileExtension: id.fileExtension)
self.englishDescription = en
self.germanDescription = de
}
init(type: FileType, uniqueId: String, description: String) {
self.type = type
self.uniqueId = uniqueId
self.description = description
/**
Only for bundle images
*/
init(resourceImage: String, type: ImageFileType) {
self.content = .mock // TODO: Add images to mock
self.type = .image(type)
self.id = resourceImage
self.englishDescription = "A test image included in the bundle"
self.germanDescription = "Ein Testbild aus dem Bundle"
}
func getDescription(for language: ContentLanguage) -> String {
switch language {
case .english: return englishDescription
case .german: return germanDescription
}
}
// MARK: Text
func textContent() -> String {
do {
return try content.storage.fileContent(for: id)
} catch {
print("Failed to load text of file \(id): \(error)")
return ""
}
}
// MARK: Images
var aspectRatio: CGFloat {
guard size.height > 0 else {
return 0
}
return size.width / size.height
}
var imageToDisplay: Image {
let imageData: Data
do {
imageData = try content.storage.fileData(for: id)
} catch {
print("Failed to load data for image \(id): \(error)")
return failureImage
}
guard let loadedImage = NSImage(data: imageData) else {
print("Failed to create image \(id)")
return failureImage
}
if self.size == .zero && loadedImage.size != .zero {
DispatchQueue.main.async {
self.size = loadedImage.size
}
}
return .init(nsImage: loadedImage)
}
private var failureImage: Image {
Image(systemSymbol: .exclamationmarkTriangle)
}
}
extension FileResource: Identifiable {
var id: String { uniqueId }
}
extension FileResource: Equatable {
static func == (lhs: FileResource, rhs: FileResource) -> Bool {
lhs.uniqueId == rhs.uniqueId
lhs.id == rhs.id
}
}
extension FileResource: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(uniqueId)
hasher.combine(id)
}
}
extension FileResource: Comparable {
static func < (lhs: FileResource, rhs: FileResource) -> Bool {
lhs.id < rhs.id
}
}

View File

@ -1,119 +0,0 @@
import SwiftUI
final class ImageResource: ObservableObject {
@Published
var type: ImageType
/// Globally unique id
@Published
var id: String
@Published
var germanDescription: String
@Published
var englishDescription: String
@Published
var size: CGSize = .zero
var aspectRatio: CGFloat {
guard size.height > 0 else {
return 0
}
return size.width / size.height
}
private let source: ImageSource
init(type: ImageType, uniqueId: String, en: String, de: String, fileUrl: URL) {
self.type = type
self.id = uniqueId
self.source = .file(fileUrl)
self.englishDescription = en
self.germanDescription = de
}
init(resourceName: String, type: ImageType) {
self.type = type
self.id = resourceName
self.source = .resource(resourceName)
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 {
lhs.id == rhs.id
}
}
extension ImageResource: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension ImageResource {
var imageToDisplay: Image {
switch source {
case .file(let url):
return image(at: url)
case .resource(let name):
return .init(name)
}
}
private func image(at url: URL) -> Image {
let imageData: Data
do {
imageData = try Data(contentsOf: url)
} catch {
print("Failed to load image data from \(url.path): \(error)")
return failureImage
}
guard let loadedImage = NSImage(data: imageData) else {
print("Failed to create image from \(url.path)")
return failureImage
}
if self.size == .zero && loadedImage.size != .zero {
DispatchQueue.main.async {
self.size = loadedImage.size
}
}
return .init(nsImage: loadedImage)
}
private var failureImage: SwiftUI.Image {
Image(systemSymbol: .exclamationmarkTriangle)
}
}

View File

@ -56,7 +56,7 @@ final class LocalizedPage: ObservableObject {
var requiredFiles: Set<String> = []
@Published
var linkPreviewImage: ImageResource?
var linkPreviewImage: FileResource?
@Published
var linkPreviewTitle: String?
@ -71,7 +71,7 @@ final class LocalizedPage: ObservableObject {
files: Set<String> = [],
externalFiles: Set<String> = [],
requiredFiles: Set<String> = [],
linkPreviewImage: ImageResource? = nil,
linkPreviewImage: FileResource? = nil,
linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil) {
self.urlString = urlString

View File

@ -13,10 +13,10 @@ final class LocalizedPost: ObservableObject {
var lastModified: Date?
@Published
var images: [ImageResource]
var images: [FileResource]
@Published
var linkPreviewImage: ImageResource?
var linkPreviewImage: FileResource?
@Published
var linkPreviewTitle: String?
@ -27,8 +27,8 @@ final class LocalizedPost: ObservableObject {
init(title: String? = nil,
content: String,
lastModified: Date? = nil,
images: [ImageResource] = [],
linkPreviewImage: ImageResource? = nil,
images: [FileResource] = [],
linkPreviewImage: FileResource? = nil,
linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil) {
self.title = title ?? ""

View File

@ -17,7 +17,7 @@ final class LocalizedTag: ObservableObject {
/// The image id of the thumbnail
@Published
var thumbnail: ImageResource?
var thumbnail: FileResource?
/// The original url in the previous site layout
let originalUrl: String?
@ -26,7 +26,7 @@ final class LocalizedTag: ObservableObject {
name: String,
subtitle: String? = nil,
description: String? = nil,
thumbnail: ImageResource? = nil,
thumbnail: FileResource? = nil,
originalUrl: String? = nil) {
self.urlComponent = urlComponent
self.name = name

View File

@ -1,48 +0,0 @@
import Foundation
import SwiftUI
/// A simple container for localized text
final class LocalizedText: ObservableObject {
@Published
var en: String
@Published
var de: String
init(en: String, de: String) {
self.en = en
self.de = de
}
var id: String {
en
}
func set(text: String, for language: ContentLanguage) {
switch language {
case .english: self.en = text
case .german: self.de = text
}
}
func getText(for language: ContentLanguage) -> String {
switch language {
case .english: return en
case .german: return de
}
}
@MainActor
func text(for language: ContentLanguage) -> Binding<String> {
Binding(
get: {
self.getText(for: language)
},
set: { newValue in
self.set(text: newValue, for: language)
}
)
}
}

View File

@ -0,0 +1,21 @@
enum CodeFileType: String {
case html
case css
case js
case cpp
case swift
init?(fileExtension: String) {
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
}

View File

@ -1,51 +1,71 @@
import Foundation
enum FileTypeCategory: String, CaseIterable {
case image
case code
case model
case text
case video
case resource
var text: String {
switch self {
case .image: return "Images"
case .code: return "Code"
case .model: return "Models"
case .text: return "Text"
case .video: return "Videos"
case .resource: return "Other"
}
}
}
extension FileTypeCategory: Hashable {
}
extension FileTypeCategory: Identifiable {
var id: String {
rawValue
}
}
enum FileType {
case image(ImageType)
case file(String)
case video(VideoType)
case resource(String)
case image(ImageFileType)
case code(CodeFileType)
case model(ModelFileType)
case text(TextFileType)
case video(VideoFileType)
case other(ResourceFileType)
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)
let ext = fileExtension?.lowercased() ?? ""
if let image = ImageFileType(fileExtension: ext) {
self = .image(image)
} else if let code = CodeFileType(fileExtension: ext) {
self = .code(code)
} else if let model = ModelFileType(fileExtension: ext) {
self = .model(model)
} else if let text = TextFileType(fileExtension: ext) {
self = .text(text)
} else if let video = VideoFileType(fileExtension: ext) {
self = .video(video)
} else {
let resource = ResourceFileType(fileExtension: ext)
self = .other(resource)
}
}
var fileExtension: String {
switch self {
case .image(let imageType): return imageType.fileExtension
case .video(let videoType): return videoType.fileExtension
default:
return "" // TODO: Fix
case .image(let type): return type.fileExtension
case .code(let type): return type.fileExtension
case .model(let type): return type.fileExtension
case .text(let type): return type.fileExtension
case .video(let type): return type.fileExtension
case .other(let type): return type.fileExtension
}
}
@ -63,7 +83,21 @@ enum FileType {
return false
}
var videoType: VideoType? {
var isTextFile: Bool {
switch self {
case .code, .text: return true
default: return false
}
}
var isOtherFile: Bool {
switch self {
case .model, .other: return true
default: return false
}
}
var videoType: VideoFileType? {
if case .video(let videoType) = self {
return videoType
}

View File

@ -0,0 +1,41 @@
import Foundation
import AppKit
enum ImageFileType: String {
case jpg
case png
case avif
case webp
case gif
case svg
case tiff
init?(fileExtension: String) {
if fileExtension == "jpeg" {
self = .jpg
return
}
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
var fileType: NSBitmapImageRep.FileType? {
switch self {
case .jpg:
return .jpeg
case .png, .avif, .webp:
return .png
case .gif: return .gif
case .tiff: return .tiff
case .svg: return nil
}
}
}
extension ImageFileType: CaseIterable {
}

View File

@ -1,53 +0,0 @@
import Foundation
import AppKit
enum ImageType {
case jpg
case png
case avif
case webp
case gif
var fileExtension: String {
switch self {
case .jpg: return "jpg"
case .png: return "png"
case .avif: return "avif"
case .webp: return "webp"
case .gif: return "gif"
}
}
var fileType: NSBitmapImageRep.FileType {
switch self {
case .jpg:
return .jpeg
case .png, .avif, .webp:
return .png
case .gif:
return .gif
}
}
}
extension ImageType: CaseIterable {
}
extension ImageType {
init?(fileExtension: String) {
switch fileExtension {
case "jpg", "jpeg":
self = .jpg
case "png":
self = .png
case "avif":
self = .avif
case "webp":
self = .webp
default:
return nil
}
}
}

View File

@ -0,0 +1,21 @@
enum ModelFileType: String {
case stl
case f3d
case step
case glb
case f3z
init?(fileExtension: String) {
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
}

View File

@ -0,0 +1,54 @@
enum ResourceFileType {
case noExtension
case zip
case cddx
case mp3
case pdf
case key
case psd
case other(String)
init(fileExtension: String) {
switch fileExtension {
case "": self = .noExtension
case "zip": self = .zip
case "cddx": self = .cddx
case "mp3": self = .mp3
case "pdf": self = .pdf
case "key": self = .key
case "psd": self = .psd
default:
self = .other(fileExtension)
}
}
var fileExtension: String {
switch self {
case .noExtension:
return ""
case .zip:
return "zip"
case .cddx:
return "cddx"
case .mp3:
return "mp3"
case .pdf:
return "pdf"
case .key:
return "key"
case .psd:
return "psd"
case .other(let ext):
return ext
}
}
}

View File

@ -0,0 +1,18 @@
enum TextFileType: String {
case json
case conf
case yaml
init?(fileExtension: String) {
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
}

View File

@ -1,24 +1,18 @@
enum VideoType: String {
enum VideoFileType: String {
case mp4
case m4v
case webm
}
extension VideoType {
init?(fileExtension: String) {
self.init(rawValue: fileExtension)
}
var fileExtension: String {
switch self {
case .mp4:
return "mp4"
case .m4v:
return "m4v"
case .webm:
return "webm"
}
rawValue
}
var htmlType: String {
@ -31,6 +25,6 @@ extension VideoType {
}
}
extension VideoType: CaseIterable {
extension VideoFileType: CaseIterable {
}