Merge pull request #17 from rock-n-code/feature/download-file
Feature: Download file
This commit is contained in:
commit
5b2b462ba3
@ -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.button.cancel" = "Understood";
|
||||
|
||||
// BrowseView
|
||||
// BrowseToolbar
|
||||
|
||||
"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_file.text" = "Upload a file";
|
||||
"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.download_item.text" = "Download item";
|
||||
|
120
Modules/Sources/Browse/UI/Components/SaveDocumentPicker.swift
Normal file
120
Modules/Sources/Browse/UI/Components/SaveDocumentPicker.swift
Normal 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?()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// DocumentPicker.swift
|
||||
// SelectDocumentPicker.swift
|
||||
// Browse
|
||||
//
|
||||
// Created by Javier Cicchelli on 17/12/2022.
|
||||
@ -10,7 +10,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct DocumentPicker: UIViewControllerRepresentable {
|
||||
struct SelectDocumentPicker: UIViewControllerRepresentable {
|
||||
|
||||
// MARK: Type aliases
|
||||
|
||||
@ -55,20 +55,21 @@ struct DocumentPicker: UIViewControllerRepresentable {
|
||||
func makeCoordinator() -> Coordinator {
|
||||
.init(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Coordinators
|
||||
|
||||
extension DocumentPicker {
|
||||
extension SelectDocumentPicker {
|
||||
class Coordinator: NSObject, UIDocumentPickerDelegate {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private var parent: DocumentPicker
|
||||
private var parent: SelectDocumentPicker
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(_ parent: DocumentPicker) {
|
||||
init(_ parent: SelectDocumentPicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
@ -28,8 +28,7 @@ struct BrowseToolbar: ToolbarContent {
|
||||
Label {
|
||||
Text(
|
||||
"browse.toolbar_item.button.add_folder.text",
|
||||
bundle: .module,
|
||||
comment: "Add folder button text."
|
||||
bundle: .module
|
||||
)
|
||||
} icon: {
|
||||
Image.newFolder
|
||||
@ -42,8 +41,7 @@ struct BrowseToolbar: ToolbarContent {
|
||||
Label {
|
||||
Text(
|
||||
"browse.toolbar_item.button.add_file.text",
|
||||
bundle: .module,
|
||||
comment: "Add file button text."
|
||||
bundle: .module
|
||||
)
|
||||
} icon: {
|
||||
Image.newFile
|
||||
@ -53,8 +51,7 @@ struct BrowseToolbar: ToolbarContent {
|
||||
Label {
|
||||
Text(
|
||||
"browse.toolbar_item.menu.add_actions.text",
|
||||
bundle: .module,
|
||||
comment: "Add actions menu text."
|
||||
bundle: .module
|
||||
)
|
||||
} icon: {
|
||||
Image.add
|
||||
@ -72,8 +69,7 @@ struct BrowseToolbar: ToolbarContent {
|
||||
Label {
|
||||
Text(
|
||||
"browse.toolbar_item.button.show_profile.text",
|
||||
bundle: .module,
|
||||
comment: "Show profile button text."
|
||||
bundle: .module
|
||||
)
|
||||
} icon: {
|
||||
Image.profile
|
||||
|
42
Modules/Sources/Browse/UI/Toolbars/DocumentToolbar.swift
Normal file
42
Modules/Sources/Browse/UI/Toolbars/DocumentToolbar.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -23,7 +23,7 @@ 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
|
||||
@State private var showSheet: SheetView?
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@ -56,14 +56,24 @@ public struct BrowseView: View {
|
||||
showCreateFolder = true
|
||||
},
|
||||
uploadFile: {
|
||||
showUploadFile = true
|
||||
showSheet = .upload(id: folder.id)
|
||||
},
|
||||
showProfile: showProfile
|
||||
)
|
||||
}
|
||||
.sheet(isPresented: $showUploadFile) {
|
||||
UploadView(id: folder.id) {
|
||||
Task { await loadItems() }
|
||||
.sheet(item: $showSheet) { sheet in
|
||||
switch sheet {
|
||||
case let .upload(id):
|
||||
UploadView(id: id) {
|
||||
Task { await loadItems() }
|
||||
}
|
||||
case let .download(id, name):
|
||||
DownloadView(
|
||||
id: id,
|
||||
name: name
|
||||
) {
|
||||
Task { await loadItems() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.createFolder(
|
||||
@ -118,7 +128,7 @@ private extension BrowseView {
|
||||
MessageView(
|
||||
type: .empty,
|
||||
action: {
|
||||
showUploadFile = true
|
||||
showSheet = .upload(id: folder.id)
|
||||
}
|
||||
)
|
||||
case .error:
|
||||
@ -162,7 +172,10 @@ private extension BrowseView {
|
||||
DocumentItem(item: item) {
|
||||
stack = .open(document)
|
||||
} download: {
|
||||
// TODO: download the item id from the backend.
|
||||
showSheet = .download(
|
||||
id: document.id,
|
||||
name: document.name
|
||||
)
|
||||
} delete: {
|
||||
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
|
||||
|
||||
struct BrowseView_Previews: PreviewProvider {
|
||||
|
@ -24,6 +24,7 @@ struct DocumentView: View {
|
||||
|
||||
@State private var status: ViewStatus = .loading
|
||||
@State private var loadedData: Data?
|
||||
@State private var showDownloadFile: Bool = false
|
||||
|
||||
private let getData = GetDataUseCase()
|
||||
|
||||
@ -38,6 +39,20 @@ struct DocumentView: View {
|
||||
content
|
||||
.navigationTitle(document.name)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
DocumentToolbar(disabled: isToolbarDisabled) {
|
||||
showDownloadFile = true
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showDownloadFile) {
|
||||
DownloadView(
|
||||
id: document.id,
|
||||
name: document.name,
|
||||
data: loadedData
|
||||
) {
|
||||
// downloaded closure.
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await loadDataIfPossible()
|
||||
}
|
||||
@ -48,6 +63,8 @@ struct DocumentView: View {
|
||||
// MARK: - UI
|
||||
|
||||
private extension DocumentView {
|
||||
var isToolbarDisabled: Bool { loadedData == nil }
|
||||
|
||||
@ViewBuilder var content: some View {
|
||||
switch status {
|
||||
case .noCredentials:
|
||||
|
104
Modules/Sources/Browse/UI/Views/DownloadView.swift
Normal file
104
Modules/Sources/Browse/UI/Views/DownloadView.swift
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,7 @@ struct UploadView: View {
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
DocumentPicker { urls in
|
||||
SelectDocumentPicker { urls in
|
||||
Task { await addFile(from: urls) }
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user