Improve storage, paths
This commit is contained in:
parent
b22b76fd32
commit
849585acc7
@ -44,6 +44,11 @@
|
|||||||
E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */; };
|
E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */; };
|
||||||
E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */; };
|
E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */; };
|
||||||
E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */; };
|
E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */; };
|
||||||
|
E22990422D107A95009F8D77 /* ImageJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22990412D107A94009F8D77 /* ImageJob.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 */; };
|
||||||
|
E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E229904B2D10BE59009F8D77 /* InitialSetupView.swift */; };
|
||||||
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
E24252012C50E0A40029FF16 /* HighlightedTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = E24252002C50E0A40029FF16 /* HighlightedTextEditor */; };
|
||||||
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; };
|
E242520A2C52C9260029FF16 /* ContentLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24252092C52C9260029FF16 /* ContentLanguage.swift */; };
|
||||||
E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.swift */; };
|
E2581DED2C75202400F1F079 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2581DEC2C75202400F1F079 /* Tag.swift */; };
|
||||||
@ -232,6 +237,11 @@
|
|||||||
E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextFieldPropertyView.swift; sourceTree = "<group>"; };
|
E229903B2D0F8A74009F8D77 /* OptionalTextFieldPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTextFieldPropertyView.swift; sourceTree = "<group>"; };
|
||||||
E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringPropertyView.swift; sourceTree = "<group>"; };
|
E229903D2D0F8EFD009F8D77 /* StringPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringPropertyView.swift; sourceTree = "<group>"; };
|
||||||
E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderOnDiskPropertyView.swift; sourceTree = "<group>"; };
|
E229903F2D0F95DA009F8D77 /* FolderOnDiskPropertyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderOnDiskPropertyView.swift; sourceTree = "<group>"; };
|
||||||
|
E22990412D107A94009F8D77 /* ImageJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageJob.swift; sourceTree = "<group>"; };
|
||||||
|
E22990452D10B7A6009F8D77 /* SecurityScopeStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityScopeStatus.swift; sourceTree = "<group>"; };
|
||||||
|
E22990472D10B7B7009F8D77 /* StorageAccessError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageAccessError.swift; sourceTree = "<group>"; };
|
||||||
|
E22990492D10BB90009F8D77 /* SecurityScopeBookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityScopeBookmark.swift; sourceTree = "<group>"; };
|
||||||
|
E229904B2D10BE59009F8D77 /* InitialSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialSetupView.swift; sourceTree = "<group>"; };
|
||||||
E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = "<group>"; };
|
E24252092C52C9260029FF16 /* ContentLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentLanguage.swift; sourceTree = "<group>"; };
|
||||||
E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
E2581DEC2C75202400F1F079 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||||
E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = "<group>"; };
|
E25A0B882CE4021400F33674 /* LocalizedPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPage.swift; sourceTree = "<group>"; };
|
||||||
@ -454,6 +464,7 @@
|
|||||||
E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */,
|
E29D318F2D0B34870051B7F4 /* PageContentAnomaly.swift */,
|
||||||
E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */,
|
E29D31842D0AE8EE0051B7F4 /* KnownHeaderElement.swift */,
|
||||||
E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */,
|
E25DA5222CFF6C2600AEF16D /* ImageGenerator.swift */,
|
||||||
|
E22990412D107A94009F8D77 /* ImageJob.swift */,
|
||||||
E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */,
|
E218502E2CFAF6990090B18B /* LocalizedWebsiteGenerator.swift */,
|
||||||
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
|
E25DA5792D01C63E00AEF16D /* PageContentGenerator.swift */,
|
||||||
E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */,
|
E29D316C2D07A4FF0051B7F4 /* PageGenerationResults.swift */,
|
||||||
@ -500,6 +511,7 @@
|
|||||||
E29D31372D043EB80051B7F4 /* Main */ = {
|
E29D31372D043EB80051B7F4 /* Main */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E229904B2D10BE59009F8D77 /* InitialSetupView.swift */,
|
||||||
E29D31422D0488950051B7F4 /* MainContentView.swift */,
|
E29D31422D0488950051B7F4 /* MainContentView.swift */,
|
||||||
E2DD04732C276F31003BFF1F /* MainView.swift */,
|
E2DD04732C276F31003BFF1F /* MainView.swift */,
|
||||||
E29D31442D0488CB0051B7F4 /* SelectedContentView.swift */,
|
E29D31442D0488CB0051B7F4 /* SelectedContentView.swift */,
|
||||||
@ -638,6 +650,9 @@
|
|||||||
E2A37D0F2CE5375E0000979F /* Storage */ = {
|
E2A37D0F2CE5375E0000979F /* Storage */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E22990492D10BB90009F8D77 /* SecurityScopeBookmark.swift */,
|
||||||
|
E22990472D10B7B7009F8D77 /* StorageAccessError.swift */,
|
||||||
|
E22990452D10B7A6009F8D77 /* SecurityScopeStatus.swift */,
|
||||||
E25DA5112CFF001900AEF16D /* Model */,
|
E25DA5112CFF001900AEF16D /* Model */,
|
||||||
E2A37D0D2CE527040000979F /* Storage.swift */,
|
E2A37D0D2CE527040000979F /* Storage.swift */,
|
||||||
);
|
);
|
||||||
@ -897,6 +912,7 @@
|
|||||||
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */,
|
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */,
|
||||||
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
|
E25DA5772D018B9900AEF16D /* File+Mock.swift in Sources */,
|
||||||
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
|
E25DA5892D01CBD300AEF16D /* Content+Generation.swift in Sources */,
|
||||||
|
E229904C2D10BE5D009F8D77 /* InitialSetupView.swift in Sources */,
|
||||||
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
|
E218502B2CF790B30090B18B /* PostContentView.swift in Sources */,
|
||||||
E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */,
|
E29D317D2D086AB00051B7F4 /* Int+Random.swift in Sources */,
|
||||||
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */,
|
E25DA56F2D00F9A100AEF16D /* PostFeedSettingsView.swift in Sources */,
|
||||||
@ -923,6 +939,7 @@
|
|||||||
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */,
|
E2B85F3B2C428F0E0047CD0C /* Post.swift in Sources */,
|
||||||
E29D31852D0AE8EE0051B7F4 /* KnownHeaderElement.swift in Sources */,
|
E29D31852D0AE8EE0051B7F4 /* KnownHeaderElement.swift in Sources */,
|
||||||
E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */,
|
E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */,
|
||||||
|
E22990422D107A95009F8D77 /* ImageJob.swift in Sources */,
|
||||||
E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */,
|
E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */,
|
||||||
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
E2A21C082CB17B870060935B /* TagView.swift in Sources */,
|
||||||
E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */,
|
E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */,
|
||||||
@ -933,6 +950,7 @@
|
|||||||
E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */,
|
E29D31942D0B7D280051B7F4 /* SvgImage.swift in Sources */,
|
||||||
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */,
|
E29D31632D06E95D0051B7F4 /* NavigationIcon.swift in Sources */,
|
||||||
E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */,
|
E25DA5832D01C7A400AEF16D /* VideoFileType.swift in Sources */,
|
||||||
|
E22990462D10B7A7009F8D77 /* SecurityScopeStatus.swift in Sources */,
|
||||||
E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */,
|
E29D31512D06168E0051B7F4 /* PostListView.swift in Sources */,
|
||||||
E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */,
|
E22990342D0F77E9009F8D77 /* PagePropertyView.swift in Sources */,
|
||||||
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */,
|
E2A37D0C2CE4036B0000979F /* LocalizedPage.swift in Sources */,
|
||||||
@ -1005,6 +1023,7 @@
|
|||||||
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
|
E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */,
|
||||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */,
|
||||||
E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */,
|
E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */,
|
||||||
|
E229904A2D10BB90009F8D77 /* SecurityScopeBookmark.swift in Sources */,
|
||||||
E29D314B2D04FC950051B7F4 /* FileToAdd.swift in Sources */,
|
E29D314B2D04FC950051B7F4 /* FileToAdd.swift in Sources */,
|
||||||
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */,
|
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */,
|
||||||
E29D31B52D0DA8490051B7F4 /* PageIcon.swift in Sources */,
|
E29D31B52D0DA8490051B7F4 /* PageIcon.swift in Sources */,
|
||||||
@ -1034,6 +1053,7 @@
|
|||||||
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */,
|
E25DA5292CFFBFBB00AEF16D /* ImageFileType.swift in Sources */,
|
||||||
E22990192D0E3546009F8D77 /* ItemType.swift in Sources */,
|
E22990192D0E3546009F8D77 /* ItemType.swift in Sources */,
|
||||||
E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */,
|
E25DA51F2CFF15C400AEF16D /* NavigationBar.swift in Sources */,
|
||||||
|
E22990482D10B7B7009F8D77 /* StorageAccessError.swift in Sources */,
|
||||||
E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */,
|
E29D31832D0A43DB0051B7F4 /* RelatedPageLink.swift in Sources */,
|
||||||
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
|
E2A21C512CBBD53F0060935B /* FileResource.swift in Sources */,
|
||||||
E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */,
|
E22990152D0E2B7F009F8D77 /* ItemSelectionView.swift in Sources */,
|
||||||
|
@ -3,36 +3,23 @@ import AppKit
|
|||||||
import SDWebImageAVIFCoder
|
import SDWebImageAVIFCoder
|
||||||
import SDWebImageWebPCoder
|
import SDWebImageWebPCoder
|
||||||
|
|
||||||
private struct ImageJob {
|
|
||||||
|
|
||||||
let image: String
|
|
||||||
|
|
||||||
let version: String
|
|
||||||
|
|
||||||
let maximumWidth: CGFloat
|
|
||||||
|
|
||||||
let maximumHeight: CGFloat
|
|
||||||
|
|
||||||
let quality: CGFloat
|
|
||||||
|
|
||||||
let type: ImageFileType
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ImageGenerator {
|
final class ImageGenerator {
|
||||||
|
|
||||||
private let storage: Storage
|
private let storage: Storage
|
||||||
|
|
||||||
//private let inputImageFolder: URL
|
private let settings: Settings
|
||||||
|
|
||||||
private let relativeImageOutputPath: String
|
private var relativeImageOutputPath: String {
|
||||||
|
settings.paths.imagesOutputFolderPath
|
||||||
|
}
|
||||||
|
|
||||||
private var generatedImages: [String : [String]] = [:]
|
private var generatedImages: [String : [String]] = [:]
|
||||||
|
|
||||||
private var jobs: [ImageJob] = []
|
private var jobs: [ImageGenerationJob] = []
|
||||||
|
|
||||||
init(storage: Storage, relativeImageOutputPath: String) {
|
init(storage: Storage, settings: Settings) {
|
||||||
self.storage = storage
|
self.storage = storage
|
||||||
self.relativeImageOutputPath = relativeImageOutputPath
|
self.settings = settings
|
||||||
do {
|
do {
|
||||||
self.generatedImages = try storage.loadListOfGeneratedImages()
|
self.generatedImages = try storage.loadListOfGeneratedImages()
|
||||||
} catch {
|
} catch {
|
||||||
@ -89,29 +76,28 @@ final class ImageGenerator {
|
|||||||
let width2x = maxWidth * 2
|
let width2x = maxWidth * 2
|
||||||
let height2x = maxHeight * 2
|
let height2x = maxHeight * 2
|
||||||
|
|
||||||
_ = generateVersion(for: image, type: .avif, maximumWidth: maxWidth, maximumHeight: maxHeight)
|
generateVersion(for: image, type: .avif, maximumWidth: maxWidth, maximumHeight: maxHeight)
|
||||||
_ = generateVersion(for: image, type: .avif, maximumWidth: width2x, maximumHeight: height2x)
|
generateVersion(for: image, type: .avif, maximumWidth: width2x, maximumHeight: height2x)
|
||||||
|
|
||||||
_ = generateVersion(for: image, type: .webp, maximumWidth: maxWidth, maximumHeight: maxHeight)
|
generateVersion(for: image, type: .webp, maximumWidth: maxWidth, maximumHeight: maxHeight)
|
||||||
_ = generateVersion(for: image, type: .webp, maximumWidth: width2x, maximumHeight: height2x)
|
generateVersion(for: image, type: .webp, maximumWidth: width2x, maximumHeight: height2x)
|
||||||
|
|
||||||
_ = generateVersion(for: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight)
|
generateVersion(for: image, type: type, maximumWidth: maxWidth, maximumHeight: maxHeight)
|
||||||
_ = generateVersion(for: image, type: type, maximumWidth: width2x, maximumHeight: height2x)
|
generateVersion(for: image, type: type, maximumWidth: width2x, maximumHeight: height2x)
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateVersion(for image: String, type: ImageFileType, maximumWidth: CGFloat, maximumHeight: CGFloat) -> String {
|
func generateVersion(for image: String, type: ImageFileType, maximumWidth: CGFloat, maximumHeight: CGFloat) {
|
||||||
let version = versionFileName(image: image, type: type, width: maximumWidth, height: maximumHeight)
|
let version = versionFileName(image: image, type: type, width: maximumWidth, height: maximumHeight)
|
||||||
let fullPath = "/" + relativeImageOutputPath + "/" + version
|
|
||||||
if exists(version) {
|
if exists(version) {
|
||||||
hasNowGenerated(version: version, for: image)
|
hasNowGenerated(version: version, for: image)
|
||||||
return fullPath
|
return
|
||||||
}
|
}
|
||||||
if hasPreviouslyGenerated(version: version, for: image), exists(version) {
|
if hasPreviouslyGenerated(version: version, for: image), exists(version) {
|
||||||
// Don't add job again
|
// Don't add job again
|
||||||
return fullPath
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let job = ImageJob(
|
let job = ImageGenerationJob(
|
||||||
image: image,
|
image: image,
|
||||||
version: version,
|
version: version,
|
||||||
maximumWidth: maximumWidth,
|
maximumWidth: maximumWidth,
|
||||||
@ -120,7 +106,6 @@ final class ImageGenerator {
|
|||||||
type: type)
|
type: type)
|
||||||
|
|
||||||
jobs.append(job)
|
jobs.append(job)
|
||||||
return fullPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func hasPreviouslyGenerated(version: String, for image: String) -> Bool {
|
private func hasPreviouslyGenerated(version: String, for image: String) -> Bool {
|
||||||
@ -149,7 +134,7 @@ final class ImageGenerator {
|
|||||||
|
|
||||||
// MARK: Image operations
|
// MARK: Image operations
|
||||||
|
|
||||||
private func generate(job: ImageJob) -> Bool {
|
private func generate(job: ImageGenerationJob) -> Bool {
|
||||||
if hasPreviouslyGenerated(version: job.version, for: job.image), exists(job.version),
|
if hasPreviouslyGenerated(version: job.version, for: job.image), exists(job.version),
|
||||||
exists(imageVersion: job.version) {
|
exists(imageVersion: job.version) {
|
||||||
return true
|
return true
|
||||||
@ -168,13 +153,40 @@ final class ImageGenerator {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let representation = create(image: originalImage, width: job.maximumWidth, height: job.maximumHeight)
|
||||||
|
|
||||||
|
guard let data = create(image: representation, type: job.type, quality: job.quality) else {
|
||||||
|
print("Failed to get data for type \(job.type)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if job.type == .avif {
|
||||||
|
return inOutputImagesFolder { folder in
|
||||||
|
let url = folder.appendingPathComponent(job.version)
|
||||||
|
let out = url.path()
|
||||||
|
let input = url.deletingPathExtension().appendingPathExtension(job.image.fileExtension!).path()
|
||||||
|
print("avifenc -q 70 \(input) \(out)")
|
||||||
|
hasNowGenerated(version: job.version, for: job.image)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard write(imageData: data, version: job.version) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
hasNowGenerated(version: job.version, for: job.image)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func create(image originalImage: NSImage, width: CGFloat, height: CGFloat) -> NSBitmapImageRep {
|
||||||
let sourceRep = originalImage.representations[0]
|
let sourceRep = originalImage.representations[0]
|
||||||
let sourceSize = NSSize(width: sourceRep.pixelsWide, height: sourceRep.pixelsHigh)
|
let sourceSize = NSSize(width: sourceRep.pixelsWide, height: sourceRep.pixelsHigh)
|
||||||
let maximumSize = NSSize(width: job.maximumWidth, height: job.maximumHeight)
|
let maximumSize = NSSize(width: width, height: height)
|
||||||
let destinationSize = sourceSize.scaledToFit(in: maximumSize)
|
let destinationSize = sourceSize.scaledToFit(in: maximumSize)
|
||||||
|
|
||||||
// create NSBitmapRep manually, if using cgImage, the resulting size is wrong
|
// create NSBitmapRep manually, if using cgImage, the resulting size is wrong
|
||||||
let rep = NSBitmapImageRep(bitmapDataPlanes: nil,
|
let representation = NSBitmapImageRep(
|
||||||
|
bitmapDataPlanes: nil,
|
||||||
pixelsWide: Int(destinationSize.width),
|
pixelsWide: Int(destinationSize.width),
|
||||||
pixelsHigh: Int(destinationSize.height),
|
pixelsHigh: Int(destinationSize.height),
|
||||||
bitsPerSample: 8,
|
bitsPerSample: 8,
|
||||||
@ -185,41 +197,26 @@ final class ImageGenerator {
|
|||||||
bytesPerRow: Int(destinationSize.width) * 4,
|
bytesPerRow: Int(destinationSize.width) * 4,
|
||||||
bitsPerPixel: 32)!
|
bitsPerPixel: 32)!
|
||||||
|
|
||||||
let ctx = NSGraphicsContext(bitmapImageRep: rep)
|
let ctx = NSGraphicsContext(bitmapImageRep: representation)
|
||||||
NSGraphicsContext.saveGraphicsState()
|
NSGraphicsContext.saveGraphicsState()
|
||||||
NSGraphicsContext.current = ctx
|
NSGraphicsContext.current = ctx
|
||||||
originalImage.draw(in: NSMakeRect(0, 0, destinationSize.width, destinationSize.height))
|
originalImage.draw(in: NSMakeRect(0, 0, destinationSize.width, destinationSize.height))
|
||||||
ctx?.flushGraphics()
|
ctx?.flushGraphics()
|
||||||
NSGraphicsContext.restoreGraphicsState()
|
NSGraphicsContext.restoreGraphicsState()
|
||||||
|
return representation
|
||||||
guard let data = create(image: rep, type: job.type, quality: job.quality) else {
|
|
||||||
print("Failed to get data for type \(job.type)")
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func write(imageData data: Data, version: String) -> Bool {
|
||||||
|
inOutputImagesFolder { folder in
|
||||||
let result = inOutputImagesFolder { folder in
|
let url = folder.appendingPathComponent(version)
|
||||||
let url = folder.appendingPathComponent(job.version)
|
|
||||||
if job.type == .avif {
|
|
||||||
let out = url.path()
|
|
||||||
let input = url.deletingPathExtension().appendingPathExtension(job.image.fileExtension!).path()
|
|
||||||
print("avifenc -q 70 \(input) \(out)")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
do {
|
do {
|
||||||
try data.write(to: url)
|
try data.write(to: url)
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to write image \(job.version): \(error)")
|
print("Failed to write image \(version): \(error)")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
guard result else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
hasNowGenerated(version: job.version, for: job.image)
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func exists(_ relativePath: String) -> Bool {
|
private func exists(_ relativePath: String) -> Bool {
|
||||||
@ -258,9 +255,9 @@ final class ImageGenerator {
|
|||||||
|
|
||||||
private func createAvif(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
|
private func createAvif(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
|
||||||
return Data()
|
return Data()
|
||||||
let newImage = NSImage(size: image.size)
|
// let newImage = NSImage(size: image.size)
|
||||||
newImage.addRepresentation(image)
|
// newImage.addRepresentation(image)
|
||||||
return SDImageAVIFCoder.shared.encodedData(with: newImage, format: .AVIF, options: [.encodeCompressionQuality: quality])
|
// return SDImageAVIFCoder.shared.encodedData(with: newImage, format: .AVIF, options: [.encodeCompressionQuality: quality])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createWebp(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
|
private func createWebp(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
|
||||||
|
16
CHDataManagement/Generator/ImageJob.swift
Normal file
16
CHDataManagement/Generator/ImageJob.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ImageGenerationJob {
|
||||||
|
|
||||||
|
let image: String
|
||||||
|
|
||||||
|
let version: String
|
||||||
|
|
||||||
|
let maximumWidth: CGFloat
|
||||||
|
|
||||||
|
let maximumHeight: CGFloat
|
||||||
|
|
||||||
|
let quality: CGFloat
|
||||||
|
|
||||||
|
let type: ImageFileType
|
||||||
|
}
|
@ -16,7 +16,7 @@ final class LocalizedWebsiteGenerator {
|
|||||||
self.localizedPostSettings = content.settings.localized(in: language)
|
self.localizedPostSettings = content.settings.localized(in: language)
|
||||||
self.imageGenerator = ImageGenerator(
|
self.imageGenerator = ImageGenerator(
|
||||||
storage: content.storage,
|
storage: content.storage,
|
||||||
relativeImageOutputPath: content.settings.paths.imagesOutputFolderPath)
|
settings: content.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var outputDirectory: URL {
|
private var outputDirectory: URL {
|
||||||
@ -85,56 +85,6 @@ final class LocalizedWebsiteGenerator {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generatePagesFolderIfNeeded() -> Bool {
|
|
||||||
let relativePath = content.settings.paths.pagesOutputFolderPath
|
|
||||||
|
|
||||||
return content.storage.write(in: .outputPath) { folder in
|
|
||||||
let outputFile = folder.appendingPathComponent(relativePath, isDirectory: true)
|
|
||||||
do {
|
|
||||||
try outputFile.ensureFolderExistence()
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generate(page: Page) -> Bool {
|
|
||||||
guard generatePagesFolderIfNeeded() else {
|
|
||||||
print("Failed to generate output folder")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let pageGenerator = PageGenerator(
|
|
||||||
content: content,
|
|
||||||
imageGenerator: imageGenerator)
|
|
||||||
|
|
||||||
let content: String
|
|
||||||
let results: PageGenerationResults
|
|
||||||
do {
|
|
||||||
(content, results) = try pageGenerator.generate(page: page, language: language)
|
|
||||||
} catch {
|
|
||||||
print("Failed to generate page \(page.id) in language \(language): \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard !content.trimmed.isEmpty else {
|
|
||||||
#warning("Generate page with placeholder content")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = page.absoluteUrl(in: language) + ".html"
|
|
||||||
guard save(content, to: path) else {
|
|
||||||
print("Failed to save page")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard imageGenerator.runJobs(callback: { _ in }) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard copy(requiredFiles: results.files) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func copy(requiredFiles: Set<FileResource>) -> Bool {
|
private func copy(requiredFiles: Set<FileResource>) -> Bool {
|
||||||
//print("Copying \(requiredVideoFiles.count) files...")
|
//print("Copying \(requiredVideoFiles.count) files...")
|
||||||
for file in requiredFiles {
|
for file in requiredFiles {
|
||||||
|
8
CHDataManagement/Main/InitialSetupView.swift
Normal file
8
CHDataManagement/Main/InitialSetupView.swift
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct InitialSetupView: View {
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
/*@START_MENU_TOKEN@*//*@PLACEHOLDER=Hello, world!@*/Text("Hello, world!")/*@END_MENU_TOKEN@*/
|
||||||
|
}
|
||||||
|
}
|
@ -55,6 +55,9 @@ struct MainView: App {
|
|||||||
@State
|
@State
|
||||||
private var showAddSheet = false
|
private var showAddSheet = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var showInitialSetupSheet = false
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var sidebar: some View {
|
var sidebar: some View {
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
@ -159,9 +162,19 @@ struct MainView: App {
|
|||||||
}.pickerStyle(.segmented)
|
}.pickerStyle(.segmented)
|
||||||
}
|
}
|
||||||
ToolbarItem(placement: .primaryAction) {
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
if content.storageIsInitialized {
|
||||||
Button(action: save) {
|
Button(action: save) {
|
||||||
Text("Save")
|
Text("Save")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Button {
|
||||||
|
selectedSection = .folders
|
||||||
|
selectedTab = .generation
|
||||||
|
} label: {
|
||||||
|
Text("Setup")
|
||||||
|
}
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("")
|
.navigationTitle("")
|
||||||
@ -176,11 +189,15 @@ struct MainView: App {
|
|||||||
.environment(\.language, language)
|
.environment(\.language, language)
|
||||||
.environmentObject(content)
|
.environmentObject(content)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showInitialSetupSheet) {
|
||||||
|
InitialSetupView()
|
||||||
|
.environment(\.language, language)
|
||||||
|
.environmentObject(content)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func save() {
|
private func save() {
|
||||||
// Save all changed files
|
|
||||||
do {
|
do {
|
||||||
try content.saveToDisk()
|
try content.saveToDisk()
|
||||||
} catch {
|
} catch {
|
||||||
@ -189,6 +206,12 @@ struct MainView: App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func loadContent() {
|
private func loadContent() {
|
||||||
|
guard content.storageIsInitialized else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.showInitialSetupSheet = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
do {
|
do {
|
||||||
try content.loadFromDisk()
|
try content.loadFromDisk()
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -20,41 +20,12 @@ extension Content {
|
|||||||
return generateInternal(page, in: language)
|
return generateInternal(page, in: language)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func startGenerating() -> Bool {
|
// MARK: Paths to items
|
||||||
guard !isGeneratingWebsite else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// TODO: Fix bug where multiple generating operations can be started
|
|
||||||
// due to dispatch of locking property on main queue
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isGeneratingWebsite = true
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func endGenerating() {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.isGeneratingWebsite = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func generateInternal(_ page: Page, in language: ContentLanguage) -> Bool {
|
|
||||||
let generator = LocalizedWebsiteGenerator(
|
|
||||||
content: self,
|
|
||||||
language: language)
|
|
||||||
if !generator.generate(page: page) {
|
|
||||||
print("Generation failed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeCleanAbsolutePath(_ path: String) -> String {
|
private func makeCleanAbsolutePath(_ path: String) -> String {
|
||||||
("/" + path).replacingOccurrences(of: "//", with: "/")
|
("/" + path).replacingOccurrences(of: "//", with: "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Paths to items
|
|
||||||
|
|
||||||
func absoluteUrlPrefixForTag(_ tag: Tag, language: ContentLanguage) -> String {
|
func absoluteUrlPrefixForTag(_ tag: Tag, language: ContentLanguage) -> String {
|
||||||
makeCleanAbsolutePath(settings.paths.tagsOutputFolderPath + "/" + tag.localized(in: language).urlComponent)
|
makeCleanAbsolutePath(settings.paths.tagsOutputFolderPath + "/" + tag.localized(in: language).urlComponent)
|
||||||
}
|
}
|
||||||
@ -101,4 +72,86 @@ extension Content {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isGeneratingWebsite = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endGenerating() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.isGeneratingWebsite = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateInternal(_ page: Page, in language: ContentLanguage) -> Bool {
|
||||||
|
let pagesFolder = settings.paths.pagesOutputFolderPath
|
||||||
|
guard storage.create(folder: pagesFolder, in: .outputPath) else {
|
||||||
|
print("Failed to generate output folder")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let imageGenerator = ImageGenerator(
|
||||||
|
storage: storage,
|
||||||
|
settings: settings)
|
||||||
|
|
||||||
|
let pageGenerator = PageGenerator(
|
||||||
|
content: self,
|
||||||
|
imageGenerator: imageGenerator)
|
||||||
|
|
||||||
|
let content: String
|
||||||
|
let results: PageGenerationResults
|
||||||
|
do {
|
||||||
|
(content, results) = try pageGenerator.generate(page: page, language: language)
|
||||||
|
} catch {
|
||||||
|
print("Failed to generate page \(page.id) in language \(language): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard !content.trimmed.isEmpty else {
|
||||||
|
#warning("Generate page with placeholder content")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = page.absoluteUrl(in: language) + ".html"
|
||||||
|
do {
|
||||||
|
try storage.write(content: content, to: path)
|
||||||
|
} catch {
|
||||||
|
print("Failed to save page \(page.id): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard imageGenerator.runJobs(callback: { _ in }) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard copy(requiredFiles: results.files) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func copy(requiredFiles: Set<FileResource>) -> Bool {
|
||||||
|
//print("Copying \(requiredVideoFiles.count) files...")
|
||||||
|
for file in requiredFiles {
|
||||||
|
guard !file.isExternallyStored else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try storage.copy(file: file.id, to: file.absoluteUrl)
|
||||||
|
} catch {
|
||||||
|
print("Failed to copy file \(file.id): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,10 @@ extension Content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadFromDisk() throws {
|
func loadFromDisk() throws {
|
||||||
let storage = Storage(baseFolder: URL(filePath: contentPath))
|
guard storageIsInitialized else {
|
||||||
|
print("Storage not initialized, not loading content")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let settings = try storage.loadSettings()
|
let settings = try storage.loadSettings()
|
||||||
let imageDescriptions = try storage.loadFileDescriptions().reduce(into: [:]) { descriptions, description in
|
let imageDescriptions = try storage.loadFileDescriptions().reduce(into: [:]) { descriptions, description in
|
||||||
|
@ -3,6 +3,10 @@ import Foundation
|
|||||||
extension Content {
|
extension Content {
|
||||||
|
|
||||||
func saveToDisk() throws {
|
func saveToDisk() throws {
|
||||||
|
guard storageIsInitialized else {
|
||||||
|
print("Storage not initialized, not saving content")
|
||||||
|
return
|
||||||
|
}
|
||||||
//print("Starting save")
|
//print("Starting save")
|
||||||
for page in pages {
|
for page in pages {
|
||||||
try storage.save(pageMetadata: page.pageFile, for: page.id)
|
try storage.save(pageMetadata: page.pageFile, for: page.id)
|
||||||
|
@ -4,6 +4,11 @@ import Combine
|
|||||||
|
|
||||||
final class Content: ObservableObject {
|
final class Content: ObservableObject {
|
||||||
|
|
||||||
|
let storage = Storage()
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var storageIsInitialized = false
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var settings: Settings
|
var settings: Settings
|
||||||
|
|
||||||
@ -28,48 +33,23 @@ final class Content: ObservableObject {
|
|||||||
@Published
|
@Published
|
||||||
var isGeneratingWebsite = false
|
var isGeneratingWebsite = false
|
||||||
|
|
||||||
@AppStorage("contentPath")
|
|
||||||
private var storedContentPath: String = ""
|
|
||||||
|
|
||||||
@Published
|
|
||||||
var contentPath: String = "" {
|
|
||||||
didSet {
|
|
||||||
storedContentPath = contentPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let storage: Storage
|
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
init(settings: Settings,
|
init(settings: Settings,
|
||||||
posts: [Post],
|
posts: [Post],
|
||||||
pages: [Page],
|
pages: [Page],
|
||||||
tags: [Tag],
|
tags: [Tag],
|
||||||
files: [FileResource],
|
files: [FileResource],
|
||||||
tagOverview: TagOverviewPage?,
|
tagOverview: TagOverviewPage?) {
|
||||||
storedContentPath: String) {
|
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.posts = posts
|
self.posts = posts
|
||||||
self.pages = pages
|
self.pages = pages
|
||||||
self.tags = tags
|
self.tags = tags
|
||||||
self.files = files
|
self.files = files
|
||||||
self.tagOverview = tagOverview
|
self.tagOverview = tagOverview
|
||||||
self.storedContentPath = storedContentPath
|
|
||||||
self.contentPath = storedContentPath
|
initialize()
|
||||||
self.storage = Storage(baseFolder: URL(filePath: storedContentPath))
|
|
||||||
do {
|
|
||||||
try storage.createFolderStructure()
|
|
||||||
} catch {
|
|
||||||
print(error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
observeContentPath()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.storage = Storage(baseFolder: URL(filePath: ""))
|
|
||||||
|
|
||||||
self.settings = .mock
|
self.settings = .mock
|
||||||
self.posts = []
|
self.posts = []
|
||||||
self.pages = []
|
self.pages = []
|
||||||
@ -77,30 +57,25 @@ final class Content: ObservableObject {
|
|||||||
self.files = []
|
self.files = []
|
||||||
self.tagOverview = nil
|
self.tagOverview = nil
|
||||||
|
|
||||||
contentPath = storedContentPath
|
initialize()
|
||||||
do {
|
}
|
||||||
try storage.createFolderStructure()
|
|
||||||
} catch {
|
private func initialize() {
|
||||||
print(error)
|
guard storage.check(contentPath: settings.paths.contentDirectoryPath) == .nominal else {
|
||||||
|
storageIsInitialized = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try? storage.update(baseFolder: URL(filePath: contentPath))
|
storage.check(outputPath: settings.paths.outputDirectoryPath)
|
||||||
observeContentPath()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func observeContentPath() {
|
|
||||||
$contentPath.sink { newValue in
|
|
||||||
let url = URL(filePath: newValue)
|
|
||||||
do {
|
do {
|
||||||
try self.storage.update(baseFolder: url)
|
try storage.createFolderStructure()
|
||||||
try self.loadFromDisk()
|
storageIsInitialized = true
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to switch content path: \(error)")
|
print("Failed to initialize storage: \(error)")
|
||||||
|
storageIsInitialized = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
var images: [FileResource] {
|
var images: [FileResource] {
|
||||||
files.filter { $0.type.isImage }
|
files.filter { $0.type.isImage }
|
||||||
|
@ -2,6 +2,9 @@ import Foundation
|
|||||||
|
|
||||||
final class PathSettings: ObservableObject {
|
final class PathSettings: ObservableObject {
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var contentDirectoryPath: String
|
||||||
|
|
||||||
@Published
|
@Published
|
||||||
var outputDirectoryPath: String
|
var outputDirectoryPath: String
|
||||||
|
|
||||||
@ -24,6 +27,7 @@ final class PathSettings: ObservableObject {
|
|||||||
var tagsOutputFolderPath: String
|
var tagsOutputFolderPath: String
|
||||||
|
|
||||||
init(file: PathSettingsFile) {
|
init(file: PathSettingsFile) {
|
||||||
|
self.contentDirectoryPath = file.contentDirectoryPath
|
||||||
self.assetsOutputFolderPath = file.assetsOutputFolderPath
|
self.assetsOutputFolderPath = file.assetsOutputFolderPath
|
||||||
self.outputDirectoryPath = file.outputDirectoryPath
|
self.outputDirectoryPath = file.outputDirectoryPath
|
||||||
self.pagesOutputFolderPath = file.pagesOutputFolderPath
|
self.pagesOutputFolderPath = file.pagesOutputFolderPath
|
||||||
@ -34,7 +38,8 @@ final class PathSettings: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var file: PathSettingsFile {
|
var file: PathSettingsFile {
|
||||||
.init(outputDirectoryPath: outputDirectoryPath,
|
.init(contentDirectoryPath: contentDirectoryPath,
|
||||||
|
outputDirectoryPath: outputDirectoryPath,
|
||||||
assetsOutputFolderPath: assetsOutputFolderPath,
|
assetsOutputFolderPath: assetsOutputFolderPath,
|
||||||
pagesOutputFolderPath: pagesOutputFolderPath,
|
pagesOutputFolderPath: pagesOutputFolderPath,
|
||||||
imagesOutputFolderPath: imagesOutputFolderPath,
|
imagesOutputFolderPath: imagesOutputFolderPath,
|
||||||
|
@ -20,6 +20,5 @@ extension Content {
|
|||||||
pages: [.empty],
|
pages: [.empty],
|
||||||
tags: [.hiking, .mountains, .nature, .sports],
|
tags: [.hiking, .mountains, .nature, .sports],
|
||||||
files: [],
|
files: [],
|
||||||
tagOverview: nil,
|
tagOverview: nil)
|
||||||
storedContentPath: dbPath)
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
struct PathSettingsFile {
|
struct PathSettingsFile {
|
||||||
|
|
||||||
|
let contentDirectoryPath: String
|
||||||
|
|
||||||
let outputDirectoryPath: String
|
let outputDirectoryPath: String
|
||||||
|
|
||||||
let assetsOutputFolderPath: String
|
let assetsOutputFolderPath: String
|
||||||
@ -15,13 +17,15 @@ struct PathSettingsFile {
|
|||||||
|
|
||||||
let tagsOutputFolderPath: String
|
let tagsOutputFolderPath: String
|
||||||
|
|
||||||
init(outputDirectoryPath: String,
|
init(contentDirectoryPath: String,
|
||||||
|
outputDirectoryPath: String,
|
||||||
assetsOutputFolderPath: String,
|
assetsOutputFolderPath: String,
|
||||||
pagesOutputFolderPath: String,
|
pagesOutputFolderPath: String,
|
||||||
imagesOutputFolderPath: String,
|
imagesOutputFolderPath: String,
|
||||||
filesOutputFolderPath: String,
|
filesOutputFolderPath: String,
|
||||||
videosOutputFolderPath: String,
|
videosOutputFolderPath: String,
|
||||||
tagsOutputFolderPath: String) {
|
tagsOutputFolderPath: String) {
|
||||||
|
self.contentDirectoryPath = contentDirectoryPath
|
||||||
self.outputDirectoryPath = outputDirectoryPath
|
self.outputDirectoryPath = outputDirectoryPath
|
||||||
self.assetsOutputFolderPath = assetsOutputFolderPath
|
self.assetsOutputFolderPath = assetsOutputFolderPath
|
||||||
self.pagesOutputFolderPath = pagesOutputFolderPath
|
self.pagesOutputFolderPath = pagesOutputFolderPath
|
||||||
@ -40,6 +44,7 @@ extension PathSettingsFile {
|
|||||||
|
|
||||||
static var `default`: PathSettingsFile {
|
static var `default`: PathSettingsFile {
|
||||||
PathSettingsFile(
|
PathSettingsFile(
|
||||||
|
contentDirectoryPath: "",
|
||||||
outputDirectoryPath: "build",
|
outputDirectoryPath: "build",
|
||||||
assetsOutputFolderPath: "asset",
|
assetsOutputFolderPath: "asset",
|
||||||
pagesOutputFolderPath: "page",
|
pagesOutputFolderPath: "page",
|
||||||
|
7
CHDataManagement/Storage/SecurityScopeBookmark.swift
Normal file
7
CHDataManagement/Storage/SecurityScopeBookmark.swift
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
enum SecurityScopeBookmark: String {
|
||||||
|
|
||||||
|
case outputPath = "outputPathBookmark"
|
||||||
|
|
||||||
|
case contentPath = "contentPathBookmark"
|
||||||
|
}
|
15
CHDataManagement/Storage/SecurityScopeStatus.swift
Normal file
15
CHDataManagement/Storage/SecurityScopeStatus.swift
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
enum SecurityScopeStatus {
|
||||||
|
|
||||||
|
case noPath
|
||||||
|
|
||||||
|
case urlMismatch
|
||||||
|
|
||||||
|
case noBookmark
|
||||||
|
|
||||||
|
case bookmarkCorrupted
|
||||||
|
|
||||||
|
case stale
|
||||||
|
|
||||||
|
case nominal
|
||||||
|
}
|
@ -1,44 +1,5 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum SecurityScopeBookmark: String {
|
|
||||||
|
|
||||||
case outputPath = "outputPathBookmark"
|
|
||||||
|
|
||||||
case contentPath = "contentPathBookmark"
|
|
||||||
}
|
|
||||||
|
|
||||||
enum StorageAccessError: Error {
|
|
||||||
|
|
||||||
case noBookmarkData
|
|
||||||
|
|
||||||
case bookmarkDataCorrupted(Error)
|
|
||||||
|
|
||||||
case folderAccessFailed(URL)
|
|
||||||
|
|
||||||
case stringConversionFailed
|
|
||||||
|
|
||||||
case fileNotFound(String)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension StorageAccessError: CustomStringConvertible {
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
case .noBookmarkData:
|
|
||||||
return "No bookmark data to access resources in folder"
|
|
||||||
case .bookmarkDataCorrupted(let error):
|
|
||||||
return "Failed to resolve bookmark: \(error)"
|
|
||||||
case .folderAccessFailed(let url):
|
|
||||||
return "Failed to access folder: \(url.path())"
|
|
||||||
case .stringConversionFailed:
|
|
||||||
return "Failed to convert string to data"
|
|
||||||
case .fileNotFound(let path):
|
|
||||||
return "File not found: \(path)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
A class that handles the storage of the website data.
|
A class that handles the storage of the website data.
|
||||||
|
|
||||||
@ -48,11 +9,10 @@ extension StorageAccessError: CustomStringConvertible {
|
|||||||
- files: Contains additional files
|
- files: Contains additional files
|
||||||
- videos: Contains raw video files
|
- videos: Contains raw video files
|
||||||
- posts: Contains the markdown files for localized posts, file name is the post id
|
- posts: Contains the markdown files for localized posts, file name is the post id
|
||||||
-
|
|
||||||
*/
|
|
||||||
final class Storage {
|
|
||||||
|
|
||||||
private(set) var baseFolder: URL
|
- Note: The base folder and output folder are stored as security-scoped bookmarks in user defaults.
|
||||||
|
*/
|
||||||
|
final class Storage: ObservableObject {
|
||||||
|
|
||||||
private let encoder = JSONEncoder()
|
private let encoder = JSONEncoder()
|
||||||
|
|
||||||
@ -60,20 +20,21 @@ final class Storage {
|
|||||||
|
|
||||||
private let fm = FileManager.default
|
private let fm = FileManager.default
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var contentFolderStatus: SecurityScopeStatus = .noBookmark
|
||||||
|
|
||||||
|
@Published
|
||||||
|
var outputFolderStatus: SecurityScopeStatus = .noBookmark
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Create the storage.
|
Create the storage.
|
||||||
*/
|
*/
|
||||||
init(baseFolder: URL) {
|
init() {
|
||||||
self.baseFolder = baseFolder
|
|
||||||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Helper
|
// MARK: Helper
|
||||||
|
|
||||||
private func subFolder(_ name: String) -> URL {
|
|
||||||
baseFolder.appending(path: name, directoryHint: .isDirectory)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func files(in folder: URL) throws -> [URL] {
|
private func files(in folder: URL) throws -> [URL] {
|
||||||
do {
|
do {
|
||||||
return try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
|
return try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.isDirectoryKey])
|
||||||
@ -96,8 +57,7 @@ final class Storage {
|
|||||||
|
|
||||||
// MARK: Folders
|
// MARK: Folders
|
||||||
|
|
||||||
func update(baseFolder: URL) throws {
|
func updateBaseFolder() throws {
|
||||||
self.baseFolder = baseFolder
|
|
||||||
try createFolderStructure()
|
try createFolderStructure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,6 +407,19 @@ final class Storage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func create(folder relativePath: String, in scopr: SecurityScopeBookmark) -> Bool {
|
||||||
|
return write(in: .outputPath) { folder in
|
||||||
|
let url = folder.appendingPathComponent(relativePath, isDirectory: true)
|
||||||
|
do {
|
||||||
|
try url.ensureFolderExistence()
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
print("Failed to create folder \(url.path()): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func write(in scope: SecurityScopeBookmark, operation: (URL) -> Bool) -> Bool {
|
func write(in scope: SecurityScopeBookmark, operation: (URL) -> Bool) -> Bool {
|
||||||
do {
|
do {
|
||||||
return try operate(in: scope, operation: operation)
|
return try operate(in: scope, operation: operation)
|
||||||
@ -486,6 +459,7 @@ final class Storage {
|
|||||||
|
|
||||||
if isStale {
|
if isStale {
|
||||||
print("Bookmark is stale, consider saving a new bookmark.")
|
print("Bookmark is stale, consider saving a new bookmark.")
|
||||||
|
#warning("Show warning about stale bookmark")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start accessing the security-scoped resource
|
// Start accessing the security-scoped resource
|
||||||
@ -496,6 +470,45 @@ final class Storage {
|
|||||||
return try operation(folderUrl)
|
return try operation(folderUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func check(contentPath: String) -> SecurityScopeStatus {
|
||||||
|
contentFolderStatus = Storage.ensure(securityScope: .contentPath, matches: contentPath)
|
||||||
|
return contentFolderStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func check(outputPath: String) -> SecurityScopeStatus {
|
||||||
|
outputFolderStatus = Storage.ensure(securityScope: .outputPath, matches: outputPath)
|
||||||
|
return outputFolderStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func ensure(securityScope: SecurityScopeBookmark, matches path: String) -> SecurityScopeStatus {
|
||||||
|
guard path != "" else {
|
||||||
|
return .noPath
|
||||||
|
}
|
||||||
|
guard let bookmarkData = UserDefaults.standard.data(forKey: securityScope.rawValue) else {
|
||||||
|
return .noBookmark
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
var isStale = false
|
||||||
|
let url = try URL(
|
||||||
|
resolvingBookmarkData: bookmarkData,
|
||||||
|
options: .withSecurityScope,
|
||||||
|
relativeTo: nil,
|
||||||
|
bookmarkDataIsStale: &isStale)
|
||||||
|
guard !isStale else {
|
||||||
|
return .stale
|
||||||
|
}
|
||||||
|
guard url.path() == path else {
|
||||||
|
return .urlMismatch
|
||||||
|
}
|
||||||
|
return .nominal
|
||||||
|
} catch {
|
||||||
|
return .bookmarkCorrupted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Writing files
|
// MARK: Writing files
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
33
CHDataManagement/Storage/StorageAccessError.swift
Normal file
33
CHDataManagement/Storage/StorageAccessError.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum StorageAccessError: Error {
|
||||||
|
|
||||||
|
case noBookmarkData
|
||||||
|
|
||||||
|
case bookmarkDataCorrupted(Error)
|
||||||
|
|
||||||
|
case folderAccessFailed(URL)
|
||||||
|
|
||||||
|
case stringConversionFailed
|
||||||
|
|
||||||
|
case fileNotFound(String)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StorageAccessError: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .noBookmarkData:
|
||||||
|
return "No bookmark data to access resources in folder"
|
||||||
|
case .bookmarkDataCorrupted(let error):
|
||||||
|
return "Failed to resolve bookmark: \(error)"
|
||||||
|
case .folderAccessFailed(let url):
|
||||||
|
return "Failed to access folder: \(url.path())"
|
||||||
|
case .stringConversionFailed:
|
||||||
|
return "Failed to convert string to data"
|
||||||
|
case .fileNotFound(let path):
|
||||||
|
return "File not found: \(path)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,16 +30,18 @@ struct PageDetailView: View {
|
|||||||
Text("Generate")
|
Text("Generate")
|
||||||
}
|
}
|
||||||
.disabled(content.isGeneratingWebsite)
|
.disabled(content.isGeneratingWebsite)
|
||||||
if let didGenerateWebsite {
|
switch didGenerateWebsite {
|
||||||
if didGenerateWebsite {
|
case .none:
|
||||||
|
Image(systemSymbol: .questionmarkCircleFill)
|
||||||
|
.foregroundStyle(.gray)
|
||||||
|
case .some(true):
|
||||||
Image(systemSymbol: .checkmarkCircleFill)
|
Image(systemSymbol: .checkmarkCircleFill)
|
||||||
.foregroundStyle(.green)
|
.foregroundStyle(.green)
|
||||||
} else {
|
case .some(false):
|
||||||
Image(systemSymbol: .xmarkCircleFill)
|
Image(systemSymbol: .xmarkCircleFill)
|
||||||
.foregroundStyle(.red)
|
.foregroundStyle(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
IdPropertyView(
|
IdPropertyView(
|
||||||
id: $page.id,
|
id: $page.id,
|
||||||
footer: "The page id is used to link to it internally.",
|
footer: "The page id is used to link to it internally.",
|
||||||
|
@ -5,9 +5,6 @@ struct PathSettingsView: View {
|
|||||||
@Environment(\.language)
|
@Environment(\.language)
|
||||||
private var language
|
private var language
|
||||||
|
|
||||||
@AppStorage("contentPath")
|
|
||||||
private var contentPath: String = ""
|
|
||||||
|
|
||||||
@EnvironmentObject
|
@EnvironmentObject
|
||||||
private var content: Content
|
private var content: Content
|
||||||
|
|
||||||
@ -20,12 +17,12 @@ struct PathSettingsView: View {
|
|||||||
|
|
||||||
FolderOnDiskPropertyView(
|
FolderOnDiskPropertyView(
|
||||||
title: "Content Folder",
|
title: "Content Folder",
|
||||||
folder: $contentPath,
|
folder: $content.settings.paths.contentDirectoryPath,
|
||||||
footer: "The folder where the raw content of the website is stored") { url in
|
footer: "The folder where the raw content of the website is stored") { url in
|
||||||
guard content.storage.save(folderUrl: url, in: .contentPath) else {
|
guard content.storage.save(folderUrl: url, in: .contentPath) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentPath = url.path()
|
content.settings.paths.contentDirectoryPath = url.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
FolderOnDiskPropertyView(
|
FolderOnDiskPropertyView(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user