Add more file properties, organize storage, add video block
This commit is contained in:
parent
96c0a75c2f
commit
6cf310d849
@ -219,14 +219,14 @@
|
||||
E2FE0F2A2D2AFBE6002963B7 /* ImageCompareIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F292D2AFBE3002963B7 /* ImageCompareIcons.swift */; };
|
||||
E2FE0F2C2D2B119A002963B7 /* ImageCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F2B2D2B1196002963B7 /* ImageCommand.swift */; };
|
||||
E2FE0F312D2B1952002963B7 /* PartialSvgImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F302D2B1952002963B7 /* PartialSvgImage.swift */; };
|
||||
E2FE0F332D2B2665002963B7 /* AudioBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F322D2B265F002963B7 /* AudioBlockProcessor.swift */; };
|
||||
E2FE0F332D2B2665002963B7 /* AudioBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F322D2B265F002963B7 /* AudioBlock.swift */; };
|
||||
E2FE0F362D2B27F9002963B7 /* BlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F352D2B27F6002963B7 /* BlockProcessor.swift */; };
|
||||
E2FE0F382D2B32F4002963B7 /* SingleFilePlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F372D2B32ED002963B7 /* SingleFilePlayer.swift */; };
|
||||
E2FE0F3A2D2B3E4F002963B7 /* AudioPlayerSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F392D2B3E4E002963B7 /* AudioPlayerSettings.swift */; };
|
||||
E2FE0F3C2D2B3F45002963B7 /* AudioPlayerSettingsFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F3B2D2B3F42002963B7 /* AudioPlayerSettingsFile.swift */; };
|
||||
E2FE0F3E2D2B4225002963B7 /* AudioSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F3D2D2B4225002963B7 /* AudioSettingsDetailView.swift */; };
|
||||
E2FE0F402D2B45D3002963B7 /* SwiftProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F3F2D2B45CD002963B7 /* SwiftProcessor.swift */; };
|
||||
E2FE0F422D2B4821002963B7 /* OtherCodeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F412D2B480B002963B7 /* OtherCodeProcessor.swift */; };
|
||||
E2FE0F402D2B45D3002963B7 /* SwiftBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F3F2D2B45CD002963B7 /* SwiftBlock.swift */; };
|
||||
E2FE0F422D2B4821002963B7 /* OtherCodeBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F412D2B480B002963B7 /* OtherCodeBlock.swift */; };
|
||||
E2FE0F462D2BC777002963B7 /* MarkdownImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F452D2BC772002963B7 /* MarkdownImageProcessor.swift */; };
|
||||
E2FE0F482D2BC7D1002963B7 /* MarkdownProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F472D2BC7CD002963B7 /* MarkdownProcessor.swift */; };
|
||||
E2FE0F4B2D2BCCAA002963B7 /* MarkdownHeadlineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F4A2D2BCCA5002963B7 /* MarkdownHeadlineProcessor.swift */; };
|
||||
@ -238,6 +238,9 @@
|
||||
E2FE0F572D2BCFD4002963B7 /* BlockLineProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F562D2BCFD4002963B7 /* BlockLineProcessor.swift */; };
|
||||
E2FE0F592D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F582D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift */; };
|
||||
E2FE0F5B2D2BCFF2002963B7 /* KeyedBlockProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F5A2D2BCFF2002963B7 /* KeyedBlockProcessor.swift */; };
|
||||
E2FE0F5E2D2BE190002963B7 /* FileResourceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F5D2D2BE18B002963B7 /* FileResourceFile.swift */; };
|
||||
E2FE0F602D2C0422002963B7 /* VideoBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F5F2D2C041E002963B7 /* VideoBlock.swift */; };
|
||||
E2FE0F622D2C0D8D002963B7 /* VersionedVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F612D2C0D8D002963B7 /* VersionedVideo.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -448,14 +451,14 @@
|
||||
E2FE0F292D2AFBE3002963B7 /* ImageCompareIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompareIcons.swift; sourceTree = "<group>"; };
|
||||
E2FE0F2B2D2B1196002963B7 /* ImageCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCommand.swift; sourceTree = "<group>"; };
|
||||
E2FE0F302D2B1952002963B7 /* PartialSvgImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartialSvgImage.swift; sourceTree = "<group>"; };
|
||||
E2FE0F322D2B265F002963B7 /* AudioBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioBlockProcessor.swift; sourceTree = "<group>"; };
|
||||
E2FE0F322D2B265F002963B7 /* AudioBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioBlock.swift; sourceTree = "<group>"; };
|
||||
E2FE0F352D2B27F6002963B7 /* BlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockProcessor.swift; sourceTree = "<group>"; };
|
||||
E2FE0F372D2B32ED002963B7 /* SingleFilePlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFilePlayer.swift; sourceTree = "<group>"; };
|
||||
E2FE0F392D2B3E4E002963B7 /* AudioPlayerSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerSettings.swift; sourceTree = "<group>"; };
|
||||
E2FE0F3B2D2B3F42002963B7 /* AudioPlayerSettingsFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerSettingsFile.swift; sourceTree = "<group>"; };
|
||||
E2FE0F3D2D2B4225002963B7 /* AudioSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSettingsDetailView.swift; sourceTree = "<group>"; };
|
||||
E2FE0F3F2D2B45CD002963B7 /* SwiftProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftProcessor.swift; sourceTree = "<group>"; };
|
||||
E2FE0F412D2B480B002963B7 /* OtherCodeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherCodeProcessor.swift; sourceTree = "<group>"; };
|
||||
E2FE0F3F2D2B45CD002963B7 /* SwiftBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftBlock.swift; sourceTree = "<group>"; };
|
||||
E2FE0F412D2B480B002963B7 /* OtherCodeBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherCodeBlock.swift; sourceTree = "<group>"; };
|
||||
E2FE0F452D2BC772002963B7 /* MarkdownImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownImageProcessor.swift; sourceTree = "<group>"; };
|
||||
E2FE0F472D2BC7CD002963B7 /* MarkdownProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownProcessor.swift; sourceTree = "<group>"; };
|
||||
E2FE0F4A2D2BCCA5002963B7 /* MarkdownHeadlineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHeadlineProcessor.swift; sourceTree = "<group>"; };
|
||||
@ -467,6 +470,9 @@
|
||||
E2FE0F562D2BCFD4002963B7 /* BlockLineProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockLineProcessor.swift; sourceTree = "<group>"; };
|
||||
E2FE0F582D2BCFE4002963B7 /* OrderedKeyBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedKeyBlockProcessor.swift; sourceTree = "<group>"; };
|
||||
E2FE0F5A2D2BCFF2002963B7 /* KeyedBlockProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedBlockProcessor.swift; sourceTree = "<group>"; };
|
||||
E2FE0F5D2D2BE18B002963B7 /* FileResourceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResourceFile.swift; sourceTree = "<group>"; };
|
||||
E2FE0F5F2D2C041E002963B7 /* VideoBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoBlock.swift; sourceTree = "<group>"; };
|
||||
E2FE0F612D2C0D8D002963B7 /* VersionedVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionedVideo.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -509,7 +515,7 @@
|
||||
E2A37D102CE537670000979F /* PageFile.swift */,
|
||||
E2A37D142CE68BEA0000979F /* PostFile.swift */,
|
||||
E2A37D162CE73F170000979F /* TagFile.swift */,
|
||||
E22990212D0ED129009F8D77 /* TagOverviewFile.swift */,
|
||||
E2FE0F5D2D2BE18B002963B7 /* FileResourceFile.swift */,
|
||||
);
|
||||
path = Model;
|
||||
sourceTree = "<group>";
|
||||
@ -525,6 +531,7 @@
|
||||
E25DA5352D0041E200AEF16D /* PostSettingsFile.swift */,
|
||||
E25DA5922D023B3600AEF16D /* PageSettingsFile.swift */,
|
||||
E21850342CFAFA570090B18B /* SettingsFile.swift */,
|
||||
E22990212D0ED129009F8D77 /* TagOverviewFile.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@ -577,6 +584,7 @@
|
||||
E29D31212D0363FA0051B7F4 /* ContentButtons.swift */,
|
||||
E29D311F2D0320E20051B7F4 /* ContentLabels.swift */,
|
||||
E2FE0F272D2AFB0A002963B7 /* ImageCompare.swift */,
|
||||
E2FE0F612D2C0D8D002963B7 /* VersionedVideo.swift */,
|
||||
);
|
||||
path = ContentElements;
|
||||
sourceTree = "<group>";
|
||||
@ -919,10 +927,11 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2FE0F5C2D2BD006002963B7 /* Types */,
|
||||
E2FE0F322D2B265F002963B7 /* AudioBlockProcessor.swift */,
|
||||
E2FE0F322D2B265F002963B7 /* AudioBlock.swift */,
|
||||
E2FE0F542D2BCFC4002963B7 /* ContentBlock.swift */,
|
||||
E2FE0F412D2B480B002963B7 /* OtherCodeProcessor.swift */,
|
||||
E2FE0F3F2D2B45CD002963B7 /* SwiftProcessor.swift */,
|
||||
E2FE0F412D2B480B002963B7 /* OtherCodeBlock.swift */,
|
||||
E2FE0F3F2D2B45CD002963B7 /* SwiftBlock.swift */,
|
||||
E2FE0F5F2D2C041E002963B7 /* VideoBlock.swift */,
|
||||
);
|
||||
path = Blocks;
|
||||
sourceTree = "<group>";
|
||||
@ -1167,19 +1176,21 @@
|
||||
E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */,
|
||||
E2FE0F172D2698D5002963B7 /* LocalizedPageId.swift in Sources */,
|
||||
E2FE0F0D2D268A09002963B7 /* PostListPageGeneratorSource.swift in Sources */,
|
||||
E2FE0F402D2B45D3002963B7 /* SwiftProcessor.swift in Sources */,
|
||||
E2FE0F402D2B45D3002963B7 /* SwiftBlock.swift in Sources */,
|
||||
E25DA58B2D020C9500AEF16D /* PageImage.swift in Sources */,
|
||||
E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */,
|
||||
E2A21C332CB5BCAC0060935B /* PageContentView.swift in Sources */,
|
||||
E22990402D0F95EC009F8D77 /* FolderOnDiskPropertyView.swift in Sources */,
|
||||
E2FE0F422D2B4821002963B7 /* OtherCodeProcessor.swift in Sources */,
|
||||
E2FE0F422D2B4821002963B7 /* OtherCodeBlock.swift in Sources */,
|
||||
E21850332CFAFA2F0090B18B /* Settings.swift in Sources */,
|
||||
E29D31892D0AED1F0051B7F4 /* ModelViewer.swift in Sources */,
|
||||
E29D31412D04887F0051B7F4 /* SelectedDetailView.swift in Sources */,
|
||||
E29D31A32D0CC98C0051B7F4 /* Item.swift in Sources */,
|
||||
E25DA57A2D01C64400AEF16D /* PageContentGenerator.swift in Sources */,
|
||||
E2FE0F622D2C0D8D002963B7 /* VersionedVideo.swift in Sources */,
|
||||
E2A21C202CB28ED20060935B /* MockImage.swift in Sources */,
|
||||
E2FE0EEE2D1C22F3002963B7 /* MarkdownLinkProcessor.swift in Sources */,
|
||||
E2FE0F602D2C0422002963B7 /* VideoBlock.swift in Sources */,
|
||||
E2FE0F022D266FCB002963B7 /* LocalizedNavigationSettings.swift in Sources */,
|
||||
E29D313F2D04822C0051B7F4 /* AddPostView.swift in Sources */,
|
||||
E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */,
|
||||
@ -1195,6 +1206,7 @@
|
||||
E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */,
|
||||
E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */,
|
||||
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
|
||||
E2FE0F5E2D2BE190002963B7 /* FileResourceFile.swift in Sources */,
|
||||
E2FE0F2A2D2AFBE6002963B7 /* ImageCompareIcons.swift in Sources */,
|
||||
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */,
|
||||
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
|
||||
@ -1215,7 +1227,7 @@
|
||||
E29D314B2D04FC950051B7F4 /* FileToAdd.swift in Sources */,
|
||||
E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */,
|
||||
E29D31B52D0DA8490051B7F4 /* PageIcon.swift in Sources */,
|
||||
E2FE0F332D2B2665002963B7 /* AudioBlockProcessor.swift in Sources */,
|
||||
E2FE0F332D2B2665002963B7 /* AudioBlock.swift in Sources */,
|
||||
E25DA51D2CFF135E00AEF16D /* GenericPage.swift in Sources */,
|
||||
E29D319B2D0C452B0051B7F4 /* PageIssue.swift in Sources */,
|
||||
E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */,
|
||||
|
@ -95,8 +95,10 @@ extension String {
|
||||
|
||||
/**
|
||||
Split the string at the first occurence of the separator
|
||||
|
||||
If the String does not contain the separator, then `before` will contain the whole string, and `after` will be empty
|
||||
*/
|
||||
func splitAtFirst(_ separator: String) -> (String, String) {
|
||||
func splitAtFirst(_ separator: String) -> (before: String, after: String) {
|
||||
let parts = components(separatedBy: separator)
|
||||
return (parts.first!, parts.dropFirst().joined(separator: separator))
|
||||
}
|
||||
|
63
CHDataManagement/Generator/Blocks/AudioBlock.swift
Normal file
63
CHDataManagement/Generator/Blocks/AudioBlock.swift
Normal file
@ -0,0 +1,63 @@
|
||||
|
||||
struct AudioBlock: KeyedBlockProcessor {
|
||||
|
||||
enum Key: String {
|
||||
case name
|
||||
case artist
|
||||
case album
|
||||
case file
|
||||
case cover
|
||||
}
|
||||
|
||||
static let blockId: ContentBlock = .audio
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
}
|
||||
|
||||
func process(_ arguments: [Key : String], markdown: Substring) -> String {
|
||||
guard let name = arguments[.name],
|
||||
let artist = arguments[.artist],
|
||||
let album = arguments[.album],
|
||||
let fileId = arguments[.file],
|
||||
let cover = arguments[.cover] else {
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
guard let image = content.image(cover) else {
|
||||
results.missing(file: cover, source: "Audio Block")
|
||||
return ""
|
||||
}
|
||||
|
||||
guard let file = content.file(fileId) else {
|
||||
results.missing(file: fileId, source: "Audio Block")
|
||||
return ""
|
||||
}
|
||||
|
||||
let coverSize = 2 * content.settings.audioPlayer.smallCoverImageSize
|
||||
let coverImage = image.imageVersion(width: coverSize, height: coverSize, type: image.type)
|
||||
let footer = SingleFilePlayer.footer(
|
||||
name: name,
|
||||
artist: artist,
|
||||
album: album,
|
||||
url: file.absoluteUrl,
|
||||
cover: coverImage.outputPath)
|
||||
|
||||
results.require(file: file)
|
||||
results.require(image: coverImage)
|
||||
results.require(footer: footer)
|
||||
results.require(headers: .audioPlayerJs, .audioPlayerCss)
|
||||
results.require(icons: .audioPlayerPlay, .audioPlayerPause)
|
||||
|
||||
return SingleFilePlayer().content
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
|
||||
struct AudioBlockProcessor: KeyedBlockProcessor {
|
||||
struct AudioBlock: KeyedBlockProcessor {
|
||||
|
||||
enum Key: String {
|
||||
case name
|
||||
|
@ -5,10 +5,13 @@ enum ContentBlock: String, CaseIterable {
|
||||
|
||||
case swift
|
||||
|
||||
case video
|
||||
|
||||
var processor: BlockProcessor.Type {
|
||||
switch self {
|
||||
case .audio: return AudioBlockProcessor.self
|
||||
case .swift: return SwiftBlockProcessor.self
|
||||
case .audio: return AudioBlock.self
|
||||
case .swift: return SwiftBlock.self
|
||||
case .video: return VideoBlock.self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
struct OtherCodeProcessor {
|
||||
struct OtherCodeBlock {
|
||||
|
||||
private let codeHighlightFooter = "<script>hljs.highlightAll();</script>"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Splash
|
||||
|
||||
struct SwiftBlockProcessor: BlockProcessor {
|
||||
struct SwiftBlock: BlockProcessor {
|
||||
|
||||
static let blockId: ContentBlock = .swift
|
||||
|
@ -8,8 +8,8 @@ extension BlockLineProcessor {
|
||||
|
||||
func process(_ markdown: Substring) -> String {
|
||||
let lines = markdown
|
||||
.between("```\(Self.blockId.self)", and: "```")
|
||||
.components(separatedBy: "\n")
|
||||
return process(lines, markdown: markdown)
|
||||
.dropFirst().dropLast()
|
||||
return process(Array(lines), markdown: markdown)
|
||||
}
|
||||
}
|
||||
|
267
CHDataManagement/Generator/Blocks/VideoBlock.swift
Normal file
267
CHDataManagement/Generator/Blocks/VideoBlock.swift
Normal file
@ -0,0 +1,267 @@
|
||||
|
||||
struct VideoBlock: OrderedKeyBlockProcessor {
|
||||
|
||||
static let blockId: ContentBlock = .audio
|
||||
|
||||
let content: Content
|
||||
|
||||
let results: PageGenerationResults
|
||||
|
||||
let language: ContentLanguage
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.content = content
|
||||
self.results = results
|
||||
self.language = language
|
||||
}
|
||||
|
||||
func process(_ arguments: [(key: Key, value: String)], markdown: Substring) -> String {
|
||||
var options: [Option] = []
|
||||
var sources: [Source] = []
|
||||
|
||||
for (key, value) in arguments {
|
||||
guard let sourceType = key.sourceType else {
|
||||
guard let option = makeOption(key: key, value: value) else {
|
||||
invalid(markdown)
|
||||
continue
|
||||
}
|
||||
options.append(option)
|
||||
continue
|
||||
}
|
||||
let fileId = value.removingSurroundingQuotes
|
||||
guard let file = content.file(fileId) else {
|
||||
results.missing(file: fileId, source: "Video Block: \(key)")
|
||||
continue
|
||||
}
|
||||
let source = Source(file: file, type: sourceType)
|
||||
sources.append(source)
|
||||
}
|
||||
|
||||
guard !sources.isEmpty else {
|
||||
invalid(markdown)
|
||||
return ""
|
||||
}
|
||||
|
||||
return VersionedVideo(sources: sources, options: options).content
|
||||
}
|
||||
|
||||
private func makeOption(key: Key, value: String) -> Option? {
|
||||
switch key {
|
||||
case .controls: return .controls
|
||||
case .autoplay: return .autoplay
|
||||
case .muted: return .muted
|
||||
case .loop: return .loop
|
||||
case .playsinline: return .playsinline
|
||||
default: break
|
||||
}
|
||||
|
||||
let value = value.removingSurroundingQuotes
|
||||
|
||||
guard value != "" else {
|
||||
return nil
|
||||
}
|
||||
switch key {
|
||||
case .height:
|
||||
guard let height = Int(value) else {
|
||||
return nil
|
||||
}
|
||||
return .height(height)
|
||||
|
||||
case .width:
|
||||
guard let width = Int(value) else {
|
||||
return nil
|
||||
}
|
||||
return .width(width)
|
||||
case .preload:
|
||||
guard let preloadOption = Option.Preload(rawValue: value) else {
|
||||
return nil
|
||||
}
|
||||
return .preload(preloadOption)
|
||||
case .poster:
|
||||
guard let image = content.image(value) else {
|
||||
results.missing(file: value, source: "Video Block: poster")
|
||||
return nil
|
||||
}
|
||||
let width = 2 * content.settings.pages.contentWidth
|
||||
let version = image.imageVersion(width: width, height: width, type: .jpg)
|
||||
results.require(image: version)
|
||||
return .poster(image: version.outputPath)
|
||||
case .src:
|
||||
guard let file = content.file(value) else {
|
||||
results.missing(file: value, source: "Video Block: src")
|
||||
return nil
|
||||
}
|
||||
results.warning("Use 'h264' and 'h265' instead of 'src'")
|
||||
return .src(file.absoluteUrl)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoBlock {
|
||||
|
||||
enum Key: String {
|
||||
|
||||
/// The H264 video file to use
|
||||
case h264
|
||||
|
||||
/// The H265 video file to use
|
||||
case h265
|
||||
|
||||
/// The WebM video file to use
|
||||
case webm
|
||||
|
||||
// MARK: Video options
|
||||
|
||||
/// Specifies that video controls should be displayed (such as a play/pause button etc).
|
||||
case controls
|
||||
|
||||
/// Specifies that the video will start playing as soon as it is ready
|
||||
case autoplay
|
||||
|
||||
/// Specifies that the video will start over again, every time it is finished
|
||||
case loop
|
||||
|
||||
/// Specifies that the audio output of the video should be muted
|
||||
case muted
|
||||
|
||||
/// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays
|
||||
case playsinline
|
||||
|
||||
/// Sets the height of the video player
|
||||
case height
|
||||
|
||||
/// Sets the width of the video player
|
||||
case width
|
||||
|
||||
/// Specifies if and how the author thinks the video should be loaded when the page loads
|
||||
case preload
|
||||
|
||||
/// Specifies an image to be shown while the video is downloading, or until the user hits the play button
|
||||
case poster
|
||||
|
||||
/// Specifies the URL of the video file
|
||||
case src
|
||||
|
||||
var isOption: Bool {
|
||||
switch self {
|
||||
case .controls, .autoplay, .loop, .muted, .playsinline, .height, .width, .preload, .poster, .src:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var sourceType: SourceType? {
|
||||
switch self {
|
||||
case .h264: .h264
|
||||
case .h265: .h265
|
||||
case .webm: .webm
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SourceType {
|
||||
case h264
|
||||
case h265
|
||||
case webm
|
||||
|
||||
var order: Int {
|
||||
switch self {
|
||||
case .h265: 1
|
||||
case .webm: 2
|
||||
case .h264: 3
|
||||
}
|
||||
}
|
||||
|
||||
var mimeType: String {
|
||||
switch self {
|
||||
case .h265, .h264: "video/mp4"
|
||||
case .webm: "video/webm"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Source {
|
||||
|
||||
let file: FileResource
|
||||
|
||||
let type: SourceType
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoBlock {
|
||||
|
||||
enum Option {
|
||||
|
||||
/// Specifies that video controls should be displayed (such as a play/pause button etc).
|
||||
case controls
|
||||
|
||||
/// Specifies that the video will start playing as soon as it is ready
|
||||
case autoplay
|
||||
|
||||
/// Specifies that the video will start over again, every time it is finished
|
||||
case loop
|
||||
|
||||
/// Specifies that the audio output of the video should be muted
|
||||
case muted
|
||||
|
||||
/// Mobile browsers will play the video right where it is instead of the default, which is to open it up fullscreen while it plays
|
||||
case playsinline
|
||||
|
||||
/// Sets the height of the video player
|
||||
case height(Int)
|
||||
|
||||
/// Sets the width of the video player
|
||||
case width(Int)
|
||||
|
||||
/// Specifies if and how the author thinks the video should be loaded when the page loads
|
||||
case preload(Preload)
|
||||
|
||||
/// Specifies an image to be shown while the video is downloading, or until the user hits the play button
|
||||
case poster(image: String)
|
||||
|
||||
/// Specifies the URL of the video file
|
||||
case src(String)
|
||||
|
||||
var rawValue: String {
|
||||
switch self {
|
||||
case .controls: return "controls"
|
||||
case .autoplay: return "autoplay"
|
||||
case .muted: return "muted"
|
||||
case .loop: return "loop"
|
||||
case .playsinline: return "playsinline"
|
||||
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 .src(let url): return "src='\(url)'"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension VideoBlock.Option {
|
||||
|
||||
/**
|
||||
The `preload` attribute specifies if and how the author thinks that the video should be loaded when the page loads.
|
||||
|
||||
The `preload` attribute allows the author to provide a hint to the browser about what he/she thinks will lead to the best user experience.
|
||||
This attribute may be ignored in some instances.
|
||||
|
||||
Note: The `preload` attribute is ignored if `autoplay` is present.
|
||||
*/
|
||||
enum Preload: String {
|
||||
|
||||
/// The author thinks that the browser should load the entire video when the page loads
|
||||
case auto
|
||||
|
||||
/// The author thinks that the browser should load only metadata when the page loads
|
||||
case metadata
|
||||
|
||||
/// The author thinks that the browser should NOT load the video when the page loads
|
||||
case none
|
||||
}
|
||||
}
|
@ -9,33 +9,15 @@ final class ImageGenerator {
|
||||
|
||||
private let settings: Settings
|
||||
|
||||
private var generatedImages: [String : Set<String>] = [:]
|
||||
|
||||
init(storage: Storage, settings: Settings) {
|
||||
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("ImageGenerator: Failed to save list of generated images")
|
||||
return false
|
||||
}
|
||||
print("ImageGenerator: Saved list of \(totalImageCount) images")
|
||||
return true
|
||||
}
|
||||
|
||||
private var avifCommands: Set<String> = []
|
||||
|
||||
/**
|
||||
@ -54,49 +36,18 @@ final class ImageGenerator {
|
||||
storage.write(content, to: "generate-images.sh")
|
||||
}
|
||||
|
||||
/**
|
||||
Remove all versions of an image, so that they will be recreated on the next run.
|
||||
|
||||
This function does not remove the images from the output folder.
|
||||
*/
|
||||
func removeVersions(of image: String) {
|
||||
generatedImages[image] = nil
|
||||
save()
|
||||
}
|
||||
|
||||
func recalculateGeneratedImages(by images: Set<String>) {
|
||||
self.generatedImages = storage.calculateImages(generatedBy: images, in: outputFolder)
|
||||
let versionCount = generatedImages.values.reduce(0) { $0 + $1.count }
|
||||
print("Image generator: \(generatedImages.count)/\(images.count) images (\(versionCount) versions)")
|
||||
}
|
||||
|
||||
private func hasPreviouslyGenerated(_ version: ImageVersion) -> Bool {
|
||||
guard let versions = generatedImages[version.image.id] else {
|
||||
return false
|
||||
}
|
||||
return versions.contains(version.versionId)
|
||||
}
|
||||
|
||||
private func needsToGenerate(_ version: ImageVersion) -> Bool {
|
||||
if hasPreviouslyGenerated(version) {
|
||||
if version.wasPreviouslyGenerated {
|
||||
return false
|
||||
}
|
||||
if exists(version) {
|
||||
// Mark as already generated
|
||||
hasNowGenerated(version)
|
||||
version.wasNowGenerated()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func hasNowGenerated(_ version: ImageVersion) {
|
||||
generatedImages[version.image.id, default: []].insert(version.versionId)
|
||||
}
|
||||
|
||||
private func removeVersions(for image: String) {
|
||||
generatedImages[image] = nil
|
||||
}
|
||||
|
||||
// MARK: Files
|
||||
|
||||
private func exists(_ version: ImageVersion) -> Bool {
|
||||
@ -155,7 +106,7 @@ final class ImageGenerator {
|
||||
guard write(imageData: data, of: version) else {
|
||||
return false
|
||||
}
|
||||
hasNowGenerated(version)
|
||||
version.wasNowGenerated()
|
||||
return true
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,14 @@ struct ImageVersion {
|
||||
var outputPath: String {
|
||||
image.outputPath(width: maximumWidth, height: maximumHeight, type: type)
|
||||
}
|
||||
|
||||
var wasPreviouslyGenerated: Bool {
|
||||
image.generatedImageVersions.contains(versionId)
|
||||
}
|
||||
|
||||
func wasNowGenerated() {
|
||||
image.generatedImageVersions.insert(versionId)
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageVersion: Identifiable {
|
||||
|
@ -8,7 +8,7 @@ struct MarkdownCodeProcessor: MarkdownProcessor {
|
||||
|
||||
private let blocks: [ContentBlock : BlockProcessor]
|
||||
|
||||
private let other: OtherCodeProcessor
|
||||
private let other: OtherCodeBlock
|
||||
|
||||
init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
|
||||
self.results = results
|
||||
@ -23,10 +23,18 @@ struct MarkdownCodeProcessor: MarkdownProcessor {
|
||||
|
||||
func process(html: String, markdown: Substring) -> String {
|
||||
let input = String(markdown)
|
||||
let rawBlockId = input.dropAfterFirst("\n").dropBeforeFirst("```").trimmed
|
||||
let rawBlockId = input.dropAfterFirst("\n").dropBeforeFirst("```").trimmed.lowercased()
|
||||
guard let blockId = ContentBlock(rawValue: rawBlockId) else {
|
||||
guard knownCodeBlocks.contains(rawBlockId) else {
|
||||
results.invalid(block: nil, markdown)
|
||||
return ""
|
||||
}
|
||||
return other.process(html: html)
|
||||
}
|
||||
return blocks[blockId]!.process(markdown)
|
||||
}
|
||||
|
||||
private let knownCodeBlocks: Set<String> = [
|
||||
"bash", "nginx", "json", "css", "html", "markdown", ""
|
||||
]
|
||||
}
|
||||
|
@ -82,7 +82,6 @@ extension Content {
|
||||
results.failed(image: image)
|
||||
}
|
||||
|
||||
imageGenerator.save()
|
||||
imageGenerator.writeAvifCommandScript()
|
||||
//let images = Set(self.images.map { $0.id })
|
||||
//imageGenerator.recalculateGeneratedImages(by: images)
|
||||
|
@ -45,9 +45,6 @@ extension Content {
|
||||
}
|
||||
|
||||
let settings = storage.loadSettings() ?? .default
|
||||
let imageDescriptions = storage.loadFileDescriptions()
|
||||
.default([])
|
||||
.reduce(into: [:]) { $0[$1.fileId] = $1 }
|
||||
|
||||
guard let tagData = storage.loadAllTags() else {
|
||||
print("Failed to load file tags")
|
||||
@ -73,30 +70,14 @@ extension Content {
|
||||
}
|
||||
if fileList.isEmpty { print("No files loaded") }
|
||||
|
||||
let externalFiles = storage.loadExternalFileList() ?? []
|
||||
if externalFiles.isEmpty { print("No external files loaded") }
|
||||
|
||||
print("Loaded data from disk, processing...")
|
||||
// All data loaded from storage, start constructing the data model
|
||||
|
||||
var files: [String : FileResource] = fileList.reduce(into: [:]) { files, fileId in
|
||||
let descriptions = imageDescriptions[fileId]
|
||||
files[fileId] = FileResource(
|
||||
content: self,
|
||||
id: fileId,
|
||||
isExternallyStored: false,
|
||||
en: descriptions?.english ?? "",
|
||||
de: descriptions?.german ?? "")
|
||||
}
|
||||
|
||||
for fileId in externalFiles {
|
||||
let descriptions = imageDescriptions[fileId]
|
||||
files[fileId] = FileResource(
|
||||
content: self,
|
||||
id: fileId,
|
||||
isExternallyStored: true,
|
||||
en: descriptions?.english ?? "",
|
||||
de: descriptions?.german ?? "")
|
||||
let files: [String : FileResource] = fileList.reduce(into: [:]) { (files, data) in
|
||||
let fileId = data.key
|
||||
let fileData = data.value.data
|
||||
let isExternal = data.value.isExternal
|
||||
files[fileId] = FileResource(content: self, id: fileId, file: fileData, isExternalFile: isExternal)
|
||||
}
|
||||
|
||||
let images = files.filter { $0.value.type.isImage }
|
||||
|
@ -13,21 +13,7 @@ extension Content {
|
||||
failedSaves += posts.count { !storage.save(post: $0.postFile, for: $0.id) }
|
||||
failedSaves += tags.count { !storage.save(tagMetadata: $0.file, for: $0.id) }
|
||||
failedSaves.increment(!storage.save(settings: settings.file(tagOverview: tagOverview)))
|
||||
|
||||
let fileDescriptions: [FileDescriptions] = files.sorted().compactMap { file in
|
||||
guard !file.english.isEmpty || !file.german.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return FileDescriptions(
|
||||
fileId: file.id,
|
||||
german: file.german.nonEmpty,
|
||||
english: file.english.nonEmpty)
|
||||
}
|
||||
|
||||
failedSaves.increment(!storage.save(fileDescriptions: fileDescriptions))
|
||||
|
||||
let externalFileList = files.filter { $0.isExternallyStored }.map { $0.id }
|
||||
failedSaves.increment(!storage.save(externalFileList: externalFileList))
|
||||
failedSaves += files.count { !storage.save(fileInfo: $0.fileInfo, for: $0.id) }
|
||||
|
||||
if failedSaves > 0 {
|
||||
print("Save partially failed with \(failedSaves) errors")
|
||||
|
@ -14,6 +14,24 @@ final class FileResource: Item {
|
||||
@Published
|
||||
var english: String
|
||||
|
||||
@Published
|
||||
var version: String?
|
||||
|
||||
@Published
|
||||
var sourceUrl: String?
|
||||
|
||||
@Published
|
||||
var generatedImageVersions: Set<String>
|
||||
|
||||
@Published
|
||||
var customOutputPath: String?
|
||||
|
||||
@Published
|
||||
var addedDate: Date
|
||||
|
||||
@Published
|
||||
var modifiedDate: Date
|
||||
|
||||
/// The dimensions of the image
|
||||
@Published
|
||||
var imageDimensions: CGSize? = nil
|
||||
@ -22,11 +40,41 @@ final class FileResource: Item {
|
||||
@Published
|
||||
var fileSize: Int? = nil
|
||||
|
||||
init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) {
|
||||
init(content: Content,
|
||||
id: String,
|
||||
isExternallyStored: Bool,
|
||||
english: String?,
|
||||
german: String?,
|
||||
version: String? = nil,
|
||||
sourceUrl: String? = nil,
|
||||
generatedImageVersions: Set<String> = [],
|
||||
customOutputPath: String? = nil,
|
||||
addedDate: Date = .now,
|
||||
modifiedDate: Date = .now) {
|
||||
self.type = FileType(fileExtension: id.fileExtension)
|
||||
self.english = en
|
||||
self.german = de
|
||||
self.isExternallyStored = isExternallyStored
|
||||
self.german = german ?? ""
|
||||
self.english = english ?? ""
|
||||
self.version = version
|
||||
self.sourceUrl = sourceUrl
|
||||
self.generatedImageVersions = generatedImageVersions
|
||||
self.customOutputPath = customOutputPath
|
||||
self.addedDate = addedDate
|
||||
self.modifiedDate = modifiedDate
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
init(content: Content, id: String, file: FileResourceFile, isExternalFile: Bool) {
|
||||
self.type = FileType(fileExtension: id.fileExtension)
|
||||
self.isExternallyStored = isExternalFile
|
||||
self.german = file.germanDescription ?? ""
|
||||
self.english = file.englishDescription ?? ""
|
||||
self.version = file.version
|
||||
self.sourceUrl = file.sourceUrl
|
||||
self.generatedImageVersions = Set(file.generatedImages ?? [])
|
||||
self.customOutputPath = file.customOutputPath
|
||||
self.addedDate = file.addedDate
|
||||
self.modifiedDate = file.modifiedDate
|
||||
super.init(content: content, id: id)
|
||||
}
|
||||
|
||||
@ -38,6 +86,12 @@ final class FileResource: Item {
|
||||
self.english = "A test image included in the bundle"
|
||||
self.german = "Ein Testbild aus dem Bundle"
|
||||
self.isExternallyStored = true
|
||||
self.version = nil
|
||||
self.sourceUrl = nil
|
||||
self.generatedImageVersions = []
|
||||
self.customOutputPath = nil
|
||||
self.addedDate = Date.now
|
||||
self.modifiedDate = Date.now
|
||||
super.init(content: .mock, id: resourceImage) // TODO: Add images to mock
|
||||
}
|
||||
|
||||
@ -143,8 +197,10 @@ final class FileResource: Item {
|
||||
|
||||
func removeGeneratedImages() {
|
||||
guard type.isImage else { return }
|
||||
content.imageGenerator.removeVersions(of: id)
|
||||
content.storage.deleteInOutputFolder(outputImageFolder)
|
||||
guard content.storage.deleteInOutputFolder(outputImageFolder) else {
|
||||
return
|
||||
}
|
||||
self.generatedImageVersions = []
|
||||
}
|
||||
|
||||
private var failureImage: Image {
|
||||
@ -236,6 +292,21 @@ final class FileResource: Item {
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource {
|
||||
|
||||
var fileInfo: FileResourceFile {
|
||||
.init(
|
||||
englishDescription: english.nonEmpty,
|
||||
germanDescription: german.nonEmpty,
|
||||
generatedImages: generatedImageVersions.sorted().nonEmpty,
|
||||
customOutputPath: customOutputPath,
|
||||
version: version,
|
||||
sourceUrl: sourceUrl,
|
||||
addedDate: addedDate,
|
||||
modifiedDate: modifiedDate)
|
||||
}
|
||||
}
|
||||
|
||||
extension FileResource: LocalizedItem {
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,19 @@
|
||||
|
||||
struct VersionedVideo: HtmlProducer {
|
||||
|
||||
let sources: [VideoBlock.Source]
|
||||
|
||||
let options: [VideoBlock.Option]
|
||||
|
||||
func populate(_ result: inout String) {
|
||||
result += "<video\(optionString)>Video not supported."
|
||||
for source in sources.sorted(using: { $0.type.order }) {
|
||||
result += "<source src='\(source.file.absoluteUrl)' type='\(source.type.mimeType)'>"
|
||||
}
|
||||
result += "</video>"
|
||||
}
|
||||
|
||||
private var optionString: String {
|
||||
options.map { " " + $0.rawValue }.joined()
|
||||
}
|
||||
}
|
@ -2,6 +2,11 @@
|
||||
extension FileResource {
|
||||
|
||||
static var mock: FileResource {
|
||||
.init(content: .mock, id: "my-file.txt", isExternallyStored: true, en: "Some text file", de: "Eine Textdatei")
|
||||
.init(
|
||||
content: .mock,
|
||||
id: "my-file.txt",
|
||||
isExternallyStored: true,
|
||||
english: "Some text file",
|
||||
german: "Eine Textdatei")
|
||||
}
|
||||
}
|
||||
|
36
CHDataManagement/Storage/Model/FileResourceFile.swift
Normal file
36
CHDataManagement/Storage/Model/FileResourceFile.swift
Normal file
@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
This struct holds metadata about a file resource that is stored in the content folder.
|
||||
*/
|
||||
struct FileResourceFile {
|
||||
|
||||
/// The file/image description in German
|
||||
let englishDescription: String?
|
||||
|
||||
/// The file/image description in English
|
||||
let germanDescription: String?
|
||||
|
||||
/// The list of generated image versions for this image
|
||||
let generatedImages: [String]?
|
||||
|
||||
/// A custom file path in the output folder where this file is located
|
||||
let customOutputPath: String?
|
||||
|
||||
/// A version string of this resource, mostly for assets
|
||||
let version: String?
|
||||
|
||||
/// A URL where the resource was copied/downloaded from
|
||||
let sourceUrl: String?
|
||||
|
||||
/// The date when the file was added
|
||||
let addedDate: Date
|
||||
|
||||
/// The date when the file was last modified
|
||||
let modifiedDate: Date
|
||||
}
|
||||
|
||||
|
||||
extension FileResourceFile: Codable {
|
||||
|
||||
}
|
@ -162,13 +162,17 @@ struct SecurityBookmark {
|
||||
ifFileExists overwrite: OverwriteBehaviour = .fail) -> Bool {
|
||||
with(relativePath: relativeSource) { source in
|
||||
if !exists(source) {
|
||||
return failIfMissing
|
||||
if !failIfMissing { return true }
|
||||
print("ContentScope: Could not find file \(relativeSource) to move")
|
||||
return false
|
||||
}
|
||||
|
||||
let destination = url.appending(path: relativeDestination)
|
||||
if exists(destination) {
|
||||
switch overwrite {
|
||||
case .fail: return false
|
||||
case .fail:
|
||||
print("ContentScope: Could not move file \(relativeSource), file exists")
|
||||
return false
|
||||
case .skip: return true
|
||||
case .write: break
|
||||
case .writeIfChanged:
|
||||
|
@ -18,18 +18,14 @@ final class Storage: ObservableObject {
|
||||
|
||||
private let filesFolderName = "files"
|
||||
|
||||
private let fileInfoFolderName = "file metadata"
|
||||
|
||||
private let pagesFolderName = "pages"
|
||||
|
||||
private let postsFolderName = "posts"
|
||||
|
||||
private let tagsFolderName = "tags"
|
||||
|
||||
private let externalFileListName = "external-files.json"
|
||||
|
||||
private let fileDescriptionFilename = "file-descriptions.json"
|
||||
|
||||
private let generatedImagesListName = "generated-images.json"
|
||||
|
||||
private let outputPathFileName = "outputPath.bin"
|
||||
|
||||
private let settingsDataFileName = "settings.json"
|
||||
@ -118,7 +114,8 @@ final class Storage: ObservableObject {
|
||||
func move(page pageId: String, to newId: String) -> Bool {
|
||||
guard let contentScope else { return false }
|
||||
|
||||
guard contentScope.move(pageFileName(pageId), to: pageFileName(newId)) else {
|
||||
guard contentScope.move(pageMetadataPath(page: pageId), to: pageMetadataPath(page: newId)) else {
|
||||
print("Failed to move page file \(pageId)")
|
||||
return false
|
||||
}
|
||||
// Move the existing content files
|
||||
@ -127,9 +124,10 @@ final class Storage: ObservableObject {
|
||||
// Copy as many files as possible, since metadata was already moved
|
||||
// Don't fail early
|
||||
if !contentScope.move(
|
||||
pageContentFileName(pageId, language),
|
||||
to: pageContentFileName(newId, language),
|
||||
pageContentPath(page: pageId, language: language),
|
||||
to: pageContentPath(page: newId, language: language),
|
||||
failIfMissing: false) {
|
||||
print("Failed to move content file \(language) of page \(pageId)")
|
||||
result = false
|
||||
}
|
||||
}
|
||||
@ -214,17 +212,6 @@ final class Storage: ObservableObject {
|
||||
return contentScope.move(tagFilePath(tag: tagId), to: tagFilePath(tag: newId))
|
||||
}
|
||||
|
||||
// MARK: File descriptions
|
||||
|
||||
func loadFileDescriptions() -> [FileDescriptions]? {
|
||||
contentScope?.decode(at: fileDescriptionFilename)
|
||||
}
|
||||
|
||||
func save(fileDescriptions: [FileDescriptions]) -> Bool {
|
||||
guard let contentScope else { return false }
|
||||
return contentScope.encode(fileDescriptions, to: fileDescriptionFilename)
|
||||
}
|
||||
|
||||
// MARK: Files
|
||||
|
||||
func size(of file: String) -> Int? {
|
||||
@ -241,9 +228,22 @@ final class Storage: ObservableObject {
|
||||
}
|
||||
|
||||
/**
|
||||
Delete a file resource from the content folder
|
||||
Completely delete a file resource from the content folder
|
||||
*/
|
||||
func delete(file fileId: String) -> Bool {
|
||||
guard let contentScope else {
|
||||
return false
|
||||
}
|
||||
guard contentScope.deleteFile(at: filePath(file: fileId)) else {
|
||||
return false
|
||||
}
|
||||
return contentScope.deleteFile(at: fileInfoPath(file: fileId))
|
||||
}
|
||||
|
||||
/**
|
||||
Delete a file resource from the content folder, making it an external file
|
||||
*/
|
||||
func removeFileContent(file fileId: String) -> Bool {
|
||||
guard let contentScope else {
|
||||
return false
|
||||
}
|
||||
@ -266,6 +266,11 @@ final class Storage: ObservableObject {
|
||||
filesFolderName + "/" + fileId
|
||||
}
|
||||
|
||||
/// The path to a metadata file for a file resource
|
||||
private func fileInfoPath(file fileId: String) -> String {
|
||||
fileInfoFolderName + "/" + fileId + ".json"
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func deleteInOutputFolder(_ relativePath: String) -> Bool {
|
||||
guard let outputScope else { return false }
|
||||
@ -298,8 +303,30 @@ final class Storage: ObservableObject {
|
||||
to: relativeOutputPath, of: outputScope)
|
||||
}
|
||||
|
||||
func loadAllFiles() -> [String]? {
|
||||
contentScope?.fileNames(inRelativeFolder: filesFolderName)
|
||||
/**
|
||||
Load the file metadata from the content folder.
|
||||
|
||||
- Returns: A dictionary with the file ids as keys and the metadata file as a value.
|
||||
*/
|
||||
func loadAllFiles() -> [String : (data: FileResourceFile, isExternal: Bool)]? {
|
||||
guard let contentScope else { return nil }
|
||||
guard let list: [String : FileResourceFile] = contentScope.decodeJsonFiles(in: fileInfoFolderName) else {
|
||||
return nil
|
||||
}
|
||||
guard let existingFiles = contentScope.fileNames(inRelativeFolder: filesFolderName).map(Set.init) else {
|
||||
return nil
|
||||
}
|
||||
return Dictionary(uniqueKeysWithValues: list.map { fileId, data in
|
||||
let isExternal = !existingFiles.contains(fileId)
|
||||
return (fileId, (data: data, isExternal: isExternal))
|
||||
})
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func save(fileInfo: FileResourceFile, for fileId: String) -> Bool {
|
||||
guard let contentScope else { return false }
|
||||
let path = fileInfoPath(file: fileId)
|
||||
return contentScope.encode(fileInfo, to: path)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -327,18 +354,6 @@ final class Storage: ObservableObject {
|
||||
return contentScope.readData(at: path)
|
||||
}
|
||||
|
||||
// MARK: External file list
|
||||
|
||||
func loadExternalFileList() -> [String]? {
|
||||
guard let contentScope else { return nil }
|
||||
return contentScope.decode(at: externalFileListName)
|
||||
}
|
||||
|
||||
func save(externalFileList: [String]) -> Bool {
|
||||
guard let contentScope else { return false }
|
||||
return contentScope.encode(externalFileList.sorted(), to: externalFileListName)
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
|
||||
func loadSettings() -> SettingsFile? {
|
||||
@ -353,17 +368,8 @@ final class Storage: ObservableObject {
|
||||
|
||||
// MARK: Image generation data
|
||||
|
||||
func loadListOfGeneratedImages() -> [String : Set<String>]? {
|
||||
guard let contentScope else { return nil }
|
||||
return contentScope.decode(at: generatedImagesListName)
|
||||
}
|
||||
|
||||
func save(listOfGeneratedImages: [String : Set<String>]) -> Bool {
|
||||
guard let contentScope else { return false }
|
||||
return contentScope.encode(listOfGeneratedImages, to: generatedImagesListName)
|
||||
}
|
||||
|
||||
func calculateImages(generatedBy imageSet: Set<String>, in folder: String) -> [String : Set<String>] {
|
||||
#warning("TODO: Move to file resource")
|
||||
guard let outputScope else { return [:] }
|
||||
guard let allImages = outputScope.fileNames(inRelativeFolder: folder) else {
|
||||
print("Failed to get list of generated images in output folder")
|
||||
@ -375,7 +381,6 @@ 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) }
|
||||
|
@ -98,7 +98,9 @@ struct AddFileView: View {
|
||||
content: content,
|
||||
id: file.uniqueId,
|
||||
isExternallyStored: file.url == nil,
|
||||
en: "", de: "")
|
||||
english: "",
|
||||
german: "")
|
||||
|
||||
content.add(resource)
|
||||
selectedFile = resource
|
||||
}
|
||||
|
@ -1,10 +1,42 @@
|
||||
import SwiftUI
|
||||
import SFSafeSymbols
|
||||
|
||||
private extension Button {
|
||||
|
||||
init(_ symbol: SFSymbol, action: @escaping @MainActor () -> Void) where Label == Image {
|
||||
self.init(action: action, label: { Image(systemSymbol: symbol) })
|
||||
}
|
||||
}
|
||||
|
||||
private struct ButtonIcon: View {
|
||||
|
||||
let symbol: SFSymbol
|
||||
|
||||
let action: @MainActor () -> Void
|
||||
|
||||
init(_ symbol: SFSymbol, action: @escaping @MainActor () -> Void) {
|
||||
self.symbol = symbol
|
||||
self.action = action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
Image(systemSymbol: symbol)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(height: 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FileDetailView: View {
|
||||
|
||||
@EnvironmentObject
|
||||
private var content: Content
|
||||
|
||||
@Environment(\.language)
|
||||
private var language
|
||||
|
||||
@ObservedObject
|
||||
var file: FileResource
|
||||
|
||||
@ -15,58 +47,97 @@ struct FileDetailView: View {
|
||||
private var selectedFile: FileResource?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
DetailTitle(
|
||||
title: "File",
|
||||
text: "A file that can be used in a post or page")
|
||||
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
Button("Show in Finder", action: showFileInFinder)
|
||||
Button("Mark as changed", action: markFileAsChanged)
|
||||
Button("Delete resource", action: deleteFile)
|
||||
if file.isExternallyStored {
|
||||
Button("Import file", action: replaceFile)
|
||||
} else {
|
||||
Button("Replace file", action: replaceFile)
|
||||
Button("Make external", action: convertToExternal)
|
||||
}
|
||||
}
|
||||
DetailTitle(
|
||||
title: "File",
|
||||
text: "A file that can be used in a post or page")
|
||||
|
||||
IdPropertyView(
|
||||
id: $file.id,
|
||||
title: "Name",
|
||||
footer: "The unique name of the file, which is also used to reference it in posts and pages.",
|
||||
validation: file.isValid,
|
||||
update: { file.update(id: $0) })
|
||||
|
||||
StringPropertyView(
|
||||
title: "German Description",
|
||||
text: $file.german,
|
||||
footer: "The description for the file in German. Descriptions are used for images and to explain the content of a file.")
|
||||
|
||||
StringPropertyView(
|
||||
title: "English Description",
|
||||
text: $file.english,
|
||||
footer: "The description for the file in English. Descriptions are used for images and to explain the content of a file.")
|
||||
|
||||
if let imageDimensions = file.imageDimensions {
|
||||
GenericPropertyView(title: "Image dimensions") {
|
||||
Text("\(Int(imageDimensions.width)) x \(Int(imageDimensions.height)) (\(file.aspectRatio))")
|
||||
GenericPropertyView(title: "Actions") {
|
||||
HStack(spacing: 10) {
|
||||
ButtonIcon(.folder, action: showFileInFinder)
|
||||
ButtonIcon(.arrowClockwise, action: markFileAsChanged)
|
||||
if file.isExternallyStored {
|
||||
ButtonIcon(.squareAndArrowDown, action: replaceFile)
|
||||
} else {
|
||||
ButtonIcon(.arrowLeftArrowRight, action: replaceFile)
|
||||
ButtonIcon(.squareDashed, action: convertToExternal)
|
||||
}
|
||||
ButtonIcon(.trash, action: deleteFile)
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(.blue)
|
||||
}
|
||||
#warning("Add button to show image versions")
|
||||
}
|
||||
if let fileSize = file.fileSize {
|
||||
GenericPropertyView(title: "File size") {
|
||||
Text(formatBytes(fileSize))
|
||||
|
||||
IdPropertyView(
|
||||
id: $file.id,
|
||||
title: "Name",
|
||||
footer: "The unique name of the file, which is also used to reference it in posts and pages.",
|
||||
validation: file.isValid,
|
||||
update: { file.update(id: $0) })
|
||||
|
||||
switch language {
|
||||
case .english:
|
||||
StringPropertyView(
|
||||
title: "Description",
|
||||
text: $file.english,
|
||||
footer: "The description for the file. Descriptions are used for images and to explain the content of a file.")
|
||||
case .german:
|
||||
StringPropertyView(
|
||||
title: "Description",
|
||||
text: $file.german,
|
||||
footer: "The description for the file. Descriptions are used for images and to explain the content of a file.")
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}.padding()
|
||||
.onAppear {
|
||||
if file.fileSize == nil {
|
||||
file.determineFileSize()
|
||||
|
||||
OptionalStringPropertyView(
|
||||
title: "Version",
|
||||
text: $file.version,
|
||||
footer: "The version of this file.")
|
||||
|
||||
OptionalStringPropertyView(
|
||||
title: "Source URL",
|
||||
text: $file.sourceUrl,
|
||||
footer: "The url where this file can be downloaded from.")
|
||||
|
||||
OptionalStringPropertyView(
|
||||
title: "Custom output path",
|
||||
text: $file.customOutputPath,
|
||||
footer: "A custom path where the file is stored in the output folder")
|
||||
|
||||
if let imageDimensions = file.imageDimensions {
|
||||
GenericPropertyView(title: "Image dimensions") {
|
||||
Text("\(Int(imageDimensions.width)) x \(Int(imageDimensions.height)) (\(file.aspectRatio))")
|
||||
}
|
||||
}
|
||||
if let fileSize = file.fileSize {
|
||||
GenericPropertyView(title: "File size") {
|
||||
Text(formatBytes(fileSize))
|
||||
}
|
||||
}
|
||||
GenericPropertyView(title: "Added") {
|
||||
Text(file.addedDate.formatted())
|
||||
}
|
||||
|
||||
GenericPropertyView(title: "Modified") {
|
||||
Text(file.modifiedDate.formatted())
|
||||
}
|
||||
|
||||
if !file.generatedImageVersions.isEmpty {
|
||||
GenericPropertyView(title: "Image versions") {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(file.generatedImageVersions.sorted(), id: \.self) { version in
|
||||
Text(version)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.padding()
|
||||
}.onAppear {
|
||||
if file.fileSize == nil {
|
||||
file.determineFileSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func formatBytes(_ bytes: Int) -> String {
|
||||
@ -133,7 +204,7 @@ struct FileDetailView: View {
|
||||
return
|
||||
}
|
||||
|
||||
guard content.storage.delete(file: file.id) else {
|
||||
guard content.storage.removeFileContent(file: file.id) else {
|
||||
print("File '\(file.id)': Failed to delete file to make it external")
|
||||
return
|
||||
}
|
||||
|
@ -184,8 +184,8 @@ struct PageIssueView: View {
|
||||
content: content,
|
||||
id: fileId,
|
||||
isExternallyStored: true,
|
||||
en: "",
|
||||
de: "")
|
||||
english: "",
|
||||
german: "")
|
||||
content.add(file)
|
||||
|
||||
retryPageCheck()
|
||||
|
Loading…
x
Reference in New Issue
Block a user