Initial commit
This commit is contained in:
@ -6,13 +6,174 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ToastUI
|
||||
|
||||
struct GrammarCheckView: View {
|
||||
fileprivate struct BuyProView: View {
|
||||
var onTryForFree: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Update to Pro")
|
||||
.font(.headline)
|
||||
|
||||
Text("Remove Character limits and ads. Use Grammar Check with higher quality.")
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Try for free") {
|
||||
// 按钮动作
|
||||
// 触发显示VIP付费界面的事件
|
||||
onTryForFree()
|
||||
}
|
||||
.padding(.vertical, 6) // 调整高度为默认高度的2/3
|
||||
.padding(.horizontal, 20)
|
||||
.background(Color.green)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(5)
|
||||
.font(.headline) // 调整字体大小为标题大小
|
||||
.alignmentGuide(.bottom) { d in d[.bottom] } // 对齐按钮的底部到 VStack 的底部
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(5)
|
||||
.padding(5)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
GrammarCheckView()
|
||||
enum ActiveSheet {
|
||||
case camera, share
|
||||
}
|
||||
extension ActiveSheet: Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
|
||||
struct GrammarCheckView: View {
|
||||
@EnvironmentObject var globalEnv: GlobalEnvironment // 引入环境对象
|
||||
|
||||
// 定义变量
|
||||
@State private var textInput: String
|
||||
@State private var inputResult : String
|
||||
@State private var progressValue: Float = 0
|
||||
@State private var showKeyboard: Bool = false
|
||||
@State private var showBuyProView: Bool = true
|
||||
@State private var showResult : Bool = false
|
||||
@State var results : [GrammarRes]
|
||||
@State private var showVIPPaymentView: Bool = false // 控制VIP付费界面的显示
|
||||
|
||||
// 提交等待,错误提示等
|
||||
@State private var isLoading = false // 控制加载指示器的显示
|
||||
@State private var showingToast = false // 控制是否显示toast
|
||||
@State private var toastText = ""
|
||||
|
||||
@State private var activeSheet: ActiveSheet?
|
||||
|
||||
@State private var isTextEditorFocused: Bool = false // 这个变量会传递给 InputView
|
||||
|
||||
// 使用默认文本进行初始化
|
||||
init() {
|
||||
let demoGrammarData = GrammarData.demoInstance()
|
||||
self.textInput = demoGrammarData.inputText
|
||||
self.inputResult = demoGrammarData.correctText
|
||||
self.results = demoGrammarData.results
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
Color.pink.opacity(0.2).edgesIgnoringSafeArea(.all) // 设置页面背景色
|
||||
|
||||
VStack {
|
||||
if showResult {
|
||||
ResultView(textContent: $textInput, results: $results, showResult: $showResult, showKeyboard: $showKeyboard)
|
||||
} else {
|
||||
InputView(textInput: $textInput, progressValue: $progressValue, showKeyboard: $showKeyboard, showBuyProView: $showBuyProView, showResult: $showResult, results: $results, isLoading: $isLoading, showingToast: $showingToast, toastText: $toastText)
|
||||
// .environment(\.isTextEditorFocused, $isTextEditorFocused) // 将焦点状态传递给 InputView
|
||||
}
|
||||
if showBuyProView {
|
||||
BuyProView(onTryForFree: {
|
||||
showVIPPaymentView = true // 显示VIP付费界面
|
||||
})
|
||||
//BuyProView()
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showVIPPaymentView) {
|
||||
VIPPaymentView() // 弹出VIP付费界面
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
Text("Grammar & Spell Checker")
|
||||
.font(.headline) // 设置标题字体
|
||||
if globalEnv.isVip {
|
||||
Image("vipimg") // 示例中使用系统图标
|
||||
.resizable() // 使图片可调整大小
|
||||
.scaledToFit() // 保持图片的宽高比
|
||||
.frame(width: 24, height: 24) // 设置图标的具体尺寸
|
||||
.foregroundColor(.yellow)
|
||||
.font(.subheadline) // 设置图标字体为比标题小
|
||||
.offset(y: -1) // 根据需要调整图标的垂直位置
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(.black)
|
||||
.navigationBarItems(leading: Button(action: {
|
||||
// 相机按钮动作
|
||||
self.activeSheet = .camera
|
||||
}) {
|
||||
Image(systemName: "camera")
|
||||
}, trailing: Button(action: {
|
||||
// 分享按钮动作
|
||||
self.activeSheet = .share
|
||||
}) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
})
|
||||
.sheet(item: $activeSheet, onDismiss: {
|
||||
// 如果需要在sheet关闭时执行某些操作,可以在这里添加
|
||||
}) { item in
|
||||
switch item {
|
||||
case .camera:
|
||||
CameraView(textInput: $textInput)
|
||||
case .share:
|
||||
ShareSheet(itemsToShare: [textInput])
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: 预览
|
||||
struct GrammarCheckView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GrammarCheckView()
|
||||
.environmentObject(IAPManager()) // 这里添加 IAPManager
|
||||
.environmentObject(globalEnvironment) // 这里添加 IAPManager
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,15 +4,187 @@
|
||||
//
|
||||
// Created by oscar on 2024/7/9.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IAPView: View {
|
||||
struct VIPPaymentView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@State private var showingToast = false
|
||||
@State private var toastText = ""
|
||||
|
||||
let features = ["Grammar Check & Spelling Correction", "Words & Dictionary", "Translate"]
|
||||
let freeValues = ["2 Times / Day", "2 Times / Day", "2 Times / Day"]
|
||||
let premiumValues = ["Unlimited", "Unlimited", "Unlimited"]
|
||||
let products = [
|
||||
("Premium Weekly", "$4.99", "Billed Weekly", "weekly", IAPProduct.premiumFeature1),
|
||||
("Premium Monthly", "$9.99 / month", "Billed Monthly", "monthly", IAPProduct.premiumFeature2),
|
||||
("Premium Yearly", "$49.99 / year", "Billed Yearly", "yearly", IAPProduct.premiumFeature3)
|
||||
]
|
||||
|
||||
@State private var selectedProductIndex: Int = 1 // 默认选择第二个商品
|
||||
@State private var selectedPlanText: String = "Billed Monthly"
|
||||
|
||||
@EnvironmentObject var iapManager: IAPManager // 确保在上级视图中已提供 IAPManager
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
NavigationView {
|
||||
VStack {
|
||||
HStack {
|
||||
Text("PREMIUM")
|
||||
.bold()
|
||||
.foregroundColor(Color(red: 1.0, green: 0.8, blue: 0.0)) // 自定义更亮的黄色
|
||||
//.foregroundColor(.yellow)
|
||||
.padding()
|
||||
.background(Color.orange)
|
||||
.cornerRadius(20)
|
||||
Spacer()
|
||||
Button("Close") {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
List {
|
||||
Section(header: Text("Get Premium Features").font(.headline)) {
|
||||
HStack {
|
||||
Text("Features")
|
||||
.frame(width: 120, alignment: .leading)
|
||||
Spacer()
|
||||
Text("Free")
|
||||
.frame(width: 120, alignment: .center)
|
||||
Spacer()
|
||||
Text("Premium")
|
||||
.frame(width: 80, alignment: .center)
|
||||
}
|
||||
.font(.headline)
|
||||
|
||||
ForEach(Array(zip(features, zip(freeValues, premiumValues))), id: \.0) { item in
|
||||
HStack {
|
||||
Text(item.0)
|
||||
.frame(width: 120, alignment: .leading)
|
||||
Spacer()
|
||||
Text(item.1.0) // Free values
|
||||
.frame(width: 120, alignment: .center)
|
||||
Spacer()
|
||||
Text(item.1.1) // Premium values
|
||||
.frame(width: 80, alignment: .center)
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(products.indices, id: \.self) { index in
|
||||
Button(action: {
|
||||
// do someting
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: selectedProductIndex == index ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundColor(selectedProductIndex == index ? .green : .secondary)
|
||||
.padding(.horizontal, 4)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
if products[index].0 == "Premium Yearly" {
|
||||
// 特别为 "Premium Yearly" 添加图标和折扣信息
|
||||
Text(products[index].0) + Text(" ⚡️") + Text(" 58% off")
|
||||
.bold()
|
||||
.foregroundColor(.red) // 折扣信息使用红色
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
Text(products[index].0)
|
||||
}
|
||||
Text(products[index].1)
|
||||
.font(.subheadline)
|
||||
}
|
||||
Spacer()
|
||||
Text(products[index].2)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.background(self.selectedProductIndex == index ? Color.yellow : Color.clear)
|
||||
.cornerRadius(5)
|
||||
.padding(.horizontal, 20) // 增加水平缩进
|
||||
.onTapGesture {
|
||||
self.selectedProductIndex = index
|
||||
self.selectedPlanText = products[index].2
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Button("Purchase") {
|
||||
// Handle purchase logic here
|
||||
buyProduct()
|
||||
}
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 80)
|
||||
.foregroundColor(.white)
|
||||
.background(Color.green)
|
||||
.cornerRadius(15)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.shadow(radius: 2) // 添加阴影效果
|
||||
.font(.headline) // 调整字体大小为标题大小
|
||||
.padding(.top, 50)
|
||||
|
||||
Text((selectedPlanText ) + ", Cancel Anytime")
|
||||
.font(.footnote)
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(.systemBackground))
|
||||
.toast(isPresented: $showingToast, dismissAfter: 5.0, onDismiss: {
|
||||
// Toast 消失后执行的动作
|
||||
presentationMode.wrappedValue.dismiss() // 关闭当前视图
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.bubble")
|
||||
.foregroundColor(.yellow)
|
||||
Text(toastText)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private func buyProduct() {
|
||||
let productId = products[selectedProductIndex].4
|
||||
logger.info("selected productId: \(productId.rawValue)")
|
||||
if let product = iapManager.products.first(where: { $0.id == productId.rawValue }) {
|
||||
Task {
|
||||
await iapManager.buy(product: product) { result in
|
||||
switch result {
|
||||
case .success(let message):
|
||||
self.toastText = message
|
||||
self.showingToast = true
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
|
||||
// 刷新用户的vip状态
|
||||
InitApp.shared.refreshUserInfo()
|
||||
case .failure(let error):
|
||||
self.toastText = error.localizedDescription
|
||||
self.showingToast = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error("Product not found")
|
||||
toastText = "Error loading product list, please try again later"
|
||||
self.showingToast = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IAPView()
|
||||
struct VIPPaymentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VIPPaymentView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,13 +6,226 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ToastUI
|
||||
|
||||
struct SettingsView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
import SafariServices
|
||||
|
||||
struct FullScreenSafariView: UIViewControllerRepresentable {
|
||||
let url: URL
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<FullScreenSafariView>) -> SFSafariViewController {
|
||||
let safariViewController = SFSafariViewController(url: url)
|
||||
safariViewController.delegate = context.coordinator
|
||||
return safariViewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<FullScreenSafariView>) {
|
||||
// 无需更新
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, SFSafariViewControllerDelegate {
|
||||
var parent: FullScreenSafariView
|
||||
|
||||
init(_ parent: FullScreenSafariView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
|
||||
parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var globalEnv: GlobalEnvironment // 引入环境对象
|
||||
|
||||
@State private var isLoading = false // 控制加载指示器的显示
|
||||
@State private var showingToast = false // 控制是否显示toast
|
||||
@State private var toastText = ""
|
||||
@State private var showVIPPaymentView = false
|
||||
|
||||
@EnvironmentObject var iapManager: IAPManager // 确保在上级视图中已提供 IAPManager
|
||||
|
||||
@State private var showingFullSafari = false
|
||||
|
||||
|
||||
@State private var showingAdvancedSettings = false // 控制高级设置显示的状态
|
||||
@State private var useSandboxEnvironment = false
|
||||
@State private var useDevelopmentEnvironment = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// Upgrade to Premium
|
||||
settingItem(icon: "arrow.up.circle", text: "Upgrade to Premium", isBold: true)
|
||||
.onTapGesture {
|
||||
showVIPPaymentView = true
|
||||
}
|
||||
|
||||
// Feedback
|
||||
settingItem(icon: "bubble.left", text: "Feedback")
|
||||
.onTapGesture {
|
||||
// Assuming there's a way to open App Store Feedback
|
||||
openAppStoreFeedback()
|
||||
}
|
||||
|
||||
// About
|
||||
settingItem(icon: "info.circle", text: "About")
|
||||
.onTapGesture {
|
||||
// Code to show About View
|
||||
self.showingFullSafari = true
|
||||
}
|
||||
|
||||
// Restore Purchases
|
||||
settingItem(icon: "arrow.clockwise.circle", text: "Restore Purchases", isBold: true)
|
||||
.onTapGesture {
|
||||
restorePurchase()
|
||||
}
|
||||
|
||||
}
|
||||
.fullScreenCover(isPresented: $showingFullSafari) {
|
||||
FullScreenSafariView(url: URL(string: globalEnvironment.userTermsURL)!, onDismiss: {
|
||||
self.showingFullSafari = false
|
||||
})
|
||||
}
|
||||
|
||||
// 留出手势操作的空间,但UI上隐藏掉
|
||||
gestureArea
|
||||
|
||||
if showingAdvancedSettings {
|
||||
VStack {
|
||||
Toggle("Sandbox", isOn: $useSandboxEnvironment)
|
||||
Toggle("TestEnv", isOn: $useDevelopmentEnvironment)
|
||||
Button("Save") {
|
||||
saveSettings()
|
||||
showingAdvancedSettings = false
|
||||
}
|
||||
}
|
||||
.padding(.vertical,10)
|
||||
.padding(.horizontal, 20)
|
||||
.background(Color.white) // Ensure background fills the view
|
||||
.cornerRadius(5)
|
||||
}
|
||||
|
||||
}
|
||||
.background(Color.pink.opacity(0.2)) // Ensure background fills the view
|
||||
.navigationBarTitle("Settings", displayMode: .inline)
|
||||
.fullScreenCover(isPresented: $showVIPPaymentView) {
|
||||
VIPPaymentView()
|
||||
}
|
||||
.onDisappear {
|
||||
self.showingAdvancedSettings = false // 确保隐藏组件在视图消失时不显示
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
// 定义隐藏功能
|
||||
private var gestureArea: some View {
|
||||
Color.clear
|
||||
.contentShape(Rectangle()) // 为透明色定义一个矩形可命中区域
|
||||
.frame(height: 150) // 可以根据需要调整手势区域的大小
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 50, coordinateSpace: .global)
|
||||
.onEnded { value in
|
||||
// 检查水平移动距离是否足够长,同时限制垂直移动不太大
|
||||
if abs(value.translation.width) > 50 && abs(value.translation.height) < 100 {
|
||||
// 切换高级设置的显示状态
|
||||
logger.info("Advanced Settings.")
|
||||
showingAdvancedSettings.toggle()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 实现保存设置的逻辑
|
||||
private func saveSettings() {
|
||||
globalEnvironment.SetEnv(isSandBox: useSandboxEnvironment, isTestEnv: useDevelopmentEnvironment)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func settingItem(icon: String, text: String, isBold: Bool = false) -> some View {
|
||||
HStack {
|
||||
Image(systemName: icon) // Icon on the left side
|
||||
.foregroundColor(.gray)
|
||||
Text(text)
|
||||
.font(.subheadline)
|
||||
.fontWeight(isBold ? .bold : .regular) // Conditional bold based on isBold parameter
|
||||
.padding(4)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white) // Background for each setting item
|
||||
.padding(.horizontal, 0) // Add some horizontal padding
|
||||
|
||||
// Indent divider to align with the text and extend to the edge
|
||||
Divider()
|
||||
.padding(.leading, 30)
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
|
||||
private func openAppStoreFeedback() {
|
||||
let appID = globalEnvironment.APPID // 替换为您的应用ID
|
||||
let urlStr = "https://apps.apple.com/app/id\(appID)?action=write-review"
|
||||
if let url = URL(string: urlStr) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func restorePurchase(){
|
||||
Task {
|
||||
await iapManager.restorePurchases() { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
self.toastText = "Restored Sucess!"
|
||||
self.showingToast = true
|
||||
case .failure(_):
|
||||
self.toastText = "Oh, something went wrong, please try again later."
|
||||
self.showingToast = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 模拟加载数据
|
||||
func loadData() {
|
||||
isLoading = true
|
||||
}
|
||||
func loadComplete(){
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -6,13 +6,211 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ToastUI
|
||||
|
||||
struct WordsView: View {
|
||||
@EnvironmentObject var globalEnv: GlobalEnvironment // 引入环境对象
|
||||
|
||||
@State private var searchText = ""
|
||||
@State private var showingResults = false
|
||||
@State private var res = WordDetails()
|
||||
@State private var isLoading = false // 控制加载指示器的显示
|
||||
@State private var showingToast = false // 控制是否显示toast
|
||||
@State private var toastText = ""
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
NavigationView {
|
||||
ZStack {
|
||||
// 设置背景色
|
||||
Color.pink.opacity(0.2).edgesIgnoringSafeArea(.all)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// 搜索框区域
|
||||
HStack {
|
||||
TextField("word", text: $searchText, onCommit: fetchWordData)
|
||||
.padding(.horizontal, 40)
|
||||
.padding(.vertical, 10)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
.font(.headline)
|
||||
.overlay(
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.gray)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading).padding(.leading, 8)
|
||||
)
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
self.searchText = ""
|
||||
self.showingResults = false
|
||||
self.hideKeyboard()
|
||||
}
|
||||
}
|
||||
.background(Color(.systemPink).opacity(0.2))
|
||||
.cornerRadius(5) // 边框圆角
|
||||
.shadow(radius: 2) // 轻微阴影
|
||||
.padding(8)
|
||||
|
||||
|
||||
// 使用 Spacer 来推动搜索框保持在顶部
|
||||
if !showingResults {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
if showingResults {
|
||||
// 结果显示区域
|
||||
HStack {
|
||||
List {
|
||||
Section(header: Text("Definitions").font(.headline)) {
|
||||
ForEach(res.explanations, id: \.self) { definition in
|
||||
Text(definition)
|
||||
.font(.system(.subheadline))
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Common Phrases").font(.headline)) {
|
||||
ForEach(res.phrases, id: \.self) { phrase in
|
||||
Text(phrase)
|
||||
.font(.system(.subheadline))
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Synonyms").font(.system(size: UIFont.systemFontSize * 1.2))) {
|
||||
ForEach(res.synonyms, id: \.self) { synonym in
|
||||
Text(synonym)
|
||||
.font(.system(.subheadline))
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.cornerRadius(5) // 边框圆角
|
||||
.frame(maxHeight: .infinity) // 限制ErrorCard占用半个屏幕 */
|
||||
}
|
||||
.listStyle(GroupedListStyle()) // 使用分组列表样式,以适应背景
|
||||
}
|
||||
}
|
||||
.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("Words", displayMode: .inline)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func hideKeyboard() {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
|
||||
// 模拟加载数据
|
||||
func loadData() {
|
||||
isLoading = true
|
||||
}
|
||||
func loadComplete(){
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private func fetchWordData() {
|
||||
// 清除输入内容前后的空格
|
||||
let trimmedText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// 检查是否为空
|
||||
if trimmedText.isEmpty {
|
||||
showingToast = true
|
||||
toastText = "Please enter some text."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
|
||||
// 检查长度是否超过200个字符
|
||||
if trimmedText.count > globalEnvironment.MaxLenWords {
|
||||
showingToast = true
|
||||
toastText = "Input too long. Please re-enter the text."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
|
||||
// 检查是否含有非法字符
|
||||
let checkRes = CommonFunc.shared.validateInputEng(input: trimmedText, isSingleWord: true)
|
||||
if !checkRes.isValid {
|
||||
showingToast = true
|
||||
toastText = checkRes.message
|
||||
return
|
||||
}
|
||||
/*
|
||||
if trimmedText.range(of: "[^a-zA-Z]", options: .regularExpression) != nil {
|
||||
showingToast = true
|
||||
toastText = "Please enter valid characters."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
*/
|
||||
|
||||
loadData() // 添加提交的提示
|
||||
NetworkManager.shared.fetchWordDetails(inputText: trimmedText) { result in
|
||||
switch result {
|
||||
case .success(let wordDetails):
|
||||
DispatchQueue.main.async {
|
||||
res = wordDetails
|
||||
showingResults = true
|
||||
}
|
||||
logger.info("fetch words succ.", context: ["word":wordDetails.word, "explanations":wordDetails.explanations, "phrases":wordDetails.phrases, "synonyms":wordDetails.synonyms])
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WordsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WordsView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#Preview {
|
||||
WordsView()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user