From 27b3856e05c1bd09c2192f9a157d453d81b3ac16 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Fri, 27 Sep 2024 15:22:34 +0200 Subject: [PATCH] Various fixes to share as image + copy text --- .../Sources/StatusKit/Row/StatusRowView.swift | 6 +- .../Row/Subviews/StatusRowActionsView.swift | 37 +++++++- .../Row/Subviews/StatusRowCardView.swift | 7 +- .../Row/Subviews/StatusRowContextMenu.swift | 84 +------------------ .../Row/Subviews/StatusRowHeaderView.swift | 1 - .../Share/StatusRowSelectableTextView.swift | 56 +++++++++++++ .../Share/StatusRowShareAsImageView.swift | 56 +++++++++++++ 7 files changed, 156 insertions(+), 91 deletions(-) create mode 100644 Packages/StatusKit/Sources/StatusKit/Share/StatusRowSelectableTextView.swift create mode 100644 Packages/StatusKit/Sources/StatusKit/Share/StatusRowShareAsImageView.swift diff --git a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift index 02abd00a..76164b14 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/StatusRowView.swift @@ -23,6 +23,7 @@ public struct StatusRowView: View { @Environment(Client.self) private var client @State private var showSelectableText: Bool = false + @State private var isShareAsImageSheetPresented: Bool = false @State private var isBlockConfirmationPresented = false public enum Context { case timeline, detail } @@ -33,7 +34,8 @@ public struct StatusRowView: View { var contextMenu: some View { StatusRowContextMenu(viewModel: viewModel, showTextForSelection: $showSelectableText, - isBlockConfirmationPresented: $isBlockConfirmationPresented) + isBlockConfirmationPresented: $isBlockConfirmationPresented, + isShareAsImageSheetPresented: $isShareAsImageSheetPresented) } public var body: some View { @@ -213,7 +215,7 @@ public struct StatusRowView: View { } .sheet(isPresented: $showSelectableText) { let content = viewModel.status.reblog?.content.asSafeMarkdownAttributedString ?? viewModel.status.content.asSafeMarkdownAttributedString - SelectTextView(content: content) + StatusRowSelectableTextView(content: content) } .environment( StatusDataControllerProvider.shared.dataController(for: viewModel.finalStatus, diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowActionsView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowActionsView.swift index 14685d14..c267f7e9 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowActionsView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowActionsView.swift @@ -10,12 +10,15 @@ struct StatusRowActionsView: View { @Environment(CurrentAccount.self) private var currentAccount @Environment(StatusDataController.self) private var statusDataController @Environment(UserPreferences.self) private var userPreferences + @Environment(Client.self) private var client + @Environment(SceneDelegate.self) private var sceneDelegate @Environment(\.openWindow) private var openWindow @Environment(\.isStatusFocused) private var isFocused @Environment(\.horizontalSizeClass) var horizontalSizeClass @State private var showTextForSelection: Bool = false + @State private var isShareAsImageSheetPresented: Bool = false @Binding var isBlockConfirmationPresented: Bool @@ -190,7 +193,8 @@ struct StatusRowActionsView: View { Menu { StatusRowContextMenu(viewModel: viewModel, showTextForSelection: $showTextForSelection, - isBlockConfirmationPresented: $isBlockConfirmationPresented) + isBlockConfirmationPresented: $isBlockConfirmationPresented, + isShareAsImageSheetPresented: $isShareAsImageSheetPresented) .onAppear { Task { await viewModel.loadAuthorRelationship() @@ -215,7 +219,36 @@ struct StatusRowActionsView: View { .fixedSize(horizontal: false, vertical: true) .sheet(isPresented: $showTextForSelection) { let content = viewModel.status.reblog?.content.asSafeMarkdownAttributedString ?? viewModel.status.content.asSafeMarkdownAttributedString - SelectTextView(content: content) + StatusRowSelectableTextView(content: content) + .tint(theme.tintColor) + } + .sheet(isPresented: $isShareAsImageSheetPresented) { + let view = + HStack { + StatusRowView(viewModel: viewModel, context: .timeline) + .padding(8) + } + .environment(\.isInCaptureMode, true) + .environment(RouterPath()) + .environment(QuickLook.shared) + .environment(theme) + .environment(client) + .environment(sceneDelegate) + .environment(UserPreferences.shared) + .environment(CurrentAccount.shared) + .environment(CurrentInstance.shared) + .environment(statusDataController) + .preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light) + .foregroundColor(theme.labelColor) + .background(theme.primaryBackgroundColor) + .frame(width: sceneDelegate.windowWidth - 12) + .tint(theme.tintColor) + let renderer = ImageRenderer(content: AnyView(view)) + renderer.isOpaque = true + renderer.scale = 3.0 + return StatusRowShareAsImageView(viewModel: viewModel, + renderer: renderer) + .tint(theme.tintColor) } } diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowCardView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowCardView.swift index e23d830f..de19bbb0 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowCardView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowCardView.swift @@ -9,7 +9,6 @@ import SwiftUI public struct StatusRowCardView: View { @Environment(\.openURL) private var openURL @Environment(\.openWindow) private var openWindow - @Environment(\.isInCaptureMode) private var isInCaptureMode: Bool @Environment(\.isCompact) private var isCompact: Bool @Environment(Theme.self) private var theme @@ -106,7 +105,7 @@ public struct StatusRowCardView: View { @ViewBuilder private func defaultLinkPreview(_ title: String, _ url: URL) -> some View { - if let imageURL = card.image, !isInCaptureMode { + if let imageURL = card.image { DefaultPreviewImage(url: imageURL, originalWidth: card.width, originalHeight: card.height) } @@ -134,7 +133,7 @@ public struct StatusRowCardView: View { private func compactLinkPreview(_ title: String, _ url: URL) -> some View { HStack(alignment: .top) { - if let imageURL = card.image, !isInCaptureMode { + if let imageURL = card.image { LazyResizableImage(url: imageURL) { state, _ in if let image = state.image { image @@ -212,7 +211,7 @@ public struct StatusRowCardView: View { private func iconLinkPreview(_ title: String, _ url: URL) -> some View { // ..where the image is known to be a square icon HStack { - if let imageURL = card.image, !isInCaptureMode { + if let imageURL = card.image { LazyResizableImage(url: imageURL) { state, _ in if let image = state.image { image diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContextMenu.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContextMenu.swift index f7dc6d0d..0652c4d5 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContextMenu.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowContextMenu.swift @@ -6,7 +6,6 @@ import SwiftUI @MainActor struct StatusRowContextMenu: View { - @Environment(\.displayScale) var displayScale @Environment(\.openWindow) var openWindow @Environment(Client.self) private var client @@ -21,6 +20,7 @@ struct StatusRowContextMenu: View { var viewModel: StatusRowViewModel @Binding var showTextForSelection: Bool @Binding var isBlockConfirmationPresented: Bool + @Binding var isShareAsImageSheetPresented: Bool var boostLabel: some View { if viewModel.status.visibility == .priv, viewModel.status.account.id == account.account?.id { @@ -101,30 +101,7 @@ struct StatusRowContextMenu: View { } Button { - let view = HStack { - StatusRowView(viewModel: viewModel, context: .timeline) - .padding(16) - } - .environment(\.isInCaptureMode, true) - .environment(Theme.shared) - .environment(preferences) - .environment(account) - .environment(currentInstance) - .environment(SceneDelegate()) - .environment(quickLook) - .environment(viewModel.client) - .environment(RouterPath()) - .preferredColorScheme(Theme.shared.selectedScheme == .dark ? .dark : .light) - .foregroundColor(Theme.shared.labelColor) - .background(Theme.shared.primaryBackgroundColor) - .frame(width: sceneDelegate.windowWidth - 12) - .tint(Theme.shared.tintColor) - let renderer = ImageRenderer(content: view) - renderer.scale = displayScale - renderer.isOpaque = false - if let image = renderer.uiImage { - viewModel.routerPath.presentedSheet = .shareImage(image: image, status: viewModel.status) - } + isShareAsImageSheetPresented = true } label: { Label("status.action.share-image", systemImage: "photo") } @@ -283,60 +260,3 @@ struct StatusRowContextMenu: View { } } } - -struct ActivityView: UIViewControllerRepresentable { - let image: Image - - func makeUIViewController(context _: UIViewControllerRepresentableContext) -> UIActivityViewController { - UIActivityViewController(activityItems: [image], applicationActivities: nil) - } - - func updateUIViewController(_: UIActivityViewController, context _: UIViewControllerRepresentableContext) {} -} - -struct SelectTextView: View { - @Environment(\.dismiss) private var dismiss - let content: AttributedString - - var body: some View { - NavigationStack { - SelectableText(content: content) - .padding() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - dismiss() - } label: { - Text("action.done").bold() - } - } - } - .background(Theme.shared.primaryBackgroundColor) - .navigationTitle("status.action.select-text") - .navigationBarTitleDisplayMode(.inline) - } - } -} - -struct SelectableText: UIViewRepresentable { - let content: AttributedString - - func makeUIView(context _: Context) -> UITextView { - let attributedText = NSMutableAttributedString(content) - attributedText.addAttribute( - .font, - value: Font.scaledBodyFont, - range: NSRange(location: 0, length: content.characters.count) - ) - - let textView = UITextView() - textView.isEditable = false - textView.attributedText = attributedText - textView.textColor = UIColor(Color.label) - textView.backgroundColor = UIColor(Theme.shared.primaryBackgroundColor) - return textView - } - - func updateUIView(_: UITextView, context _: Context) {} - func makeCoordinator() {} -} diff --git a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowHeaderView.swift b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowHeaderView.swift index 4b99f711..3de75630 100644 --- a/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowHeaderView.swift +++ b/Packages/StatusKit/Sources/StatusKit/Row/Subviews/StatusRowHeaderView.swift @@ -6,7 +6,6 @@ import SwiftUI @MainActor struct StatusRowHeaderView: View { - @Environment(\.isInCaptureMode) private var isInCaptureMode: Bool @Environment(\.isStatusFocused) private var isFocused @Environment(\.redactionReasons) private var redactionReasons diff --git a/Packages/StatusKit/Sources/StatusKit/Share/StatusRowSelectableTextView.swift b/Packages/StatusKit/Sources/StatusKit/Share/StatusRowSelectableTextView.swift new file mode 100644 index 00000000..829a82a8 --- /dev/null +++ b/Packages/StatusKit/Sources/StatusKit/Share/StatusRowSelectableTextView.swift @@ -0,0 +1,56 @@ +import SwiftUI +import DesignSystem + +struct StatusRowSelectableTextView: View { + @Environment(\.dismiss) private var dismiss + @Environment(Theme.self) private var theme + + let content: AttributedString + + var body: some View { + NavigationStack { + SelectableText(content: content) + .padding() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + dismiss() + } label: { + Text("action.done").bold() + } + } + } + .navigationTitle("status.action.select-text") + .navigationBarTitleDisplayMode(.inline) + } + .presentationBackground(.ultraThinMaterial) + .presentationCornerRadius(16) + } +} + +fileprivate struct SelectableText: UIViewRepresentable { + let content: AttributedString + + func makeUIView(context _: Context) -> UITextView { + let attributedText = NSMutableAttributedString(content) + attributedText.addAttribute( + .font, + value: Font.scaledBodyFont, + range: NSRange(location: 0, length: content.characters.count) + ) + + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.isEditable = false + textView.isScrollEnabled = true + textView.attributedText = attributedText + textView.textColor = UIColor(Color.label) + textView.select(textView) + textView.selectedRange = .init(location: 0, length: attributedText.string.utf8.count) + textView.backgroundColor = .clear + return textView + } + + func updateUIView(_: UITextView, context _: Context) {} + func makeCoordinator() {} +} diff --git a/Packages/StatusKit/Sources/StatusKit/Share/StatusRowShareAsImageView.swift b/Packages/StatusKit/Sources/StatusKit/Share/StatusRowShareAsImageView.swift new file mode 100644 index 00000000..522943cf --- /dev/null +++ b/Packages/StatusKit/Sources/StatusKit/Share/StatusRowShareAsImageView.swift @@ -0,0 +1,56 @@ +import SwiftUI +import Env +import Network +import DesignSystem + +struct StatusRowShareAsImageView: View { + @Environment(\.dismiss) private var dismiss + @Environment(Theme.self) private var theme + + let viewModel: StatusRowViewModel + @StateObject var renderer: ImageRenderer + + var rendererImage: Image { + Image(uiImage: renderer.uiImage ?? UIImage()) + } + + var body: some View { + NavigationStack { + Form { + Section { + Button { + viewModel.routerPath.presentedSheet = .shareImage(image: renderer.uiImage ?? UIImage(), + status: viewModel.status) + } label: { + Label("status.action.share-image", systemImage: "square.and.arrow.up") + } + } + #if !os(visionOS) + .listRowBackground(theme.primaryBackgroundColor.opacity(0.4)) + #endif + + Section { + rendererImage + .resizable() + .scaledToFit() + } + .listRowBackground(theme.primaryBackgroundColor) + } + .scrollContentBackground(.hidden) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + dismiss() + } label: { + Text("action.done") + .bold() + } + } + } + .navigationTitle("Share post as image") + .navigationBarTitleDisplayMode(.inline) + } + .presentationBackground(.ultraThinMaterial) + .presentationCornerRadius(16) + } +}