Initial commit

This commit is contained in:
oscarz
2024-08-12 10:49:20 +08:00
parent 3002510aaf
commit 00fd0adf89
331 changed files with 53210 additions and 130 deletions

View File

@ -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
}
}

View File

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

View File

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

View File

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

View File

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