Merge pull request #17 from rock-n-code/feature/download-file

Feature: Download file
This commit is contained in:
Javier Cicchelli 2022-12-18 01:09:24 +01:00 committed by GitHub
commit 5b2b462ba3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 330 additions and 22 deletions

View File

@ -46,12 +46,16 @@
"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.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"; "delete_item.system_alert.button.cancel" = "Understood";
// BrowseView // BrowseToolbar
"browse.toolbar_item.menu.add_actions.text" = "Add file and/or folder"; "browse.toolbar_item.menu.add_actions.text" = "Add file and/or folder";
"browse.toolbar_item.button.add_folder.text" = "Create a new folder"; "browse.toolbar_item.button.add_folder.text" = "Create a new folder";
"browse.toolbar_item.button.add_file.text" = "Upload a file"; "browse.toolbar_item.button.add_file.text" = "Upload a file";
"browse.toolbar_item.button.show_profile.text" = "Show profile"; "browse.toolbar_item.button.show_profile.text" = "Show profile";
// DocumentToolbar
"document.toolbar_item.button.download_file.text" = "Download this file";
"browse.swipe_action.delete_item.text" = "Delete item"; "browse.swipe_action.delete_item.text" = "Delete item";
"browse.swipe_action.download_item.text" = "Download item"; "browse.swipe_action.download_item.text" = "Download item";

View File

@ -0,0 +1,120 @@
//
// SaveDocumentPicker.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 SaveDocumentPicker: UIViewControllerRepresentable {
// MARK: Type aliases
typealias DownloadedClosure = (NSError?) -> Void
typealias CancelledClosure = () -> Void
// MARK: Properties
private let fileName: String
private let fileData: Data
private let downloaded: DownloadedClosure
private let cancelled: CancelledClosure?
// MARK: Initialisers
init(
fileName: String,
fileData: Data,
downloaded: @escaping DownloadedClosure,
cancelled: CancelledClosure? = nil
) {
self.fileName = fileName
self.fileData = fileData
self.downloaded = downloaded
self.cancelled = cancelled
}
// MARK: Functions
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let controller = UIDocumentPickerViewController(
forOpeningContentTypes: [.folder]
)
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 SaveDocumentPicker {
class Coordinator: NSObject, UIDocumentPickerDelegate {
// MARK: Properties
private let parent: SaveDocumentPicker
private let fileCoordinator: NSFileCoordinator = .init()
private let fileManager: FileManager = .default
// MARK: Initialisers
init(_ parent: SaveDocumentPicker) {
self.parent = parent
}
// MARK: UIDocumentPickerDelegate
func documentPicker(
_ controller: UIDocumentPickerViewController,
didPickDocumentsAt urls: [URL]
) {
guard
let url = urls.first,
url.startAccessingSecurityScopedResource()
else {
// TODO: Handle this error appropriately.
return
}
var error: NSError?
fileCoordinator.coordinate(
writingItemAt: url,
error: &error
) { url in
fileManager.createFile(
atPath: url.appendingPathComponent(parent.fileName).relativePath,
contents: parent.fileData
)
}
url.stopAccessingSecurityScopedResource()
parent.downloaded(error)
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
controller.dismiss(animated: true) { [weak self] in
self?.parent.cancelled?()
}
}
}
}

View File

