Merge pull request #13 from rock-n-code/feature/open-files

Feature: Open documents
This commit is contained in:
Javier Cicchelli 2022-12-16 01:49:42 +01:00 committed by GitHub
commit 0438b1ad83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 386 additions and 47 deletions

View File

@ -91,6 +91,7 @@ private extension ContentView {
login: login
)
}
.tint(.red)
} else {
EmptyView()
}

View File

@ -0,0 +1,49 @@
//
// DateAdapter.swift
// Browse
//
// Created by Javier Cicchelli on 15/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import Foundation
struct DateAdapter {
// MARK: Properties
private let dateFormatter: DateFormatter = .dateTimeFormatter
// MARK: Functions
func callAsFunction(value: Date?) -> String {
if let value {
return dateFormatter.string(from: value)
} else {
return .Constants.noValue
}
}
}
// MARK: - DateFormatter+Formats
private extension DateFormatter {
static let dateTimeFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .short
formatter.locale = .current
return formatter
}()
}
// MARK: - String+Constants
private extension String {
enum Constants {
static let noValue = "-"
}
}

View File

@ -0,0 +1,54 @@
//
// SizeAdapter.swift
// Browse
//
// Created by Javier Cicchelli on 15/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
import Foundation
struct SizeAdapter {
// MARK: Properties
private let measurementFormatter: MeasurementFormatter = .informationSizeFormatter
// MARK: Functions
func callAsFunction(value: Int?) -> String {
guard let value else { return .Constants.noValue }
let sizeInBytes = Measurement(
value: Double(value),
unit: UnitInformationStorage.bytes
)
return measurementFormatter.string(
from: sizeInBytes.converted(to: .megabytes)
)
}
}
// MARK: - DateFormatter+Formats
private extension MeasurementFormatter {
static let informationSizeFormatter = {
let formatter = MeasurementFormatter()
formatter.unitStyle = .medium
formatter.numberFormatter.maximumFractionDigits = 2
formatter.locale = .current
return formatter
}()
}
// MARK: - String+Constants
private extension String {
enum Constants {
static let noValue = "-"
}
}

View File

@ -0,0 +1,36 @@
//
// GetDataUseCase.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
import Foundation
struct GetDataUseCase {
// MARK: Dependencies
@Dependency(\.apiService) private var apiService
// MARK: Functions
func callAsFunction(
id: String,
username: String,
password: String
) async throws -> Data {
return try await apiService.getData(
id: id,
credentials: .init(
username: username,
password: password
)
)
}
}

View File

@ -21,6 +21,9 @@
"message.type_error.text.first" = "An error occurred while loading this data";
"message.type_error.text.second" = "Please try loading this data again at a later time.";
"message.type_error.button.text" = "Try again";
"message.type_not_supported.text.first" = "This type of document cannot be opened yet";
"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";
// BrowseView

View File

