diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift index dc7eccd..a3d61da 100644 --- a/BeReal/UI/Views/ContentView.swift +++ b/BeReal/UI/Views/ContentView.swift @@ -85,7 +85,6 @@ private extension ContentView { id: user.rootFolder.id, name: user.rootFolder.name ), - createFolder: createFolder, uploadFile: uploadFile, showProfile: showProfile, login: login diff --git a/Modules/Sources/Browse/Logic/Use Cases/CreateFolderUseCase.swift b/Modules/Sources/Browse/Logic/Use Cases/CreateFolderUseCase.swift new file mode 100644 index 0000000..cf0d596 --- /dev/null +++ b/Modules/Sources/Browse/Logic/Use Cases/CreateFolderUseCase.swift @@ -0,0 +1,47 @@ +// +// CreateFolderUseCase.swift +// Browse +// +// Created by Javier Cicchelli on 16/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import APIService +import DependencyInjection +import Dependencies + +struct CreateFolderUseCase { + + // MARK: Properties + + let apiService: APIService + + // MARK: Functions + + func callAsFunction( + id: String, + name: String, + username: String, + password: String + ) async throws { + _ = try await apiService.createFolder( + id: id, + name: name, + credentials: .init( + username: username, + password: password + ) + ) + } + +} + +// MARK: - Initialisers + +extension CreateFolderUseCase { + init() { + @Dependency(\.apiService) var apiService + + self.init(apiService: apiService) + } +} diff --git a/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings b/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings index 5336bc7..fd36893 100644 --- a/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings +++ b/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings @@ -25,15 +25,26 @@ "message.type_not_supported.text.second" = "Please be patient while the support for this type of document is being built by our development team."; "message.type_not_supported.button.text" = "Go back to folder"; +// CreateFolderViewModifier + +"create_folder.input_alert.title" = "Create a new folder"; +"create_folder.input_alert.message" = "You are about to create a new folder inside the folder you are currently in."; +"create_folder.input_alert.text_field.placeholder" = "Name of the folder to create"; +"create_folder.input_alert.button.cancel" = "Dismiss"; +"create_folder.input_alert.button.submit" = "Create folder"; +"create_folder.system_alert.title" = "An error occurred while creating a new folder"; +"create_folder.system_alert.message" = "An unexpected error occurred while trying to create a new folder into the current folder.\n\nPlease check your Internet connection and try this operation at a later time."; +"create_folder.system_alert.button.cancel" = "Understood"; + // DeleteItemViewModifier "delete_item.action_sheet.title" = "Delete an item"; "delete_item.action_sheet.message %@" = "You are about to delete an item named \"%@\" from this folder.\n\nAre you sure you wish to proceed?"; "delete_item.action_sheet.button.ok" = "Yes, please delete it."; "delete_item.action_sheet.button.cancel" = "No, I reconsidered."; -"delete_item.system_alert.title" = "..."; -"delete_item.system_alert.message" = "..."; -"delete_item.system_alert.button.dismiss" = "..."; +"delete_item.system_alert.title" = "An error occurred when deleting a folder"; +"delete_item.system_alert.message" = "An unexpected error occurred while trying to delete the indicated folder from the current folder.\n\nPlease check your Internet connection and try this operation at a later time."; +"delete_item.system_alert.button.cancel" = "Understood"; // BrowseView diff --git a/Modules/Sources/Browse/UI/Components/InputAlertView.swift b/Modules/Sources/Browse/UI/Components/InputAlertView.swift new file mode 100644 index 0000000..a1c1b36 --- /dev/null +++ b/Modules/Sources/Browse/UI/Components/InputAlertView.swift @@ -0,0 +1,134 @@ +// +// InputAlertView.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 InputAlertView: 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, + title: String, + message: String, + textFieldPlaceholder: String, + textFieldString: Binding, + 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 + ) -> UIViewController { + .init() + } + + func updateUIViewController( + _ viewController: UIViewController, + context: UIViewControllerRepresentableContext + ) { + 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() -> InputAlertView.Coordinator { + Coordinator(self) + } + +} + +// MARK: - Coordinator + +extension InputAlertView { + class Coordinator: NSObject, UITextFieldDelegate { + + // MARK: Properties + + var alert: UIAlertController? + + private let component: InputAlertView + + // MARK: Initialisers + + init(_ component: InputAlertView) { + self.component = component + } + + // MARK: UITextFieldDelegate + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + component.textFieldString = { + if let text = textField.text as NSString? { + return text.replacingCharacters(in: range, with: string) + } else { + return .empty + } + }() + + return true + } + + } +} diff --git a/Modules/Sources/Browse/UI/Extensions/String+Constants.swift b/Modules/Sources/Browse/UI/Extensions/String+Constants.swift new file mode 100644 index 0000000..2dbb2f9 --- /dev/null +++ b/Modules/Sources/Browse/UI/Extensions/String+Constants.swift @@ -0,0 +1,11 @@ +// +// String+Constants.swift +// Browse +// +// Created by Javier Cicchelli on 16/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +extension String { + static let empty = "" +} diff --git a/Modules/Sources/Browse/UI/Extensions/String+Localisations.swift b/Modules/Sources/Browse/UI/Extensions/String+Localisations.swift new file mode 100644 index 0000000..2883ece --- /dev/null +++ b/Modules/Sources/Browse/UI/Extensions/String+Localisations.swift @@ -0,0 +1,19 @@ +// +// String+Localisations.swift +// Browse +// +// Created by Javier Cicchelli on 16/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import Foundation + +extension String { + static func localise(key: String) -> Self { + NSLocalizedString( + key, + bundle: .module, + comment: .empty + ) + } +} diff --git a/Modules/Sources/Browse/UI/Extensions/View+ViewModifiers.swift b/Modules/Sources/Browse/UI/Extensions/View+ViewModifiers.swift index ce602dc..9761acf 100644 --- a/Modules/Sources/Browse/UI/Extensions/View+ViewModifiers.swift +++ b/Modules/Sources/Browse/UI/Extensions/View+ViewModifiers.swift @@ -22,6 +22,18 @@ extension View { )) } + func createFolder( + isPresenting: Binding, + id: String, + submitted: @escaping ActionClosure + ) -> some View { + modifier(CreateFolderViewModifier( + isPresenting: isPresenting, + id: id, + submitted: submitted + )) + } + func delete( item: Binding<(any FileSystemItem)?>, deleted: @escaping ActionClosure diff --git a/Modules/Sources/Browse/UI/View Modifiers/CreateFolderViewModifier.swift b/Modules/Sources/Browse/UI/View Modifiers/CreateFolderViewModifier.swift new file mode 100644 index 0000000..aad7bb6 --- /dev/null +++ b/Modules/Sources/Browse/UI/View Modifiers/CreateFolderViewModifier.swift @@ -0,0 +1,109 @@ +// +// CreateFolderViewModifier.swift +// Browse +// +// Created by Javier Cicchelli on 16/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +import DataModels +import KeychainStorage +import SwiftUI + +struct CreateFolderViewModifier: ViewModifier { + + // MARK: Storages + + @KeychainStorage(key: .KeychainStorage.account) private var account: Account? + + // MARK: States + + @State private var folderName: String = "" + @State private var showErrorAlert: Bool = false + + // MARK: Bindings + + @Binding var isPresenting: Bool + + let id: String + let submitted: ActionClosure + + private let createFolder: CreateFolderUseCase = .init() + + // MARK: Body + + func body(content: Content) -> some View { + content + .background(InputAlertView( + isPresenting: $isPresenting, + title: .localise(key: "create_folder.input_alert.title"), + message: .localise(key: "create_folder.input_alert.message"), + textFieldPlaceholder: .localise(key: "create_folder.input_alert.text_field.placeholder"), + textFieldString: $folderName, + actions: [ + .init( + title: .localise(key: "create_folder.input_alert.button.cancel"), + style: .cancel, + handler: { _ in + folderName = .empty + } + ), + .init( + title: .localise(key: "create_folder.input_alert.button.submit"), + style: .default, + handler: { _ in + Task { + await addFolder() + } + } + ) + ] + )) + .alert(isPresented: $showErrorAlert) { + Alert( + title: Text( + "create_folder.system_alert.title", + bundle: .module + ), + message: Text( + "create_folder.system_alert.message", + bundle: .module + ), + dismissButton: .cancel(Text( + "create_folder.system_alert.button.cancel", + bundle: .module + )) { + folderName = .empty + } + ) + } + + } + +} + +// MARK: - Helpers + +private extension CreateFolderViewModifier { + func addFolder() async { + guard let account else { + showErrorAlert = true + return + } + + do { + _ = try await createFolder( + id: id, + name: folderName, + username: account.username, + password: account.password + ) + + folderName = .empty + + submitted() + } catch { + showErrorAlert = true + } + } +} diff --git a/Modules/Sources/Browse/UI/View Modifiers/DeleteItemViewModifier.swift b/Modules/Sources/Browse/UI/View Modifiers/DeleteItemViewModifier.swift index aa1e6ab..6bc34a7 100644 --- a/Modules/Sources/Browse/UI/View Modifiers/DeleteItemViewModifier.swift +++ b/Modules/Sources/Browse/UI/View Modifiers/DeleteItemViewModifier.swift @@ -115,6 +115,8 @@ private extension DeleteItemViewModifier { password: account.password ) + item = nil + deleted() } catch { showErrorAlert = true diff --git a/Modules/Sources/Browse/UI/Views/BrowseView.swift b/Modules/Sources/Browse/UI/Views/BrowseView.swift index 067521c..4e13ccd 100644 --- a/Modules/Sources/Browse/UI/Views/BrowseView.swift +++ b/Modules/Sources/Browse/UI/Views/BrowseView.swift @@ -22,11 +22,11 @@ public struct BrowseView: View { @State private var items: [any FileSystemItem] = [] @State private var stack: Stack? @State private var itemToDelete: (any FileSystemItem)? + @State private var showCreateFolder: Bool = false // MARK: Properties private let folder: Folder - private let createFolder: ActionClosure private let uploadFile: ActionClosure private let showProfile: ActionClosure private let login: ActionClosure @@ -37,13 +37,11 @@ public struct BrowseView: View { public init( folder: Folder, - createFolder: @escaping ActionClosure, uploadFile: @escaping ActionClosure, showProfile: @escaping ActionClosure, login: @escaping ActionClosure ) { self.folder = folder - self.createFolder = createFolder self.uploadFile = uploadFile self.showProfile = showProfile self.login = login @@ -56,14 +54,24 @@ public struct BrowseView: View { .navigationTitle(folder.name) .toolbar { BrowseToolbar( - createFolder: createFolder, + createFolder: { + showCreateFolder = true + }, uploadFile: uploadFile, showProfile: showProfile ) } + .createFolder( + isPresenting: $showCreateFolder, + id: folder.id + ) { + Task { + await loadItems() + } + } .delete(item: $itemToDelete) { Task { - await updateItems() + await loadItems() } } .task(id: folder) { @@ -129,7 +137,6 @@ private extension BrowseView { .navigate( to: BrowseView( folder: folder, - createFolder: createFolder, uploadFile: uploadFile, showProfile: showProfile, login: login @@ -196,11 +203,6 @@ private extension BrowseView { status = .error } } - - func updateItems() async { - items = items.filter { $0.id != itemToDelete?.id } - itemToDelete = nil - } } // MARK: - Previews @@ -212,8 +214,6 @@ struct BrowseView_Previews: PreviewProvider { id: UUID().uuidString, name: "Some folder name" )) { - // create folder closure. - } uploadFile: { // upload file closure. } showProfile: { // show profile closure.