From 2e2c213ccd85437c651c68f92568de05ce2962e8 Mon Sep 17 00:00:00 2001 From: iHTCboy Date: Wed, 5 Apr 2023 22:55:38 +0800 Subject: [PATCH] Add screenshot share image and pdf --- iChatGPT.xcodeproj/project.pbxproj | 8 +- iChatGPT/AIChatView.swift | 203 ++++++++++++++++++------- iChatGPT/ChatHistoryListView.swift | 4 +- iChatGPT/ChatInputView.swift | 10 ++ iChatGPT/Models/AIChatInputModel.swift | 2 +- 5 files changed, 166 insertions(+), 61 deletions(-) diff --git a/iChatGPT.xcodeproj/project.pbxproj b/iChatGPT.xcodeproj/project.pbxproj index f92b3da..645a045 100644 --- a/iChatGPT.xcodeproj/project.pbxproj +++ b/iChatGPT.xcodeproj/project.pbxproj @@ -569,7 +569,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2023.4.03; + CURRENT_PROJECT_VERSION = 2023.04.05; DEVELOPMENT_ASSET_PATHS = "\"iChatGPT/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; @@ -587,7 +587,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.iChatGPT; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -609,7 +609,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 2023.4.03; + CURRENT_PROJECT_VERSION = 2023.04.05; DEVELOPMENT_ASSET_PATHS = "\"iChatGPT/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; @@ -627,7 +627,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3; + MARKETING_VERSION = 2.4; PRODUCT_BUNDLE_IDENTIFIER = com.37iOS.iChatGPT; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/iChatGPT/AIChatView.swift b/iChatGPT/AIChatView.swift index 8ee8696..fa79939 100644 --- a/iChatGPT/AIChatView.swift +++ b/iChatGPT/AIChatView.swift @@ -13,61 +13,15 @@ struct AIChatView: View { @State private var isScrollListTop: Bool = false @State private var isSettingsPresented: Bool = false + @State private var isSharing = false @StateObject private var chatModel = AIChatModel(roomID: ChatRoomStore.shared.lastRoomId()) @StateObject private var inputModel = AIChatInputModel() + @StateObject private var shareContent = ShareContent() var body: some View { NavigationView { VStack { - ScrollViewReader { proxy in - List { - ForEach(chatModel.contents, id: \.datetime) { item in - Section(header: Text(item.datetime)) { - VStack(alignment: .leading) { - HStack(alignment: .top) { - AvatarImageView(url: item.userAvatarUrl) - MarkdownText(item.issue.replacingOccurrences(of: "\n", with: "\n\n")) - .padding(.top, 3) - } - Divider() - HStack(alignment: .top) { - Image("chatgpt-icon") - .resizable() - .frame(width: 25, height: 25) - .cornerRadius(5) - .padding(.trailing, 10) - if item.isResponse { - MarkdownText(item.answer ?? "") - } else { - ProgressView() - Text("Loading..".localized()) - .padding(.leading, 10) - } - } - .padding([.top, .bottom], 3) - }.contextMenu { - ChatContextMenu(searchText: $inputModel.searchText, chatModel: chatModel, item: item) - } - } - } - } - .listStyle(InsetGroupedListStyle()) - .onChange(of: chatModel.isScrollListBottom) { _ in - if let lastId = chatModel.contents.last?.datetime { - withAnimation { - proxy.scrollTo(lastId, anchor: .trailing) - } - } - } - .onChange(of: isScrollListTop) { _ in - if let firstId = chatModel.contents.first?.datetime { - withAnimation { - proxy.scrollTo(firstId, anchor: .leading) - } - } - } - } - + chatList Spacer() ChatInputView(searchText: $inputModel.searchText, chatModel: chatModel) .padding([.leading, .trailing], 12) @@ -79,12 +33,8 @@ struct AIChatView: View { .markdownOrderedListBulletStyle(.custom) .markdownUnorderedListBulletStyle(.custom) .markdownImageStyle(.custom) - .navigationTitle("OpenAI ChatGPT") .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(trailing: - HStack { - addButton - }) + .navigationBarItems(trailing: addButton) .sheet(isPresented: $isSettingsPresented) { ChatAPISettingView(isKeyPresented: $isSettingsPresented, chatModel: chatModel) } @@ -99,6 +49,9 @@ struct AIChatView: View { .sheet(isPresented: $inputModel.isConfigChatRoom) { ChatRoomConfigView(isKeyPresented: $inputModel.isConfigChatRoom) } + .sheet(isPresented: $isSharing) { + ActivityView(activityItems: $shareContent.activityItems) + } .alert(isPresented: $inputModel.showingAlert) { switch inputModel.activeAlert { case .createNewChatRoom: @@ -107,6 +60,8 @@ struct AIChatView: View { return ReloadLastQuestion() case .clearAllQuestion: return ClearAllQuestion() + case .shareContents: + return ShareContents() } } .onChange(of: inputModel.isScrollToChatRoomTop) { _ in @@ -126,6 +81,61 @@ struct AIChatView: View { .environmentObject(inputModel) } + @ViewBuilder + var chatList: some View { + ScrollViewReader { proxy in + List { + ForEach(chatModel.contents, id: \.datetime) { item in + Section(header: Text(item.datetime)) { + VStack(alignment: .leading) { + HStack(alignment: .top) { + AvatarImageView(url: item.userAvatarUrl) + MarkdownText(item.issue.replacingOccurrences(of: "\n", with: "\n\n")) + .padding(.top, 3) + } + Divider() + HStack(alignment: .top) { + Image("chatgpt-icon") + .resizable() + .frame(width: 25, height: 25) + .cornerRadius(5) + .padding(.trailing, 10) + if item.isResponse { + MarkdownText(item.answer ?? "") + } else { + ProgressView() + Text("Loading..".localized()) + .padding(.leading, 10) + } + } + .padding([.top, .bottom], 3) + }.contextMenu { + ChatContextMenu(searchText: $inputModel.searchText, chatModel: chatModel, item: item) + } + } + } + } + .listStyle(InsetGroupedListStyle()) + .onChange(of: chatModel.isScrollListBottom) { _ in + if let lastId = chatModel.contents.last?.datetime { + // try fix macOS crash + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + withAnimation { + proxy.scrollTo(lastId, anchor: .trailing) + } + } + } + } + .onChange(of: isScrollListTop) { _ in + if let firstId = chatModel.contents.first?.datetime { + withAnimation { + proxy.scrollTo(firstId, anchor: .leading) + } + } + } + } + } + private var addButton: some View { Button(action: { isSettingsPresented.toggle() @@ -136,7 +146,9 @@ struct AIChatView: View { } else { Image(systemName: "key.icloud").imageScale(.large) } - }.frame(height: 40) + } + .frame(height: 40) + .padding(.trailing, 5) } } } @@ -178,6 +190,87 @@ extension AIChatView { secondaryButton: .cancel() ) } + + func ShareContents() -> Alert { + Alert(title: Text("Share"), + message: Text("Choose a sharing format"), + primaryButton: .default(Text("Image")) { + screenshotAndShare(isImage: true) + }, + secondaryButton: .default(Text("PDF")) { + screenshotAndShare(isImage: false) + } + ) + } +} + + + +// MARK: - Handle Share Image/PDF +extension AIChatView { + + private func screenshotAndShare(isImage: Bool) { + if let image = screenshot() { + if isImage { + shareContent.activityItems = [image] + isSharing = true + } else { + if let pdfData = imageToPDFData(image: image) { + let temporaryDirectoryURL = FileManager.default.temporaryDirectory + let fileName = "iChatGPT-Screenshot.pdf" + let fileURL = temporaryDirectoryURL.appendingPathComponent(fileName) + + do { + try pdfData.write(to: fileURL, options: .atomic) + shareContent.activityItems = [fileURL] + isSharing = true + } catch { + print("Error writing PDF data to file: \(error)") + } + } + } + } + } + + private func screenshot() -> UIImage? { + let controller = UIHostingController(rootView: self) + let view = controller.view + + let targetSize = UIScreen.main.bounds.size + view?.frame = CGRect(origin: .zero, size: targetSize) + view?.backgroundColor = .clear + + let renderer = UIGraphicsImageRenderer(size: targetSize) + return renderer.image { _ in + view?.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true) + } + } + + private func imageToPDFData(image: UIImage) -> Data? { + let pdfRenderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, size: image.size)) + let pdfData = pdfRenderer.pdfData { (context) in + context.beginPage() + image.draw(in: CGRect(origin: .zero, size: image.size)) + } + return pdfData + } +} + +class ShareContent: ObservableObject { + @Published var activityItems: [Any] = [] +} + +// MARK: Render UIActivityViewController +struct ActivityView: UIViewControllerRepresentable { + @Binding var activityItems: [Any] + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIActivityViewController { + let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext) { + } } // MARK: Avatar Image View diff --git a/iChatGPT/ChatHistoryListView.swift b/iChatGPT/ChatHistoryListView.swift index 1a6d396..b4b2c03 100644 --- a/iChatGPT/ChatHistoryListView.swift +++ b/iChatGPT/ChatHistoryListView.swift @@ -91,7 +91,8 @@ struct ChatHistoryListView: View { Text(" \(ChatMessageStore.shared.messages(forRoom: item.roomID).count) ") .font(.footnote) .foregroundColor(.white) - .padding(5) + .padding([.top, .bottom], 3) + .padding([.leading, .trailing], 4) .background(Color.blue.opacity(0.8)) .clipShape(Capsule()) } @@ -101,6 +102,7 @@ struct ChatHistoryListView: View { Text(ChatMessageStore.shared.lastMessage(item.roomID)?.issue ?? "No conversations".localized()) .font(.subheadline) .foregroundColor(.gray) + .lineLimit(2) Spacer() diff --git a/iChatGPT/ChatInputView.swift b/iChatGPT/ChatInputView.swift index ff17173..bc0ba79 100644 --- a/iChatGPT/ChatInputView.swift +++ b/iChatGPT/ChatInputView.swift @@ -52,6 +52,16 @@ struct ChatInputView: View { .padding(.trailing, 5) .foregroundColor(.lightGray) .buttonStyle(PlainButtonStyle()) + + Button(action: { + model.activeAlert = .shareContents + model.showingAlert.toggle() + }) { + Image(systemName: "square.and.arrow.up") + } + .padding(.trailing, 5) + .foregroundColor(.lightGray) + .buttonStyle(PlainButtonStyle()) } Button(action: { diff --git a/iChatGPT/Models/AIChatInputModel.swift b/iChatGPT/Models/AIChatInputModel.swift index c088cd7..c31c4e0 100644 --- a/iChatGPT/Models/AIChatInputModel.swift +++ b/iChatGPT/Models/AIChatInputModel.swift @@ -9,7 +9,7 @@ import Foundation enum InputViewAlert { - case createNewChatRoom, reloadLastQuestion, clearAllQuestion + case createNewChatRoom, reloadLastQuestion, clearAllQuestion, shareContents } class AIChatInputModel: ObservableObject {