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

@ -1,8 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.in-app-payments</key>
<array/>
</dict>
<dict/>
</plist>

View File

@ -6,15 +6,25 @@
//
import SwiftUI
import TrustDecision
@main
struct AIGrammarApp: App {
let persistenceController = PersistenceController.shared
init() {
//
setupLogging()
_ = InitApp.shared
}
var body: some Scene {
WindowGroup {
ContentView()
AllTabView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(IAPManager()) // IAPManager
.environmentObject(globalEnvironment) // IAPManager
}
}
}

View File

@ -7,16 +7,8 @@
import SwiftUI
@main
struct LearningToolApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
struct AllTabView: View {
var body: some View {
TabView {
GrammarCheckView()
@ -46,35 +38,7 @@ struct ContentView: View {
}
}
struct GrammarCheckView: View {
var body: some View {
// Your Grammar Check View content goes here.
Text("Grammar Check")
}
}
struct WordsView: View {
var body: some View {
// Your Words View content goes here.
Text("Words")
}
}
struct TranslateView: View {
var body: some View {
// Your Translate View content goes here.
Text("Translate")
}
}
struct SettingsView: View {
var body: some View {
// Your Settings View content goes here.
Text("Settings")
}
}
#Preview {
AllTab()
AllTabView()
}

View File

@ -9,10 +9,16 @@ import SwiftUI
struct LoadingView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
ZStack {
Color.black.opacity(0.4).edgesIgnoringSafeArea(.all) // 使
ProgressView() // iOS 14+
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.scaleEffect(1.5) //
}
}
}
#Preview {
LoadingView()
}

View File

