Full generation, file type cleanup

This commit is contained in:
Christoph Hagen
2024-12-25 18:06:05 +01:00
parent 41887a1401
commit 1e4682dad1
56 changed files with 1577 additions and 1103 deletions

View File

@@ -2,70 +2,97 @@ import Foundation
extension Content {
func generateFeed() -> Bool {
#warning("Implement feed generation")
return false
func generateWebsiteInAllLanguages() {
performGenerationIfIdle {
self.generatePagesInternal()
self.generatePostFeedPagesInternal()
self.generateTagPagesInternal()
self.generateTagOverviewPagesInternal()
self.copyRequiredFiles()
self.generateRequiredImages()
self.status("Generation completed")
}
}
func generateAllPages() -> Bool {
guard startGenerating() else { return false }
defer { endGenerating() }
func generatePostFeedPages() {
performGenerationIfIdle {
self.generatePostFeedPagesInternal()
}
}
for page in pages {
func check(content: String, of page: Page, for language: ContentLanguage, onComplete: @escaping (PageGenerationResults) -> Void) {
performGenerationIfIdle {
let results = self.results.makeResults(for: page, in: language)
let generator = PageContentParser(content: page.content, language: language, results: results)
_ = generator.generatePage(from: content)
DispatchQueue.main.async {
onComplete(results)
}
}
}
private func copyRequiredFiles() {
let count = results.requiredFiles.count
var completed = 0
for file in results.requiredFiles {
defer {
completed += 1
status("Copying required files: \(completed) / \(count)")
}
guard !file.isExternallyStored else {
continue
}
let path = file.absoluteUrl
if !storage.copy(file: file.id, to: path) {
results.general.unsavedOutput(path, source: .general)
}
}
}
private func generateRequiredImages() {
let imageGenerator = ImageGenerator(
storage: storage,
settings: settings)
let images = results.imagesToGenerate.sorted()
let count = images.count
var completed = 0
for image in images {
defer {
completed += 1
status("Generating required images: \(completed) / \(count)")
}
if imageGenerator.generate(job: image) {
continue
}
results.failed(image: image)
}
//let images = Set(self.images.map { $0.id })
//imageGenerator.recalculateGeneratedImages(by: images)
}
func generateAllPages() {
performGenerationIfIdle {
self.generatePagesInternal()
}
}
func generatePage(_ page: Page) {
performGenerationIfIdle {
for language in ContentLanguage.allCases {
guard generateInternal(page, in: language) else {
return false
}
self.generateInternal(page, in: language)
}
self.copyRequiredFiles()
self.generateRequiredImages()
}
let failedAssetCopies = results.values
.reduce(Set()) { $0.union($1.assets) }
.filter { !$0.isExternallyStored }
.filter { !storage.copy(file: $0.id, to: $0.assetUrl) }
let failedFileCopies = results.values
.reduce(Set()) { $0.union($1.files) }
.filter { !$0.isExternallyStored }
.filter { !storage.copy(file: $0.id, to: $0.absoluteUrl) }
guard imageGenerator.runJobs(callback: { _ in }) else {
return false
}
return true
}
func generatePage(_ page: Page) -> Bool {
guard startGenerating() else { return false }
defer { endGenerating() }
for language in ContentLanguage.allCases {
guard generateInternal(page, in: language) else {
return false
}
func generatePage(_ page: Page, in language: ContentLanguage) {
performGenerationIfIdle {
self.generateInternal(page, in: language)
}
guard imageGenerator.runJobs(callback: { _ in }) else {
return false
}
let failedAssetCopies = results.values
.reduce(Set()) { $0.union($1.assets) }
.filter { !$0.isExternallyStored }
.filter { !storage.copy(file: $0.id, to: $0.assetUrl) }
let failedFileCopies = results.values
.reduce(Set()) { $0.union($1.files) }
.filter { !$0.isExternallyStored }
.filter { !storage.copy(file: $0.id, to: $0.absoluteUrl) }
return true
}
func generatePage(_ page: Page, in language: ContentLanguage) -> Bool {
guard startGenerating() else { return false }
defer { endGenerating() }
return generateInternal(page, in: language)
}
// MARK: Paths to items
@@ -121,60 +148,134 @@ extension Content {
return result
}
// MARK: Images
func recalculateGeneratedImages() {
let images = Set(self.images.map { $0.id })
imageGenerator.recalculateGeneratedImages(by: images)
}
// MARK: Generation
private func startGenerating() -> Bool {
guard !isGeneratingWebsite else {
return false
}
// TODO: Fix bug where multiple generating operations can be started
// due to dispatch of locking property on main queue
self.set(isGenerating: true)
return true
}
private func endGenerating() {
set(isGenerating: false)
}
private func generateInternal(_ page: Page, in language: ContentLanguage) -> Bool {
let pageGenerator = PageGenerator(
content: self,
imageGenerator: imageGenerator)
guard let (content, results) = pageGenerator.generate(page: page, language: language) else {
print("Failed to generate page \(page.id) in language \(language)")
return false
}
private func performGenerationIfIdle(_ operation: @escaping () -> ()) {
DispatchQueue.main.async {
let id = ItemId(itemId: page.id, language: language, itemType: .page)
self.results[id] = results
guard !self.isGeneratingWebsite else {
return
}
self.set(isGenerating: true)
DispatchQueue.global(qos: .userInitiated).async {
operation()
DispatchQueue.main.async {
self.set(isGenerating: false)
}
}
}
}
private func status(_ message: String) {
DispatchQueue.main.async {
self.generationStatus = message
}
}
/**
- Note: Run on background thread
*/
private func generatePagesInternal() {
let count = pages.count
for index in pages.indices {
let page = pages[index]
status("Generating pages: \(index) / \(count)")
guard !page.isExternalUrl else {
continue
}
for language in ContentLanguage.allCases {
guard page.hasContent(in: language) else {
continue
}
generateInternal(page, in: language)
}
}
}
/**
- Note: Run on background thread
*/
private func generatePostFeedPagesInternal() {
status("Generating post feed")
for language in ContentLanguage.allCases {
let results = results.makeResults(for: .feed, in: language)
let generator = PostListPageGenerator(
language: language,
content: self,
results: results,
showTitle: false,
pageTitle: settings.localized(in: language).title,
pageDescription: settings.localized(in: language).description,
pageUrlPrefix: settings.localized(in: language).feedUrlPrefix)
generator.createPages(for: posts)
}
}
/**
- Note: Run on background thread
*/
private func generateTagPagesInternal() {
let count = tags.count
for index in tags.indices {
let tag = tags[index]
status("Generating tag pages: \(index) / \(count)")
generatePagesInternal(for: tag)
}
}
/**
- Note: Run on background thread
*/
private func generatePagesInternal(for tag: Tag) {
for language in ContentLanguage.allCases {
let results = results.makeResults(for: tag, in: language)
let posts = posts.filter { $0.tags.contains(tag) }
guard posts.count > 0 else { continue }
let localized = tag.localized(in: language)
let urlPrefix = absoluteUrlPrefixForTag(tag, language: language)
let generator = PostListPageGenerator(
language: language,
content: self,
results: results,
showTitle: true,
pageTitle: localized.name,
pageDescription: localized.description ?? "",
pageUrlPrefix: urlPrefix)
generator.createPages(for: posts)
}
}
/**
- Note: Run on background thread
*/
private func generateTagOverviewPagesInternal() {
status("Generating tag overview page")
for language in ContentLanguage.allCases {
let results = results.makeResults(for: .tagOverview, in: language)
#warning("Create layout for tag overview page")
}
}
/**
- Note: Run on background thread
*/
private func generateInternal(_ page: Page, in language: ContentLanguage) {
let results = results.makeResults(for: page, in: language)
let pageGenerator = PageGenerator(content: self)
results.require(files: page.requiredFiles)
guard let content = pageGenerator.generate(page: page, language: language, results: results) else {
print("Failed to generate page \(page.id) in language \(language)")
return
}
let path = page.absoluteUrl(in: language) + ".html"
guard storage.write(content, to: path) else {
print("Failed to save page \(page.id)")
return false
return
}
return true
}
}
prefix operator ~>
prefix func ~> (operation: () throws -> Void) -> Bool {
do {
try operation()
return true
} catch {
return false
}
}

View File

@@ -32,9 +32,6 @@ extension Content {
title: page.title,
lastModified: page.lastModifiedDate,
originalUrl: page.originalURL,
files: Set(page.files),
externalFiles: Set(page.externalFiles),
requiredFiles: Set(page.requiredFiles),
linkPreviewImage: page.linkPreviewImage.map { images[$0] },
linkPreviewTitle: page.linkPreviewTitle,
linkPreviewDescription: page.linkPreviewDescription)
@@ -115,14 +112,15 @@ extension Content {
english: convert(data.value.english, images: images))
}
let pages: [String : Page] = loadPages(pagesData, tags: tags, images: images)
let pages: [String : Page] = loadPages(pagesData, tags: tags, files: files)
let posts = postsData.map { postId, post in
let posts: [String : Post] = postsData.reduce(into: [:]) { dict, data in
let (postId, post) = data
let linkedPage = post.linkedPageId.map { pages[$0] }
let german = convert(post.german, images: images)
let english = convert(post.english, images: images)
return Post(
dict[postId] = Post(
content: self,
id: postId,
isDraft: post.isDraft,
@@ -145,25 +143,36 @@ extension Content {
self.tags = tags.values.sorted()
self.pages = pages.values.sorted(ascending: false) { $0.startDate }
self.files = files.values.sorted { $0.id }
self.posts = posts.sorted(ascending: false) { $0.startDate }
self.posts = posts.values.sorted(ascending: false) { $0.startDate }
self.tagOverview = tagOverview
self.settings = makeSettings(settings, tags: tags, pages: pages, files: files)
self.settings = makeSettings(settings, tags: tags, pages: pages, files: files, posts: posts)
print("Content loaded")
}
private func makeSettings(_ settings: SettingsFile, tags: [String : Tag], pages: [String : Page], files: [String : FileResource]) -> Settings {
private func makeSettings(_ settings: SettingsFile,
tags: [String : Tag],
pages: [String : Page],
files: [String : FileResource],
posts: [String : Post]) -> Settings {
#warning("Notify about missing links")
let navigationItems: [Item] = settings.navigationItems.compactMap {
switch $0.type {
case .tag:
return tags[$0.id]
case .page:
return pages[$0.id]
let navigationItems: [Item] = settings.navigationItems.compactMap { raw in
guard let type = ItemType(rawValue: raw, posts: posts, pages: pages, tags: tags) else {
return nil
}
switch type {
case .general:
return nil
case .post(let post):
return post
case .feed:
return nil // TODO: Provide feed object
case .page(let page):
return page
case .tagPage(let tag):
return tag
case .tagOverview:
return tagOverview
default:
return nil
}
}
@@ -182,7 +191,7 @@ extension Content {
english: .init(file: settings.english))
}
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], images: [String : FileResource]) -> [String : Page] {
private func loadPages(_ pagesData: [String : PageFile], tags: [String : Tag], files: [String : FileResource]) -> [String : Page] {
pagesData.reduce(into: [:]) { pages, data in
let (pageId, page) = data
pages[pageId] = Page(
@@ -193,9 +202,10 @@ extension Content {
createdDate: page.createdDate,
startDate: page.startDate,
endDate: page.endDate,
german: convert(page.german, images: images),
english: convert(page.english, images: images),
tags: page.tags.map { tags[$0]! })
german: convert(page.german, images: files),
english: convert(page.english, images: files),
tags: page.tags.map { tags[$0]! },
requiredFiles: page.requiredFiles?.map { files[$0]! } ?? [])
}
}

View File

@@ -63,7 +63,8 @@ private extension Page {
startDate: startDate,
endDate: hasEndDate ? endDate : nil,
german: german.pageFile,
english: english.pageFile)
english: english.pageFile,
requiredFiles: requiredFiles.nonEmpty?.map { $0.id }.sorted())
}
}
@@ -71,9 +72,6 @@ private extension LocalizedPage {
var pageFile: LocalizedPageFile {
.init(url: urlString,
files: files.sorted(),
externalFiles: externalFiles.sorted(),
requiredFiles: requiredFiles.sorted(),
title: title,
linkPreviewImage: linkPreviewImage?.id,
linkPreviewTitle: linkPreviewTitle,
@@ -140,7 +138,7 @@ extension Settings {
var file: SettingsFile {
.init(
paths: paths.file,
navigationItems: navigationItems.map { .init(type: $0.itemType, id: $0.id) },
navigationItems: navigationItems.map { $0.itemType.id },
posts: posts.file,
pages: pages.file,
german: german.file,

View File

@@ -26,13 +26,14 @@ final class Content: ObservableObject {
var tagOverview: TagOverviewPage?
@Published
var results: [ItemId : PageGenerationResults]
var results: GenerationResults
@Published
var generationStatus: String = "Ready to generate"
@Published
private(set) var isGeneratingWebsite = false
let imageGenerator: ImageGenerator
init(settings: Settings,
posts: [Post],
pages: [Page],
@@ -45,13 +46,10 @@ final class Content: ObservableObject {
self.tags = tags
self.files = files
self.tagOverview = tagOverview
self.results = [:]
self.results = .init()
let storage = Storage()
self.storage = storage
self.imageGenerator = ImageGenerator(
storage: storage,
settings: settings)
}
init() {
@@ -62,13 +60,10 @@ final class Content: ObservableObject {
self.tags = []
self.files = []
self.tagOverview = nil
self.results = [:]
self.results = .init()
let storage = Storage()
self.storage = storage
self.imageGenerator = ImageGenerator(
storage: storage,
settings: settings)
}
private func clear() {
@@ -78,7 +73,7 @@ final class Content: ObservableObject {
self.tags = []
self.files = []
self.tagOverview = nil
self.results = [:]
self.results = .init()
}
var images: [FileResource] {
@@ -86,9 +81,7 @@ final class Content: ObservableObject {
}
func set(isGenerating: Bool) {
DispatchQueue.main.async {
self.isGeneratingWebsite = isGenerating
}
self.isGeneratingWebsite = isGenerating
}
func add(_ file: FileResource) {

View File

@@ -28,8 +28,8 @@ final class FileResource: Item {
/**
Only for bundle images
*/
init(resourceImage: String, type: ImageFileType) {
self.type = .image(type)
init(resourceImage: String, type: FileType) {
self.type = type
self.english = "A test image included in the bundle"
self.german = "Ein Testbild aus dem Bundle"
self.isExternallyStored = true
@@ -87,18 +87,20 @@ final class FileResource: Item {
return makeCleanAbsolutePath(path)
}
var assetUrl: String {
let path = content.settings.paths.assetsOutputFolderPath + "/" + id
return makeCleanAbsolutePath(path)
}
private var pathPrefix: String {
switch type {
case .image: return content.settings.paths.imagesOutputFolderPath
case .video: return content.settings.paths.videosOutputFolderPath
default: return content.settings.paths.filesOutputFolderPath
if type.isImage {
return content.settings.paths.imagesOutputFolderPath
}
if type.isVideo {
return content.settings.paths.videosOutputFolderPath
}
if type.isAudio {
}
if type.isAsset {
return content.settings.paths.assetsOutputFolderPath
}
return content.settings.paths.filesOutputFolderPath
}
// MARK: File

View File

@@ -0,0 +1,240 @@
import Foundation
enum FileTypeCategory: String, CaseIterable {
case image
case code
case model
case text
case video
case resource
case asset
case audio
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 .asset: return "Assets"
case .resource: return "Other"
case .audio: return "Audio"
}
}
}
extension FileTypeCategory: Hashable {
}
extension FileTypeCategory: Identifiable {
var id: String {
rawValue
}
}
enum FileType: String {
// MARK: Images
case jpg
case png
case avif
case webp
case gif
case svg
case tiff
// MARK: Code
case html
case cpp
case swift
// MARK: Assets
case css
case js
// MARK: Text
case json
case conf
case yaml
// MARK: Model
case stl
case f3d
case step
case glb
case f3z
// MARK: Video
case mp4
case m4v
case webm
// MARK: Audio
case mp3
case aac
// MARK: Other
case noExtension
case zip
case cddx
case pdf
case key
case psd
// MARK: Unknown
case unknown
init(fileExtension: String?) {
guard let lower = fileExtension?.lowercased() else {
self = .noExtension
return
}
if lower == "jpeg" {
self = .jpg
return
}
guard let type = FileType(rawValue: lower) else {
self = .unknown
return
}
self = type
}
var category: FileTypeCategory {
switch self {
case .jpg, .png, .avif, .webp, .gif, .svg, .tiff:
return .image
case .mp4, .m4v, .webm:
return .video
case .mp3, .aac:
return .audio
case .js, .css:
return .asset
case .json, .conf, .yaml:
return .text
case .html, .cpp, .swift:
return .code
case .stl, .f3d, .step, .glb, .f3z:
return .model
case .zip, .cddx, .pdf, .key, .psd:
return .resource
case .noExtension, .unknown:
return .resource
}
}
var fileExtension: String {
switch self {
case .noExtension, .unknown: return ""
default:
return rawValue
}
}
var isImage: Bool {
switch self {
case .jpg, .png, .avif, .webp, .gif, .svg, .tiff:
return true
default:
return false
}
}
var isVideo: Bool {
switch self {
case .mp4, .m4v, .webm:
return true
default:
return false
}
}
var isAudio: Bool {
switch self {
case .mp3, .aac:
return true
default:
return false
}
}
var isAsset: Bool {
switch self {
case .js, .css:
return true
default:
return false
}
}
var isTextFile: Bool {
switch self {
case .html, .cpp, .swift, .css, .js, .json, .conf, .yaml:
return true
default:
return false
}
}
var isOtherFile: Bool {
switch self {
case .noExtension, .zip, .cddx, .pdf, .key, .psd:
return true
default:
return false
}
}
var htmlType: String? {
switch self {
case .mp4, .m4v:
return "video/mp4"
case .webm:
return "video/webm"
default:
return nil
}
}
var isSvg: Bool {
guard case .svg = self else {
return false
}
return true
}
}

View File

@@ -1,8 +1,6 @@
struct ItemId {
let itemId: String
let language: ContentLanguage
let itemType: ItemType
@@ -11,16 +9,16 @@ struct ItemId {
extension ItemId: Equatable {
static func == (lhs: ItemId, rhs: ItemId) -> Bool {
lhs.itemId == rhs.itemId && lhs.language == rhs.language && lhs.itemType == rhs.itemType
lhs.language == rhs.language &&
lhs.itemType == rhs.itemType
}
}
extension ItemId: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(itemId)
hasher.combine(language)
hasher.combine(itemType)
hasher.combine(itemType.id)
}
}
@@ -30,9 +28,6 @@ extension ItemId: Comparable {
guard lhs.itemType == rhs.itemType else {
return lhs.itemType < rhs.itemType
}
guard lhs.itemId == rhs.itemId else {
return lhs.itemId < rhs.itemId
}
return lhs.language < rhs.language
}
}

View File

@@ -1,15 +1,17 @@
enum ItemType: String, Codable {
enum ItemType {
case post
case general
case tag
case post(Post)
case page
case feed
case page(Page)
case tagPage(Tag)
case tagOverview
case file
}
extension ItemType: Equatable {
@@ -23,13 +25,52 @@ extension ItemType: Hashable {
extension ItemType: Identifiable {
var id: String {
rawValue
switch self {
case .general:
return "0-general"
case .feed:
return "1-feed"
case .post(let post):
return "2-post-\(post.id)"
case .page(let page):
return "3-page-\(page.id)"
case .tagPage(let tag):
return "5-tag-\(tag.id)"
case .tagOverview:
return "4-tag-overview"
}
}
init?(rawValue: String, posts: [String : Post], pages: [String : Page], tags: [String : Tag]) {
if rawValue == "0-general" {
self = .general
} else if rawValue == "1-feed" {
self = .feed
} else if rawValue == "4-tag-overview" {
self = .tagOverview
} else if let id = rawValue.removingPrefix("3-page-"), let page = pages[id] {
self = .page(page)
} else if let id = rawValue.removingPrefix("2-post-"), let post = posts[id] {
self = .post(post)
} else if let id = rawValue.removingPrefix("5-tag-"), let tag = tags[id] {
self = .tagPage(tag)
} else {
return nil
}
}
}
extension ItemType: Comparable {
static func < (lhs: ItemType, rhs: ItemType) -> Bool {
lhs.rawValue < rhs.rawValue
lhs.id < rhs.id
}
}
extension String {
func removingPrefix(_ prefix: String) -> String? {
guard self.hasPrefix(prefix) else { return nil }
return String(self.dropFirst(prefix.count))
}
}

View File

@@ -34,29 +34,6 @@ final class LocalizedPage: ObservableObject {
*/
let originalUrl: String?
/**
All files which occur in the content and are stored.
- Note: This property defaults to an empty set.
*/
@Published
var files: Set<String> = []
/**
All files which may occur in the content but are stored externally.
Missing files which would otherwise produce a warning are ignored when included here.
- Note: This property defaults to an empty set.
*/
@Published
var externalFiles: Set<String> = []
/**
Specifies additional files which should be copied to the destination when generating the content.
- Note: This property defaults to an empty set.
*/
@Published
var requiredFiles: Set<String> = []
@Published
var linkPreviewImage: FileResource?
@@ -71,9 +48,6 @@ final class LocalizedPage: ObservableObject {
title: String,
lastModified: Date? = nil,
originalUrl: String? = nil,
files: Set<String> = [],
externalFiles: Set<String> = [],
requiredFiles: Set<String> = [],
linkPreviewImage: FileResource? = nil,
linkPreviewTitle: String? = nil,
linkPreviewDescription: String? = nil) {
@@ -82,9 +56,6 @@ final class LocalizedPage: ObservableObject {
self.title = title
self.lastModified = lastModified
self.originalUrl = originalUrl
self.files = files
self.externalFiles = externalFiles
self.requiredFiles = requiredFiles
self.linkPreviewImage = linkPreviewImage
self.linkPreviewTitle = linkPreviewTitle
self.linkPreviewDescription = linkPreviewDescription

View File

@@ -36,12 +36,10 @@ final class Page: Item {
var tags: [Tag]
/**
Additional images required by the element.
These images are specified as: `source_name destination_name width (height)`.
Additional files to copy, because the page content references them
*/
@Published
var images: Set<String> = []
var requiredFiles: [FileResource]
init(content: Content,
id: String,
@@ -52,7 +50,8 @@ final class Page: Item {
endDate: Date?,
german: LocalizedPage,
english: LocalizedPage,
tags: [Tag]) {
tags: [Tag],
requiredFiles: [FileResource]) {
self.externalLink = externalLink
self.isDraft = isDraft
self.createdDate = createdDate
@@ -62,6 +61,7 @@ final class Page: Item {
self.german = german
self.english = english
self.tags = tags
self.requiredFiles = requiredFiles
super.init(content: content, id: id)
}
@@ -109,12 +109,20 @@ final class Page: Item {
}
override var itemType: ItemType {
.page
.page(self)
}
func contains(urlComponent: String) -> Bool {
english.urlString == urlComponent || german.urlString == urlComponent
}
func pageContent(in language: ContentLanguage) -> String? {
content.storage.pageContent(for: id, language: language)
}
func hasContent(in language: ContentLanguage) -> Bool {
content.storage.hasPageContent(for: id, language: language)
}
}
extension Page: DateItem {

View File

@@ -1,11 +1,6 @@
import Foundation
final class Post: ObservableObject {
unowned let content: Content
@Published
var id: String
final class Post: Item {
@Published
var isDraft: Bool
@@ -45,8 +40,6 @@ final class Post: ObservableObject {
german: LocalizedPost,
english: LocalizedPost,
linkedPage: Page? = nil) {
self.content = content
self.id = id
self.isDraft = isDraft
self.createdDate = createdDate
self.startDate = startDate
@@ -56,6 +49,7 @@ final class Post: ObservableObject {
self.german = german
self.english = english
self.linkedPage = linkedPage
super.init(content: content, id: id)
}
func localized(in language: ContentLanguage) -> LocalizedPost {
@@ -82,24 +76,6 @@ final class Post: ObservableObject {
}
}
extension Post: Identifiable {
}
extension Post: Equatable {
static func == (lhs: Post, rhs: Post) -> Bool {
lhs.id == rhs.id
}
}
extension Post: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
extension Post: DateItem {
}

View File

@@ -53,7 +53,7 @@ final class Tag: Item {
}
override var itemType: ItemType {
.tag
.tagPage(self)
}
func contains(urlComponent: String) -> Bool {

View File

@@ -1,21 +0,0 @@
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,116 +0,0 @@
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(ImageFileType)
case code(CodeFileType)
case model(ModelFileType)
case text(TextFileType)
case video(VideoFileType)
case other(ResourceFileType)
init(fileExtension: String?) {
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 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
}
}
var isImage: Bool {
if case .image = self {
return true
}
return false
}
var isVideo: Bool {
if case .video = self {
return true
}
return false
}
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
}
return nil
}
var isSvg: Bool {
guard case .image(let imageFileType) = self else {
return false
}
guard case .svg = imageFileType else {
return false
}
return true
}
}

View File

@@ -1,41 +0,0 @@
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,21 +0,0 @@
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

@@ -1,54 +0,0 @@
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

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

View File

@@ -1,30 +0,0 @@
enum VideoFileType: String {
case mp4
case m4v
case webm
init?(fileExtension: String) {
self.init(rawValue: fileExtension)
}
var fileExtension: String {
rawValue
}
var htmlType: String {
switch self {
case .mp4, .m4v:
return "video/mp4"
case .webm:
return "video/webm"
}
}
}
extension VideoFileType: CaseIterable {
}