Files
swiftGrammar/AIGrammar/View/TranslateView.swift
2024-08-12 10:49:20 +08:00

387 lines
15 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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()
}