@ -6,13 +6,86 @@
//
import SwiftUI
import UIKit
import Vision
struct CameraView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
func performOCR(on uiImage: UIImage, completion: @escaping (String) -> Void) {
guard let cgImage = uiImage.cgImage else { return }
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
let request = VNRecognizeTextRequest { (request, error) in
guard let observations = request.results as? [VNRecognizedTextObservation] else { return }
let recognizedStrings = observations.compactMap { $0.topCandidates(1).first?.string }
completion(recognizedStrings.joined(separator: "\n"))
}
request.recognitionLanguages = ["en-US", "zh-Hans"] //
request.usesLanguageCorrection = true
do {
try handler.perform([request])
} catch {
print("OCR失败: \(error)")
}
}
#Preview {
CameraView()
struct CameraView: UIViewControllerRepresentable {
@Binding var textInput: String
@Environment(\.presentationMode) var presentationMode
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .camera
picker.allowsEditing = true //
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
var parent: CameraView
init(_ parent: CameraView) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
//
if let editedImage = info[.editedImage] as? UIImage {
// 使OCR
performOCR(on: editedImage) { recognizedText in
//
self.parent.textInput = recognizedText
self.parent.presentationMode.wrappedValue.dismiss()
}
} else if let originalImage = info[.originalImage] as? UIImage {
// 退使
performOCR(on: originalImage) { recognizedText in
self.parent.textInput = recognizedText
self.parent.presentationMode.wrappedValue.dismiss()
}
} else {
//
self.parent.presentationMode.wrappedValue.dismiss()
}
}
func imagePickerController2(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiImage = info[.originalImage] as? UIImage {
// OCR
performOCR(on: uiImage) { recognizedText in
//
self.parent.textInput = recognizedText
}
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}

View File

@ -6,13 +6,197 @@
//
import SwiftUI
import StoreKit
struct IAPTestView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
enum IAPProductTest: String, CaseIterable {
case premiumFeature1 = "grammar_1_month"
case premiumFeature2 = "grammar_1_week"
}
class IAPManagerTest: ObservableObject {
@Published var products: [Product] = []
@Published var purchasedProducts: [Product] = []
init() {
Task {
await requestProducts()
await updatePurchasedProducts()
await listenForTransactions()
}
}
func requestProducts() async {
do {
let products = try await Product.products(for: IAPProductTest.allCases.map { $0.rawValue })
DispatchQueue.main.async {
//
self.products = products
//
for product in self.products {
print("--------------------------")
print("Product ID: \(product.id)")
print("Product Title: \(product.displayName)")
print("Product Description: \(product.description)")
print("Product Price: \(product.price)")
print("Product displayPrice: \(product.displayPrice)")
print("Product priceFormatStyle: \(product.priceFormatStyle)")
print("Product subscriptionPeriodFormatStyle: \(product.subscriptionPeriodFormatStyle)")
print("Product subscriptionPeriodUnitFormatStyle: \(product.subscriptionPeriodUnitFormatStyle)")
print("--------------------------")
}
}
} catch {
print("Failed to fetch products: \(error.localizedDescription)")
}
}
func buy(product: Product) async {
do {
let uuid = UUID()
let token = Product.PurchaseOption.appAccountToken(uuid)
print("purchase appAccountToken: \(uuid.uuidString)")
let result = try await product.purchase(options: [token])
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
// listenForTransactions
//
print("Purchase initiated for product: \(product.id)")
}
case .userCancelled:
print("User cancelled the purchase.")
case .pending:
print("Purchase is pending.")
default:
break
}
} catch {
print("Failed to purchase product: \(error.localizedDescription)")
}
}
func updatePurchasedProducts() async {
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
if let product = products.first(where: { $0.id == transaction.productID }) {
DispatchQueue.main.async {
self.purchasedProducts.append(product)
}
}
}
}
}
func listenForTransactions() async {
for await transactionResult in Transaction.updates {
if case .verified(let transaction) = transactionResult {
if let product = products.first(where: { $0.id == transaction.productID }) {
DispatchQueue.main.async {
self.purchasedProducts.append(product)
}
}
//
print("Transaction ID: \(transaction.id)")
print("Product ID: \(transaction.productID)")
print("Purchase Date: \(transaction.purchaseDate)")
//print("Transaction State: \(transaction.revocationReason ?? "None")")
//print("Original Transaction ID: \(transaction.originalID ?? "None")")
//
// jsonRepresentation
let jsonData = try transaction.jsonRepresentation
// Data String
if let jsonString = String(data: jsonData, encoding: .utf8) {
print("Transaction Receipt: \(jsonString)")
} else {
print("Failed to convert JSON data to string.")
}
await transaction.finish()
}
}
}
func restorePurchases() async {
do {
try await AppStore.sync()
await updatePurchasedProducts()
print("Purchases restored")
} catch {
print("Failed to restore purchases: \(error.localizedDescription)")
}
}
}
struct IAPTestView: View {
@StateObject var iapManager = IAPManager()
//@StateObject var iapManager = IAPManagerTest()
var body: some View {
VStack(spacing: 20) {
if iapManager.products.isEmpty {
Text("Loading products...")
} else {
ForEach(iapManager.products, id: \.id) { product in
VStack {
Text(product.displayName)
.font(.title)
Button("Buy \(product.displayName)") {
Task {
await iapManager.buy(product: product){ result in
switch result {
case .success(let message):
logger.info("succ")
case .failure(let error):
logger.error("error")
}
}
}
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
Button("Restore Purchases") {
Task {
await iapManager.restorePurchases(){ result in
switch result {
case .success(let message):
logger.info("restore purchase succ. message: \(message)")
case .failure(let error):
logger.error("restore purchase error. message: \(error)")
}
}
}
}
.padding()
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(10)
}
}
.onAppear {
Task {
//await iapManager.requestProducts()
}
}
}
}
#Preview {
IAPTestView()
}

View File

@ -6,13 +6,224 @@
//
import SwiftUI
import ToastUI
fileprivate struct ProgressBar: View {
@Binding var value: Float
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(.gray.opacity(0.3))
.frame(width: geometry.size.width, height: geometry.size.height)
Rectangle()
.foregroundColor(Color.green)
.frame(width: min(CGFloat(self.value) * geometry.size.width, geometry.size.width), height: geometry.size.height)
.animation(.linear, value: value)
}
.cornerRadius(45.0)
}
}
}
struct InputView: View {
@Binding var textInput: String
@Binding var progressValue: Float
@Binding var showKeyboard: Bool
@Binding var showBuyProView: Bool
@Binding var showResult: Bool
@Binding var results : [GrammarRes]
@Binding var isLoading: Bool //
@Binding var showingToast: Bool // toast
@Binding var toastText: String
@FocusState private var isTextEditorFocused: Bool
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
VStack {
// TextEditor
TextEditor(text: $textInput)
.onTapGesture {
// TextEditor
self.showKeyboard = true //
self.showBuyProView = false // BuyProView
}
.focused($isTextEditorFocused)
.padding(5)
.background(Color.white)
.cornerRadius(5)
.padding(5)
.onAppear {
if showKeyboard {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.isTextEditorFocused = true
}
}
}
//Divider() // 线TextEditor
//
ProgressBar(value: $progressValue).frame(height: 3)
HStack {
Button("Clear") {
textInput = ""
progressValue = 0
}
Spacer()
Button("Check") {
//
let trimmedText = textInput.trimmingCharacters(in: .whitespacesAndNewlines)
//
if trimmedText.isEmpty {
showingToast = true
toastText = "Please enter some text."
return //
}
// 200
let MaxLen = globalEnvironment.isVip ? globalEnvironment.MaxLenGrammarCheckVIP : globalEnvironment.MaxLenGrammarCheckFree
if trimmedText.count > MaxLen {
showingToast = true
toastText = "Input too long. Please re-enter the text."
logger.info("input too lang, inputlen: \(trimmedText.count), maxlen: \(MaxLen), vip: \(globalEnvironment.isVip)")
return //
}
//
let checkRes = CommonFunc.shared.validateInputEng(input: trimmedText)
if !checkRes.isValid {
showingToast = true
toastText = checkRes.message
return
}
/*
if trimmedText.range(of: "[^a-zA-Z0-9 .,;:!?'\"@-]", options: .regularExpression) != nil {
showingToast = true
toastText = "Please enter valid characters."
return //
}
*/
loadData() //
// Send the request to the server
NetworkManager.shared.checkGrammar(inputText: trimmedText) { result in
switch result {
case .success(let results):
// Update the main UI with the results
loadComplete()
DispatchQueue.main.async {
self.results = results
self.showResult = true
self.showKeyboard = false
self.showBuyProView = false
hideKeyboard()
logger.info("grammar check succ.")
}
case .failure(let error):
loadComplete()
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.GrammarCheckOK:
toastText = globalEnvironment.GrammarOKToast
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
}
}
}
}
}
.padding(.vertical, 10)
.padding(.horizontal, 40)
.background(Color.green)
.foregroundColor(.white)
.cornerRadius(5)
.font(.subheadline) //
}
.padding(.bottom)
}
.padding() // padding
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.white) //
)
.padding(5)
.onTapGesture {
//
self.isTextEditorFocused = false
showBuyProView = true
}
}
//
func loadData() {
isLoading = true
}
func loadComplete(){
isLoading = false
}
//
func toggleFocus() {
isTextEditorFocused.toggle()
}
//
private func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
struct InputView_Preview: View{
@State private var textInput: 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 private var results : [GrammarRes]
//
@State private var isLoading = false //
@State private var showingToast = false // toast
@State private var toastText = ""
init(){
let demoGrammarData = GrammarData.demoInstance()
self.textInput = demoGrammarData.inputText
self.results = demoGrammarData.results
}
var body: some View {
VStack {
InputView(textInput: $textInput, progressValue: $progressValue, showKeyboard: $showKeyboard, showBuyProView: $showBuyProView, showResult: $showResult, results: $results, isLoading: $isLoading, showingToast: $showingToast, toastText: $toastText)
}
}
}
#Preview {
InputView()
InputView_Preview()
}

View File

