From bc3f21e7e46c29872dc3a7d20f47f63ca1d0e6a3 Mon Sep 17 00:00:00 2001 From: Christoph Hagen <github@christophhagen.de> Date: Fri, 17 Jan 2025 23:24:56 +0100 Subject: [PATCH] Allow videos in posts, simplify post image view --- CHDataManagement.xcodeproj/project.pbxproj | 20 +-- .../Page Generators/FeedPageGenerator.swift | 4 +- .../Post Lists/PostListPageGenerator.swift | 17 +- .../Model/Loading/LoadingContext.swift | 11 ++ CHDataManagement/Model/LocalizedPost.swift | 12 +- .../ContentElements/PostVideo.swift | 14 ++ .../Page Elements/FeedEntry.swift | 9 +- .../Page Elements/FeedEntryData.swift | 18 ++- .../Views/Posts/PostContentView.swift | 146 +++++++++++------- .../Views/Posts/PostImagesView.swift | 96 ------------ 10 files changed, 175 insertions(+), 172 deletions(-) create mode 100644 CHDataManagement/Page Elements/ContentElements/PostVideo.swift delete mode 100644 CHDataManagement/Views/Posts/PostImagesView.swift diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 4e829eb..3e554b8 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -14,7 +14,6 @@ E21850232CF10C850090B18B /* TagSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850222CF10C840090B18B /* TagSelectionView.swift */; }; E21850272CF3B42D0090B18B /* PostDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850262CF3B42D0090B18B /* PostDetailView.swift */; }; E218502B2CF790B30090B18B /* PostContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502A2CF790AC0090B18B /* PostContentView.swift */; }; - E218502D2CF791440090B18B /* PostImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218502C2CF791440090B18B /* PostImagesView.swift */; }; E21850332CFAFA2F0090B18B /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850322CFAFA200090B18B /* Settings.swift */; }; E21850372CFCA55F0090B18B /* LocalizedPostSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */; }; E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */; }; @@ -187,6 +186,7 @@ E2FD1D2C2D35B76D00B48627 /* ListPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D2B2D35B76D00B48627 /* ListPopup.swift */; }; E2FD1D2E2D37180900B48627 /* GeneralSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D2D2D37180600B48627 /* GeneralSettings.swift */; }; E2FD1D302D37196C00B48627 /* GeneralSettingsDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D2F2D37196500B48627 /* GeneralSettingsDetailView.swift */; }; + E2FD1D322D3AEB6300B48627 /* PostVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D312D3AEB6000B48627 /* PostVideo.swift */; }; E2FE0EE62D15A0B5002963B7 /* GenerationResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */; }; E2FE0EE82D16D4A3002963B7 /* ConvertThrowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */; }; E2FE0EEC2D1C1253002963B7 /* MultiFileSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */; }; @@ -252,7 +252,6 @@ E21850222CF10C840090B18B /* TagSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagSelectionView.swift; sourceTree = "<group>"; }; E21850262CF3B42D0090B18B /* PostDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailView.swift; sourceTree = "<group>"; }; E218502A2CF790AC0090B18B /* PostContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostContentView.swift; sourceTree = "<group>"; }; - E218502C2CF791440090B18B /* PostImagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostImagesView.swift; sourceTree = "<group>"; }; E21850322CFAFA200090B18B /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; }; E21850362CFCA5580090B18B /* LocalizedPostSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostSettings.swift; sourceTree = "<group>"; }; E218503C2CFCFD8C0090B18B /* LocalizedPostFeedSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizedPostFeedSettingsView.swift; sourceTree = "<group>"; }; @@ -420,6 +419,7 @@ E2FD1D2B2D35B76D00B48627 /* ListPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListPopup.swift; sourceTree = "<group>"; }; E2FD1D2D2D37180600B48627 /* GeneralSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettings.swift; sourceTree = "<group>"; }; E2FD1D2F2D37196500B48627 /* GeneralSettingsDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsDetailView.swift; sourceTree = "<group>"; }; + E2FD1D312D3AEB6000B48627 /* PostVideo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostVideo.swift; sourceTree = "<group>"; }; E2FE0EE52D15A0B1002963B7 /* GenerationResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenerationResults.swift; sourceTree = "<group>"; }; E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConvertThrowing.swift; sourceTree = "<group>"; }; E2FE0EEB2D1C124E002963B7 /* MultiFileSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiFileSelectionView.swift; sourceTree = "<group>"; }; @@ -550,16 +550,17 @@ isa = PBXGroup; children = ( E29D31C12D0DBED70051B7F4 /* AudioPlayer */, - E29D31AB2D0DA52C0051B7F4 /* Icons */, - E2FE0F2F2D2B18B0002963B7 /* Images */, E29D318A2D0B07E60051B7F4 /* ContentBox.swift */, - E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */, - E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */, - E29D31272D0371870051B7F4 /* ContentPageVideo.swift */, - E29D31232D0366820051B7F4 /* TagList.swift */, E29D31212D0363FA0051B7F4 /* ContentButtons.swift */, E29D311F2D0320E20051B7F4 /* ContentLabels.swift */, + E29D31272D0371870051B7F4 /* ContentPageVideo.swift */, + E29D31AB2D0DA52C0051B7F4 /* Icons */, E2FE0F272D2AFB0A002963B7 /* ImageCompare.swift */, + E2FE0F2F2D2B18B0002963B7 /* Images */, + E29D31882D0AED1B0051B7F4 /* ModelViewer.swift */, + E2FD1D312D3AEB6000B48627 /* PostVideo.swift */, + E29D31822D0A43D60051B7F4 /* RelatedPageLink.swift */, + E29D31232D0366820051B7F4 /* TagList.swift */, E2FE0F612D2C0D8D002963B7 /* VersionedVideo.swift */, ); path = ContentElements; @@ -803,7 +804,6 @@ E21850082CEE01BF0090B18B /* PagePickerView.swift */, E2A21C072CB17B810060935B /* TagView.swift */, E29D31312D03B5610051B7F4 /* LocalizedPostDetailView.swift */, - E218502C2CF791440090B18B /* PostImagesView.swift */, ); path = Posts; sourceTree = "<group>"; @@ -1224,6 +1224,7 @@ E25DA51D2CFF135E00AEF16D /* GenericPage.swift in Sources */, E29D319B2D0C452B0051B7F4 /* PageIssue.swift in Sources */, E218500B2CEE02FD0090B18B /* Content+Mock.swift in Sources */, + E2FD1D322D3AEB6300B48627 /* PostVideo.swift in Sources */, E29D31472D04892E0051B7F4 /* FileListView.swift in Sources */, E218503D2CFCFD910090B18B /* LocalizedPostFeedSettingsView.swift in Sources */, E2FE0EF62D1D6DF1002963B7 /* Icon.swift in Sources */, @@ -1263,7 +1264,6 @@ E2E06E002CA4A8F00019C2AF /* Page+Mock.swift in Sources */, E29D314D2D04FCBF0051B7F4 /* FileToAddView.swift in Sources */, E2FE0F6C2D2D335E002963B7 /* LocalizedPageSettingsView.swift in Sources */, - E218502D2CF791440090B18B /* PostImagesView.swift in Sources */, E2B85F572C4BD0BB0047CD0C /* Binding+Extension.swift in Sources */, E2B85F432C4294F60047CD0C /* FeedEntry.swift in Sources */, E25DA56D2D00EBCF00AEF16D /* NavigationBarSettingsView.swift in Sources */, diff --git a/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift b/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift index 3bf0f4f..88b3d5e 100644 --- a/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift +++ b/CHDataManagement/Generator/Page Generators/FeedPageGenerator.swift @@ -35,7 +35,7 @@ final class FeedPageGenerator { linkPrefix: String) -> String { var headers = content.postPageHeaders var footer = "" - if posts.contains(where: { $0.images.count > 1 }) { + if posts.contains(where: { $0.requiresSwiper }) { // Sort swiper style sheet before default style sheet includeSwiper(in: &headers) footer = swiperInitScript(posts: posts) @@ -82,7 +82,7 @@ final class FeedPageGenerator { func swiperInitScript(posts: [FeedEntryData]) -> String { var result = "<script> window.onload = () => { " for post in posts { - guard post.images.count > 1 else { + guard post.requiresSwiper else { continue } result += ImageGallery.swiperInit(id: post.entryId) diff --git a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift index 6a95935..b1ed7aa 100644 --- a/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift +++ b/CHDataManagement/Generator/Post Lists/PostListPageGenerator.swift @@ -66,10 +66,19 @@ final class PostListPageGenerator { url: tag.absoluteUrl(in: language)) } - let images = localized.images.map { image in - image.imageSet(width: mainContentMaximumWidth, height: mainContentMaximumWidth, language: language) + let media: FeedEntryData.Media? + if localized.hasImages { + let images = localized.images.map { image in + image.imageSet(width: mainContentMaximumWidth, height: mainContentMaximumWidth, language: language) + } + images.forEach(source.results.require) + media = .images(images) + } else if localized.hasVideos { + media = .video(localized.images) + localized.images.forEach(source.results.require) + } else { + media = nil } - images.forEach(source.results.require) return FeedEntryData( entryId: post.id, @@ -78,7 +87,7 @@ final class PostListPageGenerator { link: linkUrl, tags: tags, text: localized.text.components(separatedBy: "\n\n"), - images: images) + media: media) #warning("Treat post text as markdown") } diff --git a/CHDataManagement/Model/Loading/LoadingContext.swift b/CHDataManagement/Model/Loading/LoadingContext.swift index c9c8ec2..8c786db 100644 --- a/CHDataManagement/Model/Loading/LoadingContext.swift +++ b/CHDataManagement/Model/Loading/LoadingContext.swift @@ -79,6 +79,17 @@ final class LoadingContext { return nil } + func postMedia(_ imageId: String) -> FileResource? { + guard let image = file(imageId) else { + return nil + } + if image.type.isImage || image.type.isVideo { + return image + } + error("Post Media \(imageId) is not an image or video") + return nil + } + func item(itemId: ItemId) -> Item? { switch itemId.type { case .post: diff --git a/CHDataManagement/Model/LocalizedPost.swift b/CHDataManagement/Model/LocalizedPost.swift index a651a72..c375973 100644 --- a/CHDataManagement/Model/LocalizedPost.swift +++ b/CHDataManagement/Model/LocalizedPost.swift @@ -53,6 +53,16 @@ final class LocalizedPost: ObservableObject { } linkPreview.remove(file) } + + // MARK: Images + + var hasImages: Bool { + images.contains { $0.type.isImage } + } + + var hasVideos: Bool { + images.contains { $0.type.isVideo } + } } // MARK: Storage @@ -65,7 +75,7 @@ extension LocalizedPost { title: data.title, text: data.text, lastModified: data.lastModifiedDate, - images: data.images.compactMap(context.image), + images: data.images.compactMap(context.postMedia), pageLinkText: data.pageLinkText, linkPreview: .init(context: context, data: data.linkPreview)) } diff --git a/CHDataManagement/Page Elements/ContentElements/PostVideo.swift b/CHDataManagement/Page Elements/ContentElements/PostVideo.swift new file mode 100644 index 0000000..e02ba0a --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/PostVideo.swift @@ -0,0 +1,14 @@ + +struct PostVideo: HtmlProducer { + + let videos: [FileResource] + + func populate(_ result: inout String) { + result += "<video autoplay loop muted>" + result += "Video not supported." + for video in videos { + result += "<source src='\(video.absoluteUrl)' type='\(video.type.htmlType!)'>" + } + result += "</video>" + } +} diff --git a/CHDataManagement/Page Elements/FeedEntry.swift b/CHDataManagement/Page Elements/FeedEntry.swift index b2b7113..a910d6e 100644 --- a/CHDataManagement/Page Elements/FeedEntry.swift +++ b/CHDataManagement/Page Elements/FeedEntry.swift @@ -14,7 +14,14 @@ struct FeedEntry { var content: String { var result = "<article><div class='card\(cardLinkClassText)'>" - ImageGallery(id: data.entryId, images: data.images).populate(&result) + switch data.media { + case .images(let images): + ImageGallery(id: data.entryId, images: images).populate(&result) + case .video(let videos): + PostVideo(videos: videos).populate(&result) + case .none: + break + } if let url = data.link?.url { result += "<div class='card-content' onclick=\"window.location.href='\(url)'\">" diff --git a/CHDataManagement/Page Elements/FeedEntryData.swift b/CHDataManagement/Page Elements/FeedEntryData.swift index 4ed7be5..a083c27 100644 --- a/CHDataManagement/Page Elements/FeedEntryData.swift +++ b/CHDataManagement/Page Elements/FeedEntryData.swift @@ -13,16 +13,16 @@ struct FeedEntryData { let text: [String] - let images: [ImageSet] + let media: Media? - init(entryId: String, title: String?, textAboveTitle: String, link: Link?, tags: [Tag], text: [String], images: [ImageSet]) { + init(entryId: String, title: String?, textAboveTitle: String, link: Link?, tags: [Tag], text: [String], media: Media?) { self.entryId = entryId self.title = title self.textAboveTitle = textAboveTitle self.link = link self.tags = tags self.text = text - self.images = images + self.media = media } struct Link { @@ -40,4 +40,16 @@ struct FeedEntryData { let url: String } + + enum Media { + case images([ImageSet]) + case video([FileResource]) + } + + var requiresSwiper: Bool { + if case .images(let images) = media, images.count > 1 { + return true + } + return false + } } diff --git a/CHDataManagement/Views/Posts/PostContentView.swift b/CHDataManagement/Views/Posts/PostContentView.swift index c157306..646dbfe 100644 --- a/CHDataManagement/Views/Posts/PostContentView.swift +++ b/CHDataManagement/Views/Posts/PostContentView.swift @@ -15,7 +15,11 @@ struct PostContentView: View { } var body: some View { - LocalizedPostContentView(post: post) + LocalizedPostContentView( + post: post.localized(in: language), + other: post.localized(in: language.next), + tags: $post.tags, + page: $post.linkedPage) } } @@ -28,46 +32,6 @@ extension PostContentView: MainContentView { static let itemDescription = "a post" } -private struct LocalizedTitle: View { - - @ObservedObject - private var post: LocalizedPost - - init(post: LocalizedPost) { - self.post = post - } - - var body: some View { - OptionalTextField("", text: $post.title) - .font(.system(size: 24, weight: .bold)) - .foregroundStyle(Color.primary) - .textFieldStyle(.plain) - .lineLimit(2) - .frame(minHeight: 30) - } -} - -private struct LocalizedContentEditor: View { - - @ObservedObject - private var post: LocalizedPost - - init(post: LocalizedPost) { - self.post = post - } - - var body: some View { - TextEditor(text: $post.text) - .font(.body) - .frame(minHeight: 150) - .textEditorStyle(.plain) - .padding(.vertical, 8) - .padding(.leading, 3) - .background(Color.gray.opacity(0.1)) - .cornerRadius(8) - } -} - private struct LinkedPageTagView: View { @ObservedObject @@ -80,42 +44,114 @@ private struct LinkedPageTagView: View { struct LocalizedPostContentView: View { - @ObservedObject - var post: Post - @Environment(\.language) private var language @EnvironmentObject private var content: Content - init(post: Post) { + @ObservedObject + var post: LocalizedPost + + @ObservedObject + var other: LocalizedPost + + @Binding + var tags: [Tag] + + @Binding + var page: Page? + + @State + private var fileTypeToSelect: FileTypeCategory = .image + + @State + private var showImagePicker = false + + init(post: LocalizedPost, other: LocalizedPost, tags: Binding<[Tag]>, page: Binding<Page?>) { self.post = post + self.other = other + self._tags = tags + self._page = page } var body: some View { VStack(alignment: .leading) { HStack { - Text("Images") + Text("Images/Video") .font(.headline) - Button("Transfer from \(language.next.text)", action: copyImagesFromOtherLanguage) - .disabled(post.localized(in: language.next).images.isEmpty) + Button("Images") { + fileTypeToSelect = .image + showImagePicker = true + } + .disabled(post.hasVideos) + Button("Videos") { + fileTypeToSelect = .video + showImagePicker = true + } + .disabled(post.hasImages) + Button("Transfer from \(language.next.text)") { + post.images = other.images + } + .disabled(other.images.isEmpty) } - PostImagesView(post: post.localized(in: language)) - LocalizedTitle(post: post.localized(in: language)) - if let page = post.linkedPage { + ScrollView(.horizontal) { + HStack(alignment: .center, spacing: 8) { + ForEach(post.images) { image in + if image.type.isImage { + image.imageToDisplay + .resizable() + .aspectRatio(contentMode: .fill) + .frame(maxWidth: 300, maxHeight: 200) + .cornerRadius(8) + + } else { + VStack { + Image(systemSymbol: .film) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 100) + Text(image.id) + .font(.title) + } + //.foregroundStyle(.secondary) + .frame(width: 300, height: 200) + .background(Color.gray) + .cornerRadius(8) + } + } + } + } + OptionalTextField("", text: $post.title) + .font(.system(size: 24, weight: .bold)) + .foregroundStyle(Color.primary) + .textFieldStyle(.plain) + .lineLimit(2) + .frame(minHeight: 30) + if let page = page { LinkedPageTagView(page: page) } else { - TagDisplayView(tags: $post.tags) + TagDisplayView(tags: $tags) } - LocalizedContentEditor(post: post.localized(in: language)) + TextEditor(text: $post.text) + .font(.body) + .frame(minHeight: 150) + .textEditorStyle(.plain) + .padding(.vertical, 8) + .padding(.leading, 3) + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) } .padding() + .sheet(isPresented: $showImagePicker) { + MultiFileSelectionView( + selectedFiles: $post.images, + allowedType: fileTypeToSelect) + } } private func copyImagesFromOtherLanguage() { - let images = post.localized(in: language.next).images - post.localized(in: language).images = images + post.images = other.images } } diff --git a/CHDataManagement/Views/Posts/PostImagesView.swift b/CHDataManagement/Views/Posts/PostImagesView.swift deleted file mode 100644 index 37a72c1..0000000 --- a/CHDataManagement/Views/Posts/PostImagesView.swift +++ /dev/null @@ -1,96 +0,0 @@ -import SwiftUI - -struct PostImagesView: View { - - @ObservedObject - var post: LocalizedPost - - @State - private var showImagePicker = false - - var body: some View { - ScrollView(.horizontal) { - HStack(alignment: .center, spacing: 8) { - ForEach(post.images) { image in - ZStack { - image.imageToDisplay - .resizable() - .aspectRatio(contentMode: .fill) - .frame(maxWidth: 300, maxHeight: 200) - .cornerRadius(8) - .layoutPriority(1) - VStack { - HStack(alignment: .top) { - Button(action: { remove(image) }) { - NavigationIcon(symbol: .trash, edge: .all) - } - .buttonStyle(.plain) - Spacer() - Text(image.id) - .padding(4) - .foregroundStyle(Color.white.opacity(0.8)) - .background(RoundedRectangle(cornerRadius: 8).fill(Color.black.opacity(0.7))) - } - Spacer() - HStack { - Button(action: { shiftLeft(image) }) { - NavigationIcon(symbol: .chevronLeft, edge: .trailing) - } - .buttonStyle(.plain) - Spacer() - Button(action: { shiftRight(image) }) { - NavigationIcon(symbol: .chevronRight, edge: .leading) - } - .buttonStyle(.plain) - } - } - .padding() - } - } - Button(action: { showImagePicker = true }) { - NavigationIcon(symbol: .plus, edge: .all) - } - .buttonStyle(.plain) - .padding() - } - } - .sheet(isPresented: $showImagePicker) { - MultiFileSelectionView(selectedFiles: $post.images, allowedType: .image) - } - } - - private func shiftLeft(_ image: FileResource) { - guard let index = post.images.firstIndex(of: image) else { - return - } - guard index > 0 else { - return - } - post.images.swapAt(index, index - 1) - } - - private func shiftRight(_ image: FileResource) { - guard let index = post.images.firstIndex(of: image) else { - return - } - guard index < post.images.count - 1 else { - return - } - post.images.swapAt(index, index + 1) - } - - private func remove(_ image: FileResource) { - guard let index = post.images.firstIndex(of: image) else { - return - } - post.images.remove(at: index) - } -} - -#Preview { - VStack(alignment: .leading) { - Text("Images") - .font(.headline) - PostImagesView(post: .english) - } -}