diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 4cf9e4a..79a0410 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -41,7 +41,7 @@ E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */; }; E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */; }; E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */; }; - E22990422D107A95009F8D77 /* ImageJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990412D107A94009F8D77 /* ImageJob.swift */; }; + E22990422D107A95009F8D77 /* ImageVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990412D107A94009F8D77 /* ImageVersion.swift */; }; E22990462D10B7A7009F8D77 /* SecurityScopeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990452D10B7A6009F8D77 /* SecurityScopeStatus.swift */; }; E22990482D10B7B7009F8D77 /* StorageAccessError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990472D10B7B7009F8D77 /* StorageAccessError.swift */; }; E229904A2D10BB90009F8D77 /* SecurityScopeBookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990492D10BB90009F8D77 /* SecurityScopeBookmark.swift */; }; @@ -80,7 +80,6 @@ E25DA5852D01C92700AEF16D /* ShorthandMarkdownKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */; }; E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */; }; E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58A2D020C9200AEF16D /* PageImage.swift */; }; - E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58C2D021BA000AEF16D /* WebsiteImage.swift */; }; E25DA58F2D02368D00AEF16D /* PageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA58E2D02368A00AEF16D /* PageSettings.swift */; }; E25DA5912D023A8400AEF16D /* IntegerField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5902D023A7E00AEF16D /* IntegerField.swift */; }; E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */; }; @@ -209,6 +208,9 @@ E2FE0F112D268E7E002963B7 /* PageCodeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F102D268E78002963B7 /* PageCodeProcessor.swift */; }; E2FE0F152D26918F002963B7 /* PageHtmlProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */; }; E2FE0F172D2698D5002963B7 /* LocalizedPageId.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */; }; + E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F182D2723E3002963B7 /* ImageSet.swift */; }; + E2FE0F1B2D274FDF002963B7 /* LinkPreviewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1A2D274FDA002963B7 /* LinkPreviewItem.swift */; }; + E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -246,7 +248,7 @@ E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextFieldPropertyView.swift; sourceTree = ""; }; E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringPropertyView.swift; sourceTree = ""; }; E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderOnDiskPropertyView.swift; sourceTree = ""; }; - E22990412D107A94009F8D77 /* ImageJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageJob.swift; sourceTree = ""; }; + E22990412D107A94009F8D77 /* ImageVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageVersion.swift; sourceTree = ""; }; E22990452D10B7A6009F8D77 /* SecurityScopeStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityScopeStatus.swift; sourceTree = ""; }; E22990472D10B7B7009F8D77 /* StorageAccessError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageAccessError.swift; sourceTree = ""; }; E22990492D10BB90009F8D77 /* SecurityScopeBookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityScopeBookmark.swift; sourceTree = ""; }; @@ -281,7 +283,6 @@ E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShorthandMarkdownKey.swift; sourceTree = ""; }; E25DA5882D01CBCE00AEF16D /* Content+Generation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Content+Generation.swift"; sourceTree = ""; }; E25DA58A2D020C9200AEF16D /* PageImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageImage.swift; sourceTree = ""; }; - E25DA58C2D021BA000AEF16D /* WebsiteImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteImage.swift; sourceTree = ""; }; E25DA58E2D02368A00AEF16D /* PageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettings.swift; sourceTree = ""; }; E25DA5902D023A7E00AEF16D /* IntegerField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerField.swift; sourceTree = ""; }; E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageSettingsFile.swift; sourceTree = ""; }; @@ -409,6 +410,9 @@ E2FE0F102D268E78002963B7 /* PageCodeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageCodeProcessor.swift; sourceTree = ""; }; E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageHtmlProcessor.swift; sourceTree = ""; }; E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPageId.swift; sourceTree = ""; }; + E2FE0F182D2723E3002963B7 /* ImageSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSet.swift; sourceTree = ""; }; + E2FE0F1A2D274FDA002963B7 /* LinkPreviewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewItem.swift; sourceTree = ""; }; + E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagOverviewGenerator.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -432,6 +436,7 @@ E229901A2D0E3F09009F8D77 /* Item */ = { isa = PBXGroup; children = ( + E2FE0F1A2D274FDA002963B7 /* LinkPreviewItem.swift */, E2FE0F162D2698D5002963B7 /* LocalizedPageId.swift */, E229902B2D0F6FC0009F8D77 /* ItemId.swift */, E229901D2D0E4362009F8D77 /* LocalizedItem.swift */, @@ -488,15 +493,15 @@ children = ( E2FE0F072D2689DC002963B7 /* Post Lists */, E29D31B62D0DAC030051B7F4 /* Page Content */, + E2FE0F1C2D281A7B002963B7 /* Page Generators */, E22990232D0EDBD0009F8D77 /* HeaderElement.swift */, E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */, E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */, - E22990412D107A94009F8D77 /* ImageJob.swift */, + E22990412D107A94009F8D77 /* ImageVersion.swift */, + E2FE0F182D2723E3002963B7 /* ImageSet.swift */, E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */, E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */, E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */, - E25DA5982D02401A00AEF16D /* PageGenerator.swift */, - E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */, E25DA5842D01C92600AEF16D /* ShorthandMarkdownKey.swift */, E29D31252D0370A50051B7F4 /* VideoOption.swift */, E29D318F2D0B34870051B7F4 /* GenerationAnomaly.swift */, @@ -726,7 +731,6 @@ isa = PBXGroup; children = ( E29D311E2D0320D90051B7F4 /* ContentElements */, - E25DA58C2D021BA000AEF16D /* WebsiteImage.swift */, E25DA58A2D020C9200AEF16D /* PageImage.swift */, E25DA51E2CFF15C100AEF16D /* NavigationBar.swift */, E2FE0EF92D25AFB5002963B7 /* PageHeader.swift */, @@ -848,6 +852,16 @@ path = "Post Lists"; sourceTree = ""; }; + E2FE0F1C2D281A7B002963B7 /* Page Generators */ = { + isa = PBXGroup; + children = ( + E2FE0F1D2D281ACE002963B7 /* TagOverviewGenerator.swift */, + E25DA5982D02401A00AEF16D /* PageGenerator.swift */, + E2B85F3C2C4293F80047CD0C /* FeedPageGenerator.swift */, + ); + path = "Page Generators"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -972,7 +986,7 @@ E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */, E29D31852D0AE8EE0051B7F4 /* KnownHeaderElement.swift in Sources */, E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */, - E22990422D107A95009F8D77 /* ImageJob.swift in Sources */, + E22990422D107A95009F8D77 /* ImageVersion.swift in Sources */, E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */, E229904E2D13535C009F8D77 /* SecurityBookmark.swift in Sources */, E2A21C082CB17B870060935B /* TagView.swift in Sources */, @@ -987,7 +1001,6 @@ E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */, E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */, E2A21C102CB18B3A0060935B /* FlowHStack.swift in Sources */, - E25DA58D2D021BA400AEF16D /* WebsiteImage.swift in Sources */, E25DA5992D02401E00AEF16D /* PageGenerator.swift in Sources */, E25DA5382D00420E00AEF16D /* LocalizedPostSettingsFile.swift in Sources */, E2B85F3D2C4293F80047CD0C /* FeedPageGenerator.swift in Sources */, @@ -1043,6 +1056,7 @@ E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */, E25DA5192CFF035900AEF16D /* Array+Split.swift in Sources */, + E2FE0F1B2D274FDF002963B7 /* LinkPreviewItem.swift in Sources */, E2FE0F062D267350002963B7 /* TextFieldPropertyView.swift in Sources */, E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */, @@ -1122,8 +1136,10 @@ E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */, E25DA5932D023B3C00AEF16D /* PageSettingsFile.swift in Sources */, E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */, + E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */, E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */, E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */, + E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */, E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */, E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */, E29D31792D083DE50051B7F4 /* PageContentResultsView.swift in Sources */, diff --git a/CHDataManagement/Generator/GenerationResults.swift b/CHDataManagement/Generator/GenerationResults.swift index d1c7868..723c8c2 100644 --- a/CHDataManagement/Generator/GenerationResults.swift +++ b/CHDataManagement/Generator/GenerationResults.swift @@ -26,7 +26,7 @@ final class GenerationResults: ObservableObject { var requiredFiles: Set = [] @Published - var imagesToGenerate: Set = [] + var imagesToGenerate: Set = [] @Published var invalidCommands: Set = [] @@ -38,7 +38,7 @@ final class GenerationResults: ObservableObject { var unsavedOutputFiles: Set = [] @Published - var failedImages: Set = [] + var failedImages: Set = [] @Published var emptyPages: Set = [] @@ -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(_ images: S) where S: Sequence, S.Element == ImageGenerationJob { + func generate(_ 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) } } diff --git a/CHDataManagement/Generator/ImageGenerator.swift b/CHDataManagement/Generator/ImageGenerator.swift index f9f81b8..6082722 100644 --- a/CHDataManagement/Generator/ImageGenerator.swift +++ b/CHDataManagement/Generator/ImageGenerator.swift @@ -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 = [] + + 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 } diff --git a/CHDataManagement/Generator/ImageJob.swift b/CHDataManagement/Generator/ImageJob.swift deleted file mode 100644 index 7368536..0000000 --- a/CHDataManagement/Generator/ImageJob.swift +++ /dev/null @@ -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 - } -} diff --git a/CHDataManagement/Generator/ImageSet.swift b/CHDataManagement/Generator/ImageSet.swift new file mode 100644 index 0000000..32937e7 --- /dev/null +++ b/CHDataManagement/Generator/ImageSet.swift @@ -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 = "" + result += "" + result += "" + result += "\(description.htmlEscaped())" + result += "" + return result + } +} diff --git a/CHDataManagement/Generator/ImageVersion.swift b/CHDataManagement/Generator/ImageVersion.swift new file mode 100644 index 0000000..cced42d --- /dev/null +++ b/CHDataManagement/Generator/ImageVersion.swift @@ -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 + } +} diff --git a/CHDataManagement/Generator/KnownHeaderElement.swift b/CHDataManagement/Generator/KnownHeaderElement.swift index c5f4917..1d4d327 100644 --- a/CHDataManagement/Generator/KnownHeaderElement.swift +++ b/CHDataManagement/Generator/KnownHeaderElement.swift @@ -39,8 +39,6 @@ extension KnownHeaderElement: Comparable { static func < (lhs: KnownHeaderElement, rhs: KnownHeaderElement) -> Bool { lhs.rawValue < rhs.rawValue } - - } extension KnownHeaderElement: CustomStringConvertible { diff --git a/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift b/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift index 2d33ba7..73b4826 100644 --- a/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift +++ b/CHDataManagement/Generator/Page Content/AudioPlayerCommand.swift @@ -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( diff --git a/CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift b/CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift index d31d645..0c0b101 100644 --- a/CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift +++ b/CHDataManagement/Generator/Page Content/PageHtmlProcessor.swift @@ -106,7 +106,8 @@ struct PageHtmlProcessor: CommandProcessor { results.warning("Failed to find elements in inline HTML: \(error)") return } - + checkSourceSetAttributes(sources: sources) + checkSourceAttributes(sources: sources) } private func checkSourceSetAttributes(sources: [Element]) { diff --git a/CHDataManagement/Generator/FeedPageGenerator.swift b/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift similarity index 89% rename from CHDataManagement/Generator/FeedPageGenerator.swift rename to CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift index 92e68e8..58df26a 100644 --- a/CHDataManagement/Generator/FeedPageGenerator.swift +++ b/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift @@ -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 diff --git a/CHDataManagement/Generator/PageGenerator.swift b/CHDataManagement/Generator/Page Generators/PageGenerator.swift similarity index 93% rename from CHDataManagement/Generator/PageGenerator.swift rename to CHDataManagement/Generator/Page Generators/PageGenerator.swift index 06a97b5..cdf311a 100644 --- a/CHDataManagement/Generator/PageGenerator.swift +++ b/CHDataManagement/Generator/Page Generators/PageGenerator.swift @@ -6,12 +6,11 @@ final class PageGenerator { self.content = content } - private func makeHeaders(requiredItems: Set) -> Set { + private func makeHeaders(requiredItems: Set, results: PageGenerationResults) -> Set { 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 diff --git a/CHDataManagement/Generator/Page Generators/TagOverviewGenerator.swift b/CHDataManagement/Generator/Page Generators/TagOverviewGenerator.swift new file mode 100644 index 0000000..3963172 --- /dev/null +++ b/CHDataManagement/Generator/Page Generators/TagOverviewGenerator.swift @@ -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 += "

\(localized.title)

" + 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 + } + } +} diff --git a/CHDataManagement/Generator/PageContentGenerator.swift b/CHDataManagement/Generator/PageContentGenerator.swift index f4d1e32..46b532a 100644 --- a/CHDataManagement/Generator/PageContentGenerator.swift +++ b/CHDataManagement/Generator/PageContentGenerator.swift @@ -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: `![model]()` */ diff --git a/CHDataManagement/Generator/PageGenerationResults.swift b/CHDataManagement/Generator/PageGenerationResults.swift index f7c1c4a..92befc0 100644 --- a/CHDataManagement/Generator/PageGenerationResults.swift +++ b/CHDataManagement/Generator/PageGenerationResults.swift @@ -64,7 +64,7 @@ final class PageGenerationResults: ObservableObject { private(set) var requiredFiles: Set /// The image versions required for this page - private(set) var imagesToGenerate: Set + private(set) var imagesToGenerate: Set 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) } diff --git a/CHDataManagement/Generator/Post Lists/FeedGeneratorSource.swift b/CHDataManagement/Generator/Post Lists/FeedGeneratorSource.swift index 7085fbc..5a68e8b 100644 --- a/CHDataManagement/Generator/Post Lists/FeedGeneratorSource.swift +++ b/CHDataManagement/Generator/Post Lists/FeedGeneratorSource.swift @@ -11,6 +11,10 @@ struct FeedGeneratorSource: PostListPageGeneratorSource { false } + var postsPerPage: Int { + content.settings.posts.postsPerPage + } + var pageTitle: String { content.settings.localized(in: language).title } diff --git a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift index 961541b..3c5248d 100644 --- a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift @@ -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) } diff --git a/CHDataManagement/Generator/Post Lists/PostListPageGeneratorSource.swift b/CHDataManagement/Generator/Post Lists/PostListPageGeneratorSource.swift index a5d8b09..4f2b619 100644 --- a/CHDataManagement/Generator/Post Lists/PostListPageGeneratorSource.swift +++ b/CHDataManagement/Generator/Post Lists/PostListPageGeneratorSource.swift @@ -14,4 +14,6 @@ protocol PostListPageGeneratorSource { var pageDescription: String { get } func pageUrlPrefix(for language: ContentLanguage) -> String + + var postsPerPage: Int { get } } diff --git a/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift b/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift index 27f9909..68b7d86 100644 --- a/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift +++ b/CHDataManagement/Generator/Post Lists/TagPageGeneratorSource.swift @@ -13,6 +13,10 @@ struct TagPageGeneratorSource: PostListPageGeneratorSource { true } + var postsPerPage: Int { + content.settings.posts.postsPerPage + } + var pageTitle: String { tag.localized(in: language).name } diff --git a/CHDataManagement/Generator/VideoOption.swift b/CHDataManagement/Generator/VideoOption.swift index 9e329a8..f950a9d 100644 --- a/CHDataManagement/Generator/VideoOption.swift +++ b/CHDataManagement/Generator/VideoOption.swift @@ -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)'" } } diff --git a/CHDataManagement/Main/MainView.swift b/CHDataManagement/Main/MainView.swift index 8d59c22..b58b2cf 100644 --- a/CHDataManagement/Main/MainView.swift +++ b/CHDataManagement/Main/MainView.swift @@ -4,11 +4,11 @@ import SFSafeSymbols #warning("Fix podcast") #warning("Fix CV") #warning("Fix endeavor basics (image compare)") +#warning("Fix cap mosaic GIF") #warning("Add custom url string to external files (optional)") #warning("Show all warnings on page content") -#warning("Button to delete file") -#warning("Add link to other language") +#warning("Button to delete file, show in finder, replace, mark changed (-> images)") #warning("Transfer images of posts to other language") #warning("Show tag selection view for pages") #warning("Button to replace files") @@ -16,12 +16,13 @@ import SFSafeSymbols #warning("Calculate file sizes") #warning("Specify image aspect ratio to prevent page jumps") #warning("Add version and source url properties to file resources") -#warning("Consolidate all errors in Content") +#warning("Show errors during loading of content") #warning("Generate pages for posts") #warning("Clean up mock content") #warning("Show posts linking to a page") #warning("Add author to settings and page headers") -#warning("Mark changed images for generation") +#warning("Check for files in output folder not generated by app") +#warning("Fix GIFs: Don't rescale, don't use image set") @main struct MainView: App { diff --git a/CHDataManagement/Model/Content+Generation.swift b/CHDataManagement/Model/Content+Generation.swift index 9d78a1a..37800b2 100644 --- a/CHDataManagement/Model/Content+Generation.swift +++ b/CHDataManagement/Model/Content+Generation.swift @@ -74,12 +74,14 @@ extension Content { completed += 1 status("Generating required images: \(completed) / \(count)") } - if imageGenerator.generate(job: image) { + if imageGenerator.generate(version: image) { continue } results.failed(image: image) } + imageGenerator.save() + imageGenerator.printAvifCommands() //let images = Set(self.images.map { $0.id }) //imageGenerator.recalculateGeneratedImages(by: images) } @@ -244,11 +246,16 @@ extension Content { - Note: Run on background thread */ private func generateTagOverviewPagesInternal() { + guard let tagOverview else { + print("Generator: No tag overview page to generate") + return + } status("Generating tag overview page") for language in ContentLanguage.allCases { guard shouldGenerateWebsite else { return } let results = results.makeResults(for: .tagOverview, in: language) - #warning("Create layout for tag overview page") + let generator = TagOverviewGenerator(content: self, language: language, results: results) + generator.generatePage(tags: tags, overview: tagOverview) } } diff --git a/CHDataManagement/Model/Content+Load.swift b/CHDataManagement/Model/Content+Load.swift index 8f0b210..76d5071 100644 --- a/CHDataManagement/Model/Content+Load.swift +++ b/CHDataManagement/Model/Content+Load.swift @@ -146,7 +146,27 @@ extension Content { self.files = files.values.sorted { $0.id } self.posts = posts.values.sorted(ascending: false) { $0.startDate } self.tagOverview = tagOverview - self.settings = .init(file: settings, tags: tags, pages: pages, files: files, posts: posts, tagOverview: tagOverview) + self.settings = .init(file: settings, files: files) { raw in + #warning("Notify about missing links") + 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 + } + } + print("Content loaded") } diff --git a/CHDataManagement/Model/Content.swift b/CHDataManagement/Model/Content.swift index 66aa4fc..a329f23 100644 --- a/CHDataManagement/Model/Content.swift +++ b/CHDataManagement/Model/Content.swift @@ -34,6 +34,7 @@ final class Content: ObservableObject { @Published private(set) var isGeneratingWebsite = false + @Published private(set) var shouldGenerateWebsite = false init(settings: Settings, diff --git a/CHDataManagement/Model/FileResource.swift b/CHDataManagement/Model/FileResource.swift index 7a96512..b19eca8 100644 --- a/CHDataManagement/Model/FileResource.swift +++ b/CHDataManagement/Model/FileResource.swift @@ -76,6 +76,33 @@ final class FileResource: Item { Image(systemSymbol: .exclamationmarkTriangle) } + /// The path to the output folder where image versions are stored (no leading slash) + var outputImageFolder: String { + "\(content.settings.paths.imagesOutputFolderPath)/\(id.fileNameWithoutExtension)" + } + + func outputPath(width: Int, height: Int, type: FileType?) -> String { + let prefix = "/\(outputImageFolder)/\(width)x\(height)" + guard let ext = type?.fileExtension else { + return prefix + } + return prefix + "." + ext + } + + func imageSet(width: Int, height: Int, language: ContentLanguage, quality: CGFloat = 0.7) -> ImageSet { + let description = self.localized(in: language) + return .init( + image: self, + maxWidth: width, + maxHeight: height, + description: description, + quality: quality) + } + + func imageVersion(width: Int, height: Int, type: FileType) -> ImageVersion { + .init(image: self, type: type, maximumWidth: width, maximumHeight: height) + } + // MARK: Paths /** diff --git a/CHDataManagement/Model/FileType.swift b/CHDataManagement/Model/FileType.swift index d7c582c..da8d8ae 100644 --- a/CHDataManagement/Model/FileType.swift +++ b/CHDataManagement/Model/FileType.swift @@ -103,6 +103,10 @@ enum FileType: String { case aac + case m4b + + case m4a + // MARK: Other case noExtension @@ -143,7 +147,7 @@ enum FileType: String { return .image case .mp4, .m4v, .webm: return .video - case .mp3, .aac: + case .mp3, .aac, .m4b, .m4a: return .audio case .js, .css, .ttf: return .asset @@ -160,65 +164,38 @@ enum FileType: String { } } - var fileExtension: String { + var fileExtension: String? { switch self { - case .noExtension, .unknown: return "" + case .noExtension, .unknown: return nil default: return rawValue } } var isImage: Bool { - switch self { - case .jpg, .png, .avif, .webp, .gif, .svg, .tiff: - return true - default: - return false - } + category == .image } var isVideo: Bool { - switch self { - case .mp4, .m4v, .webm: - return true - default: - return false - } + category == .video } var isAudio: Bool { - switch self { - case .mp3, .aac: - return true - default: - return false - } + category == .audio } var isAsset: Bool { - switch self { - case .js, .css, .ttf: - return true - default: - return false - } + category == .asset } var isTextFile: Bool { - switch self { - case .html, .cpp, .swift, .css, .js, .json, .conf, .yaml: - return true - default: - return false + switch category { + case .text, .code: return true + default: break } - } - - var isOtherFile: Bool { switch self { - case .noExtension, .zip, .cddx, .pdf, .key, .psd: - return true - default: - return false + case .css, .js: return true + default: return false } } diff --git a/CHDataManagement/Model/Item/LinkPreviewItem.swift b/CHDataManagement/Model/Item/LinkPreviewItem.swift new file mode 100644 index 0000000..bdb620a --- /dev/null +++ b/CHDataManagement/Model/Item/LinkPreviewItem.swift @@ -0,0 +1,9 @@ + +protocol LinkPreviewItem { + + var linkPreviewImage: FileResource? { get } + + var linkPreviewTitle: String? { get } + + var linkPreviewDescription: String? { get } +} diff --git a/CHDataManagement/Model/LocalizedPage.swift b/CHDataManagement/Model/LocalizedPage.swift index 3cb0d10..dd94420 100644 --- a/CHDataManagement/Model/LocalizedPage.swift +++ b/CHDataManagement/Model/LocalizedPage.swift @@ -71,3 +71,7 @@ final class LocalizedPage: ObservableObject { !content.containsPage(withUrlComponent: urlComponent) } } + +extension LocalizedPage: LinkPreviewItem { + +} diff --git a/CHDataManagement/Model/LocalizedPost.swift b/CHDataManagement/Model/LocalizedPost.swift index e48d965..395b4f7 100644 --- a/CHDataManagement/Model/LocalizedPost.swift +++ b/CHDataManagement/Model/LocalizedPost.swift @@ -44,3 +44,7 @@ final class LocalizedPost: ObservableObject { self.linkPreviewDescription = linkPreviewDescription } } + +extension LocalizedPost: LinkPreviewItem { + +} diff --git a/CHDataManagement/Model/LocalizedTag.swift b/CHDataManagement/Model/LocalizedTag.swift index 2e595af..4b3e25a 100644 --- a/CHDataManagement/Model/LocalizedTag.swift +++ b/CHDataManagement/Model/LocalizedTag.swift @@ -45,3 +45,14 @@ final class LocalizedTag: ObservableObject { !content.containsTag(withUrlComponent: urlComponent) } } + +extension LocalizedTag: LinkPreviewItem { + + var linkPreviewTitle: String? { + self.name + } + + var linkPreviewDescription: String? { + description + } +} diff --git a/CHDataManagement/Model/Settings/NavigationSettings.swift b/CHDataManagement/Model/Settings/NavigationSettings.swift index 8fc9668..ff80abc 100644 --- a/CHDataManagement/Model/Settings/NavigationSettings.swift +++ b/CHDataManagement/Model/Settings/NavigationSettings.swift @@ -12,39 +12,16 @@ final class NavigationSettings: ObservableObject { @Published var english: LocalizedNavigationSettings - init(navigationItems: [Item], german: LocalizedNavigationSettings, english: LocalizedNavigationSettings) { + init(navigationItems: [Item], + german: LocalizedNavigationSettings, + english: LocalizedNavigationSettings) { self.navigationItems = navigationItems self.german = german self.english = english } - init(file: NavigationSettingsFile, - tags: [String : Tag], - pages: [String : Page], - files: [String : FileResource], - posts: [String : Post], - tagOverview: TagOverviewPage?) { - - #warning("Notify about missing links") - self.navigationItems = file.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 - } - } + init(file: NavigationSettingsFile, map: (String) -> Item?) { + self.navigationItems = file.navigationItems.compactMap(map) self.german = LocalizedNavigationSettings(file: file.german) self.english = LocalizedNavigationSettings(file: file.english) } diff --git a/CHDataManagement/Model/Settings/Settings.swift b/CHDataManagement/Model/Settings/Settings.swift index beea212..12fbd18 100644 --- a/CHDataManagement/Model/Settings/Settings.swift +++ b/CHDataManagement/Model/Settings/Settings.swift @@ -37,20 +37,8 @@ final class Settings: ObservableObject { } } - init(file: SettingsFile, - tags: [String : Tag], - pages: [String : Page], - files: [String : FileResource], - posts: [String : Post], - tagOverview: TagOverviewPage?) { - - self.navigation = NavigationSettings( - file: file.navigation, - tags: tags, - pages: pages, - files: files, - posts: posts, - tagOverview: tagOverview) + init(file: SettingsFile, files: [String : FileResource], map: (String) -> Item?) { + self.navigation = NavigationSettings(file: file.navigation, map: map) self.posts = PostSettings(file: file.posts, files: files) self.pages = PageSettings(file: file.pages, files: files) diff --git a/CHDataManagement/Page Elements/ContentElements/RelatedPageLink.swift b/CHDataManagement/Page Elements/ContentElements/RelatedPageLink.swift index 52e24b1..e52a65a 100644 --- a/CHDataManagement/Page Elements/ContentElements/RelatedPageLink.swift +++ b/CHDataManagement/Page Elements/ContentElements/RelatedPageLink.swift @@ -1,39 +1,32 @@ -struct RelatedPageLink { +/** + An element showing a box with info about a related page. - struct Image { - - let url: String - - let description: String - - let size: Int - } + Contains an optional thumbnail image, a title and a description. + */ +struct RelatedPageLink: HtmlProducer { + /// The title of the linked page let title: String + /// A short description of the linked page let description: String + /// The url to the linked page let url: String - let image: Image? + /// The optional thumbnail image to display + let image: ImageSet? - var content: String { - var result = "" + func populate(_ result: inout String) { result += "" result += "" // Close related-box-wrapper, related-box - return result } } diff --git a/CHDataManagement/Page Elements/FeedEntry.swift b/CHDataManagement/Page Elements/FeedEntry.swift index 41f8d43..b2b7113 100644 --- a/CHDataManagement/Page Elements/FeedEntry.swift +++ b/CHDataManagement/Page Elements/FeedEntry.swift @@ -14,8 +14,7 @@ struct FeedEntry { var content: String { var result = "
" - ImageGallery(id: data.entryId, images: data.images) - .addContent(to: &result) + ImageGallery(id: data.entryId, images: data.images).populate(&result) if let url = data.link?.url { result += "
" diff --git a/CHDataManagement/Page Elements/FeedEntryData.swift b/CHDataManagement/Page Elements/FeedEntryData.swift index 56796b0..4ed7be5 100644 --- a/CHDataManagement/Page Elements/FeedEntryData.swift +++ b/CHDataManagement/Page Elements/FeedEntryData.swift @@ -13,9 +13,9 @@ struct FeedEntryData { let text: [String] - let images: [Image] + let images: [ImageSet] - init(entryId: String, title: String?, textAboveTitle: String, link: Link?, tags: [Tag], text: [String], images: [Image]) { + init(entryId: String, title: String?, textAboveTitle: String, link: Link?, tags: [Tag], text: [String], images: [ImageSet]) { self.entryId = entryId self.title = title self.textAboveTitle = textAboveTitle @@ -40,16 +40,4 @@ struct FeedEntryData { let url: String } - - struct Image { - - let rawImagePath: String - - let width: Int - - let height: Int - - let altText: String - - } } diff --git a/CHDataManagement/Page Elements/ImageGallery.swift b/CHDataManagement/Page Elements/ImageGallery.swift index bcc524c..1fa9e0b 100644 --- a/CHDataManagement/Page Elements/ImageGallery.swift +++ b/CHDataManagement/Page Elements/ImageGallery.swift @@ -1,49 +1,50 @@ import Foundation -struct ImageGallery { +/** + An element showing a selection of images one by one using navigation buttons. + */ +struct ImageGallery: HtmlProducer { + /// The unique id to distinguish different galleries in HTML and JavaScript let id: String - let images: [FeedEntryData.Image] + /// The images to display + let images: [ImageSet] + /// A version of the id that is safe to use in HTML and JavaScript private var htmlSafeId: String { ImageGallery.htmlSafe(id) } - init(id: String, images: [FeedEntryData.Image]) { + init(id: String, images: [ImageSet]) { self.id = id self.images = images } - func addContent(to result: inout String) { + func populate(_ result: inout String) { guard !images.isEmpty else { return } result += "
" - guard images.count > 1 else { - result += "
" - result += WebsiteImage(image: images[0]).content - result += "
" // Close swiper-slide, swiper, swiper-wrapper - return - } + let needsPagination = images.count > 1 for image in images { - // TODO: Use different images based on device result += "
" - - result += WebsiteImage(image: image).content - - result += "
" - + result += image.content + if needsPagination { + result += "
" + } result += "
" // Close swiper-slide } result += "
" // Close swiper-wrapper - result += "
" - result += "
" - result += "
" + if needsPagination { + result += "
" + result += "
" + result += "
" + } result += "
" // Close swiper } diff --git a/CHDataManagement/Page Elements/PageImage.swift b/CHDataManagement/Page Elements/PageImage.swift index eace4af..6280e9a 100644 --- a/CHDataManagement/Page Elements/PageImage.swift +++ b/CHDataManagement/Page Elements/PageImage.swift @@ -1,27 +1,34 @@ import Foundation -struct PageImage { +/** + An image that is part of the page content. + A tap/click on the image shows a fullscreen version of the image, including an optional caption. + */ +struct PageImage: HtmlProducer { + + /// The HTML id attribute used to enable fullscreen images let imageId: String - let thumbnail: FeedEntryData.Image + /// The small version of the image visible on the page + let thumbnail: ImageSet - let largeImage: FeedEntryData.Image + /// The large version of the image for fullscreen view + let largeImage: ImageSet + /// The optional caption text below the fullscreen image let caption: String? - var content: String { - var result = "" + func populate(_ result: inout String) { result += "
" - result += WebsiteImage(image: thumbnail).content + result += thumbnail.content result += "
" result += "
" - result += WebsiteImage(image: largeImage).content + result += largeImage.content if let caption { result += "
\(caption)
" } result += "
" result += "
" - return result } } diff --git a/CHDataManagement/Page Elements/PostFeedPageNavigation.swift b/CHDataManagement/Page Elements/PostFeedPageNavigation.swift index 3406b74..b5fe99e 100644 --- a/CHDataManagement/Page Elements/PostFeedPageNavigation.swift +++ b/CHDataManagement/Page Elements/PostFeedPageNavigation.swift @@ -2,30 +2,20 @@ import Foundation struct PostFeedPageNavigation { - let language: ContentLanguage + let linkPrefix: String let currentPage: Int let numberOfPages: Int - init(currentPage: Int, numberOfPages: Int, language: ContentLanguage) { + init(linkPrefix: String, currentPage: Int, numberOfPages: Int) { + self.linkPrefix = linkPrefix self.currentPage = currentPage self.numberOfPages = numberOfPages - self.language = language } private func pageLink(_ page: Int) -> String { - guard page > 1 else { return "href='/feed'" } - return "href='/feed-\(page)'" - } - - private func previousText() -> String { - switch language { - case .english: - return "Previous" - case .german: - return "Zurück" - } + "href='\(linkPrefix)\(page)'" } private func addPreviousButton(to result: inout String) { diff --git a/CHDataManagement/Page Elements/WebsiteImage.swift b/CHDataManagement/Page Elements/WebsiteImage.swift deleted file mode 100644 index 42e6013..0000000 --- a/CHDataManagement/Page Elements/WebsiteImage.swift +++ /dev/null @@ -1,48 +0,0 @@ - -struct WebsiteImage { - - static func imagePath(prefix: String, width: Int, height: Int) -> String { - "\(prefix)@\(width)x\(height)" - } - - static func imagePath(prefix: String, extension fileExtension: String, width: Int, height: Int) -> String { - "\(prefix)@\(width)x\(height).\(fileExtension)" - } - - static func imagePath(source: String, width: Int, height: Int) -> String { - let (prefix, ext) = source.fileNameAndExtension - return imagePath(prefix: prefix, extension: ext ?? ".jpg", width: width, height: height) - } - - private let prefix1x: String - - private let prefix2x: String - - private let altText: String - - private let ext: String - - init(image: FeedEntryData.Image) { - self.init(rawImagePath: image.rawImagePath, - width: image.width, - height: image.height, - altText: image.altText) - } - - init(rawImagePath: String, width: Int, height: Int, altText: String) { - let (prefix, ext) = rawImagePath.fileNameAndExtension - self.prefix1x = WebsiteImage.imagePath(prefix: prefix, width: width, height: height) - self.prefix2x = WebsiteImage.imagePath(prefix: prefix, width: 2*width, height: 2*height) - self.altText = altText.htmlEscaped() - self.ext = ext ?? "jpg" - } - - var content: String { - var result = "" - result += "" - result += "" - result += "\(altText)" - result += "" - return result - } -} diff --git a/CHDataManagement/Storage/SecurityBookmark.swift b/CHDataManagement/Storage/SecurityBookmark.swift index cda3a91..b4c53c0 100644 --- a/CHDataManagement/Storage/SecurityBookmark.swift +++ b/CHDataManagement/Storage/SecurityBookmark.swift @@ -28,6 +28,10 @@ struct SecurityBookmark { // MARK: Write + func fullPath(to relativePath: String) -> URL { + url.appending(path: relativePath, directoryHint: .notDirectory) + } + /** Write the data of an encodable value to a relative path in the content folder, or delete the file if nil is passed. @@ -68,7 +72,7 @@ struct SecurityBookmark { createParentFolder: Bool = true, ifFileExists overwrite: OverwriteBehaviour = .writeIfChanged) -> Bool { perform { url in - let file = url.appending(path: relativePath, directoryHint: .notDirectory) + let file = fullPath(to: relativePath) if exists(file) { switch overwrite { diff --git a/CHDataManagement/Storage/Storage.swift b/CHDataManagement/Storage/Storage.swift index d376d78..d00f8df 100644 --- a/CHDataManagement/Storage/Storage.swift +++ b/CHDataManagement/Storage/Storage.swift @@ -56,7 +56,6 @@ final class Storage: ObservableObject { // MARK: Pages - private func pageContentFileName(_ id: String, _ language: ContentLanguage) -> String { "\(id)-\(language.rawValue).md" } @@ -236,6 +235,23 @@ final class Storage: ObservableObject { // MARK: Files + /** + The full path to a resource file in the content folder + - Parameter file: The filename of the file + - Note: Only for resource files, since path is relative to files folder + */ + func path(toFile file: String) -> URL? { + contentScope?.fullPath(to: filePath(file: file)) + } + + /** + The full file path to a file in the output folder + - Parameter relativePath: The path of the file relative to the output folder + */ + func outputPath(to relativePath: String) -> URL? { + outputScope?.fullPath(to: relativePath) + } + private func filePath(file fileId: String) -> String { filesFolderName + "/" + fileId } @@ -337,6 +353,7 @@ final class Storage: ObservableObject { } print("Found \(allImages.count) generated images") let images = Set(allImages) + #warning("TODO: Fix counting generated images") return imageSet.reduce(into: [:]) { result, imageName in let prefix = imageName.fileNameWithoutExtension + "@" let versions = images.filter { $0.hasPrefix(prefix) } diff --git a/CHDataManagement/Views/Files/FileListView.swift b/CHDataManagement/Views/Files/FileListView.swift index 787be58..114dbda 100644 --- a/CHDataManagement/Views/Files/FileListView.swift +++ b/CHDataManagement/Views/Files/FileListView.swift @@ -1,34 +1,5 @@ import SwiftUI -enum FileFilterType: String, Hashable, CaseIterable, Identifiable { - case images - case text - case videos - case other - - var text: String { - switch self { - case .images: return "Image" - case .text: return "Text" - case .videos: return "Video" - case .other: return "Other" - } - } - - var id: String { - rawValue - } - - func matches(_ type: FileType) -> Bool { - switch self { - case .images: return type.isImage - case .text: return type.isTextFile - case .videos: return type.isVideo - case .other: return type.isOtherFile - } - } -} - struct FileListView: View { @EnvironmentObject @@ -38,21 +9,23 @@ struct FileListView: View { var selectedFile: FileResource? @State - private var selectedFileType: FileFilterType + private var selectedFileType: FileTypeCategory? = nil @State private var searchString = "" - let allowedType: FileFilterType? + let allowedType: FileTypeCategory? - init(selectedFile: Binding, allowedType: FileFilterType? = nil) { + init(selectedFile: Binding, allowedType: FileTypeCategory? = nil) { self._selectedFile = selectedFile self.allowedType = allowedType - self.selectedFileType = allowedType ?? .images } var filesBySelectedType: [FileResource] { - content.files.filter { selectedFileType.matches($0.type) } + guard let selectedFileType else { + return content.files + } + return content.files.filter { selectedFileType == $0.type.category } } var filteredFiles: [FileResource] { @@ -64,14 +37,17 @@ struct FileListView: View { var body: some View { VStack(alignment: .center) { - Picker("", selection: $selectedFileType) { - ForEach(FileFilterType.allCases) { type in - Text(type.text).tag(type) + if allowedType == nil { + Picker("", selection: $selectedFileType) { + let all: FileTypeCategory? = nil + Text("All").tag(all) + ForEach(FileTypeCategory.allCases) { type in + Text(type.text).tag(type) + } } + .pickerStyle(.menu) + .padding(.trailing, 7) } - .pickerStyle(.segmented) - .padding(.trailing, 7) - .disabled(allowedType != nil) TextField("", text: $searchString, prompt: Text("Search")) .textFieldStyle(.roundedBorder) .padding(.horizontal, 8) @@ -93,7 +69,7 @@ struct FileListView: View { return } - if newValue.matches(selectedFile.type) { + if newValue == selectedFile.type.category { return } DispatchQueue.main.async { @@ -102,10 +78,11 @@ struct FileListView: View { } } .onAppear { + if let allowedType { + selectedFileType = allowedType + } if selectedFile == nil { - DispatchQueue.main.async { - selectedFile = content.files.first - } + selectedFile = content.files.first } } } diff --git a/CHDataManagement/Views/Files/FileSelectionView.swift b/CHDataManagement/Views/Files/FileSelectionView.swift index b6acefc..6258d9e 100644 --- a/CHDataManagement/Views/Files/FileSelectionView.swift +++ b/CHDataManagement/Views/Files/FileSelectionView.swift @@ -8,9 +8,9 @@ struct FileSelectionView: View { @Binding private var selectedFile: FileResource? - let allowedType: FileFilterType? + let allowedType: FileTypeCategory? - init(selectedFile: Binding, allowedType: FileFilterType? = nil) { + init(selectedFile: Binding, allowedType: FileTypeCategory? = nil) { self._selectedFile = selectedFile self.newSelection = selectedFile.wrappedValue self.allowedType = allowedType diff --git a/CHDataManagement/Views/Files/MultiFileSelectionView.swift b/CHDataManagement/Views/Files/MultiFileSelectionView.swift index 186c46a..36598d1 100644 --- a/CHDataManagement/Views/Files/MultiFileSelectionView.swift +++ b/CHDataManagement/Views/Files/MultiFileSelectionView.swift @@ -11,12 +11,12 @@ struct MultiFileSelectionView: View { @Binding private var selectedFiles: [FileResource] - let allowedType: FileFilterType? + let allowedType: FileTypeCategory? let insertSorted: Bool @State - private var selectedFileType: FileFilterType + private var selectedFileType: FileTypeCategory? @State private var searchString = "" @@ -24,16 +24,19 @@ struct MultiFileSelectionView: View { @State private var newSelection: [FileResource] - init(selectedFiles: Binding<[FileResource]>, allowedType: FileFilterType? = nil, insertSorted: Bool = false) { + init(selectedFiles: Binding<[FileResource]>, allowedType: FileTypeCategory? = nil, insertSorted: Bool = false) { self._selectedFiles = selectedFiles self.newSelection = selectedFiles.wrappedValue self.allowedType = allowedType - self.selectedFileType = allowedType ?? .images + self.selectedFileType = allowedType ?? .image self.insertSorted = insertSorted } private var filesBySelectedType: [FileResource] { - content.files.filter { selectedFileType.matches($0.type) } + guard let selectedFileType else { + return content.files + } + return content.files.filter { selectedFileType == $0.type.category } } private var filteredFiles: [FileResource] { @@ -75,7 +78,9 @@ struct MultiFileSelectionView: View { } VStack { Picker("", selection: $selectedFileType) { - ForEach(FileFilterType.allCases) { type in + let all: FileTypeCategory? = nil + Text("All").tag(all) + ForEach(FileTypeCategory.allCases) { type in Text(type.text).tag(type) } } diff --git a/CHDataManagement/Views/Generic/FilePropertyView.swift b/CHDataManagement/Views/Generic/FilePropertyView.swift index 9d3725a..d22cfef 100644 --- a/CHDataManagement/Views/Generic/FilePropertyView.swift +++ b/CHDataManagement/Views/Generic/FilePropertyView.swift @@ -9,9 +9,9 @@ struct FilePropertyView: View { @Binding var selectedFile: FileResource? - let allowedType: FileFilterType? + let allowedType: FileTypeCategory? - init(title: LocalizedStringKey, footer: LocalizedStringKey, selectedFile: Binding, allowedType: FileFilterType? = nil) { + init(title: LocalizedStringKey, footer: LocalizedStringKey, selectedFile: Binding, allowedType: FileTypeCategory? = nil) { self.title = title self.footer = footer self._selectedFile = selectedFile diff --git a/CHDataManagement/Views/Generic/OptionalImagePropertyView.swift b/CHDataManagement/Views/Generic/OptionalImagePropertyView.swift index 7a3bb78..dd3a423 100644 --- a/CHDataManagement/Views/Generic/OptionalImagePropertyView.swift +++ b/CHDataManagement/Views/Generic/OptionalImagePropertyView.swift @@ -30,7 +30,7 @@ struct OptionalImagePropertyView: View { } } .sheet(isPresented: $showSelectionSheet) { - FileSelectionView(selectedFile: $selectedImage, allowedType: .images) + FileSelectionView(selectedFile: $selectedImage, allowedType: .image) } } } diff --git a/CHDataManagement/Views/Posts/PostImagesView.swift b/CHDataManagement/Views/Posts/PostImagesView.swift index a45ae00..96a6ff7 100644 --- a/CHDataManagement/Views/Posts/PostImagesView.swift +++ b/CHDataManagement/Views/Posts/PostImagesView.swift @@ -51,7 +51,7 @@ struct PostImagesView: View { } } .sheet(isPresented: $showImagePicker) { - MultiFileSelectionView(selectedFiles: $post.images, allowedType: .images) + MultiFileSelectionView(selectedFiles: $post.images, allowedType: .image) } } diff --git a/CHDataManagement/Views/Settings/Content/GenerationContentView.swift b/CHDataManagement/Views/Settings/Content/GenerationContentView.swift index 463dfe0..7f5d415 100644 --- a/CHDataManagement/Views/Settings/Content/GenerationContentView.swift +++ b/CHDataManagement/Views/Settings/Content/GenerationContentView.swift @@ -39,11 +39,12 @@ struct GenerationContentView: View { if content.isGeneratingWebsite { content.endCurrentGeneration() } else { - generateFullWebsite() + content.generateWebsiteInAllLanguages() } } label: { Text(content.isGeneratingWebsite ? "Cancel" : "Generate") } + .disabled(content.isGeneratingWebsite != content.shouldGenerateWebsite) if content.isGeneratingWebsite { ProgressView() .progressViewStyle(.circular) @@ -108,39 +109,6 @@ struct GenerationContentView: View { } }.padding() } - - private func generateFullWebsite() { - DispatchQueue.main.async { - content.generateWebsiteInAllLanguages() - } - #warning("Update feed generation") - /* - guard let url = content.storage.outputPath else { - print("Invalid output path") - return - } - - guard FileManager.default.fileExists(atPath: url.path) else { - print("Missing output folder") - return - } - isGeneratingWebsite = true - DispatchQueue.global(qos: .userInitiated).async { - let generator = LocalizedWebsiteGenerator( - content: content, - language: language) - _ = generator.generateWebsite { text in - DispatchQueue.main.async { - self.generatorText = text - } - } - DispatchQueue.main.async { - isGeneratingWebsite = false - self.generatorText = "Generation complete" - } - } - */ - } } #Preview { diff --git a/CHDataManagement/Views/Settings/SettingsSection.swift b/CHDataManagement/Views/Settings/SettingsSection.swift index 22c669b..a8e314e 100644 --- a/CHDataManagement/Views/Settings/SettingsSection.swift +++ b/CHDataManagement/Views/Settings/SettingsSection.swift @@ -2,8 +2,6 @@ import SFSafeSymbols enum SettingsSection: String { - //case generation = "Generation" - case folders = "Folders" case navigationBar = "Navigation Bar" @@ -21,7 +19,7 @@ extension SettingsSection { var icon: SFSymbol { switch self { case .folders: return .folder - case .navigationBar: return .menubarRectangle + case .navigationBar: return .menubarArrowUpRectangle case .postFeed: return .rectangleGrid1x2 case .pages: return .docRichtext case .tagOverview: return .tag diff --git a/CHDataManagement/Views/Settings/TagOverviewDetailView.swift b/CHDataManagement/Views/Settings/TagOverviewDetailView.swift index 2d32b23..c753563 100644 --- a/CHDataManagement/Views/Settings/TagOverviewDetailView.swift +++ b/CHDataManagement/Views/Settings/TagOverviewDetailView.swift @@ -17,10 +17,12 @@ struct TagOverviewDetailView: View { if let page = content.tagOverview?.localized(in: language) { TagOverviewDetails(page: page) + .id(language) } else { Button("Create", action: createTagOverviewPage) } } + .padding() } } @@ -70,6 +72,5 @@ private struct TagOverviewDetails: View { text: $page.linkPreviewDescription, footer: "The description to show in previews of the page") } - .padding() } }