@ -6,13 +6,201 @@
//
import SwiftUI
import Foundation
// SingleCorrectionCard访
fileprivate struct SingleCorrectionCard: View {
let correction: String
var body: some View {
Text(correction)
.padding(.vertical, 6)
.font(.system(size: UIFontMetrics.default.scaledValue(for: 15) * 4 / 5))
.background(Color.yellow.opacity(0.5))
.cornerRadius(5)
}
}
struct GrammarDetailsCardView: View {
let res: GrammarRes
var body: some View {
// 线
Divider()
.background(Color.yellow)
.frame(height: 1)
.overlay(
Rectangle()
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
.foregroundColor(.yellow)
)
.padding(3)
VStack {
Text("Error: \(res.plain)")
.frame(maxWidth: .infinity, alignment: .leading) //
.padding(.vertical, 8) // 2/3
.background(Color.gray.opacity(0.5)) // Gray
.foregroundColor(.black)
.cornerRadius(5)
Text("Reason: \(res.reason)")
.frame(maxWidth: .infinity, alignment: .leading) //
Text("Correction:")
.frame(maxWidth: .infinity, alignment: .leading) //
.padding(.top, 6) // Correction
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(Array(res.correction.enumerated()), id: \.offset) { index, correction in
SingleCorrectionCard(correction: correction)
}
}
}
}
.padding(.horizontal, 3) //
.padding(.bottom, 3) //
.background(Color.gray.opacity(0.2)) // LightGray
.cornerRadius(10)
}
}
struct ResultView: View {
//
@Binding var textContent: String
@Binding var results: [GrammarRes]
@Binding var showResult : Bool
@Binding var showKeyboard : Bool
@State private var selectedCardIndex: Int? = 0
@State private var textRes = AttributedString()
@State private var resContent: String = String("")
func getColoredText(str : String, type : String, index : Int?) -> AttributedString {
var text = AttributedString( str )
if(type == GrammarResType.ok.rawValue){
return text
}
if (type == GrammarResType.grammar.rawValue ){
text.foregroundColor = .red
}else
{
text.backgroundColor = .yellow
}
text.link = URL(string: String(index!))
return text
}
func styledText() -> Text {
var outstr = AttributedString()
var index = 0
let substr: String = textContent
var currentIndex = substr.startIndex
for res in results {
if let range = substr.range(of: res.plain, range: currentIndex..<substr.endIndex){
let tmpABStr = AttributedString(String(substr[currentIndex..<range.lowerBound]))
outstr.append(tmpABStr)
outstr.append(getColoredText(str: res.plain, type: res.type, index: index))
index = index + 1
currentIndex = range.upperBound
}
}
let tmpABStr = AttributedString(String(substr[currentIndex..<substr.endIndex]))
outstr.append(tmpABStr)
return Text(outstr)
}
/*
func styledText() -> Text {
var outstr = AttributedString()
var index = 0
for res in results {
let tmp = getColoredText(str: res.plain, type: res.type, index: index)
outstr.append(tmp)
index = index + 1
}
return Text(outstr)
}
*/
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
VStack {
// styledText
ScrollView {
styledText()
.padding()
.environment(\.openURL, OpenURLAction { url in
let path = url.absoluteString
print("index: \(path)")
self.selectedCardIndex = Int(path)
return .handled
})
Spacer() // 使SpacerText
}
.onTapGesture {
self.showResult = false
self.showKeyboard = true
}
.background(Color.white) // Gray
.frame(maxHeight: .infinity) // styledText
//index
ScrollViewReader { scrollView in
ScrollView {
LazyVStack {
ForEach(Array(results.enumerated()), id: \.offset) { index, res in
if(res.type != GrammarResType.ok.rawValue){
GrammarDetailsCardView(res: res)
.id(index) // CardViewID
}
}
}
}
.onChange(of: selectedCardIndex) { newIndex in
if let newIndex = newIndex {
withAnimation {
scrollView.scrollTo(newIndex, anchor: .top)
}
}
}
}
.frame(maxHeight: .infinity) // ErrorCard */
}
.padding() // padding
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.white) //
)
.padding(5)
}
}
struct ResultView_Preview: View {
@State var input : String = "this is demo text"
@State var results : [GrammarRes]
@State var showResult : Bool = true
@State var showKeyboard : Bool = true
init() {
let demoGrammarData = GrammarData.demoInstance()
self.input = demoGrammarData.inputText
self.results = demoGrammarData.results
}
var body: some View {
ResultView(textContent: $input, results: $results, showResult: $showResult, showKeyboard: $showKeyboard)
}
}
#Preview {
ResultView()
ResultView_Preview()
}

View File

