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.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";
|
||||||
|
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
|
// 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
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
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 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 {
|
||||||
|
@ -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:
|
||||||
|
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
|
// 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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user