Implement image comparison command

This commit is contained in:
Christoph Hagen 2025-01-05 20:16:16 +01:00
parent 29bba5e76e
commit ac7fbdd638
23 changed files with 200 additions and 40 deletions

View File

@ -214,6 +214,9 @@
E2FE0F202D29A70E002963B7 /* Array+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */; }; E2FE0F202D29A70E002963B7 /* Array+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */; };
E2FE0F222D2A84A0002963B7 /* VideoCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */; }; E2FE0F222D2A84A0002963B7 /* VideoCommandProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */; };
E2FE0F242D2A8C21002963B7 /* TagDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */; }; E2FE0F242D2A8C21002963B7 /* TagDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */; };
E2FE0F262D2AF9B0002963B7 /* ImageCompareCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */; };
E2FE0F282D2AFB11002963B7 /* ImageCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F272D2AFB0A002963B7 /* ImageCompare.swift */; };
E2FE0F2A2D2AFBE6002963B7 /* ImageCompareIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0F292D2AFBE3002963B7 /* ImageCompareIcons.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -419,6 +422,9 @@
E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = "<group>"; }; E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Remove.swift"; sourceTree = "<group>"; };
E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCommandProcessor.swift; sourceTree = "<group>"; }; E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoCommandProcessor.swift; sourceTree = "<group>"; };
E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDisplayView.swift; sourceTree = "<group>"; }; E2FE0F232D2A8C1A002963B7 /* TagDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagDisplayView.swift; sourceTree = "<group>"; };
E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompareCommand.swift; sourceTree = "<group>"; };
E2FE0F272D2AFB0A002963B7 /* ImageCompare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompare.swift; sourceTree = "<group>"; };
E2FE0F292D2AFBE3002963B7 /* ImageCompareIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCompareIcons.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -528,6 +534,7 @@
E29D31232D0366820051B7F4 /* TagList.swift */, E29D31232D0366820051B7F4 /* TagList.swift */,
E29D31212D0363FA0051B7F4 /* ContentButtons.swift */, E29D31212D0363FA0051B7F4 /* ContentButtons.swift */,
E29D311F2D0320E20051B7F4 /* ContentLabels.swift */, E29D311F2D0320E20051B7F4 /* ContentLabels.swift */,
E2FE0F272D2AFB0A002963B7 /* ImageCompare.swift */,
); );
path = ContentElements; path = ContentElements;
sourceTree = "<group>"; sourceTree = "<group>";
@ -568,12 +575,13 @@
E29D31AB2D0DA52C0051B7F4 /* Icons */ = { E29D31AB2D0DA52C0051B7F4 /* Icons */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2FE0EF52D1D6DEE002963B7 /* Icon.swift */, E29D31AC2D0DA5310051B7F4 /* AudioPlayerIcons.swift */,
E2FE0EF32D1D6D22002963B7 /* GeneralIcons.swift */,
E29D31B42D0DA8490051B7F4 /* PageIcon.swift */,
E29D31B22D0DA6E50051B7F4 /* ButtonIcons.swift */, E29D31B22D0DA6E50051B7F4 /* ButtonIcons.swift */,
E29D31B02D0DA5510051B7F4 /* ContentIcon.swift */, E29D31B02D0DA5510051B7F4 /* ContentIcon.swift */,
E29D31AC2D0DA5310051B7F4 /* AudioPlayerIcons.swift */, E2FE0EF32D1D6D22002963B7 /* GeneralIcons.swift */,
E2FE0EF52D1D6DEE002963B7 /* Icon.swift */,
E2FE0F292D2AFBE3002963B7 /* ImageCompareIcons.swift */,
E29D31B42D0DA8490051B7F4 /* PageIcon.swift */,
E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */, E29D317E2D086F490051B7F4 /* StatisticsIcons.swift */,
); );
path = Icons; path = Icons;
@ -582,6 +590,7 @@
E29D31B62D0DAC030051B7F4 /* Page Content */ = { E29D31B62D0DAC030051B7F4 /* Page Content */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E2FE0F252D2AF9AA002963B7 /* ImageCompareCommand.swift */,
E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */, E2FE0F212D2A849B002963B7 /* VideoCommandProcessor.swift */,
E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */, E2FE0F142D269188002963B7 /* PageHtmlProcessor.swift */,
E2FE0F102D268E78002963B7 /* PageCodeProcessor.swift */, E2FE0F102D268E78002963B7 /* PageCodeProcessor.swift */,
@ -998,6 +1007,7 @@
E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */, E25DA51B2CFF08BB00AEF16D /* PostFeedPageNavigation.swift in Sources */,
E22990422D107A95009F8D77 /* ImageVersion.swift in Sources */, E22990422D107A95009F8D77 /* ImageVersion.swift in Sources */,
E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */, E29D317F2D086F4C0051B7F4 /* StatisticsIcons.swift in Sources */,
E2FE0F282D2AFB11002963B7 /* ImageCompare.swift in Sources */,
E229904E2D13535C009F8D77 /* SecurityBookmark.swift in Sources */, E229904E2D13535C009F8D77 /* SecurityBookmark.swift in Sources */,
E2A21C082CB17B870060935B /* TagView.swift in Sources */, E2A21C082CB17B870060935B /* TagView.swift in Sources */,
E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */, E29D313D2D047C1B0051B7F4 /* LocalizedPageContentView.swift in Sources */,
@ -1072,6 +1082,7 @@
E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */, E29D31B82D0DAC250051B7F4 /* ButtonCommand.swift in Sources */,
E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */, E29D31962D0C186E0051B7F4 /* PathSettings.swift in Sources */,
E2B85F412C4294790047CD0C /* PageHead.swift in Sources */, E2B85F412C4294790047CD0C /* PageHead.swift in Sources */,
E2FE0F2A2D2AFBE6002963B7 /* ImageCompareIcons.swift in Sources */,
E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */, E29D316B2D07488B0051B7F4 /* PostListPageGenerator.swift in Sources */,
E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */, E218501D2CEE6CB60090B18B /* VerticalCenter.swift in Sources */,
E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */, E22990382D0F7B32009F8D77 /* OptionalImagePropertyView.swift in Sources */,
@ -1151,6 +1162,7 @@
E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */, E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */,
E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */, E2A21C362CB9A3D70060935B /* PathSettingsView.swift in Sources */,
E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */, E29D31362D0435430051B7F4 /* TabSelection.swift in Sources */,
E2FE0F262D2AF9B0002963B7 /* ImageCompareCommand.swift in Sources */,
E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */, E2FE0F1E2D281AE1002963B7 /* TagOverviewGenerator.swift in Sources */,
E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */, E29D31572D06D38B0051B7F4 /* AddTagView.swift in Sources */,
E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */, E25DA5362D0041EB00AEF16D /* PostSettingsFile.swift in Sources */,

View File

@ -12,6 +12,8 @@ extension HeaderElement {
static let defaultCssFileOrder = 42 static let defaultCssFileOrder = 42
static let audioPlayerCssOrder = 43 static let audioPlayerCssOrder = 43
static let imageCompareCssOrder = 44
} }
enum HeaderElement { enum HeaderElement {

View File

@ -1,6 +1,6 @@
import Foundation import Foundation
struct ImageSet { struct ImageSet: HtmlProducer {
let image: FileResource let image: FileResource
@ -12,12 +12,15 @@ struct ImageSet {
let description: String let description: String
init(image: FileResource, maxWidth: Int, maxHeight: Int, description: String, quality: CGFloat = 0.7) { let extraAttributes: String
init(image: FileResource, maxWidth: Int, maxHeight: Int, description: String, quality: CGFloat = 0.7, extraAttributes: String? = nil) {
self.image = image self.image = image
self.maxWidth = maxWidth self.maxWidth = maxWidth
self.maxHeight = maxHeight self.maxHeight = maxHeight
self.description = description self.description = description
self.quality = quality self.quality = quality
self.extraAttributes = extraAttributes ?? ""
} }
var jobs: [ImageVersion] { var jobs: [ImageVersion] {
@ -36,17 +39,16 @@ struct ImageSet {
] ]
} }
var content: String { func populate(_ result: inout String) {
let fileExtension = image.type.fileExtension.map { "." + $0 } ?? "" let fileExtension = image.type.fileExtension.map { "." + $0 } ?? ""
let prefix1x = "/\(image.outputImageFolder)/\(maxWidth)x\(maxHeight)" let prefix1x = "/\(image.outputImageFolder)/\(maxWidth)x\(maxHeight)"
let prefix2x = "/\(image.outputImageFolder)/\(maxWidth*2)x\(maxHeight*2)" let prefix2x = "/\(image.outputImageFolder)/\(maxWidth*2)x\(maxHeight*2)"
var result = "<picture>" result += "<picture>"
result += "<source type='image/avif' srcset='\(prefix1x).avif 1x, \(prefix2x).avif 2x'/>" result += "<source type='image/avif' srcset='\(prefix1x).avif 1x, \(prefix2x).avif 2x'/>"
result += "<source type='image/webp' srcset='\(prefix1x).webp 1x, \(prefix1x).webp 2x'/>" result += "<source type='image/webp' srcset='\(prefix1x).webp 1x, \(prefix1x).webp 2x'/>"
result += "<img srcset='\(prefix2x)\(fileExtension) 2x' src='\(prefix1x)\(fileExtension)' loading='lazy' alt='\(description.htmlEscaped())'/>" result += "<img srcset='\(prefix2x)\(fileExtension) 2x' src='\(prefix1x)\(fileExtension)' loading='lazy' alt='\(description.htmlEscaped())'\(extraAttributes)/>"
result += "</picture>" result += "</picture>"
return result
} }
} }

View File

@ -11,6 +11,10 @@ enum KnownHeaderElement: Int {
/// JavaScript file for the audio player /// JavaScript file for the audio player
case audioPlayerJs = 2 case audioPlayerJs = 2
case imageCompareJs = 5
case imageCompareCss = 6
func header(content: Content) -> HeaderElement? { func header(content: Content) -> HeaderElement? {
switch self { switch self {
case .codeHightlighting: case .codeHightlighting:
@ -29,6 +33,14 @@ enum KnownHeaderElement: Int {
if let file = content.settings.pages.audioPlayerJsFile { if let file = content.settings.pages.audioPlayerJsFile {
return .js(file: file, defer: true) return .js(file: file, defer: true)
} }
case .imageCompareJs:
if let file = content.settings.pages.imageCompareJsFile {
return .js(file: file, defer: true)
}
case .imageCompareCss:
if let file = content.settings.pages.imageCompareCssFile {
return .css(file: file, order: HeaderElement.imageCompareCssOrder)
}
} }
return nil return nil
} }
@ -53,6 +65,10 @@ extension KnownHeaderElement: CustomStringConvertible {
return "audio-player-css" return "audio-player-css"
case .audioPlayerJs: case .audioPlayerJs:
return "audio-player-js" return "audio-player-js"
case .imageCompareJs:
return "image-compare-js"
case .imageCompareCss:
return "image-compare-css"
} }
} }
} }