@ -7,12 +7,197 @@
import SwiftUI
struct RichText: View {
var text1: AttributedString {
var text = AttributedString(localized:"登录即表示同意")
text.foregroundColor = .gray
return text
}
var text2: AttributedString {
var text = AttributedString(localized:"用户协议")
text.link = URL(string: "111")
text.foregroundColor = .red
return text
}
var text3: AttributedString {
var text = AttributedString(localized:"")
text.foregroundColor = .gray
return text
}
var text4: AttributedString {
var text = AttributedString(localized:"隐私协议")
text.link = URL(string: "222")
text.foregroundColor = .red
return text
}
var text: AttributedString {
text1 + text2 + text3 + text4
}
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
VStack {
Text(text)
.environment(\.openURL, OpenURLAction { url in
let path = url.absoluteString
if path.hasPrefix("111") {
print("111...")
} else if path.hasPrefix("222") {
print("222...")
}
return .handled
})
Text("Device ID: \(globalEnvironment.deviceID)")
}
}
}
/*
import UIKit
import SwiftUI
class RichTextViewController: UIViewController, UITextViewDelegate {
var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
// UITextView
textView = UITextView(frame: self.view.bounds)
textView.delegate = self
//
textView.isEditable = false
textView.isSelectable = true
//
let attributedString = NSMutableAttributedString(string: "")
//
let linkAttributes: [NSAttributedString.Key: Any] = [
.link: URL(string: "http://baidu.com")!, // 使URL scheme
.foregroundColor: UIColor.blue
]
attributedString.setAttributes(linkAttributes, range: NSRange(location: 0, length: 4)) // ""
textView.attributedText = attributedString
// UITextView
self.view.addSubview(textView)
}
//
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
print("")
print(URL.scheme as Any)
if URL.scheme == "http" {
//
return false // falseURL
}
return true
}
}
struct RichTextView: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
// RichTextViewController textView
// textView 使
let controller = RichTextViewController()
controller.loadViewIfNeeded() // textView
return controller.textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
// UI
}
}
struct RichText: View {
var body: some View {
VStack{
Text(" ")
// 使 RichTextView
RichTextView()
.frame(maxHeight: .infinity) //
//.edgesIgnoringSafeArea(.all) //
}
}
}
*/
/*
import SwiftUI
import UIKit
struct AttributedText: UIViewRepresentable {
var attributedString: NSAttributedString
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0 //
label.attributedText = attributedString
//
let tapGesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.labelTapped(_:)))
label.addGestureRecognizer(tapGesture)
label.isUserInteractionEnabled = true
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
//
uiView.attributedText = attributedString
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var parent: AttributedText
init(_ parent: AttributedText) {
self.parent = parent
}
@objc func labelTapped(_ sender: UITapGestureRecognizer) {
//
// 使UILabel
print("Label was tapped")
}
}
}
struct RichText: View {
var body: some View {
// 使AttributedText
AttributedText(attributedString: attributedString)
}
var attributedString: NSAttributedString {
let fullString = NSMutableAttributedString(string: "Tap on ")
let clickablePart = NSAttributedString(string: "this text", attributes: [
.foregroundColor: UIColor.blue,
.underlineStyle: NSUnderlineStyle.single.rawValue
])
fullString.append(clickablePart)
fullString.append(NSAttributedString(string: " to see action."))
return fullString
}
}
*/
#Preview {
RichText()
}

View File

@ -6,13 +6,16 @@
//
import SwiftUI
import UIKit
struct ShareSheet: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
struct ShareSheet: UIViewControllerRepresentable {
var itemsToShare: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: itemsToShare, applicationActivities: nil)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
#Preview {
ShareSheet()
}

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

View File

@ -6,3 +6,84 @@
//
import Foundation
import SwiftUI
/*
class GlobalConfig : ObservableObject{
@Published var backgroundColor : UInt = 0xFFE4E1
}
*/
class GlobalEnvironment: ObservableObject {
@Published var backgroundColor : UInt = 0xFFE4E1
init(){
SetEnv(isSandBox: false, isTestEnv: false)
}
@Published var deviceID: String = ""
@Published var userID: String = ""
@Published var userName: String = ""
@Published var GID: Int = 0
@Published var isVip:Bool = false
// APP
let APPID = "6504465465"
// toast
let toastPresentMsNormal = 1.5
let toastPresentMsLong = 3.0
let toastPresentMsShot = 0.5
//
let MaxLenGrammarCheckFree = 200
let MaxLenGrammarCheckVIP = 2000
let MaxLenWords = 50
let MaxLenTranslate = 200
//
let RetCodeFreeLimited = 101000
let RetCodeDirtyInput = 101001
let GrammarCheckOK = 102000
let GrammarOKToast = "Congratulations! There are no errors in your input."
let FreeLimitedToast = "Your free usage has been used up. Please upgrade to PREMIUM for unlimited usage."
let NetWorkErrToast = "Network Error. Please try again later."
let OtherServerErrToast = "Sorry, something went wrong on the server. Please try again later."
let DirtyInputErrToast = "The text you entered contains content that does not comply with regulations. Please re-enter."
var jwtSecret: String = "mCTf-JhNRnhaaGJy_x"
var userTermsURL: String = "https://grammar.easyprompt8.com/about/"
//
// var baseHost: String = "http://192.168.2.2:1080"
var baseHost: String = "https://api.easyprompt8.com"
// URL
var feedbackURL: String { "\(baseHost)/grammar/feedback" }
var translateURL: String { "\(baseHost)/grammar/translate" }
var dictURL: String { "\(baseHost)/grammar/words" }
var grammarURL: String { "\(baseHost)/grammar/grammar" }
// URL
var userURL: String { "\(baseHost)/user/get" }
// appstore
var iapVerifyURL : String { "\(baseHost)/iap/verify" }
//
func SetEnv(isSandBox: Bool, isTestEnv: Bool){
if(isTestEnv){
self.baseHost = "https://dev.easyprompt8.com"
}else {
self.baseHost = "https://api.easyprompt8.com"
}
logger.info("baseHost: \(self.baseHost)")
// SandBox
}
}
//
let globalEnvironment = GlobalEnvironment()

View File

