diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 7617a5f..a6795d3 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -202,6 +202,8 @@ E2EC1FB02DC0D7DA00C41784 /* RouteBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */; }; E2EC1FB22DC0D8BD00C41784 /* RouteViewComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */; }; E2EC1FB42DC0FA8700C41784 /* Insert+Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */; }; + E2F3B3832DC496CB00CFA712 /* GalleryBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B3822DC496C800CFA712 /* GalleryBlock.swift */; }; + E2F3B3852DC49B7A00CFA712 /* Insert+Gallery.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F3B3842DC49B4400CFA712 /* Insert+Gallery.swift */; }; E2FD1D0D2D2DBBA600B48627 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */; }; E2FD1D192D2DC4F500B48627 /* LoadingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */; }; E2FD1D1B2D2DC63800B48627 /* LinkPreviewDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */; }; @@ -480,6 +482,8 @@ E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteBlock.swift; sourceTree = ""; }; E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteViewComponents.swift; sourceTree = ""; }; E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Route.swift"; sourceTree = ""; }; + E2F3B3822DC496C800CFA712 /* GalleryBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryBlock.swift; sourceTree = ""; }; + E2F3B3842DC49B4400CFA712 /* Insert+Gallery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Gallery.swift"; sourceTree = ""; }; E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = ""; }; E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingContext.swift; sourceTree = ""; }; E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewDetailView.swift; sourceTree = ""; }; @@ -1106,6 +1110,7 @@ E2FD1D362D3BBCB500B48627 /* Insert+Image.swift */, E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */, E2FD1D552D46CED500B48627 /* Insert+Labels.swift */, + E2F3B3842DC49B4400CFA712 /* Insert+Gallery.swift */, ); path = Commands; sourceTree = ""; @@ -1147,6 +1152,7 @@ E2FE0F342D2B27E6002963B7 /* Blocks */ = { isa = PBXGroup; children = ( + E2F3B3822DC496C800CFA712 /* GalleryBlock.swift */, E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */, E2B482212D676BEB005C309D /* PhoneScreensBlock.swift */, E2FE0F652D2C3B33002963B7 /* LabelsBlock.swift */, @@ -1480,6 +1486,7 @@ E25DA5732D018AA100AEF16D /* FileContentView.swift in Sources */, E2FD1D3F2D46405000B48627 /* PostLabelsView.swift in Sources */, E25DA5232CFF6C3700AEF16D /* ImageGenerator.swift in Sources */, + E2F3B3832DC496CB00CFA712 /* GalleryBlock.swift in Sources */, E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */, E29D31202D0320E70051B7F4 /* ContentLabels.swift in Sources */, E2FE0F572D2BCFD4002963B7 /* BlockLineProcessor.swift in Sources */, @@ -1520,6 +1527,7 @@ E2FE0F112D268E7E002963B7 /* MarkdownCodeProcessor.swift in Sources */, E2B482102D5E9FF9005C309D /* RemotePush.swift in Sources */, E22990202D0ECBE5009F8D77 /* TagOverviewDetailView.swift in Sources */, + E2F3B3852DC49B7A00CFA712 /* Insert+Gallery.swift in Sources */, E29D31C02D0DB9F20051B7F4 /* AudioPlayerContent.swift in Sources */, E22990192D0E3546009F8D77 /* ItemReference.swift in Sources */, E2FD1D372D3BBCCA00B48627 /* Insert+Image.swift in Sources */, diff --git a/CHDataManagement/Generator/Blocks/ContentBlock.swift b/CHDataManagement/Generator/Blocks/ContentBlock.swift index a9e0eb7..a3da8f6 100644 --- a/CHDataManagement/Generator/Blocks/ContentBlock.swift +++ b/CHDataManagement/Generator/Blocks/ContentBlock.swift @@ -17,6 +17,8 @@ enum ContentBlock: String, CaseIterable { case route + case gallery + var processor: BlockProcessor.Type { switch self { case .audio: return AudioBlock.self @@ -27,6 +29,7 @@ enum ContentBlock: String, CaseIterable { case .labels: return LabelsBlock.self case .screens: return PhoneScreensBlock.self case .route: return RouteBlock.self + case .gallery: return GalleryBlock.self } } } diff --git a/CHDataManagement/Generator/Blocks/GalleryBlock.swift b/CHDataManagement/Generator/Blocks/GalleryBlock.swift new file mode 100644 index 0000000..d006a2a --- /dev/null +++ b/CHDataManagement/Generator/Blocks/GalleryBlock.swift @@ -0,0 +1,46 @@ + +struct GalleryBlock: BlockLineProcessor { + + static let blockId: ContentBlock = .gallery + + 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 + } + + private var imageWidth: Int { + content.settings.pages.contentWidth + } + + func process(_ lines: [String], markdown: Substring) -> String { + var images = [FileResource]() + + for line in lines { + let imageId = line.trimmed + guard !imageId.isEmpty else { continue } + guard let image = content.image(imageId) else { + results.missing(file: imageId, source: "Route block") + continue + } + images.append(image) + } + guard let firstImage = images.first else { return "" } + + let imageSets = images.map { + $0.imageSet(width: imageWidth, height: imageWidth, language: language) + } + imageSets.forEach(results.require) + let id = firstImage.id.replacingOccurrences(of: ".", with: "-") + let gallery = ImageGallery(id: id, images: imageSets, standalone: true) + results.require(footer: gallery.standaloneFooter) + results.require(headers: .swiperJs, .swiperCss) + return gallery.content + } +} diff --git a/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift b/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift index 0360d74..340eb50 100644 --- a/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift +++ b/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift @@ -84,14 +84,7 @@ final class FeedPageGenerator { } func swiperInitScript(posts: [FeedEntryData]) -> String { - var result = "" - return result + let ids = posts.filter { $0.requiresSwiper }.map { $0.entryId } + return ImageGallery.combinedFootor(ids: ids) } } diff --git a/CHDataManagement/Page Elements/ContentElements/Images/ImageGallery.swift b/CHDataManagement/Page Elements/ContentElements/Images/ImageGallery.swift index 2129cc5..4628e29 100644 --- a/CHDataManagement/Page Elements/ContentElements/Images/ImageGallery.swift +++ b/CHDataManagement/Page Elements/ContentElements/Images/ImageGallery.swift @@ -11,14 +11,17 @@ struct ImageGallery: HtmlProducer { /// The images to display let images: [ImageSet] + let standalone: Bool + /// A version of the id that is safe to use in HTML and JavaScript private var htmlSafeId: String { ImageGallery.htmlSafe(id) } - init(id: String, images: [ImageSet]) { + init(id: String, images: [ImageSet], standalone: Bool = false) { self.id = id self.images = images + self.standalone = standalone } func populate(_ result: inout String) { @@ -26,7 +29,7 @@ struct ImageGallery: HtmlProducer { return } - result += "
" + result += "
" let needsPagination = images.count > 1 @@ -53,10 +56,27 @@ struct ImageGallery: HtmlProducer { id.replacingOccurrences(of: "-", with: "_") } - static func swiperInit(id: String) -> String { + var javascriptInit: String { + ImageGallery.swiperInit(id: id) + } + + var standaloneFooter: String { + "" + } + + static func combinedFootor(ids: [String]) -> String { + var result = ["") + return result.joined(separator: "\n") + } + + private static func swiperInit(id: String) -> String { let id = htmlSafe(id) return """ - var swiper_\(id) = new Swiper("#\(id)", { + var swiper_\(id) = new Swiper("#\(id)", { loop: true, lazy: { loadPrevNext: false, @@ -77,25 +97,3 @@ struct ImageGallery: HtmlProducer { """ } } - -/* -extension ImageGallery: HTML { - - var content: some HTML { - div(.id(id), .class("swiper")) { - div(.class("swiper-wrapper")) { - for image in images { - div(.class("swiper-slide")) { - // TODO: Use different images based on device - img(.src(image.mainImageUrl), .lazyLoad) - div(.class("swiper-lazy-preloader"), .class("swiper-lazy-preloader-white")) { } - } - } - } - div(.class("swiper-button-next")) { } - div(.class("swiper-button-prev")) { } - div(.class("swiper-pagination")) { } - } - } -} -*/ diff --git a/CHDataManagement/Views/Pages/Commands/Insert+Gallery.swift b/CHDataManagement/Views/Pages/Commands/Insert+Gallery.swift new file mode 100644 index 0000000..e0b225d --- /dev/null +++ b/CHDataManagement/Views/Pages/Commands/Insert+Gallery.swift @@ -0,0 +1,68 @@ +import SwiftUI +import SFSafeSymbols + +struct InsertableGallery: View, InsertableCommandView { + + static let title = "Gallery" + + static let sheetTitle = "Insert an image gallery" + + static let icon: SFSymbol = .photoStack + + final class Model: InsertableCommandModel { + + @Published + var images: [FileResource] = [] + + var isReady: Bool { + !images.isEmpty + } + + init() { + + } + + var command: String? { + guard !images.isEmpty else { + return nil + } + return ( + ["```\(GalleryBlock.blockId)"] + + images.map { $0.id } + + ["```"] + ).joined(separator: "\n") + } + } + + @Environment(\.colorScheme) + private var colorScheme + + @ObservedObject + private var model: Model + + @State + private var showImagePicker = false + + init(model: Model) { + self.model = model + } + + var body: some View { + VStack(spacing: 2) { + ScrollView(.horizontal) { + HStack(alignment: .center, spacing: 8) { + ForEach(model.images) { image in + PostImageView(image: image) + } + } + } + Button("Select images", action: { showImagePicker = true }) + .padding(.vertical, 2) + } + .sheet(isPresented: $showImagePicker) { + MultiFileSelectionView( + selectedFiles: $model.images, + allowedType: .image) + } + } +} diff --git a/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift b/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift index bb4a777..d92cfd2 100644 --- a/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift +++ b/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift @@ -7,6 +7,7 @@ struct InsertableItemsView: View { Text("Commands") .font(.headline) InsertableView() + InsertableView() InsertableView() InsertableView() InsertableView()