View File

@ -8,7 +8,7 @@ struct AudioPlayerCommandProcessor: CommandProcessor {
let results: PageGenerationResults let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) { init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content self.content = content
self.results = results self.results = results
} }

View File

@ -5,7 +5,7 @@ struct BoxCommandProcessor: CommandProcessor {
let results: PageGenerationResults let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) { init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.results = results self.results = results
} }

View File

@ -7,7 +7,7 @@ struct ButtonCommandProcessor: CommandProcessor {
let results: PageGenerationResults let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) { init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content self.content = content
self.results = results self.results = results
} }

View File

@ -3,7 +3,7 @@ protocol CommandProcessor {
var commandType: ShorthandMarkdownKey { get } var commandType: ShorthandMarkdownKey { get }
init(content: Content, results: PageGenerationResults) init(content: Content, results: PageGenerationResults, language: ContentLanguage)
func process(_ arguments: [String], markdown: Substring) -> String func process(_ arguments: [String], markdown: Substring) -> String
} }

View File

@ -5,7 +5,7 @@ struct IconCommandProcessor: CommandProcessor {
let results: PageGenerationResults let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) { init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.results = results self.results = results
} }

View File

@ -0,0 +1,53 @@
struct ImageCompareCommandProcessor: CommandProcessor {
let commandType: ShorthandMarkdownKey = .imageCompare
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: [String], markdown: Substring) -> String {
guard arguments.count == 2 else {
results.invalid(command: .imageCompare, markdown)
return ""
}
let leftImageId = arguments[0]
let rightImageId = arguments[1]
guard let leftImage = content.image(leftImageId) else {
results.missing(file: leftImageId, source: "Image compare")
return ""
}
guard let rightImage = content.image(rightImageId) else {
results.missing(file: rightImageId, source: "Image compare")
return ""
}
let size = content.settings.pages.contentWidth
let leftImageSet = leftImage.imageSet(
width: size, height: size,
language: language,
extraAttributes: ImageCompare.extraAttributes)
let rightImageSet = rightImage.imageSet(
width: size, height: size,
language: language,
extraAttributes: ImageCompare.extraAttributes)
results.require(imageSet: leftImageSet)
results.require(imageSet: rightImageSet)
results.require(icon: ImageCompare.requiredIcon)
results.require(headers: .imageCompareJs, .imageCompareCss)
return ImageCompare(left: leftImageSet, right: rightImageSet).content
}
}

