fix reset password
This commit is contained in:
parent
ed6ae5c757
commit
85756fb6ee
@ -7,17 +7,19 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Observation
|
import Observation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
class ResetPasswordModel {
|
class ResetPasswordModel {
|
||||||
|
|
||||||
enum Stage {
|
enum Stage {
|
||||||
case requestVerifyCode
|
case requestVerifyCode(username: String?)
|
||||||
case submitVerifyCode(username: String)
|
case submitVerifyCode(username: String)
|
||||||
case resetPassword(username: String)
|
case resetPassword(username: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
var stage: Stage = .requestVerifyCode
|
var stage: Stage = .requestVerifyCode(username: nil)
|
||||||
|
var transitionEdge: Edge = .trailing // 默认从右进入
|
||||||
|
|
||||||
private let baseParams: [String: Any] = [
|
private let baseParams: [String: Any] = [
|
||||||
"client_id": SystemConfig.getClientId(),
|
"client_id": SystemConfig.getClientId(),
|
||||||
|
|||||||
@ -5,292 +5,273 @@
|
|||||||
// Created by 安礼成 on 2026/3/9.
|
// Created by 安礼成 on 2026/3/9.
|
||||||
//
|
//
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Observation
|
||||||
|
|
||||||
|
// MARK: - 1. 根视图
|
||||||
struct ResetPasswordRootView: View {
|
struct ResetPasswordRootView: View {
|
||||||
@State private var resetPasswordModel = ResetPasswordModel()
|
@State private var resetPasswordModel = ResetPasswordModel()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
// 背景毛玻璃 (macOS 风格)
|
||||||
|
VisualEffectView(material: .underWindowBackground, blendingMode: .behindWindow)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
Group {
|
Group {
|
||||||
switch resetPasswordModel.stage {
|
switch resetPasswordModel.stage {
|
||||||
case .requestVerifyCode:
|
case .requestVerifyCode(let username):
|
||||||
GetVerifyCodeView()
|
GetVerifyCodeView(username: username ?? "")
|
||||||
case .submitVerifyCode(let username):
|
case .submitVerifyCode(let username):
|
||||||
SubmitVerifyCodeView(username: username)
|
SubmitVerifyCodeView(username: username)
|
||||||
case .resetPassword(let username):
|
case .resetPassword(let username):
|
||||||
ResetPasswordView(username: username)
|
ResetPasswordView(username: username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .move(edge: resetPasswordModel.transitionEdge).combined(with: .opacity),
|
||||||
|
removal: .move(edge: resetPasswordModel.transitionEdge == .trailing ? .leading : .trailing).combined(with: .opacity)
|
||||||
|
))
|
||||||
|
}
|
||||||
.environment(resetPasswordModel)
|
.environment(resetPasswordModel)
|
||||||
|
.frame(width: 400, height: 450)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取验证码
|
// MARK: - 2. 第一步:获取验证码
|
||||||
struct GetVerifyCodeView: View {
|
struct GetVerifyCodeView: View {
|
||||||
@Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel
|
@Environment(ResetPasswordModel.self) var resetPasswordModel
|
||||||
|
@State var username: String = ""
|
||||||
@State private var username: String = ""
|
@State private var isProcessing = false
|
||||||
@State private var showAlert = false
|
@State private var showAlert = false
|
||||||
@State private var errorMessage = ""
|
@State private var errorMessage = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 24) {
|
||||||
|
headerSection(title: "重置密码", subtitle: "请输入关联的手机号或邮箱来验证身份")
|
||||||
|
|
||||||
// 标题
|
PunchTextField(icon: "person.crop.circle", placeholder: "手机号 / 邮箱", text: $username)
|
||||||
Text("重置密码")
|
.frame(width: 280)
|
||||||
.font(.system(size: 18, weight: .medium))
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
|
||||||
TextField("手机号/邮箱", text: $username)
|
|
||||||
.textFieldStyle(.plain)
|
|
||||||
.frame(width: 260, height: 28)
|
|
||||||
.overlay(
|
|
||||||
Rectangle()
|
|
||||||
.frame(height: 1)
|
|
||||||
.foregroundColor(.blue),
|
|
||||||
alignment: .bottom
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Task { @MainActor in
|
self.sendVerifyCode()
|
||||||
//await self.sendVerifyCode()
|
|
||||||
withAnimation {
|
|
||||||
self.resetPasswordModel.stage = .submitVerifyCode(username: self.username)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Text("获取验证码")
|
Text("获取验证码")
|
||||||
.font(.system(size: 14))
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.black)
|
.frame(maxWidth: .infinity)
|
||||||
.frame(width: 160, height: 36)
|
|
||||||
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
|
|
||||||
}
|
}
|
||||||
.frame(width: 160, height: 36)
|
.buttonStyle(.borderedProminent)
|
||||||
.cornerRadius(6)
|
.controlSize(.large)
|
||||||
|
.frame(width: 280)
|
||||||
|
.disabled(!SDLUtil.isValidIdentifyContact(username) || isProcessing)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.top, 40)
|
.padding(40)
|
||||||
.frame(width: 400, height: 400)
|
|
||||||
.alert(isPresented: $showAlert) {
|
.alert(isPresented: $showAlert) {
|
||||||
Alert(title: Text("提示"), message: Text(self.errorMessage))
|
Alert(title: Text("提示"), message: Text(errorMessage))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendVerifyCode() async {
|
// 发送验证码
|
||||||
if username.isEmpty {
|
private func sendVerifyCode() {
|
||||||
self.showAlert = true
|
self.isProcessing = true
|
||||||
self.errorMessage = "手机号/邮箱为空"
|
Task { @MainActor in
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch SDLUtil.identifyContact(username) {
|
|
||||||
case .email, .phone:
|
|
||||||
do {
|
do {
|
||||||
let result = try await self.resetPasswordModel.requestVerifyCode(username: username)
|
_ = try await resetPasswordModel.requestVerifyCode(username: username)
|
||||||
print("send verify code result: \(result)")
|
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
|
||||||
|
resetPasswordModel.stage = .submitVerifyCode(username: username)
|
||||||
|
resetPasswordModel.transitionEdge = .trailing
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
self.showAlert = true
|
|
||||||
self.errorMessage = error.localizedDescription
|
self.errorMessage = error.localizedDescription
|
||||||
|
self.showAlert = true
|
||||||
|
}
|
||||||
|
self.isProcessing = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case .invalid:
|
|
||||||
self.showAlert = true
|
|
||||||
self.errorMessage = "手机号/邮箱格式错误"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 输入验证码
|
// MARK: - 3. 第二步:验证验证码
|
||||||
struct SubmitVerifyCodeView: View {
|
struct SubmitVerifyCodeView: View {
|
||||||
@Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel
|
@Environment(ResetPasswordModel.self) var resetPasswordModel
|
||||||
|
let username: String
|
||||||
|
@State private var code: String = ""
|
||||||
|
@State private var isProcessing = false
|
||||||
|
|
||||||
@State var username: String
|
// 重发逻辑
|
||||||
@State private var verifiyCode: String = ""
|
@State private var remainingSeconds = 60
|
||||||
|
@State private var isResendEnabled = false
|
||||||
|
|
||||||
|
// 错误逻辑处理
|
||||||
@State private var showAlert = false
|
@State private var showAlert = false
|
||||||
@State private var errorMessage = ""
|
@State private var errorMessage = ""
|
||||||
|
|
||||||
|
var validInputCode: Bool {
|
||||||
|
return !self.code.isEmpty && self.code.count == 4 && self.code.allSatisfy {$0.isNumber}
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 24) {
|
||||||
|
headerSection(title: "身份验证", subtitle: "验证码已发送至 \(username)")
|
||||||
|
|
||||||
// 标题
|
VStack(alignment: .trailing, spacing: 16) {
|
||||||
Text("重置密码")
|
PunchTextField(icon: "envelope.badge", placeholder: "输入 4 位验证码", text: $code)
|
||||||
.font(.system(size: 18, weight: .medium))
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
Button(isResendEnabled ? "重新获取" : "重新获取 (\(remainingSeconds)s)") {
|
||||||
TextField("手机号/邮箱", text: $username)
|
self.resendAction()
|
||||||
.textFieldStyle(.plain)
|
|
||||||
.frame(width: 260, height: 28)
|
|
||||||
.disabled(true)
|
|
||||||
.overlay(
|
|
||||||
Rectangle()
|
|
||||||
.frame(height: 1)
|
|
||||||
.foregroundColor(.blue),
|
|
||||||
alignment: .bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
TextField("验证码", text: $verifiyCode)
|
|
||||||
.textFieldStyle(.plain)
|
|
||||||
.frame(width: 260, height: 28)
|
|
||||||
.overlay(
|
|
||||||
Rectangle()
|
|
||||||
.frame(height: 1)
|
|
||||||
.foregroundColor(.blue),
|
|
||||||
alignment: .bottom
|
|
||||||
)
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Button {
|
|
||||||
Task { @MainActor in
|
|
||||||
//await self.sendVerifyCode()
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.link)
|
||||||
|
.font(.caption)
|
||||||
|
.disabled(!isResendEnabled)
|
||||||
|
}
|
||||||
|
.frame(width: 280)
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Button {
|
||||||
|
self.submitAction()
|
||||||
} label: {
|
} label: {
|
||||||
Text("再次获取")
|
Text("验证并继续")
|
||||||
.font(.system(size: 14))
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.black)
|
.frame(maxWidth: .infinity)
|
||||||
.frame(width: 160, height: 36)
|
|
||||||
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
|
|
||||||
}
|
}
|
||||||
.frame(width: 160, height: 36)
|
.buttonStyle(.borderedProminent)
|
||||||
.cornerRadius(6)
|
.controlSize(.large)
|
||||||
|
.disabled(!validInputCode)
|
||||||
|
|
||||||
|
Button("返回上一步") {
|
||||||
|
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
|
||||||
|
self.resetPasswordModel.stage = .requestVerifyCode(username: self.username)
|
||||||
|
self.resetPasswordModel.transitionEdge = .leading
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.frame(width: 280)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(40)
|
||||||
|
.task {
|
||||||
|
await startCountdown()
|
||||||
|
}
|
||||||
|
.alert(isPresented: $showAlert) {
|
||||||
|
Alert(title: Text("提示"), message: Text(self.errorMessage))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
private func resendAction() {
|
||||||
|
Task {
|
||||||
|
_ = try? await resetPasswordModel.requestVerifyCode(username: username)
|
||||||
|
await startCountdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startCountdown() async {
|
||||||
|
self.isResendEnabled = false
|
||||||
|
self.remainingSeconds = 60
|
||||||
|
while remainingSeconds > 0 {
|
||||||
|
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
self.remainingSeconds -= 1
|
||||||
|
}
|
||||||
|
self.isResendEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func submitAction() {
|
||||||
|
self.isProcessing = true
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
await self.submitVerifyCode()
|
do {
|
||||||
withAnimation {
|
_ = try await resetPasswordModel.submitVerifyCode(username: username, verifyCode: code)
|
||||||
|
withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
|
||||||
self.resetPasswordModel.stage = .resetPassword(username: username)
|
self.resetPasswordModel.stage = .resetPassword(username: username)
|
||||||
|
self.resetPasswordModel.transitionEdge = .trailing
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("设置密码")
|
|
||||||
.font(.system(size: 14))
|
|
||||||
.foregroundColor(.black)
|
|
||||||
.frame(width: 160, height: 36)
|
|
||||||
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
|
|
||||||
}
|
|
||||||
.frame(width: 160, height: 36)
|
|
||||||
.cornerRadius(6)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.top, 40)
|
|
||||||
.frame(width: 400, height: 400)
|
|
||||||
.alert(isPresented: $showAlert) {
|
|
||||||
Alert(title: Text("提示"), message: Text(self.errorMessage))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func submitVerifyCode() async {
|
|
||||||
if verifiyCode.isEmpty {
|
|
||||||
self.showAlert = true
|
|
||||||
self.errorMessage = "请输入验证码"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if verifiyCode.count != 4 {
|
|
||||||
self.showAlert = true
|
|
||||||
self.errorMessage = "验证码错误"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let result = try await self.resetPasswordModel.submitVerifyCode(username: username, verifyCode: verifiyCode)
|
|
||||||
print("submit verify code result: \(result)")
|
|
||||||
} catch {
|
} catch {
|
||||||
self.showAlert = true
|
|
||||||
self.errorMessage = error.localizedDescription
|
self.errorMessage = error.localizedDescription
|
||||||
|
self.showAlert = true
|
||||||
|
}
|
||||||
|
self.isProcessing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置密码
|
// MARK: - 4. 第三步:重置密码
|
||||||
struct ResetPasswordView: View {
|
struct ResetPasswordView: View {
|
||||||
@Environment(ResetPasswordModel.self) var resetPasswordModel: ResetPasswordModel
|
@Environment(ResetPasswordModel.self) var resetPasswordModel
|
||||||
|
let username: String
|
||||||
|
@State private var password = ""
|
||||||
|
@State private var confirm = ""
|
||||||
|
@State private var isProcessing = false
|
||||||
|
|
||||||
var username: String
|
// 判断输入是否合法
|
||||||
|
var isInputValid: Bool {
|
||||||
|
!password.isEmpty && password == confirm && password.count >= 8
|
||||||
|
}
|
||||||
|
|
||||||
@State private var password: String = ""
|
// 提示错误信息
|
||||||
@State private var confirmPassword: String = ""
|
var passwordError: String? {
|
||||||
|
if password.isEmpty || confirm.isEmpty {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
@State private var showAlert = false
|
if password != confirm {
|
||||||
@State private var errorMessage = ""
|
return "两次输入的密码不一致"
|
||||||
|
}
|
||||||
|
|
||||||
|
if password.count < 8 {
|
||||||
|
return "密码至少需要 8 位"
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 30) {
|
VStack(spacing: 24) {
|
||||||
|
headerSection(title: "设置新密码", subtitle: "请为账号 \(username) 设置一个强密码")
|
||||||
|
|
||||||
// 标题
|
VStack(spacing: 12) {
|
||||||
Text("重置密码")
|
PunchTextField(icon: "lock.shield", placeholder: "新密码 (至少8位)", text: $password, isSecure: true)
|
||||||
.font(.system(size: 18, weight: .medium))
|
PunchTextField(icon: "lock.shield", placeholder: "确认新密码", text: $confirm, isSecure: true)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
if let passwordError = self.passwordError {
|
||||||
SecureField("新密码", text: $password)
|
Text(passwordError)
|
||||||
.textFieldStyle(.plain)
|
.font(.caption)
|
||||||
.frame(width: 260, height: 28)
|
.foregroundColor(.red)
|
||||||
.overlay(
|
.frame(width: 280, alignment: .leading)
|
||||||
Rectangle()
|
|
||||||
.frame(height: 1)
|
|
||||||
.foregroundColor(.blue),
|
|
||||||
alignment: .bottom
|
|
||||||
)
|
|
||||||
|
|
||||||
SecureField("再次输入密码", text: $confirmPassword)
|
|
||||||
.textFieldStyle(.plain)
|
|
||||||
.frame(width: 260, height: 28)
|
|
||||||
.overlay(
|
|
||||||
Rectangle()
|
|
||||||
.frame(height: 1)
|
|
||||||
.foregroundColor(.blue),
|
|
||||||
alignment: .bottom
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 280)
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
Task { @MainActor in
|
self.handleReset()
|
||||||
await self.resetPassword()
|
|
||||||
}
|
|
||||||
} label: {
|
} label: {
|
||||||
Text("保存")
|
Text("重置密码并登录")
|
||||||
.font(.system(size: 14))
|
.fontWeight(.medium)
|
||||||
.foregroundColor(.black)
|
.frame(maxWidth: .infinity)
|
||||||
.frame(width: 160, height: 36)
|
|
||||||
.background(Color(red: 74/255, green: 207/255, blue: 154/255))
|
|
||||||
}
|
}
|
||||||
.frame(width: 160, height: 36)
|
.buttonStyle(.borderedProminent)
|
||||||
.cornerRadius(6)
|
.controlSize(.large)
|
||||||
|
.frame(width: 280)
|
||||||
|
.disabled(!isInputValid || isProcessing)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.top, 40)
|
.padding(40)
|
||||||
.frame(width: 400, height: 400)
|
|
||||||
.alert(isPresented: $showAlert) {
|
|
||||||
Alert(title: Text("提示"), message: Text(self.errorMessage))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func resetPassword() async {
|
|
||||||
if password.isEmpty {
|
|
||||||
self.showAlert = true
|
|
||||||
self.errorMessage = "请输入新密码"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if confirmPassword.isEmpty || confirmPassword != password {
|
|
||||||
self.showAlert = true
|
|
||||||
self.errorMessage = "两次输入的密码不一致"
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleReset() {
|
||||||
|
self.isProcessing = true
|
||||||
|
Task { @MainActor in
|
||||||
do {
|
do {
|
||||||
let result = try await self.resetPasswordModel.resetPassword(username: username, password: self.password)
|
_ = try await resetPasswordModel.resetPassword(username: username, password: password)
|
||||||
print("send verify code result: \(result)")
|
print("密码重置成功")
|
||||||
|
// 此处可添加重置成功后的跳转逻辑
|
||||||
} catch {
|
} catch {
|
||||||
self.showAlert = true
|
print("重置失败: \(error)")
|
||||||
self.errorMessage = error.localizedDescription
|
}
|
||||||
|
self.isProcessing = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,10 +30,6 @@ struct punchnetApp: App {
|
|||||||
@Environment(\.openWindow) private var openWindow
|
@Environment(\.openWindow) private var openWindow
|
||||||
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||||
|
|
||||||
@AppStorage("token") var token: String = ""
|
|
||||||
@AppStorage("network_code") var networkCode: String = ""
|
|
||||||
@AppStorage("hostname") var hostname: String = ""
|
|
||||||
|
|
||||||
private var noticeServer: UDPNoticeCenterServer
|
private var noticeServer: UDPNoticeCenterServer
|
||||||
@State private var appContext: AppContext
|
@State private var appContext: AppContext
|
||||||
@State private var userContext = UserContext()
|
@State private var userContext = UserContext()
|
||||||
@ -41,15 +37,13 @@ struct punchnetApp: App {
|
|||||||
init() {
|
init() {
|
||||||
self.noticeServer = UDPNoticeCenterServer()
|
self.noticeServer = UDPNoticeCenterServer()
|
||||||
self.noticeServer.start()
|
self.noticeServer.start()
|
||||||
|
|
||||||
self.appContext = AppContext(noticePort: self.noticeServer.port)
|
self.appContext = AppContext(noticePort: self.noticeServer.port)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup(id: "mainWindow") {
|
WindowGroup(id: "mainWindow") {
|
||||||
// RootView()
|
// RootView()
|
||||||
RegisterRootView()
|
ResetPasswordRootView()
|
||||||
.frame(width: 800, height: 500)
|
|
||||||
.navigationTitle("")
|
.navigationTitle("")
|
||||||
.environment(self.appContext)
|
.environment(self.appContext)
|
||||||
.environment(self.userContext)
|
.environment(self.userContext)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user