Merge pull request #16 from rock-n-code/feature/upload-file
Feature: Upload file
This commit is contained in:
commit
b0bba71288
@ -85,7 +85,6 @@ private extension ContentView {
|
|||||||
id: user.rootFolder.id,
|
id: user.rootFolder.id,
|
||||||
name: user.rootFolder.name
|
name: user.rootFolder.name
|
||||||
),
|
),
|
||||||
uploadFile: uploadFile,
|
|
||||||
showProfile: showProfile,
|
showProfile: showProfile,
|
||||||
login: login
|
login: login
|
||||||
)
|
)
|
||||||
|
41
Modules/Sources/Browse/Logic/Adapters/FileAdapter.swift
Normal file
41
Modules/Sources/Browse/Logic/Adapters/FileAdapter.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// FileAdapter.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 17/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import APIService
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct FileAdapter {
|
||||||
|
func callAsFunction(url: URL) throws -> File {
|
||||||
|
guard url.isFileURL else {
|
||||||
|
throw FileAdapterError.urlIsNotFileURL
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
let name = try url
|
||||||
|
.resourceValues(forKeys: [.nameKey])
|
||||||
|
.allValues
|
||||||
|
.first(where: { $0.key == .nameKey })
|
||||||
|
.map(\.value)
|
||||||
|
|
||||||
|
guard let name = name as? String else {
|
||||||
|
throw FileAdapterError.nameNotCasted
|
||||||
|
}
|
||||||
|
|
||||||
|
return .init(
|
||||||
|
name: name,
|
||||||
|
data: data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Errors
|
||||||
|
|
||||||
|
enum FileAdapterError: Error {
|
||||||
|
case urlIsNotFileURL
|
||||||
|
case nameNotCasted
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
//
|
||||||
|
// UploadFileUseCase.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 17/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import APIService
|
||||||
|
import DependencyInjection
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct UploadFileUseCase {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
let apiService: APIService
|
||||||
|
|
||||||
|
private let fileAdapter: FileAdapter = .init()
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func callAsFunction(
|
||||||
|
id: String,
|
||||||
|
url: URL,
|
||||||
|
username: String,
|
||||||
|
password: String
|
||||||
|
) async throws {
|
||||||
|
_ = try await apiService.uploadFile(
|
||||||
|
id: id,
|
||||||
|
file: fileAdapter(url: url),
|
||||||
|
credentials: .init(
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Initialisers
|
||||||
|
|
||||||
|
extension UploadFileUseCase {
|
||||||
|
init() {
|
||||||
|
@Dependency(\.apiService) var apiService
|
||||||
|
|
||||||
|
self.init(apiService: apiService)
|
||||||
|
}
|
||||||
|
}
|
91
Modules/Sources/Browse/UI/Components/DocumentPicker.swift
Normal file
91
Modules/Sources/Browse/UI/Components/DocumentPicker.swift
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// DocumentPicker.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 17/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct DocumentPicker: UIViewControllerRepresentable {
|
||||||
|
|
||||||
|
// MARK: Type aliases
|
||||||
|
|
||||||
|
typealias SelectedClosure = ([URL]) -> Void
|
||||||
|
typealias CancelledClosure = () -> Void
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private let selected: SelectedClosure
|
||||||
|
private let cancelled: CancelledClosure?
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
init(
|
||||||
|
selected: @escaping SelectedClosure,
|
||||||
|
cancelled: CancelledClosure? = nil
|
||||||
|
) {
|
||||||
|
self.selected = selected
|
||||||
|
self.cancelled = cancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
|
||||||
|
let controller = UIDocumentPickerViewController(
|
||||||
|
forOpeningContentTypes: [.item],
|
||||||
|
asCopy: true
|
||||||
|
)
|
||||||
|
|
||||||
|
controller.allowsMultipleSelection = false
|
||||||
|
controller.shouldShowFileExtensions = false
|
||||||
|
controller.delegate = context.coordinator
|
||||||
|
|
||||||
|
return controller
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(
|
||||||
|
_ uiViewController: UIDocumentPickerViewController,
|
||||||
|
context: Context
|
||||||
|
) { }
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
.init(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Coordinators
|
||||||
|
|
||||||
|
extension DocumentPicker {
|
||||||
|
class Coordinator: NSObject, UIDocumentPickerDelegate {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private var parent: DocumentPicker
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
init(_ parent: DocumentPicker) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UIDocumentPickerDelegate
|
||||||
|
|
||||||
|
func documentPicker(
|
||||||
|
_ controller: UIDocumentPickerViewController,
|
||||||
|
didPickDocumentsAt urls: [URL]
|
||||||
|
) {
|
||||||
|
parent.selected(urls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||||
|
controller.dismiss(animated: true) { [weak self] in
|
||||||
|
self?.parent.cancelled?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
134
Modules/Sources/Browse/UI/Components/InputAlert.swift
Normal file
134
Modules/Sources/Browse/UI/Components/InputAlert.swift
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
//
|
||||||
|
// InputAlert.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 16/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import DataModels
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct InputAlert: UIViewControllerRepresentable {
|
||||||
|
|
||||||
|
// MARK: Bindings
|
||||||
|
|
||||||
|
@Binding private var isPresenting: Bool
|
||||||
|
@Binding private var textFieldString: String
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private let title: String
|
||||||
|
private let message: String
|
||||||
|
private let textFieldPlaceholder: String
|
||||||
|
private let actions: [UIAlertAction]
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
init(
|
||||||
|
isPresenting: Binding<Bool>,
|
||||||
|
title: String,
|
||||||
|
message: String,
|
||||||
|
textFieldPlaceholder: String,
|
||||||
|
textFieldString: Binding<String>,
|
||||||
|
actions: [UIAlertAction]
|
||||||
|
) {
|
||||||
|
self.title = title
|
||||||
|
self.message = message
|
||||||
|
self.textFieldPlaceholder = textFieldPlaceholder
|
||||||
|
self.actions = actions
|
||||||
|
|
||||||
|
self._isPresenting = isPresenting
|
||||||
|
self._textFieldString = textFieldString
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func makeUIViewController(
|
||||||
|
context: UIViewControllerRepresentableContext<InputAlert>
|
||||||
|
) -> UIViewController {
|
||||||
|
.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(
|
||||||
|
_ viewController: UIViewController,
|
||||||
|
context: UIViewControllerRepresentableContext<InputAlert>
|
||||||
|
) {
|
||||||
|
guard
|
||||||
|
context.coordinator.alert == nil,
|
||||||
|
isPresenting
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
let alertController = {
|
||||||
|
let alert = UIAlertController(
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
preferredStyle: .alert
|
||||||
|
)
|
||||||
|
|
||||||
|
alert.addTextField { textField in
|
||||||
|
textField.placeholder = textFieldPlaceholder
|
||||||
|
textField.text = textFieldString
|
||||||
|
textField.delegate = context.coordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.forEach { action in
|
||||||
|
alert.addAction(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return alert
|
||||||
|
}()
|
||||||
|
|
||||||
|
context.coordinator.alert = alertController
|
||||||
|
|
||||||
|
Task { @MainActor in
|
||||||
|
viewController.present(
|
||||||
|
alertController,
|
||||||
|
animated: true
|
||||||
|
) {
|
||||||
|
isPresenting = false
|
||||||
|
context.coordinator.alert = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
.init(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Coordinator
|
||||||
|
|
||||||
|
extension InputAlert {
|
||||||
|
class Coordinator: NSObject, UITextFieldDelegate {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
var alert: UIAlertController?
|
||||||
|
|
||||||
|
private let parent: InputAlert
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
init(_ parent: InputAlert) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: UITextFieldDelegate
|
||||||
|
|
||||||
|
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
|
parent.textFieldString = {
|
||||||
|
if let text = textField.text as NSString? {
|
||||||
|
return text.replacingCharacters(in: range, with: string)
|
||||||
|
} else {
|
||||||
|
return .empty
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -23,11 +23,11 @@ public struct BrowseView: View {
|
|||||||
@State private var stack: Stack?
|
@State private var stack: Stack?
|
||||||
@State private var itemToDelete: (any FileSystemItem)?
|
@State private var itemToDelete: (any FileSystemItem)?
|
||||||
@State private var showCreateFolder: Bool = false
|
@State private var showCreateFolder: Bool = false
|
||||||
|
@State private var showUploadFile: Bool = false
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
private let folder: Folder
|
private let folder: Folder
|
||||||
private let uploadFile: ActionClosure
|
|
||||||
private let showProfile: ActionClosure
|
private let showProfile: ActionClosure
|
||||||
private let login: ActionClosure
|
private let login: ActionClosure
|
||||||
|
|
||||||
@ -37,12 +37,10 @@ public struct BrowseView: View {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
folder: Folder,
|
folder: Folder,
|
||||||
uploadFile: @escaping ActionClosure,
|
|
||||||
showProfile: @escaping ActionClosure,
|
showProfile: @escaping ActionClosure,
|
||||||
login: @escaping ActionClosure
|
login: @escaping ActionClosure
|
||||||
) {
|
) {
|
||||||
self.folder = folder
|
self.folder = folder
|
||||||
self.uploadFile = uploadFile
|
|
||||||
self.showProfile = showProfile
|
self.showProfile = showProfile
|
||||||
self.login = login
|
self.login = login
|
||||||
}
|
}
|
||||||
@ -57,10 +55,17 @@ public struct BrowseView: View {
|
|||||||
createFolder: {
|
createFolder: {
|
||||||
showCreateFolder = true
|
showCreateFolder = true
|
||||||
},
|
},
|
||||||
uploadFile: uploadFile,
|
uploadFile: {
|
||||||
|
showUploadFile = true
|
||||||
|
},
|
||||||
showProfile: showProfile
|
showProfile: showProfile
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.sheet(isPresented: $showUploadFile) {
|
||||||
|
UploadView(id: folder.id) {
|
||||||
|
Task { await loadItems() }
|
||||||
|
}
|
||||||
|
}
|
||||||
.createFolder(
|
.createFolder(
|
||||||
isPresenting: $showCreateFolder,
|
isPresenting: $showCreateFolder,
|
||||||
id: folder.id
|
id: folder.id
|
||||||
@ -112,7 +117,9 @@ private extension BrowseView {
|
|||||||
case .empty:
|
case .empty:
|
||||||
MessageView(
|
MessageView(
|
||||||
type: .empty,
|
type: .empty,
|
||||||
action: uploadFile
|
action: {
|
||||||
|
showUploadFile = true
|
||||||
|
}
|
||||||
)
|
)
|
||||||
case .error:
|
case .error:
|
||||||
MessageView(type: .error) {
|
MessageView(type: .error) {
|
||||||
@ -137,7 +144,6 @@ private extension BrowseView {
|
|||||||
.navigate(
|
.navigate(
|
||||||
to: BrowseView(
|
to: BrowseView(
|
||||||
folder: folder,
|
folder: folder,
|
||||||
uploadFile: uploadFile,
|
|
||||||
showProfile: showProfile,
|
showProfile: showProfile,
|
||||||
login: login
|
login: login
|
||||||
),
|
),
|
||||||
@ -214,8 +220,6 @@ struct BrowseView_Previews: PreviewProvider {
|
|||||||
id: UUID().uuidString,
|
id: UUID().uuidString,
|
||||||
name: "Some folder name"
|
name: "Some folder name"
|
||||||
)) {
|
)) {
|
||||||
// upload file closure.
|
|
||||||
} showProfile: {
|
|
||||||
// show profile closure.
|
// show profile closure.
|
||||||
} login: {
|
} login: {
|
||||||
// login closure.
|
// login closure.
|
||||||
|
78
Modules/Sources/Browse/UI/Views/UploadView.swift
Normal file
78
Modules/Sources/Browse/UI/Views/UploadView.swift
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// UploadView.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 17/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import DataModels
|
||||||
|
import Foundation
|
||||||
|
import KeychainStorage
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
struct UploadView: View {
|
||||||
|
|
||||||
|
// MARK: Storages
|
||||||
|
|
||||||
|
@KeychainStorage(key: .KeychainStorage.account) private var account: Account?
|
||||||
|
|
||||||
|
// MARK: States
|
||||||
|
|
||||||
|
@State private var urls: [URL] = []
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
let uploaded: ActionClosure
|
||||||
|
|
||||||
|
private let uploadFile: UploadFileUseCase = .init()
|
||||||
|
|
||||||
|
// MARK: Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
DocumentPicker { urls in
|
||||||
|
Task { await addFile(from: urls) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension UploadView {
|
||||||
|
func addFile(from urls: [URL]) async {
|
||||||
|
guard let url = urls.first else {
|
||||||
|
// TODO: Handle this error case.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let account else {
|
||||||
|
// TODO: Handle this error case.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await uploadFile(
|
||||||
|
id: id,
|
||||||
|
url: url,
|
||||||
|
username: account.username,
|
||||||
|
password: account.password
|
||||||
|
)
|
||||||
|
|
||||||
|
uploaded()
|
||||||
|
} catch {
|
||||||
|
// TODO: Handle this error case.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
struct UploadView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
UploadView(id: "1234567890") {
|
||||||
|
// uploaded closure.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user