Initial commit
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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() // 使用Spacer确保Text组件横向拉伸
|
||||
}
|
||||
.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) // 给每个CardView分配一个ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
|
||||
@ -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 // 返回false表示不让系统处理这个URL
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -6,13 +6,174 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ToastUI
|
||||
|
||||
struct GrammarCheckView: View {
|
||||
fileprivate struct BuyProView: View {
|
||||
var onTryForFree: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Update to Pro")
|
||||
.font(.headline)
|
||||
|
||||
Text("Remove Character limits and ads. Use Grammar Check with higher quality.")
|
||||
.font(.subheadline)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Try for free") {
|
||||
// 按钮动作
|
||||
// 触发显示VIP付费界面的事件
|
||||
onTryForFree()
|
||||
}
|
||||
.padding(.vertical, 6) // 调整高度为默认高度的2/3
|
||||
.padding(.horizontal, 20)
|
||||
.background(Color.green)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(5)
|
||||
.font(.headline) // 调整字体大小为标题大小
|
||||
.alignmentGuide(.bottom) { d in d[.bottom] } // 对齐按钮的底部到 VStack 的底部
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(5)
|
||||
.padding(5)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
GrammarCheckView()
|
||||
enum ActiveSheet {
|
||||
case camera, share
|
||||
}
|
||||
extension ActiveSheet: Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
|
||||
struct GrammarCheckView: View {
|
||||
@EnvironmentObject var globalEnv: GlobalEnvironment // 引入环境对象
|
||||
|
||||
// 定义变量
|
||||
@State private var textInput: String
|
||||
@State private var inputResult : String
|
||||
@State private var progressValue: Float = 0
|
||||
@State private var showKeyboard: Bool = false
|
||||
@State private var showBuyProView: Bool = true
|
||||
@State private var showResult : Bool = false
|
||||
@State var results : [GrammarRes]
|
||||
@State private var showVIPPaymentView: Bool = false // 控制VIP付费界面的显示
|
||||
|
||||
// 提交等待,错误提示等
|
||||
@State private var isLoading = false // 控制加载指示器的显示
|
||||
@State private var showingToast = false // 控制是否显示toast
|
||||
@State private var toastText = ""
|
||||
|
||||
@State private var activeSheet: ActiveSheet?
|
||||
|
||||
@State private var isTextEditorFocused: Bool = false // 这个变量会传递给 InputView
|
||||
|
||||
// 使用默认文本进行初始化
|
||||
init() {
|
||||
let demoGrammarData = GrammarData.demoInstance()
|
||||
self.textInput = demoGrammarData.inputText
|
||||
self.inputResult = demoGrammarData.correctText
|
||||
self.results = demoGrammarData.results
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
Color.pink.opacity(0.2).edgesIgnoringSafeArea(.all) // 设置页面背景色
|
||||
|
||||
VStack {
|
||||
if showResult {
|
||||
ResultView(textContent: $textInput, results: $results, showResult: $showResult, showKeyboard: $showKeyboard)
|
||||
} else {
|
||||
InputView(textInput: $textInput, progressValue: $progressValue, showKeyboard: $showKeyboard, showBuyProView: $showBuyProView, showResult: $showResult, results: $results, isLoading: $isLoading, showingToast: $showingToast, toastText: $toastText)
|
||||
// .environment(\.isTextEditorFocused, $isTextEditorFocused) // 将焦点状态传递给 InputView
|
||||
}
|
||||
if showBuyProView {
|
||||
BuyProView(onTryForFree: {
|
||||
showVIPPaymentView = true // 显示VIP付费界面
|
||||
})
|
||||
//BuyProView()
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showVIPPaymentView) {
|
||||
VIPPaymentView() // 弹出VIP付费界面
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
Text("Grammar & Spell Checker")
|
||||
.font(.headline) // 设置标题字体
|
||||
if globalEnv.isVip {
|
||||
Image("vipimg") // 示例中使用系统图标
|
||||
.resizable() // 使图片可调整大小
|
||||
.scaledToFit() // 保持图片的宽高比
|
||||
.frame(width: 24, height: 24) // 设置图标的具体尺寸
|
||||
.foregroundColor(.yellow)
|
||||
.font(.subheadline) // 设置图标字体为比标题小
|
||||
.offset(y: -1) // 根据需要调整图标的垂直位置
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(.black)
|
||||
.navigationBarItems(leading: Button(action: {
|
||||
// 相机按钮动作
|
||||
self.activeSheet = .camera
|
||||
}) {
|
||||
Image(systemName: "camera")
|
||||
}, trailing: Button(action: {
|
||||
// 分享按钮动作
|
||||
self.activeSheet = .share
|
||||
}) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
})
|
||||
.sheet(item: $activeSheet, onDismiss: {
|
||||
// 如果需要在sheet关闭时执行某些操作,可以在这里添加
|
||||
}) { item in
|
||||
switch item {
|
||||
case .camera:
|
||||
CameraView(textInput: $textInput)
|
||||
case .share:
|
||||
ShareSheet(itemsToShare: [textInput])
|
||||
}
|
||||
}
|
||||
.toast(isPresented: $showingToast, dismissAfter: globalEnvironment.toastPresentMsNormal) {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.bubble")
|
||||
.foregroundColor(.yellow)
|
||||
Text(toastText)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
// 加载指示器
|
||||
if isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: 预览
|
||||
struct GrammarCheckView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
GrammarCheckView()
|
||||
.environmentObject(IAPManager()) // 这里添加 IAPManager
|
||||
.environmentObject(globalEnvironment) // 这里添加 IAPManager
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,15 +4,187 @@
|
||||
//
|
||||
// Created by oscar on 2024/7/9.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IAPView: View {
|
||||
struct VIPPaymentView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@State private var showingToast = false
|
||||
@State private var toastText = ""
|
||||
|
||||
let features = ["Grammar Check & Spelling Correction", "Words & Dictionary", "Translate"]
|
||||
let freeValues = ["2 Times / Day", "2 Times / Day", "2 Times / Day"]
|
||||
let premiumValues = ["Unlimited", "Unlimited", "Unlimited"]
|
||||
let products = [
|
||||
("Premium Weekly", "$4.99", "Billed Weekly", "weekly", IAPProduct.premiumFeature1),
|
||||
("Premium Monthly", "$9.99 / month", "Billed Monthly", "monthly", IAPProduct.premiumFeature2),
|
||||
("Premium Yearly", "$49.99 / year", "Billed Yearly", "yearly", IAPProduct.premiumFeature3)
|
||||
]
|
||||
|
||||
@State private var selectedProductIndex: Int = 1 // 默认选择第二个商品
|
||||
@State private var selectedPlanText: String = "Billed Monthly"
|
||||
|
||||
@EnvironmentObject var iapManager: IAPManager // 确保在上级视图中已提供 IAPManager
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
NavigationView {
|
||||
VStack {
|
||||
HStack {
|
||||
Text("PREMIUM")
|
||||
.bold()
|
||||
.foregroundColor(Color(red: 1.0, green: 0.8, blue: 0.0)) // 自定义更亮的黄色
|
||||
//.foregroundColor(.yellow)
|
||||
.padding()
|
||||
.background(Color.orange)
|
||||
.cornerRadius(20)
|
||||
Spacer()
|
||||
Button("Close") {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
|
||||
List {
|
||||
Section(header: Text("Get Premium Features").font(.headline)) {
|
||||
HStack {
|
||||
Text("Features")
|
||||
.frame(width: 120, alignment: .leading)
|
||||
Spacer()
|
||||
Text("Free")
|
||||
.frame(width: 120, alignment: .center)
|
||||
Spacer()
|
||||
Text("Premium")
|
||||
.frame(width: 80, alignment: .center)
|
||||
}
|
||||
.font(.headline)
|
||||
|
||||
ForEach(Array(zip(features, zip(freeValues, premiumValues))), id: \.0) { item in
|
||||
HStack {
|
||||
Text(item.0)
|
||||
.frame(width: 120, alignment: .leading)
|
||||
Spacer()
|
||||
Text(item.1.0) // Free values
|
||||
.frame(width: 120, alignment: .center)
|
||||
Spacer()
|
||||
Text(item.1.1) // Premium values
|
||||
.frame(width: 80, alignment: .center)
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(products.indices, id: \.self) { index in
|
||||
Button(action: {
|
||||
// do someting
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: selectedProductIndex == index ? "checkmark.circle.fill" : "circle")
|
||||
.foregroundColor(selectedProductIndex == index ? .green : .secondary)
|
||||
.padding(.horizontal, 4)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
if products[index].0 == "Premium Yearly" {
|
||||
// 特别为 "Premium Yearly" 添加图标和折扣信息
|
||||
Text(products[index].0) + Text(" ⚡️") + Text(" 58% off")
|
||||
.bold()
|
||||
.foregroundColor(.red) // 折扣信息使用红色
|
||||
.font(.subheadline)
|
||||
} else {
|
||||
Text(products[index].0)
|
||||
}
|
||||
Text(products[index].1)
|
||||
.font(.subheadline)
|
||||
}
|
||||
Spacer()
|
||||
Text(products[index].2)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
.background(self.selectedProductIndex == index ? Color.yellow : Color.clear)
|
||||
.cornerRadius(5)
|
||||
.padding(.horizontal, 20) // 增加水平缩进
|
||||
.onTapGesture {
|
||||
self.selectedProductIndex = index
|
||||
self.selectedPlanText = products[index].2
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Button("Purchase") {
|
||||
// Handle purchase logic here
|
||||
buyProduct()
|
||||
}
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 80)
|
||||
.foregroundColor(.white)
|
||||
.background(Color.green)
|
||||
.cornerRadius(15)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.shadow(radius: 2) // 添加阴影效果
|
||||
.font(.headline) // 调整字体大小为标题大小
|
||||
.padding(.top, 50)
|
||||
|
||||
Text((selectedPlanText ) + ", Cancel Anytime")
|
||||
.font(.footnote)
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Color(.systemBackground))
|
||||
.toast(isPresented: $showingToast, dismissAfter: 5.0, onDismiss: {
|
||||
// Toast 消失后执行的动作
|
||||
presentationMode.wrappedValue.dismiss() // 关闭当前视图
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.bubble")
|
||||
.foregroundColor(.yellow)
|
||||
Text(toastText)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private func buyProduct() {
|
||||
let productId = products[selectedProductIndex].4
|
||||
logger.info("selected productId: \(productId.rawValue)")
|
||||
if let product = iapManager.products.first(where: { $0.id == productId.rawValue }) {
|
||||
Task {
|
||||
await iapManager.buy(product: product) { result in
|
||||
switch result {
|
||||
case .success(let message):
|
||||
self.toastText = message
|
||||
self.showingToast = true
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
|
||||
// 刷新用户的vip状态
|
||||
InitApp.shared.refreshUserInfo()
|
||||
case .failure(let error):
|
||||
self.toastText = error.localizedDescription
|
||||
self.showingToast = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error("Product not found")
|
||||
toastText = "Error loading product list, please try again later"
|
||||
self.showingToast = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
IAPView()
|
||||
struct VIPPaymentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VIPPaymentView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,13 +6,226 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ToastUI
|
||||
|
||||
struct SettingsView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
import SafariServices
|
||||
|
||||
struct FullScreenSafariView: UIViewControllerRepresentable {
|
||||
let url: URL
|
||||
var onDismiss: (() -> Void)?
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<FullScreenSafariView>) -> SFSafariViewController {
|
||||
let safariViewController = SFSafariViewController(url: url)
|
||||
safariViewController.delegate = context.coordinator
|
||||
return safariViewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext<FullScreenSafariView>) {
|
||||
// 无需更新
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, SFSafariViewControllerDelegate {
|
||||
var parent: FullScreenSafariView
|
||||
|
||||
init(_ parent: FullScreenSafariView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
|
||||
parent.onDismiss?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var globalEnv: GlobalEnvironment // 引入环境对象
|
||||
|
||||
@State private var isLoading = false // 控制加载指示器的显示
|
||||
@State private var showingToast = false // 控制是否显示toast
|
||||
@State private var toastText = ""
|
||||
@State private var showVIPPaymentView = false
|
||||
|
||||
@EnvironmentObject var iapManager: IAPManager // 确保在上级视图中已提供 IAPManager
|
||||
|
||||
@State private var showingFullSafari = false
|
||||
|
||||
|
||||
@State private var showingAdvancedSettings = false // 控制高级设置显示的状态
|
||||
@State private var useSandboxEnvironment = false
|
||||
@State private var useDevelopmentEnvironment = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
// Upgrade to Premium
|
||||
settingItem(icon: "arrow.up.circle", text: "Upgrade to Premium", isBold: true)
|
||||
.onTapGesture {
|
||||
showVIPPaymentView = true
|
||||
}
|
||||
|
||||
// Feedback
|
||||
settingItem(icon: "bubble.left", text: "Feedback")
|
||||
.onTapGesture {
|
||||
// Assuming there's a way to open App Store Feedback
|
||||
openAppStoreFeedback()
|
||||
}
|
||||
|
||||
// About
|
||||
settingItem(icon: "info.circle", text: "About")
|
||||
.onTapGesture {
|
||||
// Code to show About View
|
||||
self.showingFullSafari = true
|
||||
}
|
||||
|
||||
// Restore Purchases
|
||||
settingItem(icon: "arrow.clockwise.circle", text: "Restore Purchases", isBold: true)
|
||||
.onTapGesture {
|
||||
restorePurchase()
|
||||
}
|
||||
|
||||
}
|
||||
.fullScreenCover(isPresented: $showingFullSafari) {
|
||||
FullScreenSafariView(url: URL(string: globalEnvironment.userTermsURL)!, onDismiss: {
|
||||
self.showingFullSafari = false
|
||||
})
|
||||
}
|
||||
|
||||
// 留出手势操作的空间,但UI上隐藏掉
|
||||
gestureArea
|
||||
|
||||
if showingAdvancedSettings {
|
||||
VStack {
|
||||
Toggle("Sandbox", isOn: $useSandboxEnvironment)
|
||||
Toggle("TestEnv", isOn: $useDevelopmentEnvironment)
|
||||
Button("Save") {
|
||||
saveSettings()
|
||||
showingAdvancedSettings = false
|
||||
}
|
||||
}
|
||||
.padding(.vertical,10)
|
||||
.padding(.horizontal, 20)
|
||||
.background(Color.white) // Ensure background fills the view
|
||||
.cornerRadius(5)
|
||||
}
|
||||
|
||||
}
|
||||
.background(Color.pink.opacity(0.2)) // Ensure background fills the view
|
||||
.navigationBarTitle("Settings", displayMode: .inline)
|
||||
.fullScreenCover(isPresented: $showVIPPaymentView) {
|
||||
VIPPaymentView()
|
||||
}
|
||||
.onDisappear {
|
||||
self.showingAdvancedSettings = false // 确保隐藏组件在视图消失时不显示
|
||||
}
|
||||
}
|
||||
.toast(isPresented: $showingToast, dismissAfter: globalEnvironment.toastPresentMsNormal) {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.bubble")
|
||||
.foregroundColor(.yellow)
|
||||
Text(toastText)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
// 加载指示器
|
||||
if isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
|
||||
// 定义隐藏功能
|
||||
private var gestureArea: some View {
|
||||
Color.clear
|
||||
.contentShape(Rectangle()) // 为透明色定义一个矩形可命中区域
|
||||
.frame(height: 150) // 可以根据需要调整手势区域的大小
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 50, coordinateSpace: .global)
|
||||
.onEnded { value in
|
||||
// 检查水平移动距离是否足够长,同时限制垂直移动不太大
|
||||
if abs(value.translation.width) > 50 && abs(value.translation.height) < 100 {
|
||||
// 切换高级设置的显示状态
|
||||
logger.info("Advanced Settings.")
|
||||
showingAdvancedSettings.toggle()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 实现保存设置的逻辑
|
||||
private func saveSettings() {
|
||||
globalEnvironment.SetEnv(isSandBox: useSandboxEnvironment, isTestEnv: useDevelopmentEnvironment)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func settingItem(icon: String, text: String, isBold: Bool = false) -> some View {
|
||||
HStack {
|
||||
Image(systemName: icon) // Icon on the left side
|
||||
.foregroundColor(.gray)
|
||||
Text(text)
|
||||
.font(.subheadline)
|
||||
.fontWeight(isBold ? .bold : .regular) // Conditional bold based on isBold parameter
|
||||
.padding(4)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white) // Background for each setting item
|
||||
.padding(.horizontal, 0) // Add some horizontal padding
|
||||
|
||||
// Indent divider to align with the text and extend to the edge
|
||||
Divider()
|
||||
.padding(.leading, 30)
|
||||
.padding(.trailing, 10)
|
||||
}
|
||||
|
||||
private func openAppStoreFeedback() {
|
||||
let appID = globalEnvironment.APPID // 替换为您的应用ID
|
||||
let urlStr = "https://apps.apple.com/app/id\(appID)?action=write-review"
|
||||
if let url = URL(string: urlStr) {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func restorePurchase(){
|
||||
Task {
|
||||
await iapManager.restorePurchases() { result in
|
||||
switch result {
|
||||
case .success(_):
|
||||
self.toastText = "Restored Sucess!"
|
||||
self.showingToast = true
|
||||
case .failure(_):
|
||||
self.toastText = "Oh, something went wrong, please try again later."
|
||||
self.showingToast = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 模拟加载数据
|
||||
func loadData() {
|
||||
isLoading = true
|
||||
}
|
||||
func loadComplete(){
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
SettingsView()
|
||||
}
|
||||
|
||||
@ -6,13 +6,381 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVFoundation
|
||||
import ToastUI
|
||||
|
||||
struct TranslateView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
struct SubmitTextEditor: UIViewRepresentable {
|
||||
@Binding var text: String
|
||||
var onCommit: () -> Void
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let textView = UITextView()
|
||||
textView.delegate = context.coordinator
|
||||
textView.font = UIFont.preferredFont(forTextStyle: .body)
|
||||
textView.isScrollEnabled = true
|
||||
textView.returnKeyType = .send // 设置键盘的返回键类型为发送
|
||||
textView.enablesReturnKeyAutomatically = true
|
||||
|
||||
textView.backgroundColor = UIColor.white.withAlphaComponent(0.5) // 设置背景色为半透明的白色
|
||||
textView.layer.cornerRadius = 10 // 设置圆角
|
||||
textView.layer.borderColor = UIColor.blue.cgColor // 设置边框颜色为蓝色
|
||||
textView.layer.borderWidth = 1 // 设置边框宽度
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateUIView(_ textView: UITextView, context: Context) {
|
||||
textView.text = text
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UITextViewDelegate {
|
||||
var parent: SubmitTextEditor
|
||||
|
||||
init(_ parent: SubmitTextEditor) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
if text == "\n" { // 检测到换行符,即用户按下了发送键
|
||||
parent.onCommit()
|
||||
textView.resignFirstResponder() // 收起键盘
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
parent.text = textView.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TranslateView: View {
|
||||
@EnvironmentObject var globalEnv: GlobalEnvironment // 引入环境对象
|
||||
|
||||
@State private var inputText = ""
|
||||
@State private var translations: [Translation] = [
|
||||
Translation(input: "This is demo text", translation: "这是演示文本"),
|
||||
]
|
||||
@State private var showMenu = false // 控制下拉菜单的显示
|
||||
@FocusState private var isInputFocused: Bool // 状态变量来控制TextEditor的聚焦
|
||||
|
||||
@State private var currentLang = "Chinese" // 用来显示当前语言
|
||||
@State private var langCode = "chs" // 默认语言代码
|
||||
|
||||
@State private var isLoading = false // 控制加载指示器的显示
|
||||
@State private var showingToast = false // 控制是否显示toast
|
||||
@State private var toastText = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
// 设置背景色
|
||||
Color.pink.opacity(0.2).edgesIgnoringSafeArea(.all)
|
||||
|
||||
VStack {
|
||||
ScrollViewReader { scrollView in
|
||||
ScrollView {
|
||||
VStack(spacing: 10) {
|
||||
ForEach(translations) { translation in
|
||||
TranslateCardView(translation: translation, onEdit: onEdit, onCopy: onCopy, onFeedback: onFeedBack)
|
||||
.id(translation.id) // Assign an ID to each card view
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onChange(of: translations) { _ in
|
||||
if let lastId = translations.last?.id {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(lastId, anchor: .bottom) // Scroll to the last translation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SubmitTextEditor(text: $inputText, onCommit: {
|
||||
// 清除输入内容前后的空格
|
||||
let trimmedText = inputText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// 检查是否为空
|
||||
if trimmedText.isEmpty {
|
||||
showingToast = true
|
||||
toastText = "Please enter some text."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
|
||||
// 检查长度是否超过200个字符
|
||||
if trimmedText.count > globalEnvironment.MaxLenTranslate {
|
||||
showingToast = true
|
||||
toastText = "Input too long. Please re-enter the text."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
|
||||
// 检查是否含有非法字符
|
||||
/*
|
||||
if trimmedText.range(of: "[^a-zA-Z0-9 .,;:!?'\"@-]", options: .regularExpression) != nil {
|
||||
showingToast = true
|
||||
toastText = "Please enter valid characters."
|
||||
return // 提前返回,不执行网络请求
|
||||
}*/
|
||||
if currentLang == "Chinese" {
|
||||
let checkRes = CommonFunc.shared.validateInputEng(input: trimmedText)
|
||||
if !checkRes.isValid {
|
||||
showingToast = true
|
||||
toastText = checkRes.message
|
||||
return
|
||||
}
|
||||
} else {
|
||||
let checkRes = CommonFunc.shared.validateChineseInput(input: trimmedText)
|
||||
if !checkRes.isValid {
|
||||
showingToast = true
|
||||
toastText = checkRes.message
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loadData()
|
||||
NetworkManager.shared.translate(inputText: trimmedText, lang: langCode) { result in
|
||||
switch result {
|
||||
case .success(let translation):
|
||||
addTranslation(output: translation.translation)
|
||||
logger.info("translate succ.", context: ["input": inputText, "output":translation.translation, "lang":langCode])
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case .businessError(let ret, let message):
|
||||
// 业务错误,比如无免费可用次数等,需要处理逻辑。
|
||||
logger.error("Business error - Ret: \(ret), Message: \(message)")
|
||||
DispatchQueue.main.async {
|
||||
showingToast = true
|
||||
switch ret{
|
||||
case globalEnvironment.RetCodeFreeLimited:
|
||||
toastText = globalEnvironment.FreeLimitedToast
|
||||
case globalEnvironment.RetCodeDirtyInput:
|
||||
toastText = globalEnvironment.DirtyInputErrToast
|
||||
default:
|
||||
toastText = globalEnvironment.OtherServerErrToast
|
||||
}
|
||||
}
|
||||
case .other(let error):
|
||||
logger.error("network error occurred: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
showingToast = true
|
||||
toastText = globalEnvironment.NetWorkErrToast
|
||||
}
|
||||
}
|
||||
}
|
||||
loadComplete()
|
||||
}
|
||||
})
|
||||
.frame(height: 100) // 指定高度
|
||||
.padding()
|
||||
.focused($isInputFocused)
|
||||
}
|
||||
.toast(isPresented: $showingToast, dismissAfter: globalEnvironment.toastPresentMsNormal) {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.bubble")
|
||||
.foregroundColor(.yellow)
|
||||
Text(toastText)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
|
||||
// 加载指示器
|
||||
if isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
// 点击任何非TextField区域时收起键盘
|
||||
self.hideKeyboard()
|
||||
}
|
||||
.navigationBarTitle("AI Translate", displayMode: .inline)
|
||||
.navigationBarItems(trailing: Menu(currentLang) {
|
||||
Button("English -> Chinese") {
|
||||
currentLang = "Chinese"
|
||||
langCode = "chs"
|
||||
}
|
||||
Button("Chinese -> English") {
|
||||
currentLang = "English"
|
||||
langCode = "eng"
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// 模拟加载数据
|
||||
func loadData() {
|
||||
isLoading = true
|
||||
}
|
||||
func loadComplete(){
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private func onEdit(text : String){
|
||||
self.inputText = text
|
||||
self.isInputFocused = true
|
||||
}
|
||||
private func onCopy(text : String){
|
||||
UIPasteboard.general.string = text
|
||||
}
|
||||
private func onFeedBack( feedback: TranslateFeedback){
|
||||
let fb:Bool = feedback.good ? true : false
|
||||
NetworkManager.shared.sendFeedback(input: feedback.input, output: feedback.translation, isPositive: fb)
|
||||
}
|
||||
|
||||
private func addTranslation(output: String) {
|
||||
let newTranslation = Translation(input: inputText, translation: output)
|
||||
translations.append(newTranslation)
|
||||
if translations.count > 10 {
|
||||
translations.removeFirst()
|
||||
}
|
||||
inputText = "" // 清空输入框
|
||||
}
|
||||
|
||||
private func hideKeyboard() {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
struct Translation: Identifiable, Equatable {
|
||||
let id = UUID()
|
||||
var input: String
|
||||
var translation: String
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
struct TranslateFeedback {
|
||||
var input: String
|
||||
var translation: String
|
||||
var good: Bool = false
|
||||
var bad: Bool = false
|
||||
}
|
||||
|
||||
|
||||
struct TranslateCardView: View {
|
||||
var translation: Translation
|
||||
var onEdit: (String) -> Void
|
||||
var onCopy: (String) -> Void
|
||||
var onFeedback: (TranslateFeedback) -> Void
|
||||
let synthesizer = AVSpeechSynthesizer()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// 上半部分
|
||||
VStack(alignment: .leading) {
|
||||
Text("Input")
|
||||
.font(.system(size: UIFont.systemFontSize * 0.8))
|
||||
.foregroundColor(Color.black.opacity(0.6))
|
||||
.padding(.top, 0) // 留出顶部间距
|
||||
.padding(.bottom,3) // 减少与原文之间的间距
|
||||
Text(translation.input)
|
||||
.padding(.bottom, 3)
|
||||
HStack {
|
||||
/*
|
||||
Button(action: {
|
||||
speakText(translation.input)
|
||||
}) {
|
||||
Image(systemName: "speaker.wave.2")
|
||||
.foregroundColor(.blue)
|
||||
.padding(.trailing)
|
||||
}
|
||||
*/
|
||||
Spacer()
|
||||
Button(action: {
|
||||
onEdit(translation.input)
|
||||
}) {
|
||||
Image(systemName: "square.and.pencil")
|
||||
.foregroundColor(.gray)
|
||||
.frame(width: 15, height: 15) // 缩小图标大小
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.horizontal, .top])
|
||||
.padding(.bottom, 3) // 减少与原文之间的间距
|
||||
|
||||
Divider().dashed()
|
||||
|
||||
// 下半部分
|
||||
VStack(alignment: .leading) {
|
||||
Text(translation.translation)
|
||||
.padding(.vertical, 1) // 减少与分割线的间距
|
||||
.padding(.bottom, 3) // 减少与原文之间的间距
|
||||
HStack {
|
||||
/*
|
||||
Button(action: {
|
||||
speakText(translation.translation)
|
||||
}) {
|
||||
Image(systemName: "speaker.wave.2")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
*/
|
||||
Spacer()
|
||||
HStack(spacing: 15) {// 减小图标之间的间距
|
||||
Button(action: {
|
||||
onCopy(translation.translation)
|
||||
}) {
|
||||
Image(systemName: "doc.on.doc")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 15, height: 15) // 缩小图标大小
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
onFeedback(TranslateFeedback(input: translation.input, translation: translation.translation, good: true))
|
||||
}) {
|
||||
Image(systemName: "hand.thumbsup")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 15, height: 15) // 缩小图标大小
|
||||
}
|
||||
|
||||
|
||||
Button(action: {
|
||||
onFeedback(TranslateFeedback(input: translation.input, translation: translation.translation, bad: true))
|
||||
}) {
|
||||
Image(systemName: "hand.thumbsdown")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 15, height: 15) // 缩小图标大小
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 1) // 减少与原文之间的间距
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.padding(0)
|
||||
.background(Color.gray.opacity(0.2)) // 设置整个卡片的背景色为LightGray
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1))
|
||||
}
|
||||
|
||||
func speakText(_ text: String) {
|
||||
let utterance = AVSpeechUtterance(string: text)
|
||||
utterance.voice = AVSpeechSynthesisVoice(language: "en-US")
|
||||
synthesizer.speak(utterance)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func dashed() -> some View {
|
||||
self.overlay(
|
||||
Rectangle()
|
||||
.fill(Color.clear)
|
||||
//.border( StrokeStyle(lineWidth: 1, dash: [5]))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
TranslateView()
|
||||
}
|
||||
|
||||
@ -6,13 +6,211 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ToastUI
|
||||
|
||||
struct WordsView: View {
|
||||
@EnvironmentObject var globalEnv: GlobalEnvironment // 引入环境对象
|
||||
|
||||
@State private var searchText = ""
|
||||
@State private var showingResults = false
|
||||
@State private var res = WordDetails()
|
||||
@State private var isLoading = false // 控制加载指示器的显示
|
||||
@State private var showingToast = false // 控制是否显示toast
|
||||
@State private var toastText = ""
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
NavigationView {
|
||||
ZStack {
|
||||
// 设置背景色
|
||||
Color.pink.opacity(0.2).edgesIgnoringSafeArea(.all)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// 搜索框区域
|
||||
HStack {
|
||||
TextField("word", text: $searchText, onCommit: fetchWordData)
|
||||
.padding(.horizontal, 40)
|
||||
.padding(.vertical, 10)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
.font(.headline)
|
||||
.overlay(
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.gray)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading).padding(.leading, 8)
|
||||
)
|
||||
Spacer()
|
||||
|
||||
Button("Cancel") {
|
||||
self.searchText = ""
|
||||
self.showingResults = false
|
||||
self.hideKeyboard()
|
||||
}
|
||||
}
|
||||
.background(Color(.systemPink).opacity(0.2))
|
||||
.cornerRadius(5) // 边框圆角
|
||||
.shadow(radius: 2) // 轻微阴影
|
||||
.padding(8)
|
||||
|
||||
|
||||
// 使用 Spacer 来推动搜索框保持在顶部
|
||||
if !showingResults {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
if showingResults {
|
||||
// 结果显示区域
|
||||
HStack {
|
||||
List {
|
||||
Section(header: Text("Definitions").font(.headline)) {
|
||||
ForEach(res.explanations, id: \.self) { definition in
|
||||
Text(definition)
|
||||
.font(.system(.subheadline))
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Common Phrases").font(.headline)) {
|
||||
ForEach(res.phrases, id: \.self) { phrase in
|
||||
Text(phrase)
|
||||
.font(.system(.subheadline))
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Synonyms").font(.system(size: UIFont.systemFontSize * 1.2))) {
|
||||
ForEach(res.synonyms, id: \.self) { synonym in
|
||||
Text(synonym)
|
||||
.font(.system(.subheadline))
|
||||
.padding(.leading, 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.cornerRadius(5) // 边框圆角
|
||||
.frame(maxHeight: .infinity) // 限制ErrorCard占用半个屏幕 */
|
||||
}
|
||||
.listStyle(GroupedListStyle()) // 使用分组列表样式,以适应背景
|
||||
}
|
||||
}
|
||||
.toast(isPresented: $showingToast, dismissAfter: globalEnvironment.toastPresentMsNormal) {
|
||||
HStack {
|
||||
Image(systemName: "exclamationmark.bubble")
|
||||
.foregroundColor(.yellow)
|
||||
Text(toastText)
|
||||
.foregroundColor(.black)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(8)
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
|
||||
// 加载指示器
|
||||
if isLoading {
|
||||
LoadingView()
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
// 点击任何非TextField区域时收起键盘
|
||||
self.hideKeyboard()
|
||||
|
||||
}
|
||||
.navigationBarTitle("Words", displayMode: .inline)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func hideKeyboard() {
|
||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
}
|
||||
|
||||
// 模拟加载数据
|
||||
func loadData() {
|
||||
isLoading = true
|
||||
}
|
||||
func loadComplete(){
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
private func fetchWordData() {
|
||||
// 清除输入内容前后的空格
|
||||
let trimmedText = searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// 检查是否为空
|
||||
if trimmedText.isEmpty {
|
||||
showingToast = true
|
||||
toastText = "Please enter some text."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
|
||||
// 检查长度是否超过200个字符
|
||||
if trimmedText.count > globalEnvironment.MaxLenWords {
|
||||
showingToast = true
|
||||
toastText = "Input too long. Please re-enter the text."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
|
||||
// 检查是否含有非法字符
|
||||
let checkRes = CommonFunc.shared.validateInputEng(input: trimmedText, isSingleWord: true)
|
||||
if !checkRes.isValid {
|
||||
showingToast = true
|
||||
toastText = checkRes.message
|
||||
return
|
||||
}
|
||||
/*
|
||||
if trimmedText.range(of: "[^a-zA-Z]", options: .regularExpression) != nil {
|
||||
showingToast = true
|
||||
toastText = "Please enter valid characters."
|
||||
return // 提前返回,不执行网络请求
|
||||
}
|
||||
*/
|
||||
|
||||
loadData() // 添加提交的提示
|
||||
NetworkManager.shared.fetchWordDetails(inputText: trimmedText) { result in
|
||||
switch result {
|
||||
case .success(let wordDetails):
|
||||
DispatchQueue.main.async {
|
||||
res = wordDetails
|
||||
showingResults = true
|
||||
}
|
||||
logger.info("fetch words succ.", context: ["word":wordDetails.word, "explanations":wordDetails.explanations, "phrases":wordDetails.phrases, "synonyms":wordDetails.synonyms])
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case .businessError(let ret, let message):
|
||||
// 业务错误,比如无免费可用次数等,需要处理逻辑。
|
||||
logger.error("Business error - Ret: \(ret), Message: \(message)")
|
||||
DispatchQueue.main.async {
|
||||
showingToast = true
|
||||
switch ret{
|
||||
case globalEnvironment.RetCodeFreeLimited:
|
||||
toastText = globalEnvironment.FreeLimitedToast
|
||||
case globalEnvironment.RetCodeDirtyInput:
|
||||
toastText = globalEnvironment.DirtyInputErrToast
|
||||
default:
|
||||
toastText = globalEnvironment.OtherServerErrToast
|
||||
}
|
||||
}
|
||||
case .other(let error):
|
||||
logger.error("network error occurred: \(error.localizedDescription)")
|
||||
DispatchQueue.main.async {
|
||||
showingToast = true
|
||||
toastText = globalEnvironment.NetWorkErrToast
|
||||
}
|
||||
}
|
||||
}
|
||||
loadComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WordsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WordsView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#Preview {
|
||||
WordsView()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -7,16 +7,127 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct GrammarRes {
|
||||
var tPlain : String
|
||||
var tType : String
|
||||
var tReason : String
|
||||
var tCorrection : [String]
|
||||
// 确保GrammarRes遵循Codable协议,以支持JSON解析
|
||||
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"
|
||||
}
|
||||
|
||||
|
||||
// 因为tResult是动态的,所以我们定义GrammarData为类
|
||||
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 {
|
||||
// 静态方法返回包含Demo数据的GrammarData实例
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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 app’s 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 {
|
||||
// 调用NetworkManager中的IapVerify进行收据验证
|
||||
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) {
|
||||
// 逻辑来标记产品为已购买,例如更新本地数据库、发送通知等
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user