This commit is contained in:
parent
4b20775c61
commit
8cab7bac36
BIN
MobileMkch.xcodeproj/project.xcworkspace/xcuserdata/platon.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
BIN
MobileMkch.xcodeproj/project.xcworkspace/xcuserdata/platon.xcuserdatad/UserInterfaceState.xcuserstate
generated
Normal file
Binary file not shown.
@ -1,6 +1,13 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
struct UploadFile {
|
||||
let name: String
|
||||
let filename: String
|
||||
let mimeType: String
|
||||
let data: Data
|
||||
}
|
||||
|
||||
class APIClient: ObservableObject {
|
||||
private let baseURL = "https://mkch.pooziqo.xyz"
|
||||
private let apiURL = "https://mkch.pooziqo.xyz/api"
|
||||
@ -437,21 +444,21 @@ class APIClient: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func createThread(boardCode: String, title: String, text: String, passcode: String, completion: @escaping (Error?) -> Void) {
|
||||
func createThread(boardCode: String, title: String, text: String, passcode: String, files: [UploadFile] = [], completion: @escaping (Error?) -> Void) {
|
||||
if !passcode.isEmpty {
|
||||
loginWithPasscode(passcode: passcode) { error in
|
||||
if let error = error {
|
||||
completion(error)
|
||||
return
|
||||
}
|
||||
self.performCreateThread(boardCode: boardCode, title: title, text: text, completion: completion)
|
||||
self.performCreateThread(boardCode: boardCode, title: title, text: text, files: files, completion: completion)
|
||||
}
|
||||
} else {
|
||||
performCreateThread(boardCode: boardCode, title: title, text: text, completion: completion)
|
||||
performCreateThread(boardCode: boardCode, title: title, text: text, files: files, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private func performCreateThread(boardCode: String, title: String, text: String, completion: @escaping (Error?) -> Void) {
|
||||
private func performCreateThread(boardCode: String, title: String, text: String, files: [UploadFile] = [], completion: @escaping (Error?) -> Void) {
|
||||
let formURL = "\(baseURL)/boards/board/\(boardCode)/new"
|
||||
let url = URL(string: formURL)!
|
||||
|
||||
@ -485,19 +492,30 @@ class APIClient: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
var postRequest = URLRequest(url: url)
|
||||
postRequest.httpMethod = "POST"
|
||||
postRequest.setValue(formURL, forHTTPHeaderField: "Referer")
|
||||
postRequest.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
|
||||
|
||||
if files.isEmpty {
|
||||
var formData = URLComponents()
|
||||
formData.queryItems = [
|
||||
URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken),
|
||||
URLQueryItem(name: "title", value: title),
|
||||
URLQueryItem(name: "text", value: text)
|
||||
]
|
||||
|
||||
var postRequest = URLRequest(url: url)
|
||||
postRequest.httpMethod = "POST"
|
||||
postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
postRequest.setValue(formURL, forHTTPHeaderField: "Referer")
|
||||
postRequest.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
|
||||
postRequest.httpBody = formData.query?.data(using: .utf8)
|
||||
} else {
|
||||
let boundary = "Boundary-\(UUID().uuidString)"
|
||||
postRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
let body = self.buildMultipartBody(parameters: [
|
||||
"csrfmiddlewaretoken": csrfToken,
|
||||
"title": title,
|
||||
"text": text
|
||||
], files: files, boundary: boundary)
|
||||
postRequest.httpBody = body
|
||||
}
|
||||
|
||||
self.session.dataTask(with: postRequest) { _, postResponse, postError in
|
||||
DispatchQueue.main.async {
|
||||
@ -520,21 +538,21 @@ class APIClient: ObservableObject {
|
||||
}.resume()
|
||||
}
|
||||
|
||||
func addComment(boardCode: String, threadId: Int, text: String, passcode: String, completion: @escaping (Error?) -> Void) {
|
||||
func addComment(boardCode: String, threadId: Int, text: String, passcode: String, files: [UploadFile] = [], completion: @escaping (Error?) -> Void) {
|
||||
if !passcode.isEmpty {
|
||||
loginWithPasscode(passcode: passcode) { error in
|
||||
if let error = error {
|
||||
completion(error)
|
||||
return
|
||||
}
|
||||
self.performAddComment(boardCode: boardCode, threadId: threadId, text: text, completion: completion)
|
||||
self.performAddComment(boardCode: boardCode, threadId: threadId, text: text, files: files, completion: completion)
|
||||
}
|
||||
} else {
|
||||
performAddComment(boardCode: boardCode, threadId: threadId, text: text, completion: completion)
|
||||
performAddComment(boardCode: boardCode, threadId: threadId, text: text, files: files, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private func performAddComment(boardCode: String, threadId: Int, text: String, completion: @escaping (Error?) -> Void) {
|
||||
private func performAddComment(boardCode: String, threadId: Int, text: String, files: [UploadFile] = [], completion: @escaping (Error?) -> Void) {
|
||||
let formURL = "\(baseURL)/boards/board/\(boardCode)/thread/\(threadId)/comment"
|
||||
let url = URL(string: formURL)!
|
||||
|
||||
@ -568,18 +586,28 @@ class APIClient: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
var postRequest = URLRequest(url: url)
|
||||
postRequest.httpMethod = "POST"
|
||||
postRequest.setValue(formURL, forHTTPHeaderField: "Referer")
|
||||
postRequest.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
|
||||
|
||||
if files.isEmpty {
|
||||
var formData = URLComponents()
|
||||
formData.queryItems = [
|
||||
URLQueryItem(name: "csrfmiddlewaretoken", value: csrfToken),
|
||||
URLQueryItem(name: "text", value: text)
|
||||
]
|
||||
|
||||
var postRequest = URLRequest(url: url)
|
||||
postRequest.httpMethod = "POST"
|
||||
postRequest.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
postRequest.setValue(formURL, forHTTPHeaderField: "Referer")
|
||||
postRequest.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
|
||||
postRequest.httpBody = formData.query?.data(using: .utf8)
|
||||
} else {
|
||||
let boundary = "Boundary-\(UUID().uuidString)"
|
||||
postRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
let body = self.buildMultipartBody(parameters: [
|
||||
"csrfmiddlewaretoken": csrfToken,
|
||||
"text": text
|
||||
], files: files, boundary: boundary)
|
||||
postRequest.httpBody = body
|
||||
}
|
||||
|
||||
self.session.dataTask(with: postRequest) { _, postResponse, postError in
|
||||
DispatchQueue.main.async {
|
||||
@ -617,6 +645,25 @@ class APIClient: ObservableObject {
|
||||
return String(html[range])
|
||||
}
|
||||
|
||||
private func buildMultipartBody(parameters: [String: String], files: [UploadFile], boundary: String) -> Data {
|
||||
var body = Data()
|
||||
let boundaryPrefix = "--\(boundary)\r\n"
|
||||
for (key, value) in parameters {
|
||||
body.append(boundaryPrefix.data(using: .utf8)!)
|
||||
body.append("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n".data(using: .utf8)!)
|
||||
body.append("\(value)\r\n".data(using: .utf8)!)
|
||||
}
|
||||
for file in files {
|
||||
body.append(boundaryPrefix.data(using: .utf8)!)
|
||||
body.append("Content-Disposition: form-data; name=\"\(file.name)\"; filename=\"\(file.filename)\"\r\n".data(using: .utf8)!)
|
||||
body.append("Content-Type: \(file.mimeType)\r\n\r\n".data(using: .utf8)!)
|
||||
body.append(file.data)
|
||||
body.append("\r\n".data(using: .utf8)!)
|
||||
}
|
||||
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
|
||||
return body
|
||||
}
|
||||
|
||||
func checkNewThreads(forBoard boardCode: String, lastKnownThreadId: Int, completion: @escaping (Result<[Thread], Error>) -> Void) {
|
||||
let url = URL(string: "\(apiURL)/board/\(boardCode)")!
|
||||
var request = URLRequest(url: url)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
struct AddCommentView: View {
|
||||
let boardCode: String
|
||||
@ -12,6 +13,8 @@ struct AddCommentView: View {
|
||||
@State private var errorMessage: String?
|
||||
@State private var showingSuccess = false
|
||||
@FocusState private var isTextFocused: Bool
|
||||
@State private var pickedImages: [UIImage] = []
|
||||
@State private var showPicker: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
@ -56,6 +59,38 @@ struct AddCommentView: View {
|
||||
)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Фото")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Button {
|
||||
showPicker = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "photo.on.rectangle")
|
||||
Text("Выбрать фото")
|
||||
Spacer()
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
if !pickedImages.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Array(pickedImages.enumerated()), id: \.offset) { _, img in
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 64, height: 64)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: settings.passcode.isEmpty ? "exclamationmark.triangle.fill" : "checkmark.circle.fill")
|
||||
.foregroundColor(settings.passcode.isEmpty ? .orange : .green)
|
||||
@ -126,6 +161,11 @@ struct AddCommentView: View {
|
||||
} message: {
|
||||
Text("Комментарий добавлен")
|
||||
}
|
||||
.sheet(isPresented: $showPicker) {
|
||||
ImagePickerView(selectionLimit: 4) { images in
|
||||
pickedImages = images
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,7 +179,8 @@ struct AddCommentView: View {
|
||||
boardCode: boardCode,
|
||||
threadId: threadId,
|
||||
text: text,
|
||||
passcode: settings.passcode
|
||||
passcode: settings.passcode,
|
||||
files: buildUploadFiles()
|
||||
) { error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
@ -152,6 +193,22 @@ struct AddCommentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func buildUploadFiles() -> [UploadFile] {
|
||||
var result: [UploadFile] = []
|
||||
for (idx, img) in pickedImages.enumerated() {
|
||||
if let data = img.jpegData(compressionQuality: 0.9) {
|
||||
let file = UploadFile(
|
||||
name: "files",
|
||||
filename: "photo_\(idx + 1).jpg",
|
||||
mimeType: "image/jpeg",
|
||||
data: data
|
||||
)
|
||||
result.append(file)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
struct CreateThreadView: View {
|
||||
let boardCode: String
|
||||
@ -13,6 +14,8 @@ struct CreateThreadView: View {
|
||||
@State private var showingSuccess = false
|
||||
@FocusState private var titleFocused: Bool
|
||||
@FocusState private var textFocused: Bool
|
||||
@State private var pickedImages: [UIImage] = []
|
||||
@State private var showPicker: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
@ -41,6 +44,38 @@ struct CreateThreadView: View {
|
||||
)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Фото")
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
Button {
|
||||
showPicker = true
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "photo.on.rectangle")
|
||||
Text("Выбрать фото")
|
||||
Spacer()
|
||||
}
|
||||
.padding(12)
|
||||
.background(Color(.secondarySystemBackground))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
if !pickedImages.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(Array(pickedImages.enumerated()), id: \.offset) { _, img in
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 64, height: 64)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Содержание")
|
||||
@ -149,6 +184,11 @@ struct CreateThreadView: View {
|
||||
} message: {
|
||||
Text("Тред создан")
|
||||
}
|
||||
.sheet(isPresented: $showPicker) {
|
||||
ImagePickerView(selectionLimit: 4) { images in
|
||||
pickedImages = images
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -162,7 +202,8 @@ struct CreateThreadView: View {
|
||||
boardCode: boardCode,
|
||||
title: title,
|
||||
text: text,
|
||||
passcode: settings.passcode
|
||||
passcode: settings.passcode,
|
||||
files: buildUploadFiles()
|
||||
) { error in
|
||||
DispatchQueue.main.async {
|
||||
self.isLoading = false
|
||||
@ -175,6 +216,22 @@ struct CreateThreadView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func buildUploadFiles() -> [UploadFile] {
|
||||
var result: [UploadFile] = []
|
||||
for (idx, img) in pickedImages.enumerated() {
|
||||
if let data = img.jpegData(compressionQuality: 0.9) {
|
||||
let file = UploadFile(
|
||||
name: "files",
|
||||
filename: "photo_\(idx + 1).jpg",
|
||||
mimeType: "image/jpeg",
|
||||
data: data
|
||||
)
|
||||
result.append(file)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
|
||||
57
MobileMkch/ImagePicker.swift
Normal file
57
MobileMkch/ImagePicker.swift
Normal file
@ -0,0 +1,57 @@
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
struct ImagePickerView: UIViewControllerRepresentable {
|
||||
let selectionLimit: Int
|
||||
let onComplete: ([UIImage]) -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var configuration = PHPickerConfiguration()
|
||||
configuration.filter = .images
|
||||
configuration.selectionLimit = selectionLimit
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(onComplete: onComplete)
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
||||
let onComplete: ([UIImage]) -> Void
|
||||
|
||||
init(onComplete: @escaping ([UIImage]) -> Void) {
|
||||
self.onComplete = onComplete
|
||||
}
|
||||
|
||||
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
guard !results.isEmpty else {
|
||||
picker.dismiss(animated: true)
|
||||
return
|
||||
}
|
||||
let providers = results.map { $0.itemProvider }
|
||||
var images: [UIImage] = []
|
||||
let group = DispatchGroup()
|
||||
for provider in providers {
|
||||
if provider.canLoadObject(ofClass: UIImage.self) {
|
||||
group.enter()
|
||||
provider.loadObject(ofClass: UIImage.self) { object, _ in
|
||||
if let img = object as? UIImage {
|
||||
images.append(img)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
group.notify(queue: .main) {
|
||||
self.onComplete(images)
|
||||
picker.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
</array>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Нужно для выбора фото и загрузки вложений</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>background-processing</string>
|
||||
|
||||
@ -37,7 +37,7 @@ struct MobileMkchApp: App {
|
||||
|
||||
private func handleNotificationLaunch() {
|
||||
if let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let userInfo = scene.session.userInfo {
|
||||
let _ = scene.session.userInfo {
|
||||
print("Приложение запущено из уведомления")
|
||||
}
|
||||
notificationManager.clearBadge()
|
||||
|
||||
@ -89,29 +89,7 @@ struct ThreadDetailView: View {
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack {
|
||||
Button("Обновить") { loadThreadDetail() }
|
||||
if #available(iOS 16.1, *) {
|
||||
Toggle("", isOn: $activityOn)
|
||||
.toggleStyle(SwitchToggleStyle(tint: .blue))
|
||||
.labelsHidden()
|
||||
.onChange(of: activityOn) { newValue in
|
||||
guard settings.liveActivityEnabled else { return }
|
||||
if newValue {
|
||||
if let detail = threadDetail {
|
||||
LiveActivityManager.shared.start(for: detail, comments: comments, settings: settings)
|
||||
}
|
||||
} else {
|
||||
LiveActivityManager.shared.end(threadId: thread.id)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if settings.liveActivityEnabled {
|
||||
activityOn = LiveActivityManager.shared.isActive(threadId: thread.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddComment) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user