// // TranslateView.swift // AIGrammar // // Created by oscar on 2024/3/27. // import SwiftUI import AVFoundation import ToastUI struct SubmitTextEditor: UIViewRepresentable { @Binding var text: String var onCommit: () -> Void func makeUIView(context: Context) -> UITextView { let textView = UITextView() textView.delegate = context.coordinator textView.font = UIFont.preferredFont(forTextStyle: .body) textView.isScrollEnabled = true textView.returnKeyType = .send // 设置键盘的返回键类型为发送 textView.enablesReturnKeyAutomatically = true textView.backgroundColor = UIColor.white.withAlphaComponent(0.5) // 设置背景色为半透明的白色 textView.layer.cornerRadius = 10 // 设置圆角 textView.layer.borderColor = UIColor.blue.cgColor // 设置边框颜色为蓝色 textView.layer.borderWidth = 1 // 设置边框宽度 return textView } func updateUIView(_ textView: UITextView, context: Context) { textView.text = text } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UITextViewDelegate { var parent: SubmitTextEditor init(_ parent: SubmitTextEditor) { self.parent = parent } func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { if text == "\n" { // 检测到换行符,即用户按下了发送键 parent.onCommit() textView.resignFirstResponder() // 收起键盘 return false } return true } func textViewDidChange(_ textView: UITextView) { parent.text = textView.text } } } struct TranslateView: View { @EnvironmentObject var globalEnv: GlobalEnvironment // 引入环境对象 @State private var inputText = "" @State private var translations: [Translation] = [ Translation(input: "This is demo text", translation: "这是演示文本"), ] @State private var showMenu = false // 控制下拉菜单的显示 @FocusState private var isInputFocused: Bool // 状态变量来控制TextEditor的聚焦 @State private var currentLang = "Chinese" // 用来显示当前语言 @State private var langCode = "chs" // 默认语言代码 @State private var isLoading = false // 控制加载指示器的显示 @State private var showingToast = false // 控制是否显示toast @State private var toastText = "" var body: some View { NavigationView { ZStack { // 设置背景色 Color.pink.opacity(0.2).edgesIgnoringSafeArea(.all) VStack { ScrollViewReader { scrollView in ScrollView { VStack(spacing: 10) { ForEach(translations) { translation in TranslateCardView(translation: translation, onEdit: onEdit, onCopy: onCopy, onFeedback: onFeedBack) .id(translation.id) // Assign an ID to each card view } } .padding() } .onChange(of: translations) { _ in if let lastId = translations.last?.id { withAnimation { scrollView.scrollTo(lastId, anchor: .bottom) // Scroll to the last translation } } } } SubmitTextEditor(text: $inputText, onCommit: { // 清除输入内容前后的空格 let trimmedText = inputText.trimmingCharacters(in: .whitespacesAndNewlines) // 检查是否为空 if trimmedText.isEmpty { showingToast = true toastText = "Please enter some text." return // 提前返回,不执行网络请求 } // 检查长度是否超过200个字符 if trimmedText.count > globalEnvironment.MaxLenTranslate { showingToast = true toastText = "Input too long. Please re-enter the text." return // 提前返回,不执行网络请求 } // 检查是否含有非法字符 /* if trimmedText.range(of: "[^a-zA-Z0-9 .,;:!?'\"@-]", options: .regularExpression) != nil { showingToast = true toastText = "Please enter valid characters." return // 提前返回,不执行网络请求 }*/ if currentLang == "Chinese" { let checkRes = CommonFunc.shared.validateInputEng(input: trimmedText) if !checkRes.isValid { showingToast = true toastText = checkRes.message return } } else { let checkRes = CommonFunc.shared.validateChineseInput(input: trimmedText) if !checkRes.isValid { showingToast = true toastText = checkRes.message return } } loadData() NetworkManager.shared.translate(inputText: trimmedText, lang: langCode) { result in switch result { case .success(let translation): addTranslation(output: translation.translation) logger.info("translate succ.", context: ["input": inputText, "output":translation.translation, "lang":langCode]) case .failure(let error): switch error { case .businessError(let ret, let message): // 业务错误,比如无免费可用次数等,需要处理逻辑。 logger.error("Business error - Ret: \(ret), Message: \(message)") DispatchQueue.main.async { showingToast = true switch ret{ case globalEnvironment.RetCodeFreeLimited: toastText = globalEnvironment.FreeLimitedToast case globalEnvironment.RetCodeDirtyInput: toastText = globalEnvironment.DirtyInputErrToast default: toastText = globalEnvironment.OtherServerErrToast } } case .other(let error): logger.error("network error occurred: \(error.localizedDescription)") DispatchQueue.main.async { showingToast = true toastText = globalEnvironment.NetWorkErrToast } } } loadComplete() } }) .frame(height: 100) // 指定高度 .padding() .focused($isInputFocused) } .toast(isPresented: $showingToast, dismissAfter: globalEnvironment.toastPresentMsNormal) { HStack { Image(systemName: "exclamationmark.bubble") .foregroundColor(.yellow) Text(toastText) .foregroundColor(.black) } .padding() .background(Color.white) .cornerRadius(8) .shadow(radius: 10) } // 加载指示器 if isLoading { LoadingView() } } .onTapGesture { // 点击任何非TextField区域时收起键盘 self.hideKeyboard() } .navigationBarTitle("AI Translate", displayMode: .inline) .navigationBarItems(trailing: Menu(currentLang) { Button("English -> Chinese") { currentLang = "Chinese" langCode = "chs" } Button("Chinese -> English") { currentLang = "English" langCode = "eng" } }) } } // 模拟加载数据 func loadData() { isLoading = true } func loadComplete(){ isLoading = false } private func onEdit(text : String){ self.inputText = text self.isInputFocused = true } private func onCopy(text : String){ UIPasteboard.general.string = text } private func onFeedBack( feedback: TranslateFeedback){ let fb:Bool = feedback.good ? true : false NetworkManager.shared.sendFeedback(input: feedback.input, output: feedback.translation, isPositive: fb) } private func addTranslation(output: String) { let newTranslation = Translation(input: inputText, translation: output) translations.append(newTranslation) if translations.count > 10 { translations.removeFirst() } inputText = "" // 清空输入框 } private func hideKeyboard() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } /* struct Translation: Identifiable, Equatable { let id = UUID() var input: String var translation: String } */ struct TranslateFeedback { var input: String var translation: String var good: Bool = false var bad: Bool = false } struct TranslateCardView: View { var translation: Translation var onEdit: (String) -> Void var onCopy: (String) -> Void var onFeedback: (TranslateFeedback) -> Void let synthesizer = AVSpeechSynthesizer() var body: some View { VStack { // 上半部分 VStack(alignment: .leading) { Text("Input") .font(.system(size: UIFont.systemFontSize * 0.8)) .foregroundColor(Color.black.opacity(0.6)) .padding(.top, 0) // 留出顶部间距 .padding(.bottom,3) // 减少与原文之间的间距 Text(translation.input) .padding(.bottom, 3) HStack { /* Button(action: { speakText(translation.input) }) { Image(systemName: "speaker.wave.2") .foregroundColor(.blue) .padding(.trailing) } */ Spacer() Button(action: { onEdit(translation.input) }) { Image(systemName: "square.and.pencil") .foregroundColor(.gray) .frame(width: 15, height: 15) // 缩小图标大小 } } } .padding([.horizontal, .top]) .padding(.bottom, 3) // 减少与原文之间的间距 Divider().dashed() // 下半部分 VStack(alignment: .leading) { Text(translation.translation) .padding(.vertical, 1) // 减少与分割线的间距 .padding(.bottom, 3) // 减少与原文之间的间距 HStack { /* Button(action: { speakText(translation.translation) }) { Image(systemName: "speaker.wave.2") .foregroundColor(.blue) } */ Spacer() HStack(spacing: 15) {// 减小图标之间的间距 Button(action: { onCopy(translation.translation) }) { Image(systemName: "doc.on.doc") .resizable() .scaledToFit() .frame(width: 15, height: 15) // 缩小图标大小 } Button(action: { onFeedback(TranslateFeedback(input: translation.input, translation: translation.translation, good: true)) }) { Image(systemName: "hand.thumbsup") .resizable() .scaledToFit() .frame(width: 15, height: 15) // 缩小图标大小 } Button(action: { onFeedback(TranslateFeedback(input: translation.input, translation: translation.translation, bad: true)) }) { Image(systemName: "hand.thumbsdown") .resizable() .scaledToFit() .frame(width: 15, height: 15) // 缩小图标大小 } } } .padding(.bottom, 1) // 减少与原文之间的间距 } .padding() } .padding(0) .background(Color.gray.opacity(0.2)) // 设置整个卡片的背景色为LightGray .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1)) } func speakText(_ text: String) { let utterance = AVSpeechUtterance(string: text) utterance.voice = AVSpeechSynthesisVoice(language: "en-US") synthesizer.speak(utterance) } } extension View { func dashed() -> some View { self.overlay( Rectangle() .fill(Color.clear) //.border( StrokeStyle(lineWidth: 1, dash: [5])) ) } } #Preview { TranslateView() }