View File

@ -7,7 +7,7 @@ struct LabelsCommandProcessor: CommandProcessor {
let results: PageGenerationResults let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) { init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content self.content = content
self.results = results self.results = results
} }

View File

@ -11,7 +11,7 @@ struct PageHtmlProcessor: CommandProcessor {
let content: Content let content: Content
init(content: Content, results: PageGenerationResults) { init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content self.content = content
self.results = results self.results = results
} }

View File

@ -7,7 +7,7 @@ struct VideoCommandProcessor: CommandProcessor {
let results: PageGenerationResults let results: PageGenerationResults
init(content: Content, results: PageGenerationResults) { init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
self.content = content self.content = content
self.results = results self.results = results
} }

View File

@ -25,6 +25,8 @@ final class PageContentParser {
private let video: VideoCommandProcessor private let video: VideoCommandProcessor
private let imageCompare: ImageCompareCommandProcessor
// MARK: Other handlers // MARK: Other handlers
private let inlineLink: InlineLinkProcessor private let inlineLink: InlineLinkProcessor
@ -43,13 +45,14 @@ final class PageContentParser {
self.content = content self.content = content
self.results = results self.results = results
self.language = language self.language = language
self.buttonHandler = .init(content: content, results: results) self.buttonHandler = .init(content: content, results: results, language: language)
self.labelHandler = .init(content: content, results: results) self.labelHandler = .init(content: content, results: results, language: language)
self.audioPlayer = .init(content: content, results: results) self.audioPlayer = .init(content: content, results: results, language: language)
self.icons = .init(content: content, results: results) self.icons = .init(content: content, results: results, language: language)
self.box = .init(content: content, results: results) self.box = .init(content: content, results: results, language: language)
self.html = .init(content: content, results: results) self.html = .init(content: content, results: results, language: language)
self.video = .init(content: content, results: results) self.video = .init(content: content, results: results, language: language)
self.imageCompare = .init(content: content, results: results, language: language)
self.inlineLink = .init(content: content, results: results, language: language) self.inlineLink = .init(content: content, results: results, language: language)
self.code = .init(results: results) self.code = .init(results: results)
@ -136,6 +139,8 @@ final class PageContentParser {
return handleTagLink(arguments, markdown: markdown) return handleTagLink(arguments, markdown: markdown)
case .icons: case .icons:
return icons.process(arguments, markdown: markdown) return icons.process(arguments, markdown: markdown)
case .imageCompare:
return imageCompare.process(arguments, markdown: markdown)
} }
} }
@ -155,12 +160,10 @@ final class PageContentParser {
} }
results.used(file: image) results.used(file: image)
let caption = arguments.count == 2 ? arguments[1] : nil
let altText = image.localized(in: language)
let path = image.absoluteUrl
guard !image.type.isSvg else { guard !image.type.isSvg else {
let path = image.absoluteUrl
let altText = image.localized(in: language)
return SvgImage(imagePath: path, altText: altText).content return SvgImage(imagePath: path, altText: altText).content
} }
let thumbnail = image.imageSet(width: thumbnailWidth, height: thumbnailWidth, language: language) let thumbnail = image.imageSet(width: thumbnailWidth, height: thumbnailWidth, language: language)
@ -169,6 +172,7 @@ final class PageContentParser {
let largeImage = image.imageSet(width: largeImageWidth, height: largeImageWidth, language: language) let largeImage = image.imageSet(width: largeImageWidth, height: largeImageWidth, language: language)
results.require(imageSet: largeImage) results.require(imageSet: largeImage)
let caption = arguments.count == 2 ? arguments[1] : nil
return PageImage( return PageImage(
imageId: imageId.replacingOccurrences(of: ".", with: "-"), imageId: imageId.replacingOccurrences(of: ".", with: "-"),
thumbnail: thumbnail, thumbnail: thumbnail,

View File

@ -53,4 +53,10 @@ enum ShorthandMarkdownKey: String {
/// Format: `![icons](icon-id;...)` /// Format: `![icons](icon-id;...)`
case icons case icons
/**
Create an image comparison with a slider.
Format: `![compare](image1;image2)`
*/
case imageCompare = "compare"
} }

View File

@ -4,7 +4,6 @@ import SFSafeSymbols
/** /**
**Content** **Content**
- Podcast: Fix audio player, preview image - Podcast: Fix audio player, preview image
- Endeavor Basics: -> image compare command
- Article Cap Mosaic: -> GIF feature - Article Cap Mosaic: -> GIF feature
- iPhone Backgrounds: Add page, html - iPhone Backgrounds: Add page, html
@ -17,7 +16,6 @@ import SFSafeSymbols
**Features** **Features**
- GIF Support (No image set, don't rescale) - GIF Support (No image set, don't rescale)
- Image compare command `![compare](image1;image2)`
- Files: Optional Property `customFilePath` for external files to place them in another location - Files: Optional Property `customFilePath` for external files to place them in another location
- Files: Property `version` and `sourceUrl` to track asset files - Files: Property `version` and `sourceUrl` to track asset files
- Posts: Generate separate pages for posts to link to - Posts: Generate separate pages for posts to link to

View File

@ -164,14 +164,15 @@ final class FileResource: Item {
return prefix + "." + ext return prefix + "." + ext
} }
func imageSet(width: Int, height: Int, language: ContentLanguage, quality: CGFloat = 0.7) -> ImageSet { func imageSet(width: Int, height: Int, language: ContentLanguage, quality: CGFloat = 0.7, extraAttributes: String? = nil) -> ImageSet {
let description = self.localized(in: language) let description = self.localized(in: language)
return .init( return .init(
image: self, image: self,
maxWidth: width, maxWidth: width,
maxHeight: height, maxHeight: height,
description: description, description: description,
quality: quality) quality: quality,
extraAttributes: extraAttributes)
} }
func imageVersion(width: Int, height: Int, type: FileType) -> ImageVersion { func imageVersion(width: Int, height: Int, type: FileType) -> ImageVersion {

View File

@ -26,6 +26,12 @@ final class PageSettings: ObservableObject {
@Published @Published
var modelViewerJsFile: FileResource? var modelViewerJsFile: FileResource?
@Published
var imageCompareJsFile: FileResource?
@Published
var imageCompareCssFile: FileResource?
init(file: PageSettingsFile, files: [String : FileResource]) { init(file: PageSettingsFile, files: [String : FileResource]) {
self.contentWidth = file.contentWidth self.contentWidth = file.contentWidth
self.largeImageWidth = file.largeImageWidth self.largeImageWidth = file.largeImageWidth
@ -35,6 +41,7 @@ final class PageSettings: ObservableObject {
self.audioPlayerJsFile = file.audioPlayerJsFile.map { files[$0] } self.audioPlayerJsFile = file.audioPlayerJsFile.map { files[$0] }
self.audioPlayerCssFile = file.audioPlayerCssFile.map { files[$0] } self.audioPlayerCssFile = file.audioPlayerCssFile.map { files[$0] }
self.modelViewerJsFile = file.modelViewerJsFile.map { files[$0] } self.modelViewerJsFile = file.modelViewerJsFile.map { files[$0] }
self.imageCompareCssFile = file.imageCompareCssFile.map { files[$0] }
} }
var file: PageSettingsFile { var file: PageSettingsFile {
@ -45,6 +52,8 @@ final class PageSettings: ObservableObject {
codeHighlightingJsFile: codeHighlightingJsFile?.id, codeHighlightingJsFile: codeHighlightingJsFile?.id,
audioPlayerJsFile: audioPlayerJsFile?.id, audioPlayerJsFile: audioPlayerJsFile?.id,
audioPlayerCssFile: audioPlayerCssFile?.id, audioPlayerCssFile: audioPlayerCssFile?.id,
modelViewerJsFile: modelViewerJsFile?.id) modelViewerJsFile: modelViewerJsFile?.id,
imageCompareJsFile: imageCompareJsFile?.id,
imageCompareCssFile: imageCompareCssFile?.id)
} }
} }

View File

@ -0,0 +1,12 @@
extension Icon {
struct LeftRightArrow: ContentIcon {
static let name = "left-right-arrow"
static let content = """
<svg id='\(name)' viewBox='0 0 100 100'><polygon points='10 50 37 80 37 20 10 50' style='fill:currentColor'/><polygon points='90 50 64 20 64 80 90 50' style='fill:currentColor'/></svg>
"""
}
}

View File

@ -53,6 +53,10 @@ enum PageIcon: String, CaseIterable {
case audioPlayerNext = "next" case audioPlayerNext = "next"
// MARK: Image compare
case leftRightArrow = "left-right-arrow"
var icon: ContentIcon.Type { var icon: ContentIcon.Type {
switch self { switch self {
case .statisticsTime: return StatisticsTimeIcon.self case .statisticsTime: return StatisticsTimeIcon.self
@ -77,6 +81,7 @@ enum PageIcon: String, CaseIterable {
case .location: return Icon.Location.self case .location: return Icon.Location.self
case .poster: return Icon.Poster.self case .poster: return Icon.Poster.self
case .video: return Icon.Video.self case .video: return Icon.Video.self
case .leftRightArrow:return Icon.LeftRightArrow.self
} }
} }
} }

View File

@ -0,0 +1,20 @@
struct ImageCompare: HtmlProducer {
let left: ImageSet
let right: ImageSet
static let extraAttributes = " draggable='false''"
static var requiredIcon: PageIcon { .leftRightArrow }
func populate(_ result: inout String) {
result += "<div class='image-compare' style='aspect-ratio: 748/487'>"
result += "<div class='right'>\(left.content)</div>"
result += "<div class='left'>\(right.content)</div>"
result += "<div class='drag'>"
result += "<svg><use href='#\(Icon.LeftRightArrow.name)'></use></svg>"
result += "</div></div>" // Close drag, image-compare
}
}

View File

@ -16,6 +16,10 @@ struct PageSettingsFile {
let audioPlayerCssFile: String? let audioPlayerCssFile: String?
let modelViewerJsFile: String? let modelViewerJsFile: String?
let imageCompareJsFile: String?
let imageCompareCssFile: String?
} }
extension PageSettingsFile: Codable { extension PageSettingsFile: Codable {
@ -32,6 +36,8 @@ extension PageSettingsFile {
codeHighlightingJsFile: nil, codeHighlightingJsFile: nil,
audioPlayerJsFile: nil, audioPlayerJsFile: nil,
audioPlayerCssFile: nil, audioPlayerCssFile: nil,
modelViewerJsFile: nil) modelViewerJsFile: nil,
imageCompareJsFile: nil,
imageCompareCssFile: nil)
} }
} }

View File

@ -34,31 +34,45 @@ struct PageSettingsDetailView: View {
title: "Default CSS File", title: "Default CSS File",
footer: "The CSS file containing the styling of all pages", footer: "The CSS file containing the styling of all pages",
selectedFile: $content.settings.pages.defaultCssFile, selectedFile: $content.settings.pages.defaultCssFile,
allowedType: .text) allowedType: .asset)
FilePropertyView( FilePropertyView(
title: "Code Highlighting File", title: "Code Highlighting File",
footer: "The JavaScript file to provide syntax highlighting of code blocks", footer: "The JavaScript file to provide syntax highlighting of code blocks",
selectedFile: $content.settings.pages.codeHighlightingJsFile, selectedFile: $content.settings.pages.codeHighlightingJsFile,
allowedType: .text) allowedType: .asset)
FilePropertyView( FilePropertyView(
title: "Audio Player CSS File", title: "Audio Player CSS File",
footer: "The CSS file to provide the style for the audio player", footer: "The CSS file to provide the style for the audio player",
selectedFile: $content.settings.pages.audioPlayerCssFile, selectedFile: $content.settings.pages.audioPlayerCssFile,
allowedType: .text) allowedType: .asset)
FilePropertyView( FilePropertyView(
title: "Audio Player JavaScript File", title: "Audio Player JavaScript File",
footer: "The CSS file to provide the functionality for the audio player", footer: "The CSS file to provide the functionality for the audio player",
selectedFile: $content.settings.pages.audioPlayerJsFile, selectedFile: $content.settings.pages.audioPlayerJsFile,
allowedType: .text) allowedType: .asset)
FilePropertyView( FilePropertyView(
title: "3D Model Viewer File", title: "3D Model Viewer File",
footer: "The JavaScript file to provide the functionality for the 3D model viewer", footer: "The JavaScript file to provide the functionality for the 3D model viewer",
selectedFile: $content.settings.pages.modelViewerJsFile, selectedFile: $content.settings.pages.modelViewerJsFile,
allowedType: .text) allowedType: .asset)
FilePropertyView(
title: "Image Comparison CSS File",
footer: "The CSS file to provide image comparisons",
selectedFile: $content.settings.pages.imageCompareCssFile,
allowedType: .asset)
FilePropertyView(
title: "Image Comparison JaveScript File",
footer: "The JavaScript file to provide image comparisons",
selectedFile: $content.settings.pages.imageCompareJsFile,
allowedType: .asset)
} }
.padding() .padding()
} }