Add CI/CD configuration and API documentation

This commit is contained in:
2026-07-01 21:40:53 +08:00
commit c590135d68
4168 changed files with 740252 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"5.12.2","fr":60,"ip":0,"op":72,"w":500,"h":500,"nm":"Lock","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,247,0],"ix":2,"l":2},"a":{"a":0,"k":[-81.441,794.705,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[550,550,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":15,"s":[360,360,100]},{"t":30,"s":[550,550,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[-2.851,0],[0,-2.851]],"o":[[0,0],[0,0],[0,-2.851],[2.851,0],[0,0]],"v":[[-76.265,790.525],[-86.617,790.525],[-86.617,787.34],[-81.441,782.164],[-76.265,787.34]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.494,-0.366],[0,0],[0.653,0],[0,0.653],[0,0],[0,0.637],[-1.099,0],[0,0],[0,-1.099]],"o":[[0,0],[0,0.653],[-0.653,0],[0,0],[-0.462,-0.366],[0,-1.099],[0,0],[1.099,0],[0,0.653]],"v":[[-80.247,800.065],[-80.247,803.266],[-81.441,804.46],[-82.636,803.266],[-82.636,800.017],[-83.416,798.488],[-81.425,796.497],[-81.409,796.497],[-79.418,798.488]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[2.389,0.366],[0,0],[4.173,0],[0,-4.173],[0,0],[0,-2.755],[0,0],[-3.185,0],[0,0],[0,3.185],[0,0]],"o":[[0,0],[0,-4.173],[-4.173,0],[0,0],[-2.389,0.366],[0,0],[0,3.185],[0,0],[3.185,0],[0,0],[0,-2.755]],"v":[[-73.876,790.62],[-73.876,787.34],[-81.441,779.775],[-89.006,787.34],[-89.006,790.62],[-92.589,795.303],[-92.589,804.859],[-87.812,809.636],[-75.071,809.636],[-70.293,804.859],[-70.293,795.303]],"c":true},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Circle","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[530,530,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":5,"s":[520,520,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[550,550,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[520,520,100]},{"t":50,"s":[530,530,100]}],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-16.335],[16.334,0],[0,16.334],[-16.335,0]],"o":[[0,16.334],[-16.335,0],[0,-16.335],[16.334,0]],"v":[[29.576,0],[0,29.576],[-29.576,0],[0,-29.576]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.29,0.937,0.435,0.5,0.145,0.718,0.437,1,0,0.498,0.439],"ix":9}},"s":{"a":0,"k":[-11.294,-32.337],"ix":5},"e":{"a":0,"k":[9.315,28.16],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"5.9.3","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":1920,"h":1080,"nm":"1_Loading_Line","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Blue Line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,539,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-1,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[800,800],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.212477396049,0.540471813725,0.898039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":80,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.9],"y":[0]},"t":15,"s":[0]},{"t":60.0000024438501,"s":[99.9]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.9],"y":[0]},"t":0,"s":[0.01]},{"t":45.0000018328876,"s":[100]}],"ix":2},"o":{"a":0,"k":-1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"ct":1,"bm":0}],"markers":[{"tm":60.0000024438501,"cm":"END (No Loop)","dr":0}]}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
//
// ChatViewControllerWrapper.swift
// SFI
//
// Created by Mac on 2024/10/24.
//
import SwiftUI
import UIKit
import Crisp
// Step 2: Create a SwiftUI wrapper for ChatViewController
struct ChatViewControllerWrapper: UIViewControllerRepresentable {
// Initialize any properties needed to pass to ChatViewController
typealias UIViewControllerType = ChatViewController
func makeUIViewController(context: Context) -> ChatViewController {
// Return the ChatViewController
let chatViewController = ChatViewController()
return chatViewController
}
func updateUIViewController(_ uiViewController: ChatViewController, context: Context) {
// Update the view controller when needed
}
}
@@ -0,0 +1,92 @@
//
// CustomCorners.swift
// CustomCorners
//
// Created by zues on 12/09/21.
//
import SwiftUI
struct CustomCorners: Shape {
var radius: CGFloat
var corners: UIRectCorner
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}
func getSafeAreaInsets() -> UIEdgeInsets {
let windowScene = UIApplication.shared.connectedScenes
.filter({ $0.activationState == .foregroundActive })
.map({ $0 as? UIWindowScene })
.compactMap({ $0 })
.first
guard let keyWindow = windowScene?.windows.filter({ $0.isKeyWindow }).first else {
return .zero
}
return keyWindow.safeAreaInsets
}
func getRect() -> CGRect {
return UIScreen.main.bounds
}
@ViewBuilder
func BackgroundBg()->some View{
ZStack{
LinearGradient(colors: [
Color("BG1"),
Color("BG1"),
Color("BG2"),
Color("BG2"),
], startPoint: .top, endPoint: .bottom)
// Little Planet and little stars....
Image("mars")
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 30, height: 30)
.scaleEffect(getRect().height < 750 ? 0.8 : 1)
// not using offset...
// using postiton..
// this will position the object using screen basis...
.position(x: 50, y: getRect().height < 750 ? 200 : 220)
.opacity(0.7)
// Sample star points....
let stars: [CGPoint] = [
CGPoint(x: 15, y: 190),
CGPoint(x: 25, y: 250),
CGPoint(x: 20, y: 350),
CGPoint(x: getRect().width - 30, y: 240),
]
ForEach(stars,id: \.x){star in
Circle()
.fill(.white.opacity(0.3))
.frame(width: 5, height: 5)
.position(star)
.offset(y: getRect().height < 750 ? -20 : 0)
}
Spacer()
}
.ignoresSafeArea()
}
@@ -0,0 +1,33 @@
//
// HTMLTextView.swift
// SFI
//
// Created by Mac on 2024/10/27.
//
import Foundation
import SwiftUI
import UIKit
struct HTMLTextView: UIViewRepresentable {
let htmlString: String
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.isEditable = false
textView.isScrollEnabled = true
textView.backgroundColor = .clear
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
guard let data = htmlString.data(using: .utf8) else { return }
do {
let attributedString = try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)
uiView.attributedText = attributedString
} catch {
print("Error loading HTML: \(error)")
}
}
}
@@ -0,0 +1,78 @@
//
// CustomTF.swift
// LoginKit
//
// Created by zues on 04/08/23.
//
import SwiftUI
struct CustomTF: View {
var sfIcon: String
var iconTint: Color = .gray
var hint: String
/// Hides TextField
var isPassword: Bool = false
var hasDriveLine: Bool = true
@Binding var value: String
/// View Properties
@State private var showPassword: Bool = false
/// When Switching Between Hide/Reveal Password Field, The Keyboard is Closing, to avoid that using the FocusState
@FocusState private var passwordState: HideState?
enum HideState {
case hide
case reveal
}
var body: some View {
HStack(alignment: .top, spacing: 8, content: {
Image(systemName: sfIcon)
.foregroundStyle(iconTint)
/// Since I Need Same Width to Align TextFields Equally
.frame(width: 30)
/// Slightly Bringing Down
.offset(y: 2)
VStack(alignment: .leading, spacing: 8, content: {
if isPassword {
Group {
/// Revealing Password when users wants to show Password
if showPassword {
TextField(hint, text: $value)
.focused($passwordState, equals: .reveal)
} else {
SecureField(hint, text: $value)
.focused($passwordState, equals: .hide)
}
}
} else {
TextField(hint, text: $value)
}
if hasDriveLine {
Divider()
}
})
.overlay(alignment: .trailing) {
/// Password Reveal Button
if isPassword {
Button(action: {
withAnimation {
showPassword.toggle()
}
passwordState = showPassword ? .reveal : .hide
}, label: {
Image(systemName: showPassword ? "eye.slash" : "eye")
.foregroundStyle(.gray)
.padding(10)
.contentShape(.rect)
})
}
}
})
}
}
@@ -0,0 +1,28 @@
//
// GradientButton.swift
// LoginKit
//
// Created by zues on 04/08/23.
//
import SwiftUI
struct GradientButton: View {
var title: String
var icon: String
var onClick: () -> ()
var body: some View {
Button(action: onClick, label: {
HStack(spacing: 15) {
Text(title)
Image(systemName: icon)
}
////.fontWeight(.bold)
.foregroundStyle(.white)
.padding(.vertical, 12)
.padding(.horizontal, 35)
.background(.linearGradient(colors: [.main, .main2, ], startPoint: .top, endPoint: .bottom), in: .capsule)
})
}
}
@@ -0,0 +1,87 @@
//
// OTPVerificationView.swift
// AutoOtpTF
//
// Created by zues on 23/12/22.
//
import SwiftUI
struct OTPVerificationView: View {
/// - View Properties
@Binding var otpText: String
/// - Keyboard State
@FocusState private var isKeyboardShowing: Bool
var body: some View {
HStack(spacing: 0){
/// - OTP Text Boxes
/// Change Count Based on your OTP Text Size
ForEach(0..<6,id: \.self){index in
OTPTextBox(index)
}
}
.background(content: {
TextField("", text: $otpText.limit(6))
.keyboardType(.numberPad)
.textContentType(.oneTimeCode)
/// - Hiding it Out
.frame(width: 1, height: 1)
.opacity(0.001)
.blendMode(.screen)
.focused($isKeyboardShowing)
})
.contentShape(Rectangle())
/// - Opening Keyboard When Tapped
.onTapGesture {
isKeyboardShowing = true
}
.toolbar {
ToolbarItem(placement: .keyboard) {
Button("Done"){
isKeyboardShowing = false
}
.tint(.white)
////.fontWeight(.heavy)
.hSpacing(.trailing)
}
}
}
// MARK: OTP Text Box
@ViewBuilder
func OTPTextBox(_ index: Int)->some View{
ZStack{
if otpText.count > index{
/// - Finding Char At Index
let startIndex = otpText.startIndex
let charIndex = otpText.index(startIndex, offsetBy: index)
let charToString = String(otpText[charIndex])
Text(charToString)
}else{
Text(" ")
}
}
.frame(width: 45, height: 45)
.background {
/// - Highlighting Current Active Box
let status = (isKeyboardShowing && otpText.count == index)
RoundedRectangle(cornerRadius: 6, style: .continuous)
.stroke(status ? .main : Color.gray,lineWidth: status ? 3 : 0.5)
/// - Adding Animation
.animation(.easeInOut(duration: 0.2), value: isKeyboardShowing)
}
.frame(maxWidth: .infinity)
}
}
// MARK: Binding <String> Extension
extension Binding where Value == String{
func limit(_ length: Int)->Self{
if self.wrappedValue.count > length{
DispatchQueue.main.async {
self.wrappedValue = String(self.wrappedValue.prefix(length))
}
}
return self
}
}
@@ -0,0 +1,31 @@
//
// LottieView.swift
// VPNUI
//
// Created by Mac on 2024/9/26.
//
import Foundation
//import Lottie
import SwiftUI
import Lottie
struct LottieView: UIViewRepresentable {
var animationFileName: String
let loopMode: LottieLoopMode
func updateUIView(_ uiView: UIViewType, context: Context) {
}
func makeUIView(context: Context) -> Lottie.LottieAnimationView {
let animationView = LottieAnimationView(name: animationFileName)
animationView.loopMode = loopMode
animationView.play()
animationView.contentMode = .scaleAspectFit
return animationView
}
}
@@ -0,0 +1,20 @@
//
// Unitity.swift
// SFI
//
// Created by Mac on 2024/10/20.
//
import Foundation
struct TimestampConverter {
// Function to convert a timestamp to a formatted date string
static func convertTimestampToDateString(_ timestamp: Int, format: String = "yyyy-MM-dd HH:mm:ss") -> String {
let timeInterval = TimeInterval(timestamp)
let date = Date(timeIntervalSince1970: timeInterval)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
return dateFormatter.string(from: date)
}
}
@@ -0,0 +1,93 @@
//
// UniversalAlert.swift
// CustomUniversalAlert
//
// Created by zues Venkatesh on 18/09/23.
//
import SwiftUI
/// Alert Config
struct AlertConfig {
fileprivate var enableBackgroundBlur: Bool = true
fileprivate var disableOutsideTap: Bool = true
fileprivate var transitionType: TransitionType = .slide
fileprivate var slideEdge: Edge = .bottom
fileprivate var show: Bool = false
fileprivate var showView: Bool = false
init(enableBackgroundBlur: Bool = true, disableOutsideTap: Bool = true, transitionType: TransitionType = .slide, slideEdge: Edge = .bottom) {
self.enableBackgroundBlur = enableBackgroundBlur
self.disableOutsideTap = disableOutsideTap
self.transitionType = transitionType
self.slideEdge = slideEdge
}
/// TransitionType
enum TransitionType {
case slide
case opacity
}
/// Alert Present/Dismiss Methods
mutating
func present() {
show = true
}
mutating
func dismiss() {
show = false
}
}
fileprivate struct AlertView<Content: View>: View {
@Binding var config: AlertConfig
/// View Tag
var tag: Int
@ViewBuilder var content: () -> Content
/// View Properties
@State private var showView: Bool = false
var body: some View {
GeometryReader(content: { geometry in
if showView && config.transitionType == .slide {
content()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.transition(.move(edge: config.slideEdge))
} else {
content()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.opacity(showView ? 1 : 0)
}
})
.background {
ZStack {
if config.enableBackgroundBlur {
Rectangle()
.fill(.ultraThinMaterial)
} else {
Rectangle()
.fill(.primary.opacity(0.25))
}
}
.ignoresSafeArea()
.contentShape(.rect)
.onTapGesture {
if !config.disableOutsideTap {
config.dismiss()
}
}
.opacity(showView ? 1 : 0)
}
.onAppear(perform: {
config.showView = true
})
// .onChange(of: config.showView) { oldValue, newValue in
// withAnimation(.smooth(duration: 0.35, extraBounce: 0)) {
// showView = newValue
// }
// }
}
}
@@ -0,0 +1,190 @@
//
// View+Extensions.swift
// LoginKit
//
// Created by zues on 04/08/23.
//
import SwiftUI
/// Custom SwiftUI View Extensions
extension View {
/// View Alignments
@ViewBuilder
func hSpacing(_ alignment: Alignment = .center) -> some View {
self
.frame(maxWidth: .infinity, alignment: alignment)
}
@ViewBuilder
func vSpacing(_ alignment: Alignment = .center) -> some View {
self
.frame(maxHeight: .infinity, alignment: alignment)
}
/// Disable With Opacity
@ViewBuilder
func disableWithOpacity(_ condition: Bool) -> some View {
self
.disabled(condition)
.opacity(condition ? 0.5 : 1)
}
}
// Extedning View to get Screen Size and Frame....
extension View{
func getRect()->CGRect{
UIScreen.main.bounds
}
func getSafeArea()->UIEdgeInsets{
guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else{
return .zero
}
guard let safeArea = screen.windows.first?.safeAreaInsets else{
return .zero
}
return safeArea
}
}
extension Text {
init(_ attributedString: NSAttributedString) {
self.init("") // initial, empty Text
// scan the attributed string for distinctly attributed regions
attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length),
options: []) { (attrs, range, _) in
let string = attributedString.attributedSubstring(from: range).string
var text = Text(string)
// then, read applicable attributes and apply them to the Text
if let font = attrs[.font] as? UIFont {
// this takes care of the majority of formatting - text size, font family,
// font weight, if it's italic, etc.
text = text.font(.init(font))
}
if let color = attrs[.foregroundColor] as? UIColor {
text = text.foregroundColor(Color(color))
}
if let kern = attrs[.kern] as? CGFloat {
text = text.kerning(kern)
}
if #available(iOS 14.0, *) {
if let tracking = attrs[.tracking] as? CGFloat {
text = text.tracking(tracking)
}
}
if let strikethroughStyle = attrs[.strikethroughStyle] as? NSNumber,
strikethroughStyle != 0 {
if let strikethroughColor = (attrs[.strikethroughColor] as? UIColor) {
text = text.strikethrough(true, color: Color(strikethroughColor))
} else {
text = text.strikethrough(true)
}
}
if let underlineStyle = attrs[.underlineStyle] as? NSNumber,
underlineStyle != 0 {
if let underlineColor = (attrs[.underlineColor] as? UIColor) {
text = text.underline(true, color: Color(underlineColor))
} else {
text = text.underline(true)
}
}
if let baselineOffset = attrs[.baselineOffset] as? NSNumber {
text = text.baselineOffset(CGFloat(baselineOffset.floatValue))
}
// append the newly styled subtext to the rest of the text
self = self + text
}
}
}
extension Text {
init(html htmlString: String, // the HTML-formatted string
raw: Bool = false, // set to true if you don't want to embed in the doc skeleton
size: CGFloat? = nil, // optional document-wide text size
fontFamily: String = "-apple-system") { // optional document-wide font family
let fullHTML: String
if raw {
fullHTML = htmlString
} else {
var sizeCss = ""
if let size = size {
sizeCss = "font-size: \(size)px;"
}
fullHTML = """
<!doctype html>
<html>
<head>
<style>
body {
font-family: \(fontFamily);
\(sizeCss)
}
</style>
</head>
<body>
\(htmlString)
</body>
</html>
"""
}
let attributedString: NSAttributedString
if let data = fullHTML.data(using: .unicode),
let attrString = try? NSAttributedString(data: data,
options: [.documentType: NSAttributedString.DocumentType.html],
documentAttributes: nil) {
attributedString = attrString
} else {
attributedString = NSAttributedString()
}
self.init(attributedString) // uses the NSAttributedString initializer
}
}
public extension View {
// View
func hideKeyboard() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
func transparentScrolling() -> some View {
if #available(iOS 16.0, *) {
return scrollContentBackground(.hidden)
} else {
return onAppear {
UITextView.appearance().backgroundColor = .clear
}
}
}
}
struct TriangleShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.closeSubpath()
return path
}
}
@@ -0,0 +1,35 @@
//
// InviteReponse.swift
// SFI
//
// Created by Mac on 2024/10/21.
//
import Foundation
// MARK: - Welcome
struct InviteReponse: Codable {
let status, message: String?
let data: InviteReponseDataClass?
}
// MARK: - DataClass
struct InviteReponseDataClass: Codable {
let codes: [InviteCode]?
let stat: [Int]?
}
// MARK: - Code
struct InviteCode: Codable,Identifiable {
var id = UUID().uuidString
let userID: Int
let code: String
let createdAt:Int
enum CodingKeys: String, CodingKey {
case userID = "user_id"
case code
case createdAt = "created_at"
}
}
@@ -0,0 +1,20 @@
//
// File.swift
// SFI
//
// Created by Mac on 2024/10/13.
//
import Foundation
struct MessageResponse: Codable {
let message: String
let status: String?
let errors: MessageResponseErrors?
}
// MARK: - Errors
struct MessageResponseErrors: Codable {
let email: [String]
}
@@ -0,0 +1,30 @@
//
// Nodes.swift
// SFI
//
// Created by Mac on 2024/10/13.
//
import Foundation
// MARK: - Welcome
struct nodereponse: Codable {
let data: [nodereponseData]?
let message: String?
}
// MARK: - Datum
struct nodereponseData: Codable,Identifiable {
// let id: Int
var id = UUID().uuidString
let type, name, rate: String
let id2, isOnline: Int
let cacheKey: String
enum CodingKeys: String, CodingKey {
case type, name, rate
case isOnline = "is_online"
case id2 = "id"
case cacheKey = "cache_key"
}
}
@@ -0,0 +1,17 @@
//
// OnBoardingItem.swift
// AnimatedOnBoardingScreen
//
// Created by zues on 17/12/22.
//
import SwiftUI
import Lottie
// MARK: OnBoarding Item Model
struct OnboardingItem: Identifiable,Equatable{
var id: UUID = .init()
var title: String
var subTitle: String
var lottieView: LottieAnimationView = .init()
}
@@ -0,0 +1,133 @@
//
// File.swift
// SFI
//
// Created by Mac on 2024/10/21.
//
import Foundation
// MARK: - OrderInfoReponse
struct OrderInfoReponse: Codable {
let status, message: String?
let data: [OrderInfoDatum]?
}
struct OrderInfoSingleReponse: Codable {
let status, message: String?
let data: OrderInfoDatum?
}
// MARK: - Datum
struct OrderInfoDatum: Codable ,Identifiable {
var id = UUID().uuidString
let inviteUserID: String?
let planID: Int?
let siteID, couponID: String?
let type: Int
let period: String
let couponCode: String?
let tradeNo: String
let callbackNo: String?
let totalAmount: Double
let handlingAmount, discountAmount, surplusAmount, refundAmount: Double?
let balanceAmount, surplusOrderIDS,paymentID: Int?
let status: Int
// let actualCommissionBalance, paidAt: String?
let createdAt, updatedAt: Int
let commissionStatus, commissionBalance: Int?
let tixianstatus: String?
let plan: PlanOrder
var status_zh: String {
switch status {
case 0:
return "待支付"
case 1:
return "已支付"
case 2:
return "已取消"
default:
return "未知"
}
}
var period_zh: String {
switch period {
case "month_price":
return "月付"
case "quarter_price":
return "季付"
case "half_year_price":
return "半年付"
case "year_price":
return "年付"
default:
return "月付"
}
}
enum CodingKeys: String, CodingKey {
case inviteUserID = "invite_user_id"
case planID = "plan_id"
case siteID = "site_id"
case couponID = "coupon_id"
case paymentID = "payment_id"
case type, period
case couponCode = "coupon_code"
case tradeNo = "trade_no"
case callbackNo = "callback_no"
case totalAmount = "total_amount"
case handlingAmount = "handling_amount"
case discountAmount = "discount_amount"
case surplusAmount = "surplus_amount"
case refundAmount = "refund_amount"
case balanceAmount = "balance_amount"
case surplusOrderIDS = "surplus_order_ids"
case status
case commissionStatus = "commission_status"
case commissionBalance = "commission_balance"
// case actualCommissionBalance = "actual_commission_balance"
// case paidAt = "paid_at"
case createdAt = "created_at"
case updatedAt = "updated_at"
case tixianstatus, plan
}
}
// MARK: - Plan
struct PlanOrder: Codable {
let id: Int
let name: String
let speedLimit: Int?
let show, sort, renew, groupID, transferEnable: Int?
let content: String
let monthPrice, quarterPrice, halfYearPrice, yearPrice: Int?
let twoYearPrice, threeYearPrice, onetimePrice, resetPrice: Double?
let resetTrafficMethod: Int?
let capacityLimit: Int?
let createdAt, updatedAt: Int
enum CodingKeys: String, CodingKey {
case id
case groupID = "group_id"
case transferEnable = "transfer_enable"
case name
case speedLimit = "speed_limit"
case show, sort, renew, content
case monthPrice = "month_price"
case quarterPrice = "quarter_price"
case halfYearPrice = "half_year_price"
case yearPrice = "year_price"
case twoYearPrice = "two_year_price"
case threeYearPrice = "three_year_price"
case onetimePrice = "onetime_price"
case resetPrice = "reset_price"
case resetTrafficMethod = "reset_traffic_method"
case capacityLimit = "capacity_limit"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}
@@ -0,0 +1,28 @@
//
// PaymentReponse.swift
// SFI
//
// Created by Mac on 2024/10/21.
//
import Foundation
// MARK: - PaymentReponse
struct PaymentReponse: Codable {
let status, message: String?
let data: [PaymentReponseDatum]?
}
// MARK: - Datum
struct PaymentReponseDatum: Codable {
let id: Int
let name, payment: String
let icon,handlingFeePercent,handlingFeeFixed: String?
enum CodingKeys: String, CodingKey {
case id, name, payment, icon
case handlingFeeFixed = "handling_fee_fixed"
case handlingFeePercent = "handling_fee_percent"
}
}
@@ -0,0 +1,46 @@
//
// PlanResponse.swift
// SFI
//
// Created by Mac on 2024/10/12.
//
import Foundation
// MARK: - Welcome
struct PlanResponse: Codable {
let data: [DatuPlanResponse]?
let message: String?
}
// MARK: - Datum
struct DatuPlanResponse: Codable,Identifiable {
var id = UUID()
let idOLD, groupID, transferEnable: Int?
let name: String
let deviceLimit: Int?
let show: Int?
let renew: Int?
let content: String?
let monthPrice, quarterPrice, halfYearPrice, yearPrice: Int?
let onetimePrice: Int?
let createdAt, updatedAt: Int?
enum CodingKeys: String, CodingKey {
case idOLD = "id"
case groupID = "group_id"
case transferEnable = "transfer_enable"
case name
case deviceLimit = "device_limit"
case show, renew, content
case monthPrice = "month_price"
case quarterPrice = "quarter_price"
case halfYearPrice = "half_year_price"
case yearPrice = "year_price"
case onetimePrice = "onetime_price"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}
@@ -0,0 +1,27 @@
//
// Server.swift
// Server
//
// Created by zues on 12/09/21.
//
import SwiftUI
// Sample Server and data...
//struct Server: Identifiable {
//
// var id = UUID().uuidString
// var name: String
// var flag: String
//}
//var servers = [
// Server(name: "Auto Select", flag: "us"),
// Server(name: "United States", flag: "us"),
// Server(name: "India", flag: "in"),
// Server(name: "United Kingdom", flag: "uk"),
// Server(name: "France", flag: "fr"),
// Server(name: "Germany", flag: "ge"),
// Server(name: "Singapore", flag: "si"),
//]
@@ -0,0 +1,67 @@
//
// File.swift
// SFI
//
// Created by Mac on 2024/10/12.
//
import Foundation
// MARK: - Welcome
struct SubscribeReponse: Codable {
let data: SubscribeReponseClass?
let message: String?
}
// MARK: - DataClass
struct SubscribeReponseClass: Codable {
let planID: Int?
let token: String
let u, d, transferEnable: Int
let email, uuid: String
let plan: Plan?
let subscribeURL: String
enum CodingKeys: String, CodingKey {
case planID = "plan_id"
case token
case u, d
case transferEnable = "transfer_enable"
case email, uuid, plan
case subscribeURL = "subscribe_url"
}
}
// MARK: - Plan
struct Plan: Codable {
let id, groupID: Int
let name: String
let show,renew: Int
let content: String?
let monthPrice, quarterPrice, halfYearPrice, yearPrice: Int?
let onetimePrice: Int?
let createdAt, updatedAt: Int
enum CodingKeys: String, CodingKey {
case id
case groupID = "group_id"
case name
case show, renew, content
case monthPrice = "month_price"
case quarterPrice = "quarter_price"
case halfYearPrice = "half_year_price"
case yearPrice = "year_price"
case onetimePrice = "onetime_price"
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}
@@ -0,0 +1,56 @@
//
// TicketChatReponse.swift
// SFI
//
// Created by Mac on 2024/10/20.
//
import Foundation
// MARK: - TicketChatReponse
struct TicketChatReponse: Codable {
let status, message: String?
let data: TicketChatReponseDataClass?
}
// MARK: - DataClass
struct TicketChatReponseDataClass: Codable {
let id, level, replyStatus, status: Int
let subject: String
let message: [Message]
let createdAt, updatedAt, userID: Int
enum CodingKeys: String, CodingKey {
case id
case level
case replyStatus = "reply_status"
case status, subject, message
case createdAt = "created_at"
case updatedAt = "updated_at"
case userID = "user_id"
}
}
struct Message: Codable ,Identifiable,Equatable {
var id = UUID().uuidString
let idold, ticketID: Int
let isMe: Bool
let message: String
let photo:String?
let createdAt, updatedAt: Int
let profilePic : String?
enum CodingKeys: String, CodingKey {
case idold = "id"
case ticketID = "ticket_id"
case isMe = "is_me"
case message
case photo
case profilePic
case createdAt = "created_at"
case updatedAt = "updated_at"
}
}
@@ -0,0 +1,36 @@
//
// TicketsReponse.swift
// SFI
//
// Created by Mac on 2024/10/20.
//
import Foundation
// MARK: - Welcome
struct TicketsReponse: Codable {
let status, message: String?
let data: [TicketsReponseDatum]?
}
//Message
// MARK: - Datum
struct TicketsReponseDatum: Codable,Identifiable {
var id = UUID().uuidString
let idOLD, level, replyStatus, status: Int
let subject: String
let message: String?
let createdAt, updatedAt, userID: Int
enum CodingKeys: String, CodingKey {
case idOLD = "id"
case level
case replyStatus = "reply_status"
case status, subject, message
case createdAt = "created_at"
case updatedAt = "updated_at"
case userID = "user_id"
}
}
@@ -0,0 +1,30 @@
//
// UserInfo.swift
// LoginKit
//
// Created by Mac on 2024/10/12.
//
import Foundation
struct UserInfo: Codable {
struct Data: Codable {
let email: String
let transfer_enable: Int
let device_limit: Int?
let last_login_at: Int
let created_at: Int
let banned: Int
let remind_expire: Int
let remind_traffic: Int
let expired_at: Int?
let balance: Int
let commission_balance: Int
let plan_id: Int
let discount: Int?
let commission_rate: Int?
let telegram_id: Int?
let uuid: String
let avatar_url: String
}
let data: Data
}
@@ -0,0 +1,183 @@
//
// File.swift
// LoginKit
//
// Created by Mac on 2024/10/12.
//
import Foundation
import Foundation
class UserManager {
static let shared = UserManager()
public let configURL = "https://api.gooapis.com/api/yaya/"
public let appversion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
// UserDefaults keys
private let loginStatusKey = "isLoggedIn"
private let userEmailKey = "userEmail"
private let avatarurlKey = "avatar_url"
private let autodataKey = "autodata"
private let subURLDataKey = "subURLData"
private let baseURLKey = "baseURLKey"
private let crisptoken = "crisptoken"
/// UserDefaults
private let defaults = UserDefaults.standard
func crispTokenData()-> String{
return defaults.string(forKey: crisptoken) ?? ""
}
func baseURL()-> String{
return defaults.string(forKey: "baseURLKey") ?? ""
}
func baseDYURL()-> String{
defaults.string(forKey: "baseDYURL") ?? ""
}
func mainregisterURL()-> String{
defaults.string(forKey: "mainregisterURLKey") ?? ""
}
func paymentURL()-> String{
defaults.string(forKey: "paymentURLKey") ?? ""
}
func telegramUrl()-> String{
defaults.string(forKey: "telegramUrlKey") ?? ""
}
func kefuUrl()-> String{
defaults.string(forKey: "kefuUrlKey") ?? ""
}
func websiteURL()-> String{
defaults.string(forKey: "websiteURLKey") ?? ""
}
func storeCrispTokenData (data: String) {
defaults.set(data, forKey: crisptoken)
defaults.synchronize()
}
/// URL
func storebaseURLData(data: String) {
defaults.set(data, forKey: "baseURLKey")
defaults.synchronize()
}
func storemainregisterURLData(data: String) {
defaults.set(data, forKey: "mainregisterURLKey")
defaults.synchronize()
}
func storepaymentURLData(data: String) {
defaults.set(data, forKey: "paymentURLKey")
defaults.synchronize()
}
func storetelegramUrlData(data: String) {
defaults.set(data, forKey: "telegramUrlKey")
defaults.synchronize()
}
func storekefuUrlData(data: String) {
defaults.set(data, forKey: "kefuUrlKey")
defaults.synchronize()
}
func storewebsiteURLData(data: String) {
defaults.set(data, forKey: "websiteURLKey")
defaults.synchronize()
}
func storebaseDYURL(data:String){
defaults.setValue(data, forKey: "baseDYURL")
defaults.synchronize()
}
///
func updateLoginStatus(_ loggedIn: Bool) {
defaults.set(loggedIn, forKey: loginStatusKey)
defaults.synchronize()
}
func saveUserInfoToLocal(userInfo: UserInfo) {
do {
let data = try JSONEncoder().encode(userInfo)
defaults.set(data, forKey: "userInfo")
defaults.synchronize()
} catch {
}
}
///
func isUserLoggedIn() -> Bool {
return defaults.bool(forKey: loginStatusKey)
}
///
func storeUserInfo(email: String, avator: String) {
defaults.set(email, forKey: userEmailKey)
defaults.set(avator, forKey: avatarurlKey)
defaults.synchronize()
}
///
func getUserInfo() -> (email: String, avator: String) {
let email = defaults.string(forKey: userEmailKey) ?? ""
let avator = defaults.string(forKey: avatarurlKey) ?? ""
return (email, avator)
}
///
func storeAutoData(data: String) {
defaults.set(data, forKey: autodataKey)
defaults.synchronize()
}
/// URL
func getSuburlData() -> String {
return defaults.string(forKey: subURLDataKey) ?? ""
}
/// URL
func storeSuburlData(data: String) {
defaults.set(data, forKey: subURLDataKey)
defaults.synchronize()
}
///
func getAutoData() -> String {
return defaults.string(forKey: autodataKey) ?? ""
}
///
func clearUserData() {
defaults.removeObject(forKey: loginStatusKey)
defaults.removeObject(forKey: userEmailKey)
defaults.removeObject(forKey: avatarurlKey)
defaults.removeObject(forKey: autodataKey)
defaults.removeObject(forKey: subURLDataKey)
defaults.removeObject(forKey: "goGrouptag")
defaults.removeObject(forKey: "selectedNode")
defaults.removeObject(forKey: "paymentURLKey")
defaults.removeObject(forKey: "baseDYURL")
defaults.removeObject(forKey: "serverData")
defaults.removeObject(forKey: "lastFetchTime")
let dictionary = defaults.dictionaryRepresentation()
dictionary.keys.forEach { key in
defaults.removeObject(forKey: key)
}
}
}
@@ -0,0 +1,286 @@
//
// ActiveDashboardViewNewUI.swift
// SFI
//
// Created by Zeus on 2024/10/11.
//
import Foundation
import Foundation
import Libbox
import Library
import SwiftUI
import ApplicationLibrary
@MainActor
public struct ActiveDashboardViewNewUI: View {
@Environment(\.scenePhase) var scenePhase
@Environment(\.selection) private var parentSelection
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@State private var isLoading = true
@State private var profileList: [ProfilePreview] = []
@State private var selectedProfileID: Int64 = 0
@State private var alert: Alert?
@State private var selection = DashboardPage.overview
@State private var systemProxyAvailable = false
@State private var systemProxyEnabled = false
@State private var alerthandleXufei = false
@State private var tixingdingyueEnabled = false
public init() {}
public var body: some View {
if isLoading {
ProgressView().onAppear {
Task {
await doReload()
}
}
} else {
if ApplicationLibrary.inPreview {
body1
} else {
body1
.onAppear {
Task {
await doReloadSystemProxy()
}
}
.onChangeCompat(of: profile.status) { newStatus in
if newStatus == .connected {
Task {
await doReloadSystemProxy()
}
}
}
}
}
}
private var body1: some View {
VStack {
#if os(iOS) || os(tvOS)
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict {
/* Picker("Page", selection: $selection) {
ForEach(DashboardPage.allCases) { page in
page.label
}
}
.pickerStyle(.segmented)
#if os(iOS)
.padding([.leading, .trailing])
.navigationBarTitleDisplayMode(.inline)
#endif
TabView(selection: $selection) {
ForEach(DashboardPage.allCases) { page in
page.contentView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
.tag(page)
}
}
.tabViewStyle(.page(indexDisplayMode: .always))
*/
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
} else {
if tixingdingyueEnabled {
XufeiTixingButton {
alerthandleXufei = true
}.alert(Text("续费提醒"), isPresented: $alerthandleXufei) {
Button("续费"){
environments.opentixingSubnodes.send()
}
} message: {
Text("您的账户已经过期,请续费后继续体验畅快感受;如果您刚购买完请耐心等待,会员时长会在1分钟内到账。")
}
}else{
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
}
}
#elseif os(macOS)
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
#endif
}.onReceive(environments.updateTixingdingyueEnabled, perform: { _ in
tixingdingyueEnabled = false
})
.onReceive(environments.profileUpdate) { _ in
Task {
await doReload()
}
}
.onReceive(environments.selectedProfileUpdate) { _ in
Task {
selectedProfileID = await SharedPreferences.selectedProfileID.get()
if profile.status.isConnected {
await doReloadSystemProxy()
}
}
}
.alertBinding($alert)
}
private func doReload() async {
defer {
isLoading = false
}
if ApplicationLibrary.inPreview {
profileList = [
ProfilePreview(Profile(id: 0, name: "profile local", type: .local, path: "")),
ProfilePreview(Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0))),
]
systemProxyAvailable = true
systemProxyEnabled = true
selectedProfileID = 0
} else {
do {
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
if profileList.isEmpty {
print("profileList.isEmpty 是否提醒续费\(tixingdingyueEnabled), \(UserManager.shared.getSuburlData().count)")
//add profile remote
if tixingdingyueEnabled {
return
}
Task {
if UserManager.shared.getSuburlData().count > 5 {
await createProfile()
}
}
await environments.reload()
return
}
selectedProfileID = await SharedPreferences.selectedProfileID.get()
if profileList.filter({ profile in
profile.id == selectedProfileID
})
.isEmpty {
selectedProfileID = profileList[0].id
await SharedPreferences.selectedProfileID.set(selectedProfileID)
}
// try await ProfileManager.list().forEach { p in
// await ProfileManager.update(p)
// }
} catch {
alert = Alert(error)
return
}
}
environments.emptyProfiles = profileList.isEmpty
}
private nonisolated func doReloadSystemProxy() async {
do {
let status = try LibboxNewStandaloneCommandClient()!.getSystemProxyStatus()
await MainActor.run {
systemProxyAvailable = status.available
systemProxyEnabled = status.enabled
}
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private func createProfile() async {
do {
try await createProfileBackground()
} catch {
print("createProfile : \(error)")
}
environments.profileUpdate.send()
}
private nonisolated func createProfileBackground() async throws {
print("正在创建 profile")
let nextProfileID = try await ProfileManager.nextID()
var savePath = ""
let remoteURL: String = UserManager.shared.getSuburlData()
var lastUpdated: Date? = nil
var remoteContent = try HTTPClient().getString(remoteURL)
print("第一次订阅URL的内容 remoteContent: \(remoteContent.count)")
if(remoteContent.count < 3 && UserManager.shared.paymentURL().count < 3){
//TODO (便,)
//
remoteContent = try HTTPClient().getString(UserManager.shared.baseDYURL())
print("请求服务器的议定 remoteContent: \(remoteContent.count)")
}else{
if(remoteContent.count < 3){
//
print("提醒续费,正常用户")
await MainActor.run {
tixingdingyueEnabled = true
}
}else{
print("正常有订阅的付费客户")
//
await MainActor.run {
tixingdingyueEnabled = false
}
}
}
if remoteContent.isEmpty {
return
}
var error: NSError?
LibboxCheckConfig(remoteContent, &error)
if let error {
throw error
}
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
try remoteContent.write(to: profileConfig, atomically: true, encoding: .utf8)
savePath = profileConfig.relativePath
lastUpdated = .now
try await ProfileManager.create(Profile(
name: "VPN",
type: ProfileType.remote,
path: savePath,
remoteURL: remoteURL,
autoUpdate: true,
autoUpdateInterval: 60,
lastUpdated: lastUpdated
))
#if os(iOS) || os(tvOS)
try UIProfileUpdateTask.configure()
let list = try await ProfileManager.list()
list.forEach { profileOne in
print("createProfileBackground 查询到所有订阅节点:{ \n \(profileOne.id ?? 0 ) \(profileOne.path) \n \(profileOne.remoteURL ?? "") }")
}
await MainActor.run {
environments.openProfileGetSuccess.send()
}
#else
try await ProfileUpdateTask.configure()
#endif
}
}
@@ -0,0 +1,10 @@
//
// BgView.swift
// SFI
//
// Created by Mac on 2024/10/12.
//
import Foundation
//
@@ -0,0 +1,45 @@
//
// DashboardViewNewUI.swift
// SFI
//
// Created by Zeus on 2024/10/11.
//
import Foundation
import Libbox
import Library
import SwiftUI
import ApplicationLibrary
struct DashboardViewNewUI: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@State private var alert: Alert?
var body: some View {
VStack {
ActiveDashboardViewNewUI()
}
.alertBinding($alert)
.onChangeCompat(of: profile.status) { newValue in
if newValue == .disconnecting || newValue == .connected {
Task {
await checkServiceError()
}
}
}
}
private nonisolated func checkServiceError() async {
var error: NSError?
let message = LibboxReadServiceError(&error)
if error != nil {
return
}
await MainActor.run {
alert = Alert(title: Text("VPN 服务异常"), message: Text(message))
}
}
}
@@ -0,0 +1,9 @@
//
// GroupListViewModel.swift
// SFI
//
// Created by Mac on 2024/10/19.
//
import Foundation
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,126 @@
//
// QuestionView.swift
// SFI
//
// Created by Mac on 2024/10/24.
//
import ApplicationLibrary
import Foundation
import SwiftUI
struct QuestionView: View {
@Environment(\.presentationMode) var presentationMode //
var body: some View {
NavigationView(content: {
VStack{
ZStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss() //
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10) //
Spacer(minLength: 0)
}
Text("问题解答")
.fontWeight(.bold).lineLimit(1)
}
.padding()
// .padding(.top,edges.top)
.foregroundColor(.white)
ScrollView(.vertical, showsIndicators: false) {
VStack {
HStack{
Text("App软件无法使用问题?") // Text
Spacer()
}
Section("") {
Text("1:确认网络没问题,包括 WIFI 和 移动网络可用 \n2:确认手机可访问浏览器正常数据,可以尝试访问 https://www.apple.com 试试网络是否连接正常。")
.buttonStyle(.plain).font(.subheadline)
.foregroundColor(Color.white.opacity(0.8))
}
Divider() // 线
HStack{
Text("初次连接提示创建“VPN连接”失败?") // Text
Spacer()
}
Section("") {
Text("到这种情况可能是VPN功能被系统被禁止了。可以尝试到设置中开启“VPN连接”权限,若还有问题请随时联系客服。")
.buttonStyle(.plain).font(.subheadline)
.foregroundColor(Color.white.opacity(0.8))
}
Divider() // 线
HStack{
Text("指定的国家连接不上?") // Text
Spacer()
}
Section("") {
Text("快连每个国家的背后都有成干上百的网络节点在进行智能调控,若遇到指定国家连接不上的情况请随时联系客服为您定位原因,请放心一定能解决!")
.buttonStyle(.plain).font(.subheadline)
.foregroundColor(Color.white.opacity(0.8))
}
Divider() // 线
HStack{
Text("连接成功了,但无法访问外网?") // Text
Spacer()
}
Section("") {
Text("恭喜您遇到了严重的软件BUG,请尽快联系客服复现问题,领取BUG奖励~!!!")
.buttonStyle(.plain).font(.subheadline).foregroundColor(Color.white.opacity(0.8))
}
Divider() // 线
HStack{
Text("遇到VPN无法连接怎么办?") // Text
Spacer()
}
Section("") {
Text("1:确定您所安装的UUVPN是最新版本 2:尝试重启客户端 3:联系我们的客服 ").multilineTextAlignment(.leading)
.buttonStyle(.plain).font(.subheadline).foregroundColor(Color.white.opacity(0.8))
}
}.padding(.leading,10).padding(.trailing,10)
}.foregroundColor(.white)
}.navigationBarHidden(true).edgesIgnoringSafeArea(.bottom).background(
BackgroundBg()
)
})
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true) //
}
}
@@ -0,0 +1,391 @@
//
// InviteListView.swift
// SFI
//
// Created by Mac on 2024/10/21.
//
import Foundation
import SwiftUI
import ApplicationLibrary
//InviteListView
struct InviteListView : View{
@Binding var isPresented: Bool
@State private var codeslist: [InviteCode] = []
@State private var isLoading = false
@State private var errorMessage: String?
@State private var alert: Alert?
@State private var stat: [Int]?
@Environment(\.openURL) private var openURL
@Environment(\.presentationMode) var presentationMode //
var body: some View {
NavigationView(content: {
VStack {
// Navigation Bar
ZStack{
HStack{
Button(action: {
isPresented = false
presentationMode.wrappedValue.dismiss() //
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10) //
Spacer()
Button(action: {
Task{
await genoneInvite()
}
}, label: {
Text("生成邀请码").foregroundColor(.white)
})
}
Spacer()
VStack(spacing: 5){
Text("我的邀请")
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
}
.foregroundColor(.white)
Spacer()
}
.padding(.all)
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.padding()
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding()
}
//
// // Subscription Plans
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 16) {
VStack {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 2), alignment: .leading) {
if let st = stat {
StatusItem("邀请注册人数") {
StatusLine("人数", "\(st[0])")
}
StatusItem("邀请佣金比例") {
StatusLine("比例", "\(st[3])%")
}
StatusItem("确认中的佣金") {
StatusLine("金额(¥)", "\(st[1])")
}
StatusItem("累计获得佣金") {
StatusLine("金额(¥)", "\(st[2])")
}
}
}.background {
GeometryReader { geometry in
Rectangle()
.fill(.clear)
.frame(height: 1)
.onChangeCompat(of: geometry.size.width) { newValue in
//updateColumnCount(newValue)
}
.onAppear {
// updateColumnCount(geometry.size.width)
}
}.padding()
}
}
.frame(alignment: .topLeading)
.padding([.top, .leading, .trailing])
if codeslist.isEmpty {
// Text("Loading plans...") // Loading indicator
if !isLoading
{
Text("暂无记录").font(.caption)
.foregroundColor(.gray)
.multilineTextAlignment(.center) //
.padding()
}
} else {
ForEach(codeslist) { plan in
Button(action: {
UIPasteboard.general.string = UserManager.shared.mainregisterURL()+plan.code
alert = Alert(okMessage: "复制成功: " + UserManager.shared.mainregisterURL()+plan.code )
}, label: {
InviteItemView(invicode: plan)
})
}
}
}
}.padding(.top, 20)
Spacer()
// Subscription Notice
Text(" ")
.font(.footnote)
.foregroundColor(.gray)
.padding(.bottom, 20)
}
.background(
LinearGradient(colors: [
Color("BG1"),
Color("BG1"),
Color("BG2"),
Color("BG2"),
], startPoint: .top, endPoint: .bottom)
)
.navigationBarHidden(true)
// .background(.white)
.onAppear(){
Task{
await getCodesList()
}
}.edgesIgnoringSafeArea(.bottom)
})
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true) //
.alertBinding($alert)
}
public func genoneInvite() async {
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/invite/save")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
alert = Alert(error)
return
}
guard let data = data else {
return
}
struct InviteReponse2: Codable {
let status, message: String?
}
// Parse the user info response
if let response = try? JSONDecoder().decode(InviteReponse2.self, from: data),let message = response.message {
if let status = response.status, status == "success" {
alert = Alert(okMessage: message){
Task{
await getCodesList()
}
}
}else{
alert = Alert(errorMessage: message)
}
}
}
}
task.resume()
}
private struct StatusLine: View {
private let name: String
private let value: String
init(_ name: String, _ value: String) {
self.name = name
self.value = value
}
var body: some View {
HStack {
Text(name)
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
Text(value)
.font(.subheadline)
}
}
}
private struct StatusItem<T>: View where T: View {
private let title: String
@ViewBuilder private let content: () -> T
init(_ title: String, @ViewBuilder content: @escaping () -> T) {
self.title = title
self.content = content
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(title)
.font(.subheadline)
Spacer()
}.padding(.bottom, 8)
content()
}
.frame(minWidth: 125, alignment: .topLeading)
#if os(tvOS)
.padding(EdgeInsets(top: 20, leading: 26, bottom: 20, trailing: 26))
#else
.padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13))
#endif
.background(backgroundColor.opacity(0.3))
.cornerRadius(10)
}
private var backgroundColor: Color {
#if os(iOS)
return Color(.gray)
#elseif os(macOS)
return Color(nsColor: .textBackgroundColor)
#elseif os(tvOS)
return Color(uiColor: .black)
#endif
}
}
public func getCodesList() async {
isLoading = true
errorMessage = nil
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/invite/fetch")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "数据请求失败"
return
}
// if let jsonString = String(data: data, encoding: .utf8) {
// print("Response data: \(jsonString)")
// } else {
// print("Failed to convert data to string.")
// }
//
// Parse the user info response
if let Subscribe = try? JSONDecoder().decode(InviteReponse.self, from: data) {
if let codes = Subscribe.data?.codes {
codeslist = codes
stat = Subscribe.data?.stat
}else{
self.errorMessage = Subscribe.message ?? ""
}
}else{
self.errorMessage = "数据请求失败: JSON 解析错误"
}
}
}
task.resume()
}
}
struct InviteItemView: View {
var invicode : InviteCode
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("邀请码:" + invicode.code )
.font(.headline)
.foregroundColor(.black).multilineTextAlignment(.leading)
.frame(maxWidth: .infinity)
Spacer()
Text("创建时间:"+TimestampConverter.convertTimestampToDateString(invicode.createdAt))
.font(.caption)
.fontWeight(.bold)
.padding(2)
.frame(maxWidth: .infinity)
.foregroundColor(.gray)
// .offset(x: -16, y:16)
.offset(y:10)
}
}
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 3)
.padding(.horizontal, 20)
}
}
@@ -0,0 +1,204 @@
//
// InviteView.swift
// SFI
//
// Created by Mac on 2024/10/12.
//
import Foundation
import SwiftUI
struct InviteView: View {
@Binding var isPresented: Bool
@State private var inviteCode: String = "" // This can be dynamically updated.
@State private var alert: Alert?
var body: some View {
ZStack(){
LinearGradient(colors: [
Color("BG1"),
Color("BG1"),
Color("BG2"),
Color("BG2"),
], startPoint: .top, endPoint: .bottom)
VStack(spacing: 20) {
Spacer().frame(height: 30)
// Invitation code section
HStack {
Text("我的邀请码:")
.font(.headline)
.foregroundColor(.black)
Text(inviteCode)
.font(.headline)
.foregroundColor(.green)
}
// Illustration image
// Image("panda_invite") // Add your asset with this name
LottieView(animationFileName: "7a4ce4b4" , loopMode: .loop)
.aspectRatio(contentMode: .fill)
//// .frame(width: getRect().width,height: getRect().width)
.scaleEffect(0.3)
.scaledToFit()
.frame(width: 200, height: 150)
// Invite description
VStack(spacing: 5) {
Text("邀请您的朋友成为 VIP,邀请好友享首次付费30%返佣,赶紧邀请好友一同使用开心上网!")
.multilineTextAlignment(.center)
.font(.headline)
.padding(10)
.foregroundColor(.black)
}
// Copy invite code button
Button(action: {
// Copy invite code action
if inviteCode.count <= 1 {
Task {
await reloadInvite(copy: true)
}
}else{
UIPasteboard.general.string = UserManager.shared.mainregisterURL()+inviteCode
alert = Alert(okMessage: "复制成功")
}
}) {
Text("复制邀请链接")
.font(.system(size: 16))
.frame(maxWidth: .infinity)
.padding()
.background(LinearGradient(gradient: Gradient(colors: [Color.green.opacity(0.7), Color.green]), startPoint: .leading, endPoint: .trailing))
.cornerRadius(25)
.foregroundColor(.white)
}
.padding(.horizontal, 40)
Spacer().frame(height: 30)
}
.background(
RoundedRectangle(cornerRadius: 20)
.fill(Color.white)
.shadow(radius: 10)
)
.padding()
}
.onAppear(perform: {
Task {
await reloadInvite(copy: false)
}
})
.alertBinding($alert)
.edgesIgnoringSafeArea(.all)
}
public func reloadInvite(copy: Bool) async {
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/invite/fetch")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
alert = Alert(error)
return
}
guard let data = data else {
return
}
// Parse the user info response
if let invites = try? JSONDecoder().decode(InviteReponse.self, from: data) {
if let invite = invites.data?.codes?.first?.code {
self.inviteCode = invite
if copy {
UIPasteboard.general.string = UserManager.shared.mainregisterURL()+inviteCode
alert = Alert(okMessage: "复制成功")
}
}else{
//
Task{
await genoneInvite()
}
}
}else{
alert = Alert(errorMessage: "访问失败")
}
}
}
task.resume()
}
public func genoneInvite() async {
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/invite/save")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
alert = Alert(error)
return
}
guard let data = data else {
return
}
// Parse the user info response
if let _ = try? JSONDecoder().decode(InviteReponse.self, from: data) {
Task{
await reloadInvite(copy: false)
}
// if invites.status == "success" {}
/**
{
"status": "success",
"message": "\u64cd\u4f5c\u6210\u529f",
"data": true,
"error": null
}
*/
}
}
}
task.resume()
}
}
@@ -0,0 +1,182 @@
//
// ForgotPassword.swift
// LoginKit
//
// Created by zues on 04/08/23.
//
import SwiftUI
struct ForgotPassword: View {
@Binding var showResetView: Bool
/// View Properties
//@State private var emailID: String = ""
@Binding var emailID: String
/// Environment Properties
@Environment(\.dismiss) private var dismiss
@State private var askOTP: Bool = false
//@State private var otpText: String = ""
@Binding var otpText: String
@State private var isLoading = false
@State private var errorMessage: String?
var body: some View {
VStack(alignment: .leading, spacing: 15, content: {
/// Back Button
Button(action: {
dismiss()
}, label: {
Image(systemName: "arrow.left")
.font(.title2)
.foregroundStyle(.gray)
})
.padding(.top, 10)
Text("忘记密码?")
.font(.largeTitle)
//.fontWeight(.heavy)
.padding(.top, 5)
Text("请输入您的电子邮件,以便我们可以发送验证码。")
.font(.caption)
//.fontWeight(.semibold)
.foregroundStyle(.gray)
.padding(.top, -5)
VStack(spacing: 25) {
/// Custom Text Fields
CustomTF(sfIcon: "at", hint: "输入邮箱", value: $emailID)
if isLoading {
ProgressView()
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding()
}
/// SignUp Button
GradientButton(title: "发送验证码", icon: "arrow.right") {
/// YOUR CODE
/// After the Link sent
Task {
await sendemail(email:emailID)
}
}
.hSpacing(.trailing)
/// Disabling Until the Data is Entered
.disableWithOpacity(emailID.isEmpty || isLoading)
}
.padding(.top, 20)
Spacer(minLength: 0)
})/// OTP Prompt
.sheet(isPresented: $askOTP, onDismiss: {
/// YOUR CODE
/// Reset OTP if You Want
// otpText = ""
}, content: {
if #available(iOS 16.4, *) {
/// Since I wanted a Custom Sheet Corner Radius
OTPView(otpText: $otpText,onButtonClick: {
print(otpText)
Task {
dismiss()
// try? await Task.sleep(for: .seconds(0))
/// Showing the Reset View
showResetView = true
}
})
.presentationDetents([.height(350)])
.presentationCornerRadius(30)
} else {
if #available(iOS 16.0, *) {
OTPView(otpText: $otpText,onButtonClick: {
print(otpText)
Task {
dismiss()
// try? await Task.sleep(for: .seconds(0))
/// Showing the Reset View
showResetView = true
}
}).presentationDetents([.height(350)])
} else {
OTPView(otpText: $otpText,onButtonClick: {
print(otpText)
Task {
dismiss()
// try? await Task.sleep(for: .seconds(0))
/// Showing the Reset View
showResetView = true
}
})
// Fallback on earlier versions
}
}
})
.padding(.vertical, 15)
.padding(.horizontal, 25)
/// Since this is going to be a Sheet.
.interactiveDismissDisabled()
}
@MainActor
public func sendemail(email: String) async {
isLoading = true
errorMessage = nil
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())passport/comm/sendEmailVerify?email=\(email)&recaptcha_data=")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "验证码发送失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "验证码发送失败:数据返回空"
return
}
// Parse the user info response
if let message = try? JSONDecoder().decode(MessageResponse.self, from: data) {
dump(message)
if let _ = message.errors{
self.errorMessage = message.message
}else{
askOTP.toggle()
}
}
}
}
task.resume()
}
}
@@ -0,0 +1,693 @@
//
// Login.swift
// LoginKit
//
// Created by zues on 04/08/23.
//
import SwiftUI
import Crisp
// MARK: - Errors
struct Errors: Codable {
let email: [String]?
}
struct LoginResponseSuccess: Codable {
let data: LoginResponseSuccessClass?
let message: String?
let status: String?
let errors: Errors?
}
// MARK: - DataClass
struct LoginResponseSuccessClass: Codable {
let token: String
let isAdmin: Int?
let authData: String
enum CodingKeys: String, CodingKey {
case token
case isAdmin = "is_admin"
case authData = "auth_data"
}
}
struct Login: View {
@Binding var isLoggedIn: Bool
@Binding var showSignup: Bool
/// View Properties
@State private var emailID: String = ""
@State private var password: String = ""
@State private var showForgotPasswordView: Bool = false
/// Reset Password View (with New Password and Confimration Password View)
@State private var showResetView: Bool = false
/// Optional, Present If you want to ask OTP for login
@State private var askOTP: Bool = false
@State private var otpText: String = ""
@State private var showKefuView: Bool = false
@State private var userInfo: UserInfo? = nil
@State private var isLoading = false
@State private var errorMessage: String?
@State private var alert: Alert?
@State private var okMessage: String?
@FocusState private var focusedField: Bool
@State private var isresetpwdLoading = false
@State private var resetpwdErrorMessage = ""
@AppStorage("isFirstOpen") private var isFirstOpen: Bool?
@State var onboardingItems: [OnboardingItem] = [
.init(title: "最稳定的加速器",
subTitle: "順暢瀏覽社交媒體,同時支持Netflix及Youtube等流媒體內容平台,4K清晰度不卡頓。",
lottieView: .init(name: "12c853b3",bundle: .main)),
.init(title: "永不跑路的VPN加速器",
subTitle: "一鍵訪問匿名互聯網,告別繁瑣的配置,跨平臺設備支持,隨時隨地連接網絡。",
lottieView: .init(name: "2c630b55",bundle: .main)),
.init(title: "加密访问,全球畅连",
subTitle: "隱藏您的IP地址。互聯網流量通過TLS加密保護免受任何黑客探測、監控和攻擊。",
lottieView: .init(name: "5a519db7",bundle: .main))
]
// MARK: Current Slide Index
@State var currentIndex: Int = 0
var onBoardView: some View {
GeometryReader{
let size = $0.size
VStack(spacing: 40, content: {
Spacer().frame(height: 10)
HStack(spacing: 0){
ForEach($onboardingItems) { $item in
let isLastSlide = (currentIndex == onboardingItems.count - 1)
VStack{
// MARK: Top Nav Bar
HStack{
Button(action: {
if currentIndex > 0{
currentIndex -= 1
playAnimation()
}
}, label: {
Image(systemName: "arrow.backward")
}).tint(Color("Main"))
.opacity(currentIndex > 0 ? 1 : 0)
Spacer(minLength: 0)
// Button(""){
// currentIndex = onboardingItems.count - 1
// playAnimation()
// }
// .opacity(isLastSlide ? 0 : 1)
}
.animation(.easeInOut, value: currentIndex)
.tint(Color("Main"))
//.fontWeight(.bold)
// MARK: Movable Slides
VStack(spacing: 15){
let offset = -CGFloat(currentIndex) * size.width
// MARK: Resizable Lottie View
ResizableLottieView(onboardingItem: $item)
.frame(height: size.width*0.8)
.onAppear {
// MARK: Intially Playing First Slide Animation
if currentIndex == indexOf(item){
item.lottieView.play(toProgress: 0.7)
}
}
.offset(x: offset)
.animation(.easeInOut(duration: 0.5), value: currentIndex)
Text(item.title)
.font(.title.bold())
.offset(x: offset)
.animation(.easeInOut(duration: 0.5).delay(0.1), value: currentIndex)
Text(item.subTitle)
.font(.system(size: 14))
.multilineTextAlignment(.center)
.padding(.horizontal,15)
.foregroundColor(.gray)
.offset(x: offset)
.animation(.easeInOut(duration: 0.5).delay(0.2), value: currentIndex)
}
Spacer(minLength: 0)
// MARK: Next / Login Button
VStack(spacing: 15){
Button(isLastSlide ? "登录" : "下一步"){
if currentIndex < onboardingItems.count - 1{
// MARK: Pausing Previous Animation
let currentProgress = onboardingItems[currentIndex].lottieView.currentProgress
onboardingItems[currentIndex].lottieView.currentProgress = (currentProgress == 0 ? 0.7 : currentProgress)
currentIndex += 1
// MARK: Playing Next Animation from Start
playAnimation()
}
if isLastSlide {
withAnimation(){
isFirstOpen = false
}
}
}
// .fontWeight(.bold)
.foregroundColor(.white)
.padding(.vertical,isLastSlide ? 13 : 12)
.frame(maxWidth: .infinity)
.background {
Capsule()
.fill(Color("Main2"))
}
.padding(.horizontal,isLastSlide ? 50 : 100)
if currentIndex == onboardingItems.count - 1{
HStack{
// Button("Terms of Service"){}
// Button("Privacy Policy"){}
}
.font(.caption2)
//.underline(true, color: .primary)
.offset(y: 5)
}
}.onTapGesture {
if currentIndex < onboardingItems.count - 1{
// MARK: Pausing Previous Animation
let currentProgress = onboardingItems[currentIndex].lottieView.currentProgress
onboardingItems[currentIndex].lottieView.currentProgress = (currentProgress == 0 ? 0.7 : currentProgress)
currentIndex += 1
// MARK: Playing Next Animation from Start
playAnimation()
}
if isLastSlide {
withAnimation(){
isFirstOpen = false
}
}
}
}
.animation(.easeInOut, value: isLastSlide)
.padding(15)
.frame(width: size.width, height: size.height)
}
}
.frame(width: size.width * CGFloat(onboardingItems.count),alignment: .leading)
}).ignoresSafeArea(.all)
// if !#available(iOS 15.0, *) {
//
// }
}
}
func playAnimation(){
onboardingItems[currentIndex].lottieView.currentProgress = 0
onboardingItems[currentIndex].lottieView.play(toProgress: 0.7)
}
// MARK: Retreving Index of the Item in the Array
func indexOf(_ item: OnboardingItem)->Int{
if let index = onboardingItems.firstIndex(of: item){
return index
}
return 0
}
var body: some View {
if isFirstOpen ?? true {
onBoardView
}else{
loginView
}
}
var loginView: some View {
VStack(alignment: .leading, spacing: 15, content: {
Spacer(minLength: 0)
// HStack{
// Spacer()
// Image("applogo").resizable().frame(width: 100, height: 100).cornerRadius(20)
// Spacer()
// }
Text("登录")
.font(.largeTitle)
//.fontWeight(.heavy)
Text("请按登录按钮继续")
.font(.callout)
//.fontWeight(.semibold)
.foregroundStyle(.gray)
.padding(.top, -5)
VStack(spacing: 25) {
/// Custom Text Fields
CustomTF(sfIcon: "at", hint: "Email", value: $emailID)
CustomTF(sfIcon: "lock", hint: "密码", isPassword: true, value: $password)
.padding(.top, 5).focused($focusedField)
HStack(content: {
Button("忘记密码?") {
showForgotPasswordView.toggle()
}
.font(.callout)
//.fontWeight(.heavy)
.tint(.white)
Button {
showKefuView.toggle()
} label: {
Text("联系客服") .font(.callout)
Image(systemName: "person.crop.circle.badge.questionmark")
}.tint(.white)
})
.hSpacing(.trailing)
/// Login Button
///
if isLoading {
ProgressView()
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding()
}
if let okmsg = okMessage {
Text(okmsg)
.bold()
.foregroundColor(Color("Main"))
.multilineTextAlignment(.center) //
.padding()
}
if let userInfo = userInfo {
Text("Logged in as: \(userInfo.data.email)")
// AsyncImage(url: URL(string: userInfo.data.avatar_url))
// .frame(width: 64, height: 64)
// .clipShape(Circle())
}
GradientButton(title: "登录", icon: "arrow.right") {
/// YOUR CODE
//askOTP.toggle()
Task{
await loginUser()
}
}
.hSpacing(.trailing)
/// Disabling Until the Data is Entered
.disableWithOpacity(emailID.isEmpty || password.isEmpty || isLoading)
}
.padding(.top, 20)
Spacer(minLength: 0)
HStack(spacing: 6) {
Text("没有账号?")
.foregroundStyle(.gray)
Button("注册") {
showSignup.toggle()
}
//.fontWeight(.bold)
.tint(.white)
}
.font(.callout)
.hSpacing()
})
.onAppear(){
// config
Task{
await getConfig()
}
}
.padding(.vertical, 15)
.padding(.horizontal, 25)
//.toolbar(.hidden, for: .navigationBar)
/// Asking Email ID For Sending Reset Link
.sheet(isPresented: $showForgotPasswordView, content: {
if #available(iOS 16.4, *) {
/// Since I wanted a Custom Sheet Corner Radius
ForgotPassword(showResetView: $showResetView, emailID: $emailID, otpText: $otpText)
.presentationDetents([.height(300)])
.presentationCornerRadius(30)
} else {
ForgotPassword(showResetView: $showResetView, emailID: $emailID, otpText: $otpText)
// .presentationDetents([.height(300)])
}
})
.fullScreenCover(isPresented: $showKefuView, content: {
SupportView()
})
/// Resetting New Password
.sheet(isPresented: $showResetView, content: {
if #available(iOS 16.4, *) {
/// Since I wanted a Custom Sheet Corner Radius
PasswordResetView(isresetpwdLoading: $isresetpwdLoading, resetpwdErrorMessage: $resetpwdErrorMessage) { password in
print(otpText + " " + password)
Task{
await resetpwd(password:password)
}
}
.presentationDetents([.height(350)])
.presentationCornerRadius(30)
} else {
PasswordResetView(isresetpwdLoading: $isresetpwdLoading, resetpwdErrorMessage: $resetpwdErrorMessage){password in
print(otpText + " " + password)
Task{
await resetpwd(password:password)
}
}
// .presentationDetents([.height(350)])
}
})
.alertBinding($alert)
/// OTP Prompt
.sheet(isPresented: $askOTP, onDismiss: {
/// YOUR CODE
/// Reset OTP if You Want
// otpText = ""
}, content: {
if #available(iOS 16.4, *) {
/// Since I wanted a Custom Sheet Corner Radius
OTPView(otpText: $otpText)
.presentationDetents([.height(350)])
.presentationCornerRadius(30)
} else {
OTPView(otpText: $otpText)
//.presentationDetents([.height(350)])
}
})
}
func resetpwd(password:String) async {
isresetpwdLoading = true
let loginUrl = URL(string: "\(UserManager.shared.baseURL())passport/auth/forget?email=\(emailID)&password=\(password)&email_code=\(otpText)")!
var request = URLRequest(url: loginUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
print(loginUrl)
let loginData = ["email": emailID, "password": password, "email_code": otpText]
guard let httpBody = try? JSONSerialization.data(withJSONObject: loginData) else {
self.resetpwdErrorMessage = "请求格式错误"
self.isresetpwdLoading = false
return
}
request.httpBody = httpBody
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
isresetpwdLoading = false
if let error = error {
resetpwdErrorMessage = ( "密码重置失败: \(error.localizedDescription)")
return
}
guard let data = data else {
resetpwdErrorMessage = ("密码重置失败: 数据为空")
return
}
struct resetpwdResponseSuccess: Codable {
let message: String?
let status: String?
}
// Parse the login response and get the authorization token
if let jsonResponse = try? JSONDecoder().decode(resetpwdResponseSuccess.self, from: data){
dump(jsonResponse)
if let status = jsonResponse.status , status == "success" {
withAnimation {
showResetView = false
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
alert = Alert(okMessage: "密码重置成功!")
}
}else{
if let message = jsonResponse.message {
resetpwdErrorMessage = ( "\(message)")
}else{
resetpwdErrorMessage = ( "密码重置失败: 未知错误")
}
}
}else{
resetpwdErrorMessage = ("密码重置失败: 数据 json 格式错误")
}
}
}
task.resume()
}
// MARK: - Networking Logic
func getConfig() async {
let loginUrl = URL(string: UserManager.shared.configURL)!
var request = URLRequest(url: loginUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(Bundle.main.bundleIdentifier ?? "", forHTTPHeaderField: "bid")
request.addValue(UserManager.shared.appversion, forHTTPHeaderField: "appver")
request.addValue(UIDevice.current.model, forHTTPHeaderField: "model")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let _ = error {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
Task{
await getConfig()
}
}
return
}
guard let data = data else {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
Task{
await getConfig()
}
}
return
}
// Parse the login response and get the authorization token
struct configReponse: Codable {
let baseURL, mainregisterURL,paymentURL,crisptoken: String
let telegramurl, kefuurl, websiteURL,baseDYURL: String
let message: String
let code: Int
}
if let jsonString = String(data: data, encoding: .utf8) {
print("Response data: \(jsonString)")
}
if let jsonResponse = try? JSONDecoder().decode(configReponse.self, from: data){
// dump(jsonResponse)
if jsonResponse.code == 1 {
//save data
UserManager.shared.storebaseURLData(data: jsonResponse.baseURL)
UserManager.shared.storemainregisterURLData(data: jsonResponse.mainregisterURL)
UserManager.shared.storepaymentURLData(data: jsonResponse.paymentURL)
UserManager.shared.storetelegramUrlData(data: jsonResponse.telegramurl)
UserManager.shared.storekefuUrlData(data: jsonResponse.kefuurl)
UserManager.shared.storewebsiteURLData(data: jsonResponse.websiteURL)
UserManager.shared.storebaseDYURL(data: jsonResponse.baseDYURL)
//crisptoken
CrispSDK.configure(websiteID: jsonResponse.crisptoken)
}
}else{
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
Task{
await getConfig()
}
}
}
}
}
task.resume()
}
func loginUser() async {
isLoading = true
errorMessage = nil
focusedField = false
let loginUrl = URL(string: "\(UserManager.shared.baseURL())passport/auth/login")!
var request = URLRequest(url: loginUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let loginData = ["email": emailID, "password": password, "captchaData": ""]
guard let httpBody = try? JSONSerialization.data(withJSONObject: loginData) else {
self.errorMessage = "Invalid login data"
self.isLoading = false
return
}
request.httpBody = httpBody
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "登录失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "登录失败: 数据为空"
return
}
// Parse the login response and get the authorization token
if let jsonResponse = try? JSONDecoder().decode(LoginResponseSuccess.self, from: data){
dump(jsonResponse)
if let _ = jsonResponse.errors, let message = jsonResponse.message{
// Proceed to fetch user info
// fetchUserInfo(token: token)
self.errorMessage = "\(message)"
}else{
if let authData = jsonResponse.data?.authData {
UserManager.shared.updateLoginStatus(true)
UserManager.shared.storeAutoData(data: authData)
self.okMessage = "登录成功,正在跳转..."
self.isLoading = true
UserManager.shared.storeUserInfo(email: emailID, avator: "")
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
self.isLoggedIn = true
}
//
// Task{
// await fetchUserInfo(token: authData)
// }
}else{
if let message = jsonResponse.message {
self.errorMessage = "\(message)"
}
}
}
}else{
self.errorMessage = "登录失败: 数据 json 格式错误"
}
//{"data":{"token":"880e8785746c0bf72b2e01a882a678e7","is_admin":1,"auth_data":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Miwic2Vzc2lvbiI6Ijc4MzE1Yjc5YmNlNTliYWIzOTg5MTIzMTFhNDkwN2NiIn0.00fdwJ85bOSycfnkOPmhF7pSR1VV9WczBEeE9aXncQQ"}}
}
}
task.resume()
}
func fetchUserInfo(token: String) async {
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/info")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("\(token)", forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
self.errorMessage = "Failed to get user info: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "No user data received"
return
}
// Parse the user info response
if let userInfo = try? JSONDecoder().decode(UserInfo.self, from: data) {
self.userInfo = userInfo
// Save user info to local storage
UserManager.shared.saveUserInfoToLocal(userInfo: userInfo)
UserManager.shared.storeUserInfo(email: userInfo.data.email, avator: userInfo.data.avatar_url)
//
let isLoggedIn = UserManager.shared.isUserLoggedIn()
let userInfo = UserManager.shared.getUserInfo()
let autoData = UserManager.shared.getAutoData()
print("Logged in: \(isLoggedIn)")
print("Email: \(userInfo.email), avator: \(userInfo.avator)")
print("Auto Data: \(autoData)")
self.isLoggedIn = true
}
}
}
task.resume()
}
// Save user info to local storage (UserDefaults)
/**
{"data":{"email":"ceshi1@qq.com","transfer_enable":119185342464,"device_limit":null,"last_login_at":1725893596,"created_at":1725893596,"banned":0,"remind_expire":1,"remind_traffic":1,"expired_at":null,"balance":0,"commission_balance":0,"plan_id":1,"discount":null,"commission_rate":null,"telegram_id":null,"uuid":"0b0d55e3-9f6f-4ee0-9a73-2901655cfaf2","avatar_url":"https:\/\/cravatar.cn\/avatar\/845735bc7a6186ae5bad56e7cb87d88b?s=64&d=identicon"}}
*/
}
@@ -0,0 +1,75 @@
//
// ContentView.swift
// LoginKit
//
// Created by zues on 04/08/23.
//
import SwiftUI
struct LoginContentView: View {
@Binding var isLoggedIn: Bool
/// View Properties
@State private var showSignup: Bool = false
/// Keyboard Status
@State private var isKeyboardShowing: Bool = false
var body: some View {
NavigationView{
if showSignup {
// showSignup true SignUp
SignUp(isLoggedIn: $isLoggedIn,showSignup: $showSignup).onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification), perform: { _ in
/// Disabling it for signup view
if !showSignup {
isKeyboardShowing = true
}
})
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification), perform: { _ in
isKeyboardShowing = false
}).background(BackgroundBg())
} else {
/// Checking if any Keyboard is Visible
// showSignup false Login
Login(isLoggedIn: $isLoggedIn, showSignup: $showSignup).onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification), perform: { _ in
/// Disabling it for signup view
if !showSignup {
isKeyboardShowing = true
}
})
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification), perform: { _ in
isKeyboardShowing = false
}).background(BackgroundBg())
}
}
.preferredColorScheme(.dark)
//
// /// iOS 17 Bounce Animations
// if #available(iOS 17, *) {
// /// Since this Project Supports iOS 16 too.
// CircleView()
// .animation(.smooth(duration: 0.45, extraBounce: 0), value: showSignup)
// .animation(.smooth(duration: 0.45, extraBounce: 0), value: isKeyboardShowing)
// } else {
// CircleView()
// .animation(.easeInOut(duration: 0.3), value: showSignup)
// .animation(.easeInOut(duration: 0.3), value: isKeyboardShowing)
// }
// }
}
/// Moving Blurred background
@ViewBuilder
func CircleView() -> some View {
Circle()
.fill(.linearGradient(colors: [.main, .main2], startPoint: .top, endPoint: .bottom))
.frame(width: 200, height: 200)
/// Moving When the Signup Pages Loads/Dismisses
.offset(x: showSignup ? 90 : -90, y: -90 - (isKeyboardShowing ? 200 : 0))
.blur(radius: 15)
.hSpacing(showSignup ? .trailing : .leading)
.vSpacing(.top)
}
}
@@ -0,0 +1,62 @@
//
// OTPView.swift
// LoginKit
//
// Created by zues on 04/08/23.
//
import SwiftUI
struct OTPView: View {
@Binding var otpText: String
/// Environment Properties
@Environment(\.dismiss) private var dismiss
var onButtonClick: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 15, content: {
/// Back Button
Button(action: {
dismiss()
}, label: {
Image(systemName: "arrow.left")
.font(.title2)
.foregroundStyle(.gray)
})
.padding(.top, 15)
Text("输入验证码")
.font(.largeTitle)
//.fontWeight(.heavy)
.padding(.top, 5)
Text("一个 6 位数的验证码已发送至您的电子邮件。")
.font(.caption)
//.fontWeight(.semibold)
.foregroundStyle(.gray)
.padding(.top, -5)
VStack(spacing: 25) {
/// Custom OTP TextField
OTPVerificationView(otpText: $otpText)
/// SignUp Button
GradientButton(title: "下一步", icon: "arrow.right") {
/// YOUR CODE
onButtonClick?()
}
.hSpacing(.trailing)
/// Disabling Until the Data is Entered
.disableWithOpacity(otpText.isEmpty)
}
.padding(.top, 20)
Spacer(minLength: 0)
})
.padding(.vertical, 15)
.padding(.horizontal, 25)
/// Since this is going to be a Sheet.
.interactiveDismissDisabled()
}
}
@@ -0,0 +1,74 @@
//
// PasswordResetView.swift
// LoginKit
//
// Created by zues on 04/08/23.
//
import SwiftUI
struct PasswordResetView: View {
/// View Properties
@State private var password: String = ""
@State private var confirmPassword: String = ""
@Binding var isresetpwdLoading: Bool
@Binding var resetpwdErrorMessage : String
var onButtonClick: ((String) -> Void)?
/// Environment Properties
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack(alignment: .leading, spacing: 15, content: {
/// Back Button
Button(action: {
dismiss()
}, label: {
Image(systemName: "xmark")
.font(.title2)
.foregroundStyle(.gray)
})
.padding(.top, 10)
Text("重置密码")
.font(.largeTitle)
//.fontWeight(.heavy)
.padding(.top, 5)
VStack(spacing: 25) {
/// Custom Text Fields
CustomTF(sfIcon: "lock", hint: "密码", value: $password)
CustomTF(sfIcon: "lock", hint: "再次确认密码", value: $confirmPassword)
.padding(.top, 5)
if isresetpwdLoading {
ProgressView()
}
if !resetpwdErrorMessage.isEmpty {
Text(resetpwdErrorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding()
}
/// SignUp Button
GradientButton(title: "重置密码", icon: "arrow.right") {
/// YOUR CODE
/// Reset Password
onButtonClick?(password)
}
.hSpacing(.trailing)
/// Disabling Until the Data is Entered
.disableWithOpacity(password.isEmpty || confirmPassword.isEmpty || confirmPassword != password || isresetpwdLoading)
}
.padding(.top, 20)
Spacer(minLength: 0)
})
.padding(.vertical, 15)
.padding(.horizontal, 25)
/// Since this is going to be a Sheet.
.interactiveDismissDisabled()
}
}
@@ -0,0 +1,214 @@
//
// Signup.swift
// LoginKit
//
// Created by zues on 04/08/23.
//
import SwiftUI
struct SignUp: View {
@Binding var isLoggedIn: Bool
@Binding var showSignup: Bool
@State private var isLoading = false
@State private var errorMessage: String?
/// View Properties
@State private var emailID: String = ""
@State private var fullName: String = ""
@State private var password: String = ""
/// Optional, Present If you want to ask OTP for Signup
@State private var askOTP: Bool = false
@State private var otpText: String = ""
var body: some View {
VStack(alignment: .leading, spacing: 15, content: {
// HStack{
// Spacer()
// Image("applogo").resizable().frame(width: 100, height: 100).cornerRadius(20)
// Spacer()
// }
Text("注册")
.font(.largeTitle)
//.fontWeight(.heavy)
.padding(.top, 25)
Text("请按注册按钮继续")
.font(.callout)
//.fontWeight(.semibold)
.foregroundStyle(.gray)
.padding(.top, -5)
VStack(spacing: 25) {
/// Custom Text Fields
CustomTF(sfIcon: "at", hint: "Email", value: $emailID)
CustomTF(sfIcon: "lock", hint: "密码", isPassword: true, value: $password)
.padding(.top, 5)
CustomTF(sfIcon: "person", hint: "邀请人(可不填)", value: $fullName)
.padding(.top, 5)
Text("通过注册,你同意我们的[条款和条件](https://minipanda.soccertt.com/teams.html)和[隐私政策](https://minipanda.soccertt.com/privacy.html)")
.font(.caption)
.tint(.white)
.foregroundStyle(.gray)
.frame(height: 50)
if isLoading {
ProgressView()
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding()
}
/// SignUp Button
GradientButton(title: "注册", icon: "arrow.right") {
/// YOUR CODE
//askOTP.toggle()
Task{
await logupUser()
}
}
.hSpacing(.trailing)
/// Disabling Until the Data is Entered || fullName.isEmpty
.disableWithOpacity(emailID.isEmpty || password.isEmpty || isLoading)
}
.padding(.top, 20)
Spacer(minLength: 0)
HStack(spacing: 6) {
Text("已经有账号?")
.foregroundStyle(.gray)
Button("登录") {
showSignup = false
}
//.fontWeight(.bold)
.tint(.white)
}
.font(.callout)
.hSpacing()
})
.padding(.vertical, 15)
.padding(.horizontal, 25)
//.toolbar(.hidden, for: .navigationBar)
/// OTP Prompt
.sheet(isPresented: $askOTP, onDismiss: {
/// YOUR CODE
/// Reset OTP if You Want
// otpText = ""
}, content: {
if #available(iOS 16.4, *) {
/// Since I wanted a Custom Sheet Corner Radius
OTPView(otpText: $otpText)
.presentationDetents([.height(350)])
.presentationCornerRadius(30)
} else {
OTPView(otpText: $otpText)
// .presentationDetents([.height(350)])
}
})
}
// MARK: - Networking Logic
func logupUser() async {
isLoading = true
errorMessage = nil
let loginUrl = URL(string: "\(UserManager.shared.baseURL())passport/auth/register")!
var request = URLRequest(url: loginUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let loginData = ["email": emailID, "password": password, "captchaData": "","email_code":"","invite_code":fullName]
guard let httpBody = try? JSONSerialization.data(withJSONObject: loginData) else {
self.errorMessage = "Invalid login data"
self.isLoading = false
return
}
request.httpBody = httpBody
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "注册失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "注册失败:数据返回空"
return
}
// Parse the login response and get the authorization token
if let jsonResponse = try? JSONDecoder().decode(LoginResponseSuccess.self, from: data){
if let authData = jsonResponse.data?.authData {
UserManager.shared.updateLoginStatus(true)
UserManager.shared.storeAutoData(data: authData)
self.isLoggedIn = true
}else{
if let message = jsonResponse.message {
self.errorMessage = "\(message)"
}
}
}else{
self.errorMessage = "注册失败: 返回JSON格式错误"
}
}
}
task.resume()
}
}
/***
{
"status": "success",
"message": "\u64cd\u4f5c\u6210\u529f",
"data": {
"token": "56ffc6d0465212eca2856aa25dc1644e",
"is_admin": null,
"auth_data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MTU0MDMsInNlc3Npb24iOiJiYzk3N2RhOTczN2RjZWVlM2FiNDc2NzAzMjBjOTI2OCJ9.WUPCzTlgsOMGM-DrsSMRrfBqgAB8GEysDyAofq6TgVo"
},
"error": null
}
*/
@@ -0,0 +1,75 @@
//
// MessageCenterView.swift
// SFI
//
// Created by Mac on 2024/10/24.
//
import Foundation
//
//
// QuestionView.swift
// SFI
//
// Created by Mac on 2024/10/24.
//
import ApplicationLibrary
import Foundation
import SwiftUI
struct MessageCenterView: View {
@Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView(content: {
VStack{
ZStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10)
Spacer(minLength: 0)
}
Text("消息通知")
.fontWeight(.bold).lineLimit(1)
}
.padding()
// .padding(.top,edges.top)
.foregroundColor(.white)
Text("暂无消息").font(.subheadline).padding()
Spacer()
}.navigationBarHidden(true).edgesIgnoringSafeArea(.bottom).foregroundColor(.white).background(
LinearGradient(colors: [
Color("BG1"),
Color("BG1"),
Color("BG2"),
Color("BG2"),
], startPoint: .top, endPoint: .bottom))
})
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
@@ -0,0 +1,167 @@
//
// OnBoardingScreen.swift
// AnimatedOnBoardingScreen
//
// Created by zues on 17/12/22.
//
import SwiftUI
import Lottie
struct OnBoardingScreen: View {
// MARK: OnBoarding Slides Model Data
@State var onboardingItems: [OnboardingItem] = [
]
// MARK: Current Slide Index
@State var currentIndex: Int = 0
var body: some View {
GeometryReader{
let size = $0.size
HStack(spacing: 0){
ForEach($onboardingItems) { $item in
let isLastSlide = (currentIndex == onboardingItems.count - 1)
VStack{
// MARK: Top Nav Bar
HStack{
Button(action: {
if currentIndex > 0{
currentIndex -= 1
playAnimation()
}
}, label: {
Image(systemName: "arrow.backward")
})
.opacity(currentIndex > 0 ? 1 : 0)
Spacer(minLength: 0)
Button("跳过"){
currentIndex = onboardingItems.count - 1
playAnimation()
}
.opacity(isLastSlide ? 0 : 1)
}
.animation(.easeInOut, value: currentIndex)
.tint(Color("Green"))
//.fontWeight(.bold)
// MARK: Movable Slides
VStack(spacing: 15){
let offset = -CGFloat(currentIndex) * size.width
// MARK: Resizable Lottie View
ResizableLottieView(onboardingItem: $item)
.frame(height: size.width)
.onAppear {
// MARK: Intially Playing First Slide Animation
if currentIndex == indexOf(item){
item.lottieView.play(toProgress: 0.7)
}
}
.offset(x: offset)
.animation(.easeInOut(duration: 0.5), value: currentIndex)
Text(item.title)
.font(.title.bold())
.offset(x: offset)
.animation(.easeInOut(duration: 0.5).delay(0.1), value: currentIndex)
Text(item.subTitle)
.font(.system(size: 14))
.multilineTextAlignment(.center)
.padding(.horizontal,15)
.foregroundColor(.gray)
.offset(x: offset)
.animation(.easeInOut(duration: 0.5).delay(0.2), value: currentIndex)
}
Spacer(minLength: 0)
// MARK: Next / Login Button
VStack(spacing: 15){
Button {
if currentIndex < onboardingItems.count - 1{
// MARK: Pausing Previous Animation
let currentProgress = onboardingItems[currentIndex].lottieView.currentProgress
onboardingItems[currentIndex].lottieView.currentProgress = (currentProgress == 0 ? 0.7 : currentProgress)
currentIndex += 1
// MARK: Playing Next Animation from Start
playAnimation()
}
if isLastSlide {
withAnimation(){
}
}
} label: {
Text(isLastSlide ? "登录" : "下一步")
.foregroundColor(.white)
.padding(.vertical, isLastSlide ? 13 : 12)
.background(
Capsule()
.fill(Color("Green"))
)
.frame(maxWidth: .infinity) //
.padding(.horizontal, isLastSlide ? 50 : 100)
.contentShape(Rectangle()) //
}
/* Button(isLastSlide ? "" : ""){
}
.foregroundColor(.white)
.padding(.vertical,isLastSlide ? 13 : 12)
.frame(maxWidth: .infinity)
.background {
Capsule()
.fill(Color("Green"))
}
.padding(.horizontal,isLastSlide ? 50 : 100)
*/
if currentIndex == onboardingItems.count - 1{
HStack{
// Button("Terms of Service"){}
// Button("Privacy Policy"){}
}
.font(.caption2)
//.underline(true, color: .primary)
.offset(y: 5)
}
}
}
.animation(.easeInOut, value: isLastSlide)
.padding(15)
.frame(width: size.width, height: size.height)
}
}
.frame(width: size.width * CGFloat(onboardingItems.count),alignment: .leading)
}
}
func playAnimation(){
onboardingItems[currentIndex].lottieView.currentProgress = 0
onboardingItems[currentIndex].lottieView.play(toProgress: 0.7)
}
// MARK: Retreving Index of the Item in the Array
func indexOf(_ item: OnboardingItem)->Int{
if let index = onboardingItems.firstIndex(of: item){
return index
}
return 0
}
}
//struct OnBoardingScreen_Previews: PreviewProvider {
// static var previews: some View {
// ContentView()
// }
//}
@@ -0,0 +1,38 @@
//
// ResizableLottieView.swift
// AnimatedOnBoardingScreen
//
// Created by zues on 17/12/22.
//
import SwiftUI
// MARK: Resizable Lottie View Without Background
struct ResizableLottieView: UIViewRepresentable{
@Binding var onboardingItem: OnboardingItem
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .clear
setupLottieView(view)
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
func setupLottieView(_ to: UIView){
let lottieView = onboardingItem.lottieView
lottieView.loopMode = .loop
lottieView.backgroundColor = .clear
lottieView.translatesAutoresizingMaskIntoConstraints = false
// MARK: Applying Constraints
let constraints = [
lottieView.widthAnchor.constraint(equalTo: to.widthAnchor),
lottieView.heightAnchor.constraint(equalTo: to.heightAnchor),
]
to.addSubview(lottieView)
to.addConstraints(constraints)
}
}
@@ -0,0 +1,256 @@
//
// OrderListView.swift
// SFI
//
// Created by Mac on 2024/10/21.
//
import Foundation
import SwiftUI
struct OrderListView: View {
// @Binding var isPresented: Bool
@State private var planList: [OrderInfoDatum] = []
@State private var isLoading = false
@State private var errorMessage: String?
@State private var alert: Alert?
@Environment(\.openURL) private var openURL
@Environment(\.presentationMode) var presentationMode //
var body: some View {
NavigationView(content: {
VStack {
// Navigation Bar
ZStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss() //
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10) //
Spacer()
}
Spacer()
VStack(spacing: 5){
Text("我的订单")
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
}
.foregroundColor(.white)
Spacer()
}
.padding(.all)
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.padding()
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding()
}
//
// // Subscription Plans
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 16) {
if planList.isEmpty {
// Text("Loading plans...") // Loading indicator
if !isLoading
{
Text("暂无记录").font(.caption)
.foregroundColor(.gray)
.multilineTextAlignment(.center) //
.padding()
}
} else {
ForEach(planList) { plan in
NavigationLink(destination: OrderPaymentView(orderinfo: plan )) {
OrderItemView(orderinfo: plan)
}
//
// Button(action: {
//
// }, label: {
// OrderItemView(orderinfo: plan)
// })
}
}
}
}.padding(.top, 20)
Spacer()
// Subscription Notice
Text("订阅须知:仅支持微信和支付宝付款")
.font(.footnote)
.foregroundColor(.gray)
.padding(.bottom, 20)
}
.navigationBarHidden(true)
.background(
BackgroundBg()
)
.onAppear(){
Task{
await getOrderList()
}
}.edgesIgnoringSafeArea(.bottom)
})
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true) //
.alertBinding($alert)
}
public func getOrderList() async {
isLoading = true
errorMessage = nil
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/fetch")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "数据请求失败"
return
}
// Parse the user info response
if let Subscribe = try? JSONDecoder().decode(OrderInfoReponse.self, from: data) {
if let data = Subscribe.data {
planList = data
}else{
self.errorMessage = Subscribe.message ?? ""
}
}else{
self.errorMessage = "数据请求失败: JSON 解析错误"
}
}
}
task.resume()
}
}
struct OrderItemView: View {
var orderinfo : OrderInfoDatum
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(orderinfo.plan.name + " " + "\(orderinfo.plan.transferEnable ?? 0)GB/月")
.font(.headline)
.foregroundColor(.black).multilineTextAlignment(.leading)
Text("订单号:"+orderinfo.tradeNo)
.font(.headline)
.foregroundColor(.black).multilineTextAlignment(.leading)
//.fontWeight(orderinfo.status == 2 ? .bold : .regular)
Text("订单金额:¥\(String(format: "%.2f", orderinfo.totalAmount/100))")
.font(.subheadline)
.foregroundColor(.black)
HStack (spacing: 4) {
Text(orderinfo.status_zh)
.font(.caption)
.fontWeight(.bold)
.padding(2)
.background(orderinfo.status == 1 ? Color.green: (orderinfo.status == 0 ? Color.red : Color.gray) )
.cornerRadius(4)
.foregroundColor(.white)
.offset(y:10)
Text(orderinfo.period_zh)
.font(.caption)
.fontWeight(.bold)
.padding(2)
.background(Color("Main"))
.cornerRadius(4)
.foregroundColor(.white)
// .offset(x: -16, y:16)
.offset(y:10)
Spacer()
Text("创建时间:"+TimestampConverter.convertTimestampToDateString(orderinfo.createdAt))
.font(.caption)
.fontWeight(.bold)
.padding(2)
.foregroundColor(.gray)
// .offset(x: -16, y:16)
.offset(y:10)
}
}
}
.padding()
.background(Color.white)
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 3)
.padding(.horizontal, 20)
}
}
//struct SubscriptionView_Previews: PreviewProvider {
// static var previews: some View {
// SubscriptionView()
// }
//}
@@ -0,0 +1,445 @@
//
// OrderPaymentView.swift
// SFI
//
// Created by Mac on 2024/10/21.
//
import Foundation
//
import SwiftUI
struct OrderPaymentView: View {
var orderinfo : OrderInfoDatum
@State private var alert: Alert?
@State private var isLoading = false
@State private var errorMessage: String?
@Environment(\.openURL) private var openURL
@State private var showAlert = false
@State private var payment :PaymentReponseDatum?
@State private var ischeckout = false
@Environment(\.presentationMode) var presentationMode //
var body: some View {
NavigationView(content: {
VStack{
// Navigation Bar
ZStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss() //
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10) //
Spacer()
if orderinfo.status == 0 {
Button(action: {
alert = Alert(YesOrNOMessage: "确定取消订单吗?"){
Task{
await cancelorder(orderNO: orderinfo.tradeNo)
}
}
}, label: {
Text("取消订单")
.foregroundColor(.red)
})
}
}
Spacer()
Text("订单详情")
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
.foregroundColor(.white)
Spacer()
}
.padding(.all)
VStack(spacing: 20) {
ScrollView(.vertical, showsIndicators: false) {
// Product Details Section
VStack(alignment: .leading, spacing: 10) {
Text("订单详情")
.font(.headline)
.foregroundColor(.white)
HStack {
Text("订单号: ")
.font(.subheadline)
.foregroundColor(.white)
Text(orderinfo.tradeNo)
.font(.subheadline)
.foregroundColor(.white)
}
HStack {
Text("创建时间: ")
.font(.subheadline)
.foregroundColor(.white)
Text(TimestampConverter.convertTimestampToDateString(orderinfo.createdAt))
.font(.subheadline)
.foregroundColor(.white)
}
Spacer().frame(height: 20)
Text("商品详情")
.font(.headline)
.foregroundColor(.white)
HStack {
Text("商品名称: ")
.font(.subheadline)
.foregroundColor(.white)
Text(orderinfo.plan.name)
.font(.subheadline)
.foregroundColor(.yellow)
}
HStack {
Text("类型/周期: ")
.font(.subheadline)
.foregroundColor(.white)
Text(orderinfo.period_zh)
.font(.subheadline)
.foregroundColor(.white)
}
HStack {
Text("产品流量: ")
.font(.subheadline)
.foregroundColor(.white)
Text("\(orderinfo.plan.transferEnable ?? 0)GB/月")
.font(.subheadline)
.foregroundColor(.white)
}
// Pending payment stamp (badge)
Spacer()
HStack {
Spacer()
Text(orderinfo.status_zh)
.font(.subheadline)
.padding(10)
.background(orderinfo.status == 1 ? Color.green: (orderinfo.status == 0 ? Color.red : Color.gray))
.cornerRadius(10)
.foregroundColor(.white)
}
}
.padding()
.background(Color.black.opacity(0.2))
.cornerRadius(10)
// Payment Method Section
VStack(alignment: .leading) {
Text("支付方式")
.font(.headline)
.foregroundColor(.white)
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.padding()
.frame(maxWidth: .infinity)
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding()
.frame(maxWidth: .infinity)
}else{
Text("\(payment?.name ?? "") (\(payment?.handlingFeePercent ?? "")%手续费)")
.font(.subheadline)
.padding()
.frame(maxWidth: .infinity)
.background(Color.red.opacity(0.3))
.foregroundColor(.white)
.cornerRadius(10)
}
}
.padding()
.background(Color.black.opacity(0.2))
.cornerRadius(10)
// Order Summary Section
VStack(alignment: .leading) {
Text("订单摘要")
.font(.headline)
.foregroundColor(.white)
HStack {
Text("商品价格:")
.font(.subheadline)
.foregroundColor(.white)
Spacer()
Text("¥\(String(format: "%.2f", orderinfo.totalAmount/100))")
.font(.subheadline)
.foregroundColor(.white)
}
HStack {
Text("手续费:")
.font(.subheadline)
.foregroundColor(.white)
Spacer()
Text("¥\(String(format: "%.2f", shouxufei()))")
.font(.subheadline)
.foregroundColor(.white)
}
HStack {
Text("总计:")
.font(.subheadline)
.foregroundColor(.white)
Spacer()
Text("¥\(String(format: "%.2f", total()))")
.font(.headline)
.foregroundColor(.white)
}
}
.padding()
.background(Color.black.opacity(0.2))
.cornerRadius(10)
if orderinfo.status == 0 {
// Pay Now Button
Button(action: {
// Trigger the payment action
showAlert = true
}, label: {
HStack {
Image(systemName: "cart")
Text("立即支付")
if ischeckout {
ProgressView()
.progressViewStyle(CircularProgressViewStyle()).tint(.white)
}
}
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.cornerRadius(10)
}).disabled(ischeckout)
.padding()
.alert(isPresented: $showAlert) {
Alert(
title: Text("提示"),
message: Text("确认要支付吗?"),
primaryButton: .default(Text("确定"),action: {
Task{
if let paymentID = payment?.id {
await submitOrder(orderNO:orderinfo.tradeNo,paymentID:paymentID)
}
}
}),
secondaryButton: .cancel(Text("取消"))
)
}
}
}
}
}
.padding()
.navigationBarHidden(true)
.onAppear(){
Task{
await getPayment()
}
}
.edgesIgnoringSafeArea(.bottom)
.background(
BackgroundBg()
)
})
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true) //
.alertBinding($alert)
}
func shouxufei() -> Double {
return orderinfo.totalAmount/100 * (Double(payment?.handlingFeePercent ?? "") ?? 0.0)/100
}
func total() -> Double{
return orderinfo.totalAmount/100 + shouxufei()
}
func cancelorder(orderNO:String) async {
//cancel?trade_no=2024102108102956235125484
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/cancel?trade_no=\(orderNO)")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
alert = Alert(error)
return
}
guard let data = data else {
alert = Alert(errorMessage: "取消失败,返回数据为空")
return
}
struct cancelResponse: Codable {
let status, message: String?
}
// Parse the user info response
if let message = try? JSONDecoder().decode(cancelResponse.self, from: data) {
if let status = message.status,status == "success" {
alert = Alert(okMessage: "取消成功", {
presentationMode.wrappedValue.dismiss() //
})
}else{
alert = Alert(errorMessage: message.message ?? "" )
}
//2024101311320180720&sitename=
}
}
}
task.resume()
}
func submitOrder(orderNO:String,paymentID:Int) async {
ischeckout = true
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/checkout?trade_no=\(orderNO)&method=\(paymentID)")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
ischeckout = false
if let error = error {
alert = Alert(error)
return
}
guard let data = data else {
alert = Alert(errorMessage: "支付失败,返回数据为空")
return
}
struct saveOrderResponse: Codable {
let status, message, data: String?
}
// Parse the user info response
if let message = try? JSONDecoder().decode(saveOrderResponse.self, from: data) {
if let payurl = message.data,payurl.count > 10 {
openURL(URL(string:payurl)!)
}else{
alert = Alert(errorMessage: message.message ?? "" )
}
//2024101311320180720&sitename=
}
}
}
task.resume()
}
func getPayment() async {
isLoading = true
errorMessage = nil
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/getPaymentMethod")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "数据请求失败"
return
}
// Parse the user info response
if let Subscribe = try? JSONDecoder().decode(PaymentReponse.self, from: data) {
if let data = Subscribe.data {
payment = data.first
}else{
self.errorMessage = Subscribe.message ?? ""
}
}else{
self.errorMessage = "数据请求失败: JSON 解析错误"
}
}
}
task.resume()
}
}
//struct PaymentView_Previews: PreviewProvider {
// static var previews: some View {
// OrderPaymentView()
// }
//}
@@ -0,0 +1,183 @@
//
// SettingsAboutView.swift
// SFI
//
// Created by Mac on 2024/10/12.
//
import Foundation
//
import SwiftUI
struct SettingsAboutView: View {
@State private var alert: Alert?
@Environment(\.openURL) private var openURL //
@Environment(\.presentationMode) var presentationMode //
var body: some View {
NavigationView(content: {
VStack() {
ZStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss() //
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10) //
Spacer(minLength: 0)
}
Text("关于我们")
.fontWeight(.bold).lineLimit(1)
}
.padding()
// .padding(.top,edges.top)
.foregroundColor(.white)
Spacer().frame(height: 40)
// Logo
VStack {
Image("applogo")
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
.foregroundColor(.orange) // 使 Image("logo")
Text("UUVPN for iOS \n在您的 iPhone 和 iPad 上体验最快的全球网络连接工具")
.font(.subheadline)
.padding(.top, 10)
.frame(width: 300)
.multilineTextAlignment(.center)
}
Spacer().frame(height: 40)
//
List {
HStack {
Text("当前版本")
Spacer()
Text( Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0").foregroundColor(.gray)
}.padding(.vertical, 10).listRowBackground(Color.clear)
Button(action: {
alert = Alert(okMessage: "没有检测到更新,已经是最新版本.")
}, label: {
HStack {
Text("检查更新")
Spacer()
Image(systemName: "chevron.right").foregroundColor(.gray)
}.padding(.vertical, 10)
}).listRowBackground(Color.clear)
Button(action: {
if let url = URL(string: UserManager.shared.websiteURL()) {
openURL(url) { accepted in // completion URL OpenURLAction
print(accepted ? "Success" : "Failure")
}
}
}, label: {
HStack {
Text("官方网址")
Spacer()
Image(systemName: "chevron.right").foregroundColor(.gray)
}.padding(.vertical, 10)
}).listRowBackground(Color.clear)
Button(action: {
if let url = URL(string: UserManager.shared.telegramUrl()) {
openURL(url) { accepted in // completion URL OpenURLAction
print(accepted ? "Success" : "Failure")
}
}
}, label: {
HStack {
Text("订阅Telegram频道")
Spacer()
Image(systemName: "chevron.right").foregroundColor(.gray)
}.padding(.vertical, 10)
}).listRowBackground(Color.clear)
}
.listStyle(PlainListStyle()) //
Spacer()
//
VStack {
HStack {
Button(action: {
//
if let url = URL(string: "https://minipanda.soccertt.com/teams.html") {
openURL(url) { accepted in // completion URL OpenURLAction
print(accepted ? "Success" : "Failure")
}
}
}) {
Text("用户协议")
.foregroundColor(.white)
.font(.subheadline)
.underline()
}
Button(action: {
//
if let url = URL(string: "https://minipanda.soccertt.com/privacy.html") {
openURL(url) { accepted in // completion URL OpenURLAction
print(accepted ? "Success" : "Failure")
}
}
}) {
Text("隐私政策")
.foregroundColor(.white)
.font(.subheadline)
.underline()
}
}
.padding(.horizontal)
Text("Copyright 2014 - 2024, UUVPN 版权所有")
.font(.footnote)
.foregroundColor(.gray)
.padding(.top, 5)
}
.padding(.bottom, 10)
}
.background(
BackgroundBg()
)
.alertBinding($alert)
// .padding()
.navigationBarHidden(true)
// .edgesIgnoringSafeArea(.bottom)
})
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true) //
}
}
#Preview {
SettingsAboutView()
}
@@ -0,0 +1,497 @@
//
// SideMenuView.swift
// SFI
//
// Created by Mac on 2024/10/12.
//
import Foundation
import SwiftUI
import ApplicationLibrary
import WebKit
import Libbox
import Library
struct SideMenuView: View {
@Binding var isPresented: Bool
@Binding var isLoggedIn: Bool
@State private var isLoading = false
@State private var errorMessage: String?
@State private var plandes: SubscribeReponseClass?
@State private var size: CGSize = .zero
@State private var avator = UserManager.shared.getUserInfo().avator
@State private var emailid = UserManager.shared.getUserInfo().email
@EnvironmentObject private var environments: ExtensionEnvironments
@State private var alert: Alert?
@State private var showAlert = false
@State private var isInviteActive = false
@State private var isLogouting = false
@AppStorage("paymentURLKey") private var paymentURLKey = ""
var body: some View {
NavigationView {
VStack() {
// Navigation Bar
//
ZStack{
HStack{
Button(action: {
isPresented = false
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10)
Spacer(minLength: 0)
}
Text("设置")
.fontWeight(.bold).lineLimit(1)
}
.padding()
// .padding(.top,edges.top)
.foregroundColor(.white)
ScrollView(.vertical, showsIndicators: false) {
// Profile Section
VStack {
HStack {
// AsyncImage(url: URL(string: avator)){ phase in
// if let image = phase.image {
// image // Displays the loaded image.
// } else if phase.error != nil {
// Color.red // Indicates an error.
// } else {
// Color.blue // Acts as a placeholder.
// }
// }.frame(width: 50, height: 50)
// .clipShape(Circle())
// Image("applogo").resizable().frame(width: 50, height: 50).clipShape(Circle())
VStack(alignment: .leading, spacing: 5) {
Text("账户信息:")
.font(.headline).fontWeight(.light)
Text(emailid)
.font(.headline)
.bold()
}
Spacer()
/*
Spacer()
Button {
showAlert = true
} label: {
Text("退").foregroundColor(.red)
Image(systemName: "rectangle.portrait.and.arrow.forward")
.foregroundColor(.red)
.aspectRatio(contentMode: .fill)
.frame(width: 4, height: 4)
}*/
}
.padding()
.background(Color.white.opacity(0.1))
.cornerRadius(12)
.shadow(color: .gray.opacity(0.1), radius: 5, x: 0, y: 5)
if (paymentURLKey.count > 3){
HStack {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.padding()
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center)
.padding()
}else{
if let plandesss = self.plandes {
if plandesss.transferEnable > 0 {
VStack(alignment: .leading, spacing: 5) {
// Text("") .font(.subheadline)
Text(plandesss.plan?.name ?? "")
.font(.subheadline)
let u = plandesss.d/1024/1024/1024
let t = plandesss.transferEnable/1024/1024/1024
Spacer().frame(height: 10)
Text("已用: \(u) GB / 总计:\(t) GB")
.font(.subheadline).bold()
HStack {
// White bar on the left
// RoundedRectangle(cornerRadius: 10)
// .fill(Color.red)
// .frame(width: 300*CGFloat(plandesss.d)/CGFloat(plandesss.transferEnable), height: 20)
//
Text("").frame(height: 20).frame(minWidth: (CGFloat(plandesss.d)/CGFloat(plandesss.transferEnable))<=0.1 ? 20 : 300*(CGFloat(plandesss.d)/CGFloat(plandesss.transferEnable))).background(
RoundedRectangle(cornerRadius: 10)
.fill(
Color.red
)
)
Spacer()
}
.frame(height: 20)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(
Color.gray.opacity(0.5)
)
)
.padding()
// ScrollView {
// }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
// AttributedText(htmlContent: plan.content, size: $size)
// .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, idealHeight: size.height, maxHeight: .infinity)
// .background(.clear)
}
Spacer()
}else{
Spacer()
Text("暂无订阅")
.font(.subheadline)
Spacer()
}
}
}
}.padding()
.background(Color.white.opacity(0.1))
.cornerRadius(12)
.shadow(color: .gray.opacity(0.1), radius: 5, x: 0, y: 5)
}
} .frame(maxWidth: .infinity)
.padding()
.foregroundColor(.white)
// Menu Items
// List {
// NavigationLink(destination: WebShuntView()) {
// MenuItem(icon: "globe", title: "App", color: .white, isNew: false)
// }
LazyVStack(spacing: 10, content: {
NavigationLink(destination: SupportTicketView()) {
MenuItem(icon: "listclipboard", title: "我的工单", color: .white, isNew: false)
}
Divider()
if (paymentURLKey.count > 3){
NavigationLink(destination: UpgradeView()) {
MenuItem(icon: "crown.fill", title: "升级套餐", color: .yellow, isNew: true)
}
Divider()
NavigationLink(destination: OrderListView()) {
MenuItem(icon: "checkout", title: "我的订单", color: .white, isNew: false)
}
Divider()
NavigationLink(destination: InviteListView(isPresented: $isInviteActive)) {
MenuItem(icon: "star.fill", title: "邀请中心", color: .white, isNew: false)
}
Divider()
}else{
NavigationLink(destination: MessageCenterView()) {
MenuItem(icon: "message.badge.circle", title: "消息通知", color: .white, isNew: false)
}
Divider()
}
NavigationLink(destination: QuestionView()) {
MenuItem(icon: "questionmark.app", title: "问题解答", color: .white, isNew: false)
}
Divider()
NavigationLink(destination: SupportView()) {
MenuItem(icon: "headphones", title: "联系客服", color: .white, isNew: false)
}
Divider()
NavigationLink(destination: AboutUsView()) {
MenuItem(icon: "info.circle.fill", title: "关于我们", color: .white, isNew: false)
}
Divider()
}).padding()
Button(action: {
// Trigger the payment action
// showAlert = true
alert = Alert(YesOrNOMessage: "确定退出登录吗?", {
isLogouting=true
UserManager.shared.clearUserData()
//
Task {
//await reloadSubscribe()
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
isLoggedIn = false
}
})
}, label: {
HStack {
Image(systemName: "trash.fill")
Text("退出登录")
}
.font(.headline)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.cornerRadius(10)
}).padding()
}
// }
// .listStyle(PlainListStyle())
}.background(
BackgroundBg()
).modifier(ActivityIndicatorModifier(isLoading: isLogouting, color: Color.black.opacity(0.8), lineWidth: 1))
.onAppear {
Task {
await reloadSubscribe()
}
}.navigationBarHidden(true)
}.alertBinding($alert)
}
private func writeProfilePreviewList() async throws {
let profiles = try await ProfileManager.list()
for profile in profiles {
print("detele : \(profile.path) \(profile.remoteURL ?? "" )")
await deleteProfile(profile)
}
}
private func deleteProfile(_ profile: Profile) async {
do {
_ = try await ProfileManager.delete(profile)
} catch {
alert = Alert(error)
return
}
environments.profileUpdate.send()
}
@MainActor
public func reloadSubscribe() async {
isLoading = true
errorMessage = nil
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/getSubscribe")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "套餐数据请求失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "套餐数据请求失败:数据为空"
return
}
// Parse the user info response
if let Subscribe = try? JSONDecoder().decode(SubscribeReponse.self, from: data) {
if let data = Subscribe.data {
plandes = data
emailid = data.email
UserManager.shared.storeUserInfo(email: emailid, avator: "")
// UserManager.shared.storeSuburlData(data: data.subscribeURL)
}else{
self.errorMessage = Subscribe.message ?? ""
}
}else{
self.errorMessage = "套餐数据请求失败"
}
}
}
task.resume()
}
// Sample Destination Views for each menu item
struct UpgradeView: View {
@State private var isSubscriptionActive = false
var body: some View {
SubscriptionView(isPresented: $isSubscriptionActive)
}
}
struct WebShuntView: View {
var body: some View {
VStack(alignment: .trailing, content: {
LogView()
ProfileView()
})
}
}
struct Free30DaysView: View {
@State private var islogined = false
var body: some View {
InviteView(isPresented: $islogined)
}
}
struct AboutUsView: View {
var body: some View {
SettingsAboutView()
}
}
}
struct MenuItem: View {
let icon: String
let title: String
let color: Color
let isNew: Bool?
var body: some View {
// Button {
//
// } label: {
//
// }
HStack {
if UIImage(systemName: icon) != nil{
Image(systemName: icon)
.foregroundColor(color).tint(.white)
.frame(width: 24, height: 24)
}else{
Image(icon).resizable()
.foregroundColor(.white).tint(.white)
.frame(width: 24, height: 24)
}
Text(title)
.foregroundColor(.white)
.font(.system(size: 18))
Spacer()
if isNew == true {
Circle()
.fill(Color.red)
.frame(width: 8, height: 8)
}
}
.padding(.vertical, 10)
}
}
struct AttributedText: UIViewRepresentable {
let htmlContent: String
@Binding var size: CGSize
private let webView = WKWebView()
var sizeObserver: NSKeyValueObservation?
func makeUIView(context: Context) -> WKWebView {
webView.scrollView.isScrollEnabled = false //<-- Here
webView.navigationDelegate = context.coordinator
return webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.loadHTMLString(htmlContent, baseURL: nil)
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, WKNavigationDelegate {
let parent: AttributedText
var sizeObserver: NSKeyValueObservation?
init(parent: AttributedText) {
self.parent = parent
sizeObserver = parent.webView.scrollView.observe(\.contentSize, options: [.new], changeHandler: { (object, change) in
parent.size = change.newValue ?? .zero
})
}
}
}
@@ -0,0 +1,477 @@
//
// SubscriptionView.swift
// SFI
//
// Created by Mac on 2024/10/12.
//
import Foundation
import SwiftUI
struct SubscriptionView: View {
@Binding var isPresented: Bool
@State private var planList: [DatuPlanResponse] = []
@State private var isLoading = false
@State private var errorMessage: String?
@State private var alert: Alert?
@State var orderinfo : OrderInfoDatum?
@Environment(\.openURL) private var openURL
let adimages = ["10-35-24", "10-35-15", "10-35-03"] // 广
let adimagesString = ["UUVPN 采用最高安全 ECC 加密技术,仅需一键操作,军事级的加密技术将护航您的任何互联网访问", "UUVPN 拥有170 个热门城市的300+高速服务器,您可以随时随地获得高速安全的互联网体验", "UUVPN 支持所有当前主流的平台系统,您可以在几乎任何设备上使用"]
@Environment(\.presentationMode) var presentationMode //
//
@State private var isNavigationActive = false //
@State private var currentIndex = 0
//
let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
var body: some View {
NavigationView(content: {
VStack {
ZStack{
HStack{
Button(action: {
withAnimation {
isPresented = false // Close the side menu
}
presentationMode.wrappedValue.dismiss() //
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10) //
Spacer()
NavigationLink(destination: OrderListView()) {
Text("我的订单").foregroundColor(.red)
}.listRowBackground(Color.clear)
}
Spacer()
VStack(spacing: 5){
Text( "升级套餐")
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
}
.foregroundColor(.white)
Spacer()
}
.padding(.all)
if let orderinfoS = orderinfo{
NavigationLink(destination: OrderPaymentView(orderinfo: orderinfoS),isActive: $isNavigationActive, label: {
EmptyView() //
})
}
//
// // Subscription Plans
ScrollView(.vertical, showsIndicators: false) {
// VPN Info and Trial Offer
VStack(spacing: 8) {
//
TabView(selection: $currentIndex) {
ForEach(0..<adimages.count, id: \.self) { index in
VStack(spacing: 8) {
Image(adimages[index])
.resizable()
.scaledToFill()
.frame(width: UIScreen.main.bounds.width * 0.8,height: 150)
.cornerRadius(10)
.tag(index)
Text(adimagesString[index])
.font(.subheadline)
.multilineTextAlignment(.center)
.foregroundColor(.gray)
.padding(.horizontal, 20)
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) //
.frame(height: 220)
.onReceive(timer) { _ in
// 广
withAnimation {
currentIndex = (currentIndex + 1) % adimages.count
}
}
//
HStack(spacing: 8) {
ForEach(0..<adimages.count, id: \.self) { index in
Circle()
.fill(index == currentIndex ? Color.green : Color.gray)
.frame(width: 5, height: 5)
}
}
// .padding(.top, 3)
// Free Trial Text
HStack(spacing: 8) {
Image(systemName: "checkmark.shield.fill")
.foregroundColor(.green)
Text("首次订阅免费试用 2 天")
.font(.title3)
.fontWeight(.bold)
}
.padding(.top, 10)
Text("订阅可随时取消,试用期内取消不收取任何费用")
.font(.footnote)
.foregroundColor(.gray)
}
.padding(.top, 20)
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.padding()
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding()
}
VStack(spacing: 16) {
if planList.isEmpty {
// Text("Loading plans...") // Loading indicator
} else {
ForEach(planList) { plan in
Button(action: {
//print( plan.content ?? "")
Task{
await subPlanOrder(plan_id:plan.idOLD ?? 0,amount : plan.monthPrice ?? 0)
}
}, label: {
SubscriptionPlanView(planName: plan.name, originalPrice: "¥\(String(format: "%.2f", Double(plan.monthPrice ?? 0)/100)) /月", discountedPrice: "", discountPercentage: "", monthlyPrice: "", content: plan.content ?? "", bestPlan: plan.idOLD == planList.count,planContent: "\(plan.transferEnable ?? 0)GB/月")
// Text( plan.content ?? "").padding()
})
}
}
}
}.padding(.top, 20)
Spacer()
// Subscription Notice
Text("订阅须知:仅支持微信和支付宝付款")
.font(.footnote)
.foregroundColor(.gray)
.padding(.bottom, 20)
}
.background(
LinearGradient(colors: [
Color("BG1"),
Color("BG1"),
Color("BG2"),
Color("BG2"),
], startPoint: .top, endPoint: .bottom)
)
// .background(.white)
.onAppear(){
Task{
await getPlanList()
}
}
.navigationBarHidden(true)
.edgesIgnoringSafeArea(.bottom)
})
.navigationBarHidden(true)
.alertBinding($alert)
.navigationBarBackButtonHidden(true) //
}
public func subPlanOrder(plan_id: Int,amount: Int) async {
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/save?plan_id=\(plan_id)&period=month_price&coupon_code=")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
alert = Alert(error)
return
}
guard let data = data else {
alert = Alert(errorMessage: "提交订单信息错误,请重试")
return
}
struct saveOrderResponse: Codable {
let status, message, data: String?
}
// Parse the user info response
if let message = try? JSONDecoder().decode(saveOrderResponse.self, from: data) {
if let dataNO = message.data,dataNO.count > 10 {
Task{
await getOrderInfo(OrderNO: dataNO)
}
}else{
alert = Alert(errorMessage: message.message ?? "" )
}
//2024101311320180720&sitename=
}
}
}
task.resume()
}
func getOrderInfo(OrderNO: String) async {
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/order/detail?trade_no=\(OrderNO)")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "数据请求失败"
return
}
// Parse the user info response
if let Subscribe = try? JSONDecoder().decode(OrderInfoSingleReponse.self, from: data) {
if let data = Subscribe.data {
orderinfo = data
isNavigationActive = true
}else{
self.errorMessage = Subscribe.message ?? ""
}
}else{
self.errorMessage = "数据请求失败: JSON 解析错误"
}
}
}
task.resume()
}
func getPlanList() async {
isLoading = true
errorMessage = nil
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/plan/fetch")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "数据请求失败"
return
}
// Parse the user info response
if let Subscribe = try? JSONDecoder().decode(PlanResponse.self, from: data) {
if let data = Subscribe.data {
planList = data
}else{
self.errorMessage = Subscribe.message ?? ""
}
}else{
self.errorMessage = "数据请求失败: JSON 解析错误"
}
}
}
task.resume()
}
}
struct SubscriptionPlanView: View {
var planName: String
var originalPrice: String
var discountedPrice: String
var discountPercentage: String
var monthlyPrice: String
var content: String
var bestPlan: Bool = false
var planContent: String
var body: some View {
VStack(spacing: 5, content: {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text(planName)
.font(.headline)
.fontWeight(bestPlan ? .bold : .regular)
.foregroundColor(bestPlan ? .green : .black)
Text(planContent).font(.subheadline).fontWeight(bestPlan ? .bold : .regular).foregroundColor( .black)
if bestPlan {
Text("Best")
.font(.caption)
.fontWeight(.bold)
.padding(4)
.background(Color.red)
.cornerRadius(4)
.foregroundColor(.white)
.offset(x: -15, y:16)
}
}
Spacer()
VStack(alignment: .trailing, spacing: 4) {
HStack {
/* if bestPlan {
Text("\(discountPercentage) OFF")
.font(.caption)
.padding(4)
.foregroundColor(.white).background(.green)
.cornerRadius(16)
}else{
Text("\(discountPercentage) OFF")
.font(.caption)
.padding(4)
.foregroundColor(.black).background(.gray.opacity(0.5))
.cornerRadius(16)
}
Text(discountedPrice)
.strikethrough()
.foregroundColor(.gray)
*/
Text(originalPrice)
.font(.title3)
.fontWeight(.bold)
.foregroundColor(.black)
}
Text(monthlyPrice)
.font(.footnote)
.foregroundColor(.gray)
}
}
// Text(extractChinese(from: content)).font(.subheadline).foregroundStyle(.black).padding()
//HTMLTextView(htmlString:content)
}).padding()
.background(Color.white.opacity(0.9))
.cornerRadius(10)
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 3)
.padding(.horizontal, 20)
}
func extractChinese(from text: String) -> String {
let pattern = "[\\u4e00-\\u9fa5]+" //
let regex = try? NSRegularExpression(pattern: pattern, options: [])
let nsString = text as NSString
let results = regex?.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
//
let chineseStrings = results?.compactMap { result -> String? in
if let range = Range(result.range, in: text) {
return String(text[range])
}
return nil
}
dump(chineseStrings)
return chineseStrings?.joined(separator: " ") ?? ""
}
}
//struct SubscriptionView_Previews: PreviewProvider {
// static var previews: some View {
// SubscriptionView()
// }
//}
@@ -0,0 +1,749 @@
//
// SupportTicketChatView.swift
// SFI
//
// Created by Mac on 2024/10/20.
//
import Foundation
import SwiftUI
import Combine
// 1. KeyboardResponder
class KeyboardResponder: ObservableObject {
@Published var keyboardHeight: CGFloat = 0
private var cancellable: AnyCancellable?
private var cancellableSet: Set<AnyCancellable> = []
init() {
/*
cancellable = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.merge(with: NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification))
.sink { [weak self] notification in
if let userInfo = notification.userInfo {
if notification.name == UIResponder.keyboardWillShowNotification,
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
self?.keyboardHeight = keyboardFrame.cgRectValue.height
print(" \(keyboardFrame.cgRectValue.height)" )
}
else {
print(" \(notification.name)" )
// self?.keyboardHeight = 0 // Reset height when keyboard hides
}
}
}
*/
/*
let showPublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
let hidePublisher = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
showPublisher
.merge(with: hidePublisher)
.compactMap { notification in
//
guard let userInfo = notification.userInfo else { return 0 }
let endFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
return notification.name == UIResponder.keyboardWillShowNotification ? endFrame?.height ?? 0 : 0
}
.assign(to: \.keyboardHeight, on: self)
.store(in: &cancellableSet)
*/
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect }
.map { $0.height }
.sink { [weak self] height in
self?.keyboardHeight = height
}
.store(in: &cancellableSet)
NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in CGFloat.zero }
.assign(to: &$keyboardHeight)
}
}
struct SupportTicketChatView : View {
@State var message = ""
@State var imagePicker = false
@State var imgData : Data = Data(count: 0)
@State private var alert: Alert?
var ticketID:Int
@State private var isMessageInputBarPresented = false
//StateObject is the owner of the object....
@State private var alltickets: [Message] = []
@State private var isLoading = false
@State private var isSendingMsg = false
@State private var errorMessage: String?
@State private var showAlert = false
@State private var tickchatData: TicketChatReponseDataClass?
@ObservedObject private var keyboard = KeyboardResponder()
@StateObject var allMessages = Messages()
@Environment(\.presentationMode) var presentationMode //
var body: some View{
NavigationView(content: {
VStack{
Spacer()
ZStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss() //
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10) //
Spacer()
if tickchatData?.status == 0 {
Button(action: {
alert = Alert(YesOrNOMessage: "确定关闭工单吗?", {
Task{
await closeThisTicket()
}
})
}, label: {
Text("关闭工单")
.foregroundColor(.white)
})
}
}
Spacer()
VStack(spacing: 5){
Text(tickchatData?.subject ?? "")
.fontWeight(.bold).lineLimit(1).frame(width: UIScreen.main.bounds.width*0.5)
if tickchatData?.status == 0 {
Text("待回复").font(.caption).foregroundColor(.red)
}else if tickchatData?.status == 1 {
Text("已关闭").font(.caption).foregroundColor(.gray)
}else if tickchatData?.status == 2 {
Text("已回复").font(.caption).foregroundColor(.green)
}
}
.foregroundColor(.white)
Spacer()
}
.padding(.all)
VStack{
// Displaying Message....
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle()).tint(Color("Main"))
.padding().padding(.horizontal)
Spacer()
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding(.horizontal)
Spacer()
}
if (alltickets.count > 0 ){
Spacer()
ScrollView(.vertical, showsIndicators: false, content: {
ScrollViewReader{reader in
VStack(spacing: 20){
ForEach(allMessages.messages){msg in
// Chat Bubbles...
Text("\(TimestampConverter.convertTimestampToDateString(msg.updatedAt))").font(.caption).foregroundColor(.black.opacity(0.8))
ChatBubble(msg: msg)
}
// when ever a new data is inserted scroll to bottom...
.onChange(of: allMessages.messages) { (value) in
// scrolling only user message...
//if value.last!.myMsg{}//
withAnimation {
reader.scrollTo(value.last?.id)
}
}
}
.padding([.horizontal,.bottom])
.padding(.top, 25)
}
})
}
}
// since bottom edge is ignored....
.padding(.bottom,getSafeAreaInsets().bottom)// + keyboard.keyboardHeight
.background((self.isLoading || errorMessage?.isEmpty==false)
?
AnyView(Color.clear.clipped())
:
AnyView(Color.white.clipShape(RoundedShape())))
}
.navigationBarHidden(true)
.edgesIgnoringSafeArea(.bottom)//
//.background(Color("bg").ignoresSafeArea(.all, edges: .all))
// .ignoresSafeArea(.all, edges: .top)
.background(
LinearGradient(colors: [
Color("BG1"),
Color("BG1"),
Color("BG2"),
Color("BG2"),
], startPoint: .top, endPoint: .bottom).ignoresSafeArea(.all, edges: .all))
.onAppear(){
Task{
await getPlanList()
}
}
})
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true) //
.alertBinding($alert)
.overlay(
GeometryReader { geometry in
VStack {
Spacer() // Pus
Group {
if isMessageInputBarPresented {
MessageInputBar(isPresented: $isMessageInputBarPresented, message: $message,isSendingMsg: $isSendingMsg) { message in
print("发送消息: \(message)")
Task{
await SendMsg(msg:message)
}
}//.shadow(radius: 5)
//.padding(.bottom, keyboard.keyboardHeight) // Adjust for keyboard height
//.animation(.easeIn(duration: 0.3), value: keyboard.keyboardHeight) // Smooth transition
}
}//.edgesIgnoringSafeArea(.bottom)
}.frame(height: geometry.size.height * 1 / 2) // 1/2
.offset(y: geometry.size.height / 2) // 1/2
}
)
//.background(Color("Color").edgesIgnoringSafeArea(.top))
// Full Screen Image Picker...
.fullScreenCover(isPresented: self.$imagePicker, onDismiss: {
// when ever image picker closes...
// verifying if image is selected or cancelled...
if self.imgData.count != 0{
//allMessages.writeMessage(id: Date(), msg: "", photo: self.imgData, myMsg: true, profilePic: "p1")
}
}) {
ImagePicker(imagePicker: self.$imagePicker, imgData: self.$imgData)
}
}
struct MessageInputBar: View {
@Binding var isPresented: Bool
@Binding var message:String
@Binding var isSendingMsg: Bool
@ObservedObject private var keyboard = KeyboardResponder()
var onSend: (String) -> Void
var body: some View {
VStack {
Spacer()
HStack(spacing: 1){
HStack(spacing: 15){
//TextField("", text: self.$message).foregroundColor(.black)
CustomTF(sfIcon: "message", hint: "输入消息", hasDriveLine:false, value: $message).foregroundColor(.black)
/* Button(action: {
// toogling image picker...
imagePicker.toggle()
}, label: {
Image(systemName: "paperclip.circle.fill")
.font(.system(size: 22))
.foregroundColor(.gray)
})*/
}
.padding(.vertical, 12)
.padding(.horizontal)
.background(Color.black.opacity(0.06))
.clipShape(Capsule())
//.animation(.easeInOut(duration: 0.3)) //
// Send Button...
// hiding view...
if message != "" {
if isSendingMsg == false {
Button(action: {
onSend(message)
// appeding message...
}, label: {
Image(systemName: "paperplane.fill")
.font(.system(size: 22))
.foregroundColor(Color("Main"))
// rotating the image...
.rotationEffect(.init(degrees: 45))
// adjusting padding shape...
.padding(.vertical,12)
.padding(.leading,12)
.padding(.trailing,17)
.background(Color.black.opacity(0.07))
.clipShape(Circle())
})
}else{
ProgressView()
.progressViewStyle(CircularProgressViewStyle()).tint(Color("Main"))
}
}
}
.padding(.horizontal)
.padding(.bottom)
.background(Color.white)
}
}
}
public func getPlanList() async {
isLoading = true
errorMessage = nil
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/fetch?id=\(ticketID)")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "数据请求失败"
return
}
// Parse the user info response
if let Subscribe = try? JSONDecoder().decode(TicketChatReponse.self, from: data), let data = Subscribe.data {
tickchatData = data
if let message = Subscribe.data?.message {
alltickets = message
alltickets.forEach { msg in
allMessages.messages.append(msg)
}
if (alltickets.count > 0 && tickchatData?.status == 0){
isMessageInputBarPresented.toggle()
}
}else{
self.errorMessage = Subscribe.message ?? ""
}
}else{
self.errorMessage = "数据请求失败: JSON 解析错误"
}
}
}
task.resume()
}
public func closeThisTicket() async {
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/close?id=\(ticketID)")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let error = error {
self.alert = Alert(errorMessage: "数据请求失败: \(error.localizedDescription)")
return
}
guard let data = data else {
self.alert = Alert(errorMessage: "数据请求失败")
return
}
// MARK: - TicketSendMsgReponse
struct TicketSendMsgReponse: Codable {
let status, message: String?
}
// Parse the user info response
if let SendMsgReponse = try? JSONDecoder().decode(TicketSendMsgReponse.self, from: data), let message = SendMsgReponse.message {
if let stutas = SendMsgReponse.status , stutas == "success" {
self.alert = Alert(okMessage: message)
}else{
self.alert = Alert(errorMessage: message)
}
}else{
self.alert = Alert(errorMessage: "数据请求失败: JSON 解析错误")
}
}
}
task.resume()
}
public func SendMsg(msg: String) async {
isSendingMsg = true
if let encodedMsg = msg.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/reply?id=\(ticketID)&message=\(encodedMsg)"){
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isSendingMsg = false
if let error = error {
self.alert = Alert(errorMessage: "数据请求失败: \(error.localizedDescription)")
return
}
guard let data = data else {
self.alert = Alert(errorMessage: "数据请求失败")
return
}
// MARK: - TicketSendMsgReponse
struct TicketSendMsgReponse: Codable {
let status, message: String?
}
// Parse the user info response
if let SendMsgReponse = try? JSONDecoder().decode(TicketSendMsgReponse.self, from: data), let message = SendMsgReponse.message {
let randomInt = Int.random(in: 100...1000)
if let stutas = SendMsgReponse.status , stutas == "success" {
withAnimation(.easeIn){
allMessages.writeMessage(idold: randomInt, msg: msg, myMsg: true, profilePic: "p1")
}
self.message = ""
}else{
self.alert = Alert(errorMessage: message)
}
}else{
self.alert = Alert(errorMessage: "数据请求失败: JSON 解析错误")
}
}
}
task.resume()
}
}
}
// Chat Bubbles...
struct ChatBubble : View {
var msg : Message
var body: some View{
// Automatic scroll To Bottom...
// First Assigning Id To Each Row...
HStack(alignment: .top,spacing: 10){
if msg.isMe{
// pushing msg to left...
// minimum space ...
// Modifying for Image...
Spacer(minLength: 25)
if msg.photo == nil{
Text("\(msg.message)")
.padding(.all)
.background(Color.black.opacity(0.06))
.clipShape(BubbleArrow(myMsg: msg.isMe)).foregroundColor(.black)
}
else{
// Image(uiImage: UIImage(data: msg.photo!)!).resizable()
AsyncImage(url: URL(string: msg.photo ?? ""))
.frame(width: UIScreen.main.bounds.width - 150, height: 150)
.clipShape(BubbleArrow(myMsg: msg.isMe))
}
// profile Image...
Image(msg.profilePic ?? "p1" )
.resizable()
.frame(width: 30, height: 30)
.clipShape(Circle())
}
else{
// pushing msg to right...
// profile Image...
Image(msg.profilePic ?? "p2")
.resizable()
.frame(width: 30, height: 30)
.clipShape(Circle())
if msg.photo == nil{
Text(msg.message)
.foregroundColor(.white)
.padding(.all)
.background(Color("Main"))
.clipShape(BubbleArrow(myMsg: msg.isMe))
// Text(":\(TimestampConverter.convertTimestampToDateString(msg.updatedAt)).font(.caption).foregroundColor(.black.opacity(0.8))")
}
else{
// Image(uiImage: UIImage(data: msg.photo!)!)
// .resizable()
AsyncImage(url: URL(string: msg.photo ?? ""))
.frame(width: UIScreen.main.bounds.width - 150, height: 150)
.clipShape(BubbleArrow(myMsg: msg.isMe))
}
Spacer(minLength: 25)
}
}
.id(msg.id)
}
}
// Bubble Arrow...
struct BubbleArrow : Shape {
var myMsg : Bool
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: myMsg ? [.topLeft,.bottomLeft,.bottomRight] : [.topRight,.bottomLeft,.bottomRight], cornerRadii: CGSize(width: 10, height: 10))
return Path(path.cgPath)
}
}
// Custom Rounded Shape...
struct RoundedShape : Shape {
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: [.topLeft,.topRight], cornerRadii: CGSize(width: 35, height: 35))
return Path(path.cgPath)
}
}
// Model Data For Message...
/*struct Message : Identifiable,Equatable{
var id : Date
var message : String
var myMsg : Bool
var profilePic : String
var photo: Data?
}*/
class Messages : ObservableObject{
@Published var messages : [Message] = []
// sample data...
init() {
/*let strings = ["Hii","Hello !!!!","What's Up, What Are You Doing ???","Nothing Just Simply Enjoying Quarintine Holidays..You???","Same :))","Ohhhhh","What About Your Country ???","Very Very Bad...","Ok Be Safe","Bye....","Ok...."]
//simple logic for two side messages
for i in 0..<strings.count{
messages.append(Message(id: Date(), message: strings[i], myMsg: i % 2 == 0 ? true : false, profilePic: i % 2 == 0 ? "p1" : "p2"))
}*/
}
func writeMessage(idold: Int,msg: String,myMsg: Bool,profilePic: String?){
messages.append(Message(idold: idold, ticketID: 0, isMe: myMsg, message: msg, photo: nil, createdAt: Int(Date().timeIntervalSince1970), updatedAt: Int(Date().timeIntervalSince1970), profilePic: profilePic))
//messages.append(Message(id: id, message: msg, myMsg: myMsg, profilePic: profilePic, photo: photo))
}
}
// Image Picker...
struct ImagePicker : UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator {
return ImagePicker.Coordinator(parent1: self)
}
@Binding var imagePicker : Bool
@Binding var imgData : Data
func makeUIViewController(context: Context) -> UIImagePickerController{
let picker = UIImagePickerController()
picker.sourceType = .photoLibrary
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
}
class Coordinator : NSObject,UIImagePickerControllerDelegate,UINavigationControllerDelegate{
var parent : ImagePicker
init(parent1 : ImagePicker) {
parent = parent1
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
parent.imagePicker.toggle()
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let image = info[.originalImage] as! UIImage
parent.imgData = image.jpegData(compressionQuality: 0.5)!
parent.imagePicker.toggle()
}
}
}
@@ -0,0 +1,530 @@
//
// SupportticketView.swift
// SFI
//
// Created by Mac on 2024/10/20.
//
import Foundation
import SwiftUI
import ApplicationLibrary
struct SupportTicketView : View {
var edges = getSafeAreaInsets()
@State var selectedTab = "Chats"
@Namespace var animation
@Environment(\.presentationMode) var presentationMode //
@State private var TicketsList: [TicketsReponseDatum] = []
@State private var isLoading = false
@State private var errorMessage: String?
@State private var alert: Alert?
@State private var showInputSheet = false
@State private var tickettitle = ""
@State private var userInput = ""
@State private var isInputLoading = false
@State private var submitmessage = ""
var body: some View{
NavigationView(content: {
VStack(spacing: 0){
VStack{
ZStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss() //
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10) //
Spacer(minLength: 0)
Button(action: {
showInputSheet.toggle()
}, label: {
// Image(systemName: "note.text.badge.plus")
Text("提交新工单").foregroundColor(.red)
})
}
Text("我的工单").fontWeight(.bold).lineLimit(1)
}
.padding()
// .padding(.top,edges.top)
.foregroundColor(.white)
// HStack(spacing: 20){
//
// ForEach(tabs,id: \.self){title in
//
// TabButton(selectedTab: $selectedTab, title: title, animation: animation)
// }
// }
// .padding()
// .background(Color.white.opacity(0.08))
// .cornerRadius(15)
// .padding(.vertical)
}
.padding(.bottom)
// .background(Color("top"))
// .clipShape(CustomCorner(corner: .bottomLeft, size: 65))
ZStack{
// Color("top")
// Color("bg")
// .clipShape(CustomCorner(corner: .topRight, size: 65))
ScrollView(.vertical, showsIndicators: false, content: {
VStack(spacing: 20){
/*HStack{
Text("All Chats")
.font(.title2)
.fontWeight(.bold)
Spacer(minLength: 0)
Button(action: {}, label: {
Image(systemName: "slider.horizontal.3")
.font(.system(size: 22))
.foregroundColor(.primary)
})
}
.padding([.horizontal,.top])*/
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.padding()
}
if let errorMessage = errorMessage {
Text(errorMessage)
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center) //
.padding()
}
if TicketsList.count > 0 {
ForEach(TicketsList){chatData in
// Chat View...
NavigationLink(destination: SupportTicketChatView(ticketID: chatData.idOLD)) {
ChatView(chatData: chatData)
}
}
}else{
if !isLoading
{
Text("暂无记录").font(.caption)
.foregroundColor(.gray)
.multilineTextAlignment(.center) //
.padding()
}
}
}
.padding(.vertical)
})
//.clipShape(CustomCorner(corner: .topRight, size: 65))
// its cutting off inside view may be its a bug....
}
}.onAppear(){
Task{
await getPlanList()
}
}
.navigationBarHidden(true)
//.background(Color("bg").ignoresSafeArea(.all, edges: .all))
.background(
LinearGradient(colors: [
Color("BG1"),
Color("BG1"),
Color("BG2"),
Color("BG2"),
], startPoint: .top, endPoint: .bottom).ignoresSafeArea(.all, edges: .all))
// .ignoresSafeArea(.all, edges: .top)
.sheet(isPresented: $showInputSheet) {
InputSheet(tickettitle: $tickettitle, userInput: $userInput, isInputLoading: $isInputLoading, submitmessage: $submitmessage, onSubmit: {
Task {
//
await submitTicketData(title: tickettitle, userInput: userInput)
}
})
}
})
.navigationBarHidden(true)
.alertBinding($alert)
.navigationBarBackButtonHidden(true) //
}
public func submitTicketData(title: String, userInput: String) async{
isInputLoading = true
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/save?subject=\(title)&level=0&message=\(userInput)")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
isInputLoading = false
if let error = error {
submitmessage = ( "数据请求失败: \(error.localizedDescription)")
return
}
guard let data = data else {
submitmessage = ( "数据请求失败")
return
}
// MARK: - TicketSendMsgReponse
struct TicketSendMsgReponse: Codable {
let status, message: String?
}
// Parse the user info response
if let SendMsgReponse = try? JSONDecoder().decode(TicketSendMsgReponse.self, from: data), let message = SendMsgReponse.message {
if let stutas = SendMsgReponse.status , stutas == "success" {
submitmessage = message
//1s
withAnimation {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
showInputSheet = false
//
Task{
await getPlanList()
}
}
}
}else{
submitmessage = (message)
}
}else{
submitmessage = ( "数据请求失败: JSON 解析错误")
}
}
}
task.resume()
}
public func getPlanList() async {
isLoading = true
errorMessage = nil
let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/ticket/fetch")!
var request = URLRequest(url: userInfoUrl)
request.httpMethod = "GET"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
if let error = error {
self.errorMessage = "数据请求失败: \(error.localizedDescription)"
return
}
guard let data = data else {
self.errorMessage = "数据请求失败"
return
}
// Parse the user info response
if let Subscribe = try? JSONDecoder().decode(TicketsReponse.self, from: data) {
if let data = Subscribe.data {
TicketsList = data
}else{
self.errorMessage = Subscribe.message ?? ""
}
}else{
self.errorMessage = "数据请求失败: JSON 解析错误"
}
}
}
task.resume()
}
}
var tabs = ["Chats","Status","Calls"]
struct TabButton : View {
@Binding var selectedTab : String
var title : String
var animation : Namespace.ID
var body: some View{
Button(action: {
withAnimation{
selectedTab = title
}
}, label: {
Text(title)
.foregroundColor(.white)
.padding(.vertical,10)
.padding(.horizontal)
// Sliding Effect...
.background(
ZStack{
if selectedTab == title{
RoundedRectangle(cornerRadius: 10)
.fill(Color("top"))
.matchedGeometryEffect(id: "Tab", in: animation)
}
}
)
})
}
}
struct CustomCorner : Shape {
var corner : UIRectCorner
var size : CGFloat
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corner, cornerRadii: CGSize(width: size, height: size))
return Path(path.cgPath)
}
}
struct ChatView : View {
var chatData : TicketsReponseDatum
var body: some View{
HStack(spacing: 10){
VStack(spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 8, content: {
Text(chatData.subject)
.fontWeight(.bold)
.lineLimit(1).foregroundColor(.white)
// Text("\(chatData.message ?? "")")
// .font(.caption)
// .lineLimit(1)
HStack(content: {
if(chatData.status == 1){
Text("工单状态 : 已关闭").fontWeight(.light).foregroundColor(.gray).font(.subheadline)
}else if(chatData.status == 0){
Text("工单状态 : 待回复").fontWeight(.light).foregroundColor(.red).font(.subheadline)
}else if(chatData.status == 2){
Text("工单状态 : 已回复").fontWeight(.light).foregroundColor(.green).font(.subheadline)
}
Spacer(minLength: 0)
Text("\(TimestampConverter.convertTimestampToDateString(chatData.updatedAt))")
.font(.subheadline).foregroundColor(.white)
})
})
}
Divider()
}
}
.padding(.horizontal)
}
}
// Model And Sample Data....
//
//struct Chat : Identifiable {
//
// var id = UUID().uuidString
// var name : String
// var image : String
// var msg : String
// var time : String
//}
//
//// were going to do custom grouping of views....
//
//struct HomeData {
//
// var groupName : String
// var groupData : [Chat]
//}
//var FriendsChat : [Chat] = [
//
// Chat(name: "iJustine",image: "p0", msg: "Hey EveryOne !!!", time: "02:45"),
// Chat(name: "Kavsoft",image: "p1", msg: "Learn - Develop - Deploy", time: "03:45"),
// Chat(name: "SwiftUI",image: "p2", msg: "New Framework For iOS", time: "04:55"),
// Chat(name: "Bill Gates",image: "p3", msg: "Founder Of Microsoft", time: "06:25"),
// Chat(name: "Tim Cook",image: "p4", msg: "Apple lnc CEO", time: "07:19"),
// Chat(name: "Jeff",image: "p5", msg: "I dont Know How To Spend Money :)))", time: "08:22"),
//]
//
//var GroupChat : [Chat] = [
//
// Chat(name: "iTeam",image: "p0", msg: "Hey EveryOne !!!", time: "02:45"),
// Chat(name: "Kavsoft - Developers",image: "p1", msg: "Next Video :))))", time: "03:45"),
// Chat(name: "SwiftUI - Community",image: "p2", msg: "New File Importer/Exporter", time: "04:55"),
//]
//
//var data = [
//
// // Group 1
// HomeData(groupName: "", groupData: FriendsChat),
//]
struct InputSheet: View {
@Binding var tickettitle: String
@Binding var userInput: String
@Binding var isInputLoading: Bool
@Binding var submitmessage:String
var onSubmit: () -> Void
var body: some View {
VStack(spacing: 20) {
Spacer()
Text("新的工单").fontWeight(.bold).lineLimit(1)
Text("请输入工单标题").font(.subheadline) .foregroundColor(.gray)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.leading)
.padding(.leading, 5) //
.padding(.top, 8) //
TextEditor( text: $tickettitle)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.frame(width: UIScreen.main.bounds.width * 0.9,height: 50)
.background(Color.clear) //
.cornerRadius(10) //
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.5), lineWidth: 1)
) //
.background(Color.clear) // //
.font(.system(size: 18))
.transparentScrolling()
Text("请输入工单内容").font(.subheadline) .foregroundColor(.gray)
.frame(maxWidth: .infinity)
.padding(.leading, 5) //
.padding(.top, 8) //
.multilineTextAlignment(.leading)
TextEditor(text: $userInput)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
.frame(width: UIScreen.main.bounds.width * 0.9,height: UIScreen.main.bounds.height * 0.4)
.background(Color.clear) //
.cornerRadius(10) //
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.5), lineWidth: 1)
) //
.font(.system(size: 18))
.transparentScrolling()
Spacer()
if !submitmessage.isEmpty{
Text(submitmessage).font(.subheadline).foregroundColor(Color.red.opacity(0.9))
}
Button(action: {
if( tickettitle.count > 0 && userInput.count > 0){
onSubmit() //
}
}) {
if isInputLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle()).tint(Color("Main"))
} else {
Text("提交工单")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
.padding()
.frame(maxWidth: .infinity)
.padding(.bottom, 20)
}
.background(Color("bg").ignoresSafeArea(.all, edges: .all))
.ignoresSafeArea(edges: .bottom) //
}
}
@@ -0,0 +1,66 @@
//
// SupportView.swift
// SFI
//
// Created by Mac on 2024/10/21.
//
import Foundation
import SwiftUI
import Crisp
struct SupportView: View {
@Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView(content: {
VStack{
ZStack{
HStack{
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "chevron.left")
.foregroundColor(.white)
Text("返回")
.foregroundColor(.white)
}).padding(10)
Spacer(minLength: 0)
}
Text("客服(技术支持)")
.fontWeight(.bold).lineLimit(1)
}
.padding()
// .padding(.top,edges.top)
.foregroundColor(.white)
ChatViewControllerWrapper()
// SomeUIElement()
// WebView(url: URL(string: UserManager.shared.kefuUrl())!)
}.navigationBarHidden(true).edgesIgnoringSafeArea(.bottom)
.background(
LinearGradient(colors: [
Color("BG1"),
Color("BG1"),
Color("BG2"),
Color("BG2"),
], startPoint: .top, endPoint: .bottom)
)
})
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
}
@@ -0,0 +1,40 @@
//
// WebView.swift
// SFI
//
// Created by Mac on 2024/10/12.
//
import Foundation
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
var url: URL
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
}
struct WebViewContent: UIViewRepresentable {
var content: String
func makeUIView(context: Context) -> WKWebView {
return WKWebView()
}
func updateUIView(_ webView: WKWebView, context: Context) {
// let request = URLRequest(url: url)
// webView.load(request)
webView.loadHTMLString(content, baseURL: nil)
}
}