Merge pull request #16 from rock-n-code/feature/upload-file

Feature: Upload file
This commit is contained in:
Javier Cicchelli 2022-12-17 21:08:11 +01:00 committed by GitHub
commit b0bba71288
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 406 additions and 9 deletions

View File

@ -85,7 +85,6 @@ private extension ContentView {
id: user.rootFolder.id,
name: user.rootFolder.name
),
uploadFile: uploadFile,
showProfile: showProfile,
login: login
)

View 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
}

View File

@ -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)
}
}

View 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?()
}
}
}
}

View 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
}
}
}

View File

@ -23,11 +23,11 @@ public struct BrowseView: View {
@State private var stack: Stack?
@State private var itemToDelete: (any FileSystemItem)?
@State private var showCreateFolder: Bool = false
@State private var showUploadFile: Bool = false
// MARK: Properties
private let folder: Folder
private let uploadFile: ActionClosure
private let showProfile: ActionClosure
private let login: ActionClosure
@ -37,12 +37,10 @@ public struct BrowseView: View {
public init(
folder: Folder,
uploadFile: @escaping ActionClosure,
showProfile: @escaping ActionClosure,
login: @escaping ActionClosure
) {
self.folder = folder
self.uploadFile = uploadFile
self.showProfile = showProfile
self.login = login
}
@ -57,10 +55,17 @@ public struct BrowseView: View {
createFolder: {
showCreateFolder = true
},
uploadFile: uploadFile,
uploadFile: {
showUploadFile = true
},
showProfile: showProfile
)
}
.sheet(isPresented: $showUploadFile) {
UploadView(id: folder.id) {
Task { await loadItems() }
}
}
.createFolder(
isPresenting: $showCreateFolder,
id: folder.id
@ -112,7 +117,9 @@ private extension BrowseView {
case .empty:
MessageView(
type: .empty,
action: uploadFile
action: {
showUploadFile = true
}
)
case .error:
MessageView(type: .error) {
@ -137,7 +144,6 @@ private extension BrowseView {
.navigate(
to: BrowseView(
folder: folder,
uploadFile: uploadFile,
showProfile: showProfile,
login: login
),
@ -214,8 +220,6 @@ struct BrowseView_Previews: PreviewProvider {
id: UUID().uuidString,
name: "Some folder name"
)) {
// upload file closure.
} showProfile: {
// show profile closure.
} login: {
// login closure.

View 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.
}
}
}