@ -7,16 +7,127 @@
import Foundation
struct GrammarRes {
var tPlain : String
var tType : String
var tReason : String
var tCorrection : [String]
// GrammarResCodableJSON
struct GrammarRes: Codable {
var plain: String
var type: String
var reason: String
var correction: [String]
}
struct GrammarData {
var tInputText : String
var tCorrectText : String
var tResult : [GrammarRes]
enum GrammarResType : String {
case ok = "ok"
case grammar = "grammar"
case spell = "spell"
}
// tResultGrammarData
class GrammarData {
var inputText: String
var correctText: String
var results: [GrammarRes]
init(inputText: String, correctText: String, tResult: [GrammarRes] = []) {
self.inputText = inputText
self.correctText = correctText
self.results = tResult
}
// tResult
func parseResult(from jsonString: String) -> Bool {
//
let cleanedString = jsonString.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: ", ]", with: "]")
guard let jsonData = cleanedString.data(using: .utf8) else {
print("Error: Cannot create jsonData")
return false
}
do {
// JSON
self.results = try JSONDecoder().decode([GrammarRes].self, from: jsonData)
return true
} catch {
print("Error: \(error)")
return false
}
}
}
extension GrammarData {
// DemoGrammarData
static func demoInstance() -> GrammarData {
//let demoInputText = "This is a demo text with more complex grammar checking algorithm of the pro version. This app have been designed to help you to write correctly and appear more professional. I work really hard to make this app as god as possible. If you are happy with the results, please consider supporting the app by subscribing to the PRO plan. The text writen here is to show you how much the app is capable of with the pro version. is this something you would like to use? "
let demoInputText = "Paris are hosting the Olypmic Games in 2024. Athletes from arround the world comes to compet in many sports, wich makes it an excting event to watch."
let demoCorrectText = demoInputText
// Demo
let demoErrors = [
GrammarRes(plain: "This is a demo text with more complex grammar checking algorithm of the pro version.",
type: "ok",
reason: "",
correction: []),
GrammarRes(
plain: "This app have",
type: "grammar",
reason: "subject-verb agreement",
correction: ["This app has"]
),
GrammarRes(
plain: "been designed to help you to write correctly and appear more professional.",
type: "ok",
reason: "",
correction: []
),
GrammarRes(
plain: "I work really hard",
type: "ok",
reason: "",
correction: []
),
GrammarRes(
plain: "to make this app as god",
type: "spell",
reason: "typo",
correction: ["as good"]
),
GrammarRes(
plain: "as possible.",
type: "ok",
reason: "",
correction: []
),
GrammarRes(
plain: "If you are happy with the results, please consider supporting the app by subscribing to the PRO plan.",
type: "ok",
reason: "",
correction: []
),
GrammarRes(
plain: "The text writen",
type: "spell",
reason: "misspelling",
correction: ["written"]
),
GrammarRes(
plain: "here is to show you how much the app is capable of with the pro version.",
type: "ok",
reason: "",
correction: []
),
GrammarRes(
plain: "is this something you would like to use?",
type: "grammar",
reason: "capitalization",
correction: ["Is this something you would like to use?"]
)
]
return GrammarData(inputText: demoInputText, correctText: demoCorrectText, tResult: demoErrors)
}
}

View File

@ -6,3 +6,16 @@
//
import Foundation
import SwiftUI
extension Color {
static func hex(_ hex: UInt, alpha: Double = 1.0) -> Color {
return Color(
red: Double((hex >> 16) & 0xFF) / 255.0,
green: Double((hex >> 8) & 0xFF) / 255.0,
blue: Double(hex & 0xFF) / 255.0,
opacity: alpha
)
}
}

View File

@ -6,3 +6,55 @@
//
import Foundation
class CommonFunc {
static let shared = CommonFunc()
private let profanityList: [String] = ["fuck", "shit", "porn", "习近平", "鸡巴", "阴茎"] // Example profanity words
private init() {} // Private initializer to ensure singleton usage
func validateInputEng(input: String, isSingleWord: Bool = false) -> (isValid: Bool, message: String) {
// Check for profanity
for badWord in profanityList {
if input.lowercased().contains(badWord) {
return (false, globalEnvironment.DirtyInputErrToast)
}
}
// Check characters based on the isSingleWord flag
let pattern = isSingleWord ? "^[A-Za-z]+$" : "^[A-Za-z0-9 .,;:!?'\"@-]+$"
let regex = try! NSRegularExpression(pattern: pattern)
let range = NSRange(location: 0, length: input.utf16.count)
if regex.firstMatch(in: input, options: [], range: range) == nil {
let characterSet = isSingleWord ? "english letters" : "english letters, numbers, and punctuation"
return (false, "Input should only contain \(characterSet).")
}
return (true, "Input is valid.")
}
func validateChineseInput(input: String) -> (isValid: Bool, message: String) {
// Check for profanity
for word in profanityList {
if input.contains(word) {
return (false, globalEnvironment.DirtyInputErrToast)
}
}
// Check if all characters are valid Chinese characters or punctuation
for character in input {
if !character.isPunctuation && !(0x4E00...0x9FFF).contains(character.unicodeScalars.first!.value) &&
!(0x3400...0x4DBF).contains(character.unicodeScalars.first!.value) &&
!(0x20000...0x2A6DF).contains(character.unicodeScalars.first!.value) {
//
let res = self.validateInputEng(input: input)
if !res.isValid{
return (false, "Input contains invalid characters.")
}
}
}
return (true, "Input is valid.")
}
}

View File

@ -6,3 +6,228 @@
//
import Foundation
import StoreKit
import SwiftUI
import SwiftyBeaver
enum IAPProduct: String, CaseIterable {
case premiumFeature1 = "grammar_1_week"
case premiumFeature2 = "grammar_1_month"
case premiumFeature3 = "grammar_1_year"
var weight: Int {
switch self {
case .premiumFeature1:
return 1
case .premiumFeature2:
return 2
case .premiumFeature3:
return 3
}
}
}
class IAPManager: ObservableObject {
@Published var products: [Product] = [] //
@Published var purchasedProducts: [Product] = [] //
private var processedTransactionIDs = Set<UInt64>() // ID
init() {
Task {
await requestProducts()
await updatePurchasedProducts()
await listenForTransactions()
}
}
//
func requestProducts() async {
do {
let products = try await Product.products(for: IAPProduct.allCases.map { $0.rawValue })
DispatchQueue.main.async {
//
self.products = products.sorted { product1, product2 in
let weight1 = IAPProduct(rawValue: product1.id)?.weight ?? 0
let weight2 = IAPProduct(rawValue: product2.id)?.weight ?? 0
return weight1 < weight2
}
//
for product in self.products {
if let receiptData = try? product.jsonRepresentation {
if let jsonString = String(data: receiptData, encoding: .utf8) {
logger.info("product details for \(product.id): \(jsonString)")
} else {
logger.info("product details for \(product.id): ", context: ["Tile": product.displayName, "Price": product.price, "DisplayPrice": product.displayPrice])
}
}else {
logger.info("product details for \(product.id): ", context: ["Tile": product.displayName, "Price": product.price, "DisplayPrice": product.displayPrice])
}
}
}
} catch {
logger.error("Failed to fetch products: \(error.localizedDescription)")
}
}
//
func buy(product: Product, completion: @escaping (Result<String, Error>) -> Void) async {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
if case .verified(let transaction) = verification {
//
let appAccountToken = transaction.appAccountToken
let productId = transaction.productID
let transactionId = transaction.id
//
if let receiptData = try? transaction.jsonRepresentation {
if let jsonString = String(data: receiptData, encoding: .utf8) {
logger.info("Transaction details for \(transactionId): \(jsonString)")
} else {
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
}
} else {
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
}
//
await validateReceipt(receiptData: transaction.jsonRepresentation, appAccountToken: appAccountToken, productId: productId, transactionId: transactionId, env: transaction.environment.rawValue)
await transaction.finish()
await updatePurchasedProducts()
DispatchQueue.main.async {
//
completion(.success("Purchase Successful"))
}
}
case .userCancelled:
//
logger.error("User cancelled the purchase.")
DispatchQueue.main.async {
//
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchase Cancelled"])))
}
case .pending:
//
logger.error("Purchase is pending.")
DispatchQueue.main.async {
//
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchase Pending"])))
}
default:
break
}
} catch {
logger.error("Failed to purchase product: \(error.localizedDescription)")
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
// https://developer.apple.com/documentation/storekit/transaction/3851204-currententitlements
// A sequence of the latest transactions that entitle a user to in-app purchases and subscriptions.
func updatePurchasedProducts() async {
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
if let product = products.first(where: { $0.id == transaction.productID }) {
DispatchQueue.main.async {
self.purchasedProducts.append(product)
}
}
}
}
}
// https://developer.apple.com/documentation/storekit/transaction/3851206-updates
// The asynchronous sequence that emits a transaction when the system creates or updates transactions that occur outside of the app or on other devices.
// Note that after a successful in-app purchase on the same device, StoreKit returns the transaction through Product.PurchaseResult.success(_:).
func listenForTransactions() async {
for await transactionResult in Transaction.updates {
if case .verified(let transaction) = transactionResult {
if let product = products.first(where: { $0.id == transaction.productID }) {
DispatchQueue.main.async {
self.purchasedProducts.append(product)
}
}
//
let appAccountToken = transaction.appAccountToken
let productId = transaction.productID
let transactionId = transaction.id
//
if let receiptData = try? transaction.jsonRepresentation {
if let jsonString = String(data: receiptData, encoding: .utf8) {
logger.info("Transaction details for \(transactionId): \(jsonString)")
} else {
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
}
} else {
logger.info("Transaction details for \(transactionId)", context: ["ProductID" : productId, "AppAccountToken" : appAccountToken ?? "", "OriTransactionID":transaction.originalID.value])
}
//
await validateReceipt(receiptData: transaction.jsonRepresentation, appAccountToken: appAccountToken, productId: productId, transactionId: transactionId, env: transaction.environment.rawValue)
await transaction.finish()
}
}
}
// https://developer.apple.com/documentation/storekit/appstore/3791906-sync
// Synchronizes your apps transaction information and subscription status with information from the App Store.
func restorePurchases(completion: @escaping (Result<String, Error>) -> Void) async {
do {
try await AppStore.sync()
await updatePurchasedProducts()
completion(.success("Purchase Successful"))
logger.info("Purchases restored")
} catch {
logger.error("Failed to restore purchases: \(error.localizedDescription)")
completion(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Purchase Pending"])))
}
}
//
func validateReceipt(receiptData: Data, appAccountToken: UUID?, productId: String, transactionId: UInt64, env: String) async {
// NetworkManagerIapVerify
NetworkManager.shared.IapVerify(receiptData: receiptData, appAccountToken: appAccountToken, productId: productId, transactionId: transactionId, env: env) { result in
switch result {
case .success(let response):
DispatchQueue.main.async {
logger.info("Receipt verification succeeded: \(response.ret)", context: ["ProductID":productId, "TransactionID":transactionId])
//
self.updatePurchasedStatus(productId: productId)
}
case .failure(let error):
//
DispatchQueue.main.async {
switch error {
case .businessError(let ret, let message):
logger.error("Business error - Ret: \(ret), Message: \(message)", context: ["ProductID":productId, "TransactionID":transactionId])
case .other(let error):
logger.error("network occurred: \(error.localizedDescription)", context: ["ProductID":productId, "TransactionID":transactionId])
}
}
}
}
}
//
private func updatePurchasedStatus(productId: String) {
//
}
}

