Initial commit
This commit is contained in:
@ -6,13 +6,381 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
import ToastUI
|
||||
|
||||
struct TranslateView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user