@ -1,5 +1,5 @@
// //
// DocumentPicker.swift // SelectDocumentPicker.swift
// Browse // Browse
// //
// Created by Javier Cicchelli on 17/12/2022. // Created by Javier Cicchelli on 17/12/2022.
@ -10,7 +10,7 @@ import Foundation
import SwiftUI import SwiftUI
import UIKit import UIKit
struct DocumentPicker: UIViewControllerRepresentable { struct SelectDocumentPicker: UIViewControllerRepresentable {
// MARK: Type aliases // MARK: Type aliases
@ -55,20 +55,21 @@ struct DocumentPicker: UIViewControllerRepresentable {
func makeCoordinator() -> Coordinator { func makeCoordinator() -> Coordinator {
.init(self) .init(self)
} }
} }
// MARK: - Coordinators // MARK: - Coordinators
extension DocumentPicker { extension SelectDocumentPicker {
class Coordinator: NSObject, UIDocumentPickerDelegate { class Coordinator: NSObject, UIDocumentPickerDelegate {
// MARK: Properties // MARK: Properties
private var parent: DocumentPicker private var parent: SelectDocumentPicker
// MARK: Initialisers // MARK: Initialisers
init(_ parent: DocumentPicker) { init(_ parent: SelectDocumentPicker) {
self.parent = parent self.parent = parent
} }

View File

@ -28,8 +28,7 @@ struct BrowseToolbar: ToolbarContent {
Label { Label {
Text( Text(
"browse.toolbar_item.button.add_folder.text", "browse.toolbar_item.button.add_folder.text",
bundle: .module, bundle: .module
comment: "Add folder button text."
) )
} icon: { } icon: {
Image.newFolder Image.newFolder
@ -42,8 +41,7 @@ struct BrowseToolbar: ToolbarContent {
Label { Label {
Text( Text(
"browse.toolbar_item.button.add_file.text", "browse.toolbar_item.button.add_file.text",
bundle: .module, bundle: .module
comment: "Add file button text."
) )
} icon: { } icon: {
Image.newFile Image.newFile
@ -53,8 +51,7 @@ struct BrowseToolbar: ToolbarContent {
Label { Label {
Text( Text(
"browse.toolbar_item.menu.add_actions.text", "browse.toolbar_item.menu.add_actions.text",
bundle: .module, bundle: .module
comment: "Add actions menu text."
) )
} icon: { } icon: {
Image.add Image.add
@ -72,8 +69,7 @@ struct BrowseToolbar: ToolbarContent {
Label { Label {
Text( Text(
"browse.toolbar_item.button.show_profile.text", "browse.toolbar_item.button.show_profile.text",
bundle: .module, bundle: .module
comment: "Show profile button text."
) )
} icon: { } icon: {
Image.profile Image.profile

View File

@ -0,0 +1,42 @@
//
// DocumentToolbar.swift
// Browse
//
// Created by Javier Cicchelli on 18/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import DataModels
import SwiftUI
struct DocumentToolbar: ToolbarContent {
// MARK: Properties
let disabled: Bool
let downloadFile: ActionClosure
// MARK: Body
var body: some ToolbarContent {
ToolbarItem(placement: .primaryAction) {
Button {
downloadFile()
} label: {
Label {
Text(
"document.toolbar_item.button.download_file.text",
bundle: .module
)
} icon: {
Image.download
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
}
.disabled(disabled)
}
}
}

View File

@ -23,7 +23,7 @@ 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 @State private var showSheet: SheetView?
// MARK: Properties // MARK: Properties
@ -56,14 +56,24 @@ public struct BrowseView: View {
showCreateFolder = true showCreateFolder = true
}, },
uploadFile: { uploadFile: {
showUploadFile = true showSheet = .upload(id: folder.id)
}, },
showProfile: showProfile showProfile: showProfile
) )
} }
.sheet(isPresented: $showUploadFile) { .sheet(item: $showSheet) { sheet in
UploadView(id: folder.id) { switch sheet {
Task { await loadItems() } case let .upload(id):
UploadView(id: id) {
Task { await loadItems() }
}
case let .download(id, name):
DownloadView(
id: id,
name: name
) {
Task { await loadItems() }
}
} }
} }
.createFolder( .createFolder(
@ -118,7 +128,7 @@ private extension BrowseView {
MessageView( MessageView(
type: .empty, type: .empty,
action: { action: {
showUploadFile = true showSheet = .upload(id: folder.id)
} }
) )
case .error: case .error:
@ -162,7 +172,10 @@ private extension BrowseView {
DocumentItem(item: item) { DocumentItem(item: item) {
stack = .open(document) stack = .open(document)
} download: { } download: {
// TODO: download the item id from the backend. showSheet = .download(
id: document.id,
name: document.name
)
} delete: { } delete: {
itemToDelete = item itemToDelete = item
} }
@ -211,6 +224,17 @@ private extension BrowseView {
} }
} }
// MARK: - Enumerations
private extension BrowseView {
enum SheetView: Identifiable {
case upload(id: String)
case download(id: String, name: String)
var id: String { UUID().uuidString }
}
}
// MARK: - Previews // MARK: - Previews
struct BrowseView_Previews: PreviewProvider { struct BrowseView_Previews: PreviewProvider {

View File

@ -24,6 +24,7 @@ struct DocumentView: View {
@State private var status: ViewStatus = .loading @State private var status: ViewStatus = .loading
@State private var loadedData: Data? @State private var loadedData: Data?
@State private var showDownloadFile: Bool = false
private let getData = GetDataUseCase() private let getData = GetDataUseCase()
@ -38,6 +39,20 @@ struct DocumentView: View {
content content
.navigationTitle(document.name) .navigationTitle(document.name)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar {
DocumentToolbar(disabled: isToolbarDisabled) {
showDownloadFile = true
}
}
.sheet(isPresented: $showDownloadFile) {
DownloadView(
id: document.id,
name: document.name,
data: loadedData
) {
// downloaded closure.
}
}
.task { .task {
await loadDataIfPossible() await loadDataIfPossible()
} }
@ -48,6 +63,8 @@ struct DocumentView: View {
// MARK: - UI // MARK: - UI
private extension DocumentView { private extension DocumentView {
var isToolbarDisabled: Bool { loadedData == nil }
@ViewBuilder var content: some View { @ViewBuilder var content: some View {
switch status { switch status {
case .noCredentials: case .noCredentials:

View File

@ -0,0 +1,104 @@
//
// DownloadView.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 DownloadView: View {
// MARK: Storages
@KeychainStorage(key: .KeychainStorage.account) private var account: Account?
// MARK: States
@State private var data: Data?
// MARK: Properties
private let id: String
private let name: String
private let downloaded: ActionClosure
private let getData: GetDataUseCase = .init()
// MARK: Initialisers
init(
id: String,
name: String,
data: Data? = nil,
downloaded: @escaping ActionClosure
) {
self.id = id
self.name = name
self.downloaded = downloaded
self._data = .init(initialValue: data)
}
// MARK: Body
var body: some View {
if let data {
SaveDocumentPicker(
fileName: name,
fileData: data
) { error in
guard error == nil else {
// TODO: Handle this error case.
return
}
downloaded()
}
} else {
LoadingView()
.task {
await loadData()
}
}
}
}
// MARK: - Helpers
private extension DownloadView {
func loadData() async {
guard let account else {
// TODO: Handle this error case.
return
}
do {
data = try await getData(
id: id,
username: account.username,
password: account.password
)
} catch {
// TODO: Handle this error case.
}
}
}
// MARK: - Previews
struct DownloadView_Previews: PreviewProvider {
static var previews: some View {
DownloadView(
id: "1234567890",
name: "some-name.txt"
) {
// Downloaded closure.
}
}
}

View File

@ -32,7 +32,7 @@ struct UploadView: View {
// MARK: Body // MARK: Body
var body: some View { var body: some View {
DocumentPicker { urls in SelectDocumentPicker { urls in
Task { await addFile(from: urls) } Task { await addFile(from: urls) }
} }
} }