View File

@ -6,5 +6,88 @@
//
import Foundation
import Alamofire
import TrustDecision
import SwiftJWT
import SwiftyBeaver
class InitApp {
static let shared = InitApp(environment: globalEnvironment)
private let userDefaults = UserDefaults.standard
private let deviceIDKey = "DeviceID"
private let vipStatusKey = "VIPStatus"
private var deviceID: String = ""
private var vipStatus: Bool = false
var environment: GlobalEnvironment
init(environment: GlobalEnvironment) {
self.environment = environment
initializeApp()
}
private func initializeApp() {
fetchDeviceID()
}
private func fetchDeviceID() {
if let savedDeviceID = userDefaults.string(forKey: deviceIDKey) {
self.environment.deviceID = savedDeviceID
logger.info("DeviceID from UserDefaults: \(savedDeviceID)")
//
getUser()
} else {
getDeviceIDFromTrustDecision()
}
}
private func getDeviceIDFromTrustDecision() {
var options = [String : NSObject]()
let responseCallback: ([String : Any]) -> Void = { response in
DispatchQueue.main.async { // 线UI
if let deviceId = response["device_id"] as? String {
self.deviceID = deviceId
self.userDefaults.set(deviceId, forKey: self.deviceIDKey)
self.environment.deviceID = deviceId
logger.info("Device ID from TrustDecision: \(self.deviceID)")
// VIP
self.getUser()
}
}
}
options["callback"] = unsafeBitCast(responseCallback as @convention(block) ([String : Any]) -> Void, to: AnyObject.self) as? NSObject
let manager = TDMobRiskManager.sharedManager()
manager?.pointee.initWithOptions(options)
}
private func getUser() {
NetworkManager.shared.getUserProfile() { result in
DispatchQueue.main.async {
switch result {
case .success(let userData):
self.vipStatus = userData.vip == 1
self.environment.GID = userData.id
self.environment.userID = userData.userid
self.environment.userName = userData.username
self.environment.isVip = userData.vip == 1
logger.info("getUserProfile: ID: \(userData.id), userID: \(userData.userid), userName: \(userData.username), vip: \(userData.vip)")
case .failure(let error):
switch error {
case .businessError(let ret, let message):
logger.error("Business error - Ret: \(ret), Message: \(message)")
case .other(let error):
logger.error("network occurred: \(error.localizedDescription)")
}
}
}
}
}
func refreshUserInfo(){
getUser()
}
}