@ -18,6 +18,9 @@ struct DocumentItem: View {
let download: ActionClosure
let delete: ActionClosure
private let dateAdapter = DateAdapter()
private let sizeAdapter = SizeAdapter()
// MARK: Body
var body: some View {
@ -34,11 +37,11 @@ struct DocumentItem: View {
.itemName()
HStack {
Text("lastModified")
Text(dateAdapter(value: document?.lastModifiedAt))
Spacer()
Text("fileSize")
Text(sizeAdapter(value: document?.size))
}
.font(.subheadline)
.foregroundColor(.secondary)

View File

@ -48,7 +48,7 @@ struct MessageView: View {
bundle: .module
)
.font(.body)
.foregroundColor(.primary)
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
}
.tint(.red)
@ -66,6 +66,7 @@ struct MessageView: View {
extension MessageView {
enum MessageType {
case noCredentials
case notSupported
case empty
case error
}
@ -76,6 +77,8 @@ private extension MessageView.MessageType {
switch self {
case .noCredentials:
return "message.type_no_credentials.text.first"
case .notSupported:
return "message.type_not_supported.text.first"
case .empty:
return "message.type_empty.text.first"
case .error:
@ -87,6 +90,8 @@ private extension MessageView.MessageType {
switch self {
case .noCredentials:
return "message.type_no_credentials.text.second"
case .notSupported:
return "message.type_not_supported.text.second"
case .empty:
return "message.type_empty.text.second"
case .error:
@ -98,6 +103,8 @@ private extension MessageView.MessageType {
switch self {
case .noCredentials:
return "message.type_no_credentials.button.text"
case .notSupported:
return "message.type_not_supported.button.text"
case .empty:
return "message.type_empty.button.text"
case .error:
@ -121,6 +128,11 @@ struct MessageView_Previews: PreviewProvider {
}
.previewDisplayName("View of type no credentials")
MessageView(type: .notSupported) {
// action closure.
}
.previewDisplayName("View of type not supported")
MessageView(type: .empty) {
// action closure.
}

View File

@ -8,6 +8,7 @@
enum Stack {
case browse(Folder)
case open(Document)
}
// MARK: - Computed
@ -16,6 +17,8 @@ extension Stack {
var tag: String {
if case .browse(let folder) = self {
return folder.id
} else if case .open(let document) = self {
return document.id
} else {
return .Constants.noId
}

View File

@ -0,0 +1,16 @@
//
// ViewStatus.swift
// Browse
//
// Created by Javier Cicchelli on 16/12/2022.
// Copyright © 2022 Röck+Cöde. All rights reserved.
//
enum ViewStatus {
case noCredentials
case notSupported
case loading
case loaded
case empty
case error
}

View File

@ -58,7 +58,9 @@ struct BrowseToolbar: ToolbarContent {
)
} icon: {
Image.add
.foregroundColor(.red)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
}
}
@ -75,7 +77,9 @@ struct BrowseToolbar: ToolbarContent {
)
} icon: {
Image.profile
.foregroundColor(.red)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
}
}
}

View File

@ -30,6 +30,7 @@ struct StackNavigationViewModifier<Destination: View>: ViewModifier {
) {
EmptyView()
}
.hidden()
)
}

View File

@ -79,6 +79,8 @@ private extension BrowseView {
type: .noCredentials,
action: login
)
case .notSupported:
EmptyView()
case .loading:
LoadingView()
case .loaded:
@ -87,13 +89,7 @@ private extension BrowseView {
case is Folder:
makeFolderItem(for: item)
case is Document:
DocumentItem(item: item) {
// TODO: show the item id in a viewer...
} download: {
// TODO: download the item id from the backend.
} delete: {
// TODO: delete the item id from the backend.
}
makeDocumentItem(for: item)
default:
EmptyView()
}
@ -118,27 +114,50 @@ private extension BrowseView {
@ViewBuilder func makeFolderItem(
for item: any FileSystemItemIdentifiable
) -> some View {
let folder = Folder(
id: item.id,
name: item.name
)
FolderItem(item: item) {
stack = .browse(folder)
} delete: {
// TODO: delete the item id from the backend.
if let folder = item as? Folder {
FolderItem(item: item) {
stack = .browse(folder)
} delete: {
// TODO: delete the item id from the backend.
}
.navigate(
to: BrowseView(
folder: folder,
createFolder: createFolder,
uploadFile: uploadFile,
showProfile: showProfile,
login: login
),
tagged: .browse(folder),
in: $stack
)
} else {
EmptyView()
}
}
@ViewBuilder func makeDocumentItem(
for item: any FileSystemItemIdentifiable
) -> some View {
if let document = item as? Document {
DocumentItem(item: item) {
stack = .open(document)
} download: {
// TODO: download the item id from the backend.
} delete: {
// TODO: delete the item id from the backend.
}
.navigate(
to: DocumentView(
document: document,
login: login
),
tagged: .open(document),
in: $stack
)
} else {
EmptyView()
}
.navigate(
to: BrowseView(
folder: folder,
createFolder: createFolder,
uploadFile: uploadFile,
showProfile: showProfile,
login: login
),
tagged: .browse(folder),
in: $stack
)
}
}
@ -173,18 +192,6 @@ private extension BrowseView {
}
}
// MARK: - Enumerations
private extension BrowseView {
enum ViewStatus {
case noCredentials
case loading
case loaded
case empty
case error
}
}
// MARK: - Previews
struct BrowseView_Previews: PreviewProvider {

View File

@ -0,0 +1,152 @@
//
// DocumentView.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 DocumentView: View {
// MARK: Environments
@Environment(\.dismiss) private var dismiss
// MARK: Storages
@KeychainStorage(key: .KeychainStorage.account) private var account: Account?
// MARK: States
@State private var status: ViewStatus = .loading
@State private var loadedData: Data?
private let getData = GetDataUseCase()
// MARK: Properties
let document: Document
let login: ActionClosure
// MARK: Body
var body: some View {
content
.navigationTitle(document.name)
.navigationBarTitleDisplayMode(.inline)
.task {
await loadDataIfPossible()
}
}
}
// MARK: - UI
private extension DocumentView {
@ViewBuilder var content: some View {
switch status {
case .noCredentials:
MessageView(
type: .noCredentials,
action: login
)
case .notSupported:
MessageView(type: .notSupported) {
dismiss()
}
case .loading:
LoadingView()
case .loaded:
Image(uiImage: imageFromData)
.resizable()
.scaledToFit()
case .empty:
EmptyView()
case .error:
MessageView(type: .error) {
Task {
await loadDataIfPossible()
}
}
}
}
}
// MARK: - Helpers
private extension DocumentView {
// MARK: Computed
var imageFromData: UIImage {
guard
let loadedData,
let image = UIImage(data: loadedData)
else {
return .init()
}
return image
}
// MARK: Functions
func loadDataIfPossible() async {
guard document.contentType == .Constants.supportedContentType else {
status = .notSupported
return
}
guard let account else {
status = .noCredentials
return
}
do {
status = .loading
let data = try await getData(
id: document.id,
username: account.username,
password: account.password
)
if data.isEmpty {
status = .error
} else {
loadedData = data
status = .loaded
}
} catch {
status = .error
}
}
}
// MARK: - String+Constants
private extension String {
enum Constants {
static let supportedContentType = "image/jpeg"
}
}
// MARK: - Previews
struct DocumentView_Previews: PreviewProvider {
static var previews: some View {
DocumentView(document: .init(
id: "1234567890",
name: "Some document name goes in here...",
contentType: "some content type",
size: .random(in: 1 ... 100),
lastModifiedAt: .now
)) {
// login closure.
}
}
}

View File

@ -15,10 +15,8 @@ struct LogInLabelStyle: LabelStyle {
configuration.title
.font(.body)
.foregroundColor(.primary)
configuration.icon
.tint(.primary)
Spacer()
}

View File

@ -90,8 +90,8 @@ public struct ProfileView: View {
"profile.button.log_out.text",
bundle: .module
)
.font(.body)
.fontWeight(.semibold)
.foregroundColor(.primary)
.frame(maxWidth: .infinity)
}
.tint(.red)