View File

@ -6,3 +6,38 @@
//
import Foundation
import SwiftyBeaver
let logger = SwiftyBeaver.self
func setupLogging() {
let console = ConsoleDestination()
let file = FileDestination()
//file.logFileURL = URL(fileURLWithPath: "/path/to/your/log/file.log")
// use custom format and set console output to short time, log level & message
// console.format = "$DHH:mm:ss$d $L $M"
// or use this for JSON output:
// console.format = "$J"
// In Xcode 15, specifying the logging method as .logger to display color, subsystem, and category information in the console.(Relies on the OSLog API)
//console.logPrintWay = .logger(subsystem: "Main", category: "UI")
// If you prefer not to use the OSLog API, you can use print instead.
// console.logPrintWay = .print
console.format = "$DHH:mm:ss$d $C$L$c $N.$F:$l - $M"
//
console.levelColor.verbose = "⚪️ " // White
console.levelColor.debug = "🔵 " // Blue
console.levelColor.info = "🟢 " // Green
console.levelColor.warning = "🟡 " // Yellow
console.levelColor.error = "🔴 " // Red
logger.addDestination(console)
logger.addDestination(file)
}

View File

@ -10,35 +10,50 @@ import Alamofire
import SwiftJWT
import SwiftyBeaver
// jwt
struct MyClaims: Claims {
var deviceID: String
var gid: Int
var exp1: Int
}
// {"ret":%d, "message":"%s", "data":jsonData}
struct APIResponse<T: Decodable>: Decodable {
let ret: Int
let message: String
let data: T?
}
//
enum NetworkError: Error {
case businessError(ret: Int, message: String)
case other(Error)
}
// 使
struct GrammarCheckRsp : Codable{
let data: [GrammarRes]
}
//
struct Translation: Identifiable, Codable, Equatable {
var id = UUID()
var input: String
var translation: String
}
//
struct TranslationResponse: Codable {
let translation: String
}
//
struct WordDetails {
var word: String = ""
var explanations: [String] = []
var phrases: [String] = []
var synonyms: [String] = []
}
//
struct WordDetailsResponse: Codable {
let word: String
let explain: [String]?
@ -56,6 +71,7 @@ struct WordDetailsResponse: Codable {
}
}
// 使
struct VIPStatusResponse: Codable {
let id: Int
let userid: String
@ -63,73 +79,122 @@ struct VIPStatusResponse: Codable {
let vip: Int
}
// 使
struct IAPVerifyRsp: Codable {
let ret: String
//let productType : String
}
//
struct NetworkManager {
static let shared = NetworkManager()
private let jwtSecret = globalEnvironment.jwtSecret
//
func getHeaderTimezoneInfo() -> [String: String] {
let timezone = TimeZone.current
let timezoneIdentifier = timezone.identifier // "America/New_York"
let secondsFromGMT = timezone.secondsFromGMT() // GMT
return [
"timezone": timezoneIdentifier,
"secondsfromgmt": String(secondsFromGMT)
]
}
//
func checkGrammar(inputText: String, completion: @escaping ([GrammarRes]?, Error?) -> Void) {
func checkGrammar(inputText: String, completion: @escaping (Result<[GrammarRes], NetworkError>) -> Void) {
let url = globalEnvironment.grammarURL
let headers: HTTPHeaders = createAuthorizationHeader()
var headers: HTTPHeaders = createAuthorizationHeader()
let timezoneHeaders = getHeaderTimezoneInfo()
headers.add(name: "timezone", value: timezoneHeaders["timezone"]!)
headers.add(name: "secondsfromgmt", value: timezoneHeaders["secondsfromgmt"]!)
let parameters: [String: Any] = [
"input": inputText,
"lang": "eng"
]
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: headers)
.responseDecodable(of: [GrammarRes].self) { response in
switch response.result {
case .success(let results):
completion(results, nil)
case .failure(let error):
completion(nil, error)
// 使
performRequest(
endpoint: url,
parameters: parameters,
method: .post,
encoding: URLEncoding.httpBody,
headers: headers,
completion: { (result: Result<[GrammarRes], NetworkError>) in
switch result {
case .success(let results):
completion(.success(results))
case .failure(let error):
//
completion(.failure(error))
}
}
}
)
}
//
func fetchWordDetails(inputText: String, lang: String = "eng", completion: @escaping (Result<WordDetails, Error>) -> Void) {
func fetchWordDetails(inputText: String, lang: String = "eng", completion: @escaping (Result<WordDetails, NetworkError>) -> Void) {
guard !inputText.isEmpty else { return }
let parameters: [String: Any] = ["input": inputText, "lang": lang]
let url = globalEnvironment.dictURL
let headers: HTTPHeaders = createAuthorizationHeader()
var headers: HTTPHeaders = createAuthorizationHeader()
let timezoneHeaders = getHeaderTimezoneInfo()
headers.add(name: "timezone", value: timezoneHeaders["timezone"]!)
headers.add(name: "secondsfromgmt", value: timezoneHeaders["secondsfromgmt"]!)
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: headers)
.responseDecodable(of: WordDetailsResponse.self) { response in
switch response.result {
// 使
performRequest(
endpoint: url,
parameters: parameters,
method: .post,
encoding: URLEncoding.httpBody,
headers: headers,
completion: { (result: Result<WordDetailsResponse, NetworkError>) in
switch result {
case .success(let detailsResponse):
print("Success: Received data for \(detailsResponse.word)")
let details = detailsResponse.toWordDetails() // Convert here
completion(.success(details))
case .failure(let error):
print("Error: \(error)")
print("Response Data: \(String(data: response.data ?? Data(), encoding: .utf8) ?? "No data")")
//
completion(.failure(error))
}
}
)
}
//
func translate(inputText: String, lang: String = "chs", completion: @escaping (Result<Translation, Error>) -> Void) {
func translate(inputText: String, lang: String = "chs", completion: @escaping (Result<Translation, NetworkError>) -> Void) {
guard !inputText.isEmpty else { return }
let url = globalEnvironment.translateURL
let parameters: [String: Any] = ["input": inputText, "lang": lang]
let headers: HTTPHeaders = createAuthorizationHeader()
var headers: HTTPHeaders = createAuthorizationHeader()
let timezoneHeaders = getHeaderTimezoneInfo()
headers.add(name: "timezone", value: timezoneHeaders["timezone"]!)
headers.add(name: "secondsfromgmt", value: timezoneHeaders["secondsfromgmt"]!)
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: headers)
.responseDecodable(of: TranslationResponse.self) { response in
switch response.result {
// 使
performRequest(
endpoint: url,
parameters: parameters,
method: .post,
encoding: URLEncoding.httpBody,
headers: headers,
completion: { (result: Result<TranslationResponse, NetworkError>) in
switch result {
case .success(let translationResponse):
let newTranslation = Translation(input: inputText, translation: translationResponse.translation)
completion(.success(newTranslation))
case .failure(let error):
//
completion(.failure(error))
}
}
)
}
//
@ -146,16 +211,15 @@ struct NetworkManager {
AF.request(url, method: .post, parameters: parameters, encoding: URLEncoding.httpBody, headers: headers).response { response in
switch response.result {
case .success(let data):
print("Feedback sent successfully: \(String(describing: data))")
logger.info("Feedback sent successfully.", context: ["input":input, "isPositive": isPositive])
case .failure(let error):
print("Error sending feedback: \(error)")
logger.error("Error sending feedback: \(error)", context: ["input":input, "isPositive": isPositive])
}
}
}
// VIP
func getUserProfile(completion: @escaping (Result<VIPStatusResponse, Error>) -> Void) {
func getUserProfile(completion: @escaping (Result<VIPStatusResponse, NetworkError>) -> Void) {
let url = globalEnvironment.userURL
let parameters: [String: Any] = [:]
@ -167,27 +231,52 @@ struct NetworkManager {
method: .post,
encoding: URLEncoding.httpBody,
headers: headers, // 使JWT token
completion: { (result: Result<VIPStatusResponse, Error>) in
completion: { (result: Result<VIPStatusResponse, NetworkError>) in
switch result {
case .success(let vipData):
logger.info("VIP Status: \(vipData.vip)")
completion(.success(vipData))
case .failure(let error):
logger.error("Error: \(error.localizedDescription)")
//
completion(.failure(error))
}
}
)
}
// VIP
func IapVerify(receiptData: Data, appAccountToken: UUID?, productId: String, transactionId: UInt64, env: String, completion: @escaping (Result<IAPVerifyRsp, NetworkError>) -> Void) {
let url = globalEnvironment.iapVerifyURL
let parameters: [String: Any] = ["transid":transactionId, "appaccounttoken": appAccountToken ?? "", "productid":productId, "receiptdata":receiptData, "env": env]
let headers: HTTPHeaders = createAuthorizationHeader()
performRequest(
endpoint: url,
parameters: parameters,
method: .post,
encoding: URLEncoding.httpBody,
headers: headers, // 使JWT token
completion: { (result: Result<IAPVerifyRsp, NetworkError>) in
switch result {
case .success(let rsp):
completion(.success(rsp))
case .failure(let error):
//
completion(.failure(error))
}
}
)
}
//
func performRequest<T: Decodable>(
endpoint: String,
parameters: Parameters, // 使 Alamofire Parameters
method: HTTPMethod = .post,
encoding: URLEncoding = .httpBody,
headers: HTTPHeaders? = nil,
completion: @escaping (Result<T, Error>) -> Void
completion: @escaping (Result<T, NetworkError>) -> Void
) {
let url = endpoint
let defaultHeaders: HTTPHeaders = [.contentType("application/x-www-form-urlencoded")]
@ -200,15 +289,18 @@ struct NetworkManager {
if apiResponse.ret == 0, let data = apiResponse.data {
completion(.success(data))
} else {
let error = NSError(domain: "", code: apiResponse.ret, userInfo: [NSLocalizedDescriptionKey: apiResponse.message])
completion(.failure(error))
completion(.failure(.businessError(ret: apiResponse.ret, message: apiResponse.message)))
}
case .failure(let error):
completion(.failure(error))
if let httpResponse = response.response {
logger.error("network erorr.", context: ["url":url, "StatusCode":httpResponse.statusCode])
}
completion(.failure(.other(error)))
}
}
}
// jwt token
private func createAuthorizationHeader() -> HTTPHeaders {
// Generate JWT and return headers
guard let jwtToken = generateJWT(deviceID: globalEnvironment.deviceID, gID: globalEnvironment.GID, jwtSecret: jwtSecret) else {