Merge pull request #12 from rock-n-code/integration/browse
Integration: Browse + stack navigation
This commit is contained in:
commit
144ee0f705
@ -12,6 +12,7 @@ import Login
|
|||||||
import KeychainStorage
|
import KeychainStorage
|
||||||
import Profile
|
import Profile
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UseCases
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
@ -24,49 +25,94 @@ struct ContentView: View {
|
|||||||
@State private var user: User?
|
@State private var user: User?
|
||||||
@State private var showSheet: SheetView?
|
@State private var showSheet: SheetView?
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
private let getUser: GetUserUseCase = .init()
|
||||||
|
|
||||||
// MARK: Body
|
// MARK: Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Container(user: user) {
|
||||||
BrowseView {
|
// TODO: create a new folder
|
||||||
// ...
|
} uploadFile: {
|
||||||
} uploadFile: {
|
// TODO: upload a new file
|
||||||
// ...
|
} showProfile: {
|
||||||
} showProfile: {
|
showSheet = .profile
|
||||||
showSheet = .profile
|
} login: {
|
||||||
}
|
showSheet = .login
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
shouldShowLogin()
|
|
||||||
}
|
|
||||||
.onChange(of: account) { _ in
|
|
||||||
shouldShowLogin()
|
|
||||||
}
|
}
|
||||||
.sheet(item: $showSheet) { sheet in
|
.sheet(item: $showSheet) { sheet in
|
||||||
switch sheet {
|
switch sheet {
|
||||||
case .login:
|
case .login:
|
||||||
LoginView {
|
LoginView {
|
||||||
user = $1
|
|
||||||
account = $0
|
account = $0
|
||||||
|
user = $1
|
||||||
}
|
}
|
||||||
case .profile:
|
case .profile:
|
||||||
ProfileView(user: user) {
|
ProfileView(user: user) {
|
||||||
user = nil
|
|
||||||
account = nil
|
account = nil
|
||||||
|
user = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.task(id: account) {
|
||||||
|
await loadUserOrLogin()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Views
|
||||||
|
|
||||||
|
private extension ContentView {
|
||||||
|
struct Container: View {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
let user: User?
|
||||||
|
let createFolder: ActionClosure
|
||||||
|
let uploadFile: ActionClosure
|
||||||
|
let showProfile: ActionClosure
|
||||||
|
let login: ActionClosure
|
||||||
|
|
||||||
|
// MARK: Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let user {
|
||||||
|
NavigationView {
|
||||||
|
BrowseView(
|
||||||
|
folder: .init(
|
||||||
|
id: user.rootFolder.id,
|
||||||
|
name: user.rootFolder.name
|
||||||
|
),
|
||||||
|
createFolder: createFolder,
|
||||||
|
uploadFile: uploadFile,
|
||||||
|
showProfile: showProfile,
|
||||||
|
login: login
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
private extension ContentView {
|
private extension ContentView {
|
||||||
func shouldShowLogin() {
|
func loadUserOrLogin() async {
|
||||||
showSheet = account == nil
|
guard let account else {
|
||||||
? .login
|
showSheet = .login
|
||||||
: nil
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
showSheet = nil
|
||||||
|
user = try? await getUser(
|
||||||
|
username: account.username,
|
||||||
|
password: account.password
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,8 @@ let package = Package(
|
|||||||
"APIService",
|
"APIService",
|
||||||
"DataModels",
|
"DataModels",
|
||||||
"Dependencies",
|
"Dependencies",
|
||||||
"KeychainStorage"
|
"KeychainStorage",
|
||||||
|
"UseCases"
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -39,6 +40,16 @@ let package = Package(
|
|||||||
"KeychainAccess"
|
"KeychainAccess"
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "UseCases",
|
||||||
|
dependencies: [
|
||||||
|
"Cores",
|
||||||
|
"APIService",
|
||||||
|
"DataModels",
|
||||||
|
"Dependencies",
|
||||||
|
"KeychainStorage"
|
||||||
|
]
|
||||||
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "APIServiceTests",
|
name: "APIServiceTests",
|
||||||
dependencies: ["APIService"]
|
dependencies: ["APIService"]
|
||||||
|
@ -7,25 +7,26 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import APIService
|
import APIService
|
||||||
|
import DataModels
|
||||||
import DependencyInjection
|
import DependencyInjection
|
||||||
import Dependencies
|
import Dependencies
|
||||||
|
|
||||||
struct GetUserUseCase {
|
public struct GetUserUseCase {
|
||||||
|
|
||||||
// MARK: Dependencies
|
// MARK: Dependencies
|
||||||
|
|
||||||
@Dependency(\.apiService) private var apiService
|
@Dependency(\.apiService) private var apiService
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Initialisers
|
||||||
|
|
||||||
let authenticated: AuthenticatedClosure
|
public init() {}
|
||||||
|
|
||||||
// MARK: Functions
|
// MARK: Functions
|
||||||
|
|
||||||
func callAsFunction(
|
public func callAsFunction(
|
||||||
username: String,
|
username: String,
|
||||||
password: String
|
password: String
|
||||||
) async throws {
|
) async throws -> User {
|
||||||
let me = try await apiService.getUser(
|
let me = try await apiService.getUser(
|
||||||
credentials: .init(
|
credentials: .init(
|
||||||
username: username,
|
username: username,
|
||||||
@ -33,21 +34,15 @@ struct GetUserUseCase {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
authenticated(
|
return .init(
|
||||||
.init(
|
profile: .init(
|
||||||
username: username,
|
firstName: me.firstName,
|
||||||
password: password
|
lastName: me.lastName
|
||||||
),
|
),
|
||||||
.init(
|
rootFolder: .init(
|
||||||
profile: .init(
|
id: me.rootItem.id,
|
||||||
firstName: me.firstName,
|
name: me.rootItem.name,
|
||||||
lastName: me.lastName
|
lastModifiedAt: me.rootItem.lastModifiedAt
|
||||||
),
|
|
||||||
rootFolder: .init(
|
|
||||||
id: me.rootItem.id,
|
|
||||||
name: me.rootItem.name,
|
|
||||||
lastModifiedAt: me.rootItem.lastModifiedAt
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
41
Modules/Sources/Browse/Logic/Models/Document.swift
Normal file
41
Modules/Sources/Browse/Logic/Models/Document.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//
|
||||||
|
// Document.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 13/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Document {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
public let id: String
|
||||||
|
public let name: String
|
||||||
|
public let contentType: String
|
||||||
|
public let size: Int
|
||||||
|
public let lastModifiedAt: Date
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
contentType: String,
|
||||||
|
size: Int,
|
||||||
|
lastModifiedAt: Date
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.contentType = contentType
|
||||||
|
self.size = size
|
||||||
|
self.lastModifiedAt = lastModifiedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - FileSystemIdIdentifiable
|
||||||
|
|
||||||
|
extension Document: FileSystemItemIdentifiable {}
|
34
Modules/Sources/Browse/Logic/Models/Folder.swift
Normal file
34
Modules/Sources/Browse/Logic/Models/Folder.swift
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
//
|
||||||
|
// Folder.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 13/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
public struct Folder {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
public let id: String
|
||||||
|
public let name: String
|
||||||
|
|
||||||
|
// MARK: Initialisers
|
||||||
|
|
||||||
|
public init(
|
||||||
|
id: String,
|
||||||
|
name: String
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - FileSystemIdIdentifiable
|
||||||
|
|
||||||
|
extension Folder: FileSystemItemIdentifiable {}
|
||||||
|
|
||||||
|
// MARK: - Equatable
|
||||||
|
|
||||||
|
extension Folder: Equatable {}
|
16
Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift
Normal file
16
Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// FileSystemItem.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 13/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
protocol FileSystemItem {
|
||||||
|
var id: String { get }
|
||||||
|
var name: String { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Type aliases
|
||||||
|
|
||||||
|
typealias FileSystemItemIdentifiable = FileSystemItem & Identifiable & Hashable
|
53
Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift
Normal file
53
Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// GetItemsUseCase.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 13/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import APIService
|
||||||
|
import DependencyInjection
|
||||||
|
import Dependencies
|
||||||
|
|
||||||
|
struct GetItemsUseCase {
|
||||||
|
|
||||||
|
// MARK: Dependencies
|
||||||
|
|
||||||
|
@Dependency(\.apiService) private var apiService
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func callAsFunction(
|
||||||
|
id: String,
|
||||||
|
username: String,
|
||||||
|
password: String
|
||||||
|
) async throws -> [any FileSystemItemIdentifiable] {
|
||||||
|
let items = try await apiService.getItems(
|
||||||
|
id: id,
|
||||||
|
credentials: .init(
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return items
|
||||||
|
.compactMap { item -> any FileSystemItemIdentifiable in
|
||||||
|
if item.isDirectory {
|
||||||
|
return Folder(
|
||||||
|
id: item.id,
|
||||||
|
name: item.name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return Document(
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
contentType: item.contentType ?? "-",
|
||||||
|
size: item.size ?? 0,
|
||||||
|
lastModifiedAt: item.lastModifiedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -6,9 +6,28 @@
|
|||||||
Copyright © 2022 Röck+Cöde. All rights reserved.
|
Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// LoadingView
|
||||||
|
|
||||||
|
"loading.loading_data.text" = "Loading data\nfrom the API...";
|
||||||
|
|
||||||
|
// MessageView
|
||||||
|
|
||||||
|
"message.type_no_credentials.text.first" = "No user credentials have been found in your device";
|
||||||
|
"message.type_no_credentials.text.second" = "Please login again with your credentials to load this data.";
|
||||||
|
"message.type_no_credentials.button.text" = "Log in";
|
||||||
|
"message.type_empty.text.first" = "No data has been found for this folder";
|
||||||
|
"message.type_empty.text.second" = "Please populate this folder by uploading some file from your device.";
|
||||||
|
"message.type_empty.button.text" = "Upload a file";
|
||||||
|
"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";
|
||||||
|
|
||||||
|
// BrowseView
|
||||||
|
|
||||||
"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";
|
||||||
|
|
||||||
"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";
|
||||||
|
@ -6,42 +6,86 @@
|
|||||||
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import DataModels
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct DocumentItem: View {
|
struct DocumentItem: View {
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
let name: String
|
let item: FileSystemItem
|
||||||
let lastModified: String
|
let select: ActionClosure
|
||||||
let fileSize: String
|
let download: ActionClosure
|
||||||
|
let delete: ActionClosure
|
||||||
|
|
||||||
// MARK: Body
|
// MARK: Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 16) {
|
Button {
|
||||||
Image.document
|
select()
|
||||||
.icon(size: 32)
|
} label: {
|
||||||
.foregroundColor(.red)
|
HStack(spacing: 16) {
|
||||||
|
Image.document
|
||||||
VStack {
|
.icon(size: 32)
|
||||||
Text(name)
|
.foregroundColor(.red)
|
||||||
.itemName()
|
|
||||||
|
|
||||||
HStack {
|
VStack(spacing: 8) {
|
||||||
Text(lastModified)
|
Text(item.name)
|
||||||
|
.itemName()
|
||||||
|
|
||||||
Spacer()
|
HStack {
|
||||||
|
Text("lastModified")
|
||||||
Text(fileSize)
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("fileSize")
|
||||||
|
}
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
}
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
.swipeActions(
|
||||||
|
edge: .trailing,
|
||||||
|
allowsFullSwipe: true
|
||||||
|
) {
|
||||||
|
Button {
|
||||||
|
delete()
|
||||||
|
} label: {
|
||||||
|
Label {
|
||||||
|
Text(
|
||||||
|
"browse.swipe_action.delete_item.text",
|
||||||
|
bundle: .module
|
||||||
|
)
|
||||||
|
} icon: {
|
||||||
|
Image.trash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tint(.red)
|
||||||
|
|
||||||
|
Button {
|
||||||
|
download()
|
||||||
|
} label: {
|
||||||
|
Label {
|
||||||
|
Text(
|
||||||
|
"browse.swipe_action.download_item.text",
|
||||||
|
bundle: .module
|
||||||
|
)
|
||||||
|
} icon: {
|
||||||
|
Image.download
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tint(.orange)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension DocumentItem {
|
||||||
|
var document: Document? { item as? Document }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Image+Constants
|
// MARK: - Image+Constants
|
||||||
@ -54,18 +98,34 @@ private extension Image {
|
|||||||
|
|
||||||
struct DocumentItem_Previews: PreviewProvider {
|
struct DocumentItem_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
DocumentItem(
|
DocumentItem(item: Document(
|
||||||
|
id: "1234567890",
|
||||||
name: "Some document name goes in here...",
|
name: "Some document name goes in here...",
|
||||||
lastModified: "Some few hours ago",
|
contentType: "some content type",
|
||||||
fileSize: "23,5 Mbytes"
|
size: .random(in: 1 ... 100),
|
||||||
)
|
lastModifiedAt: .now
|
||||||
|
)) {
|
||||||
|
// select closure.
|
||||||
|
} download: {
|
||||||
|
// download closure.
|
||||||
|
} delete: {
|
||||||
|
// delete closure.
|
||||||
|
}
|
||||||
.previewDisplayName("Document item")
|
.previewDisplayName("Document item")
|
||||||
|
|
||||||
DocumentItem(
|
DocumentItem(item: Document(
|
||||||
|
id: "1234567890",
|
||||||
name: "Some very, extremely long document name goes in here...",
|
name: "Some very, extremely long document name goes in here...",
|
||||||
lastModified: "Yesterday",
|
contentType: "some content type",
|
||||||
fileSize: "235,6 Kbytes"
|
size: .random(in: 1 ... 100),
|
||||||
)
|
lastModifiedAt: .now
|
||||||
|
)) {
|
||||||
|
// select closure.
|
||||||
|
} download: {
|
||||||
|
// download closure.
|
||||||
|
} delete: {
|
||||||
|
// delete closure.
|
||||||
|
}
|
||||||
.previewDisplayName("Document item with long name")
|
.previewDisplayName("Document item with long name")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,31 +6,56 @@
|
|||||||
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import DataModels
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct FolderItem: View {
|
struct FolderItem: View {
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
let name: String
|
let item: FileSystemItem
|
||||||
|
let select: ActionClosure
|
||||||
|
let delete: ActionClosure
|
||||||
|
|
||||||
// MARK: Body
|
// MARK: Body
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 16) {
|
Button {
|
||||||
Image.folder
|
select()
|
||||||
.icon(size: 32)
|
} label: {
|
||||||
.foregroundColor(.red)
|
HStack(spacing: 16) {
|
||||||
|
Image.folder
|
||||||
Text(name)
|
.icon(size: 32)
|
||||||
.itemName()
|
.foregroundColor(.red)
|
||||||
|
|
||||||
Image.chevronRight
|
Text(item.name)
|
||||||
.icon(size: 16)
|
.itemName()
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.font(.headline)
|
Image.chevronRight
|
||||||
|
.icon(size: 16)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
.swipeActions(
|
||||||
|
edge: .trailing,
|
||||||
|
allowsFullSwipe: true
|
||||||
|
) {
|
||||||
|
Button {
|
||||||
|
delete()
|
||||||
|
} label: {
|
||||||
|
Label {
|
||||||
|
Text(
|
||||||
|
"browse.swipe_action.delete_item.text",
|
||||||
|
bundle: .module
|
||||||
|
)
|
||||||
|
} icon: {
|
||||||
|
Image.trash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tint(.red)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -46,10 +71,24 @@ private extension Image {
|
|||||||
|
|
||||||
struct BrowseItem_Previews: PreviewProvider {
|
struct BrowseItem_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
FolderItem(name: "Some folder name goes in here...")
|
FolderItem(item: Folder(
|
||||||
.previewDisplayName("Folder item")
|
id: "1234567890",
|
||||||
|
name: "Some folder name goes in here..."
|
||||||
|
)) {
|
||||||
|
// select closure.
|
||||||
|
} delete: {
|
||||||
|
// delete closure.
|
||||||
|
}
|
||||||
|
.previewDisplayName("Folder item")
|
||||||
|
|
||||||
FolderItem(name: "Some very, extremely long folder name goes in here...")
|
FolderItem(item: Folder(
|
||||||
.previewDisplayName("Folder item with long name")
|
id: "1234567890",
|
||||||
|
name: "Some very, extremely long folder name goes in here..."
|
||||||
|
)) {
|
||||||
|
// select closure.
|
||||||
|
} delete: {
|
||||||
|
// delete closure.
|
||||||
|
}
|
||||||
|
.previewDisplayName("Folder item with long name")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
Modules/Sources/Browse/UI/Components/LoadingView.swift
Normal file
35
Modules/Sources/Browse/UI/Components/LoadingView.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// LoadingView.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 15/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct LoadingView: View {
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.large)
|
||||||
|
.tint(.red)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"loading.loading_data.text",
|
||||||
|
bundle: .module
|
||||||
|
)
|
||||||
|
.font(.body)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
struct LoadingView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
|
}
|
134
Modules/Sources/Browse/UI/Components/MessageView.swift
Normal file
134
Modules/Sources/Browse/UI/Components/MessageView.swift
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
//
|
||||||
|
// MessageView.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 15/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import DataModels
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MessageView: View {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
let type: MessageType
|
||||||
|
let action: ActionClosure
|
||||||
|
|
||||||
|
// MARK: Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 72) {
|
||||||
|
Image.warning
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 120, height: 120)
|
||||||
|
|
||||||
|
VStack(spacing: 48) {
|
||||||
|
Text(
|
||||||
|
type.firstText,
|
||||||
|
bundle: .module
|
||||||
|
)
|
||||||
|
.font(.title)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
type.secondText,
|
||||||
|
bundle: .module
|
||||||
|
)
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.regular)
|
||||||
|
}
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
|
Button(action: action) {
|
||||||
|
Text(
|
||||||
|
type.button,
|
||||||
|
bundle: .module
|
||||||
|
)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.tint(.red)
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.buttonBorderShape(.roundedRectangle(radius: 8))
|
||||||
|
.controlSize(.large)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 48)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Enumerations
|
||||||
|
|
||||||
|
extension MessageView {
|
||||||
|
enum MessageType {
|
||||||
|
case noCredentials
|
||||||
|
case empty
|
||||||
|
case error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension MessageView.MessageType {
|
||||||
|
var firstText: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .noCredentials:
|
||||||
|
return "message.type_no_credentials.text.first"
|
||||||
|
case .empty:
|
||||||
|
return "message.type_empty.text.first"
|
||||||
|
case .error:
|
||||||
|
return "message.type_error.text.first"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var secondText: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .noCredentials:
|
||||||
|
return "message.type_no_credentials.text.second"
|
||||||
|
case .empty:
|
||||||
|
return "message.type_empty.text.second"
|
||||||
|
case .error:
|
||||||
|
return "message.type_error.text.second"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var button: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .noCredentials:
|
||||||
|
return "message.type_no_credentials.button.text"
|
||||||
|
case .empty:
|
||||||
|
return "message.type_empty.button.text"
|
||||||
|
case .error:
|
||||||
|
return "message.type_error.button.text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Image+Constants
|
||||||
|
|
||||||
|
private extension Image {
|
||||||
|
static let warning = Image(systemName: "exclamationmark.circle.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
struct MessageView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
MessageView(type: .noCredentials) {
|
||||||
|
// action closure.
|
||||||
|
}
|
||||||
|
.previewDisplayName("View of type no credentials")
|
||||||
|
|
||||||
|
MessageView(type: .empty) {
|
||||||
|
// action closure.
|
||||||
|
}
|
||||||
|
.previewDisplayName("View of type empty")
|
||||||
|
|
||||||
|
MessageView(type: .error) {
|
||||||
|
// action closure.
|
||||||
|
}
|
||||||
|
.previewDisplayName("View of type error")
|
||||||
|
}
|
||||||
|
}
|
35
Modules/Sources/Browse/UI/Enumerations/Stack.swift
Normal file
35
Modules/Sources/Browse/UI/Enumerations/Stack.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// Stack.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 15/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
enum Stack {
|
||||||
|
case browse(Folder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Computed
|
||||||
|
|
||||||
|
extension Stack {
|
||||||
|
var tag: String {
|
||||||
|
if case .browse(let folder) = self {
|
||||||
|
return folder.id
|
||||||
|
} else {
|
||||||
|
return .Constants.noId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Hashable
|
||||||
|
|
||||||
|
extension Stack: Hashable {}
|
||||||
|
|
||||||
|
// MARK: - String+Constants
|
||||||
|
|
||||||
|
private extension String {
|
||||||
|
enum Constants {
|
||||||
|
static let noId = "-"
|
||||||
|
}
|
||||||
|
}
|
14
Modules/Sources/Browse/UI/Extensions/Image+Constants.swift
Normal file
14
Modules/Sources/Browse/UI/Extensions/Image+Constants.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// Image+Constants.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 14/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension Image {
|
||||||
|
static let trash = Image(systemName: "trash")
|
||||||
|
static let download = Image(systemName: "arrow.down.doc")
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// View+ViewModifiers.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 14/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func navigate(
|
||||||
|
to destination: some View,
|
||||||
|
tagged tag: Stack,
|
||||||
|
in stack: Binding<Stack?>
|
||||||
|
) -> some View {
|
||||||
|
modifier(StackNavigationViewModifier(
|
||||||
|
tag: tag,
|
||||||
|
stack: stack,
|
||||||
|
destination: { destination }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// StackNavigationViewModifiers.swift
|
||||||
|
// Browse
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 14/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct StackNavigationViewModifier<Destination: View>: ViewModifier {
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
let tag: Stack
|
||||||
|
|
||||||
|
@Binding var stack: Stack?
|
||||||
|
|
||||||
|
@ViewBuilder var destination: Destination
|
||||||
|
|
||||||
|
// MARK: Functions
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.background(
|
||||||
|
NavigationLink(
|
||||||
|
destination: destination,
|
||||||
|
tag: tag,
|
||||||
|
selection: $stack
|
||||||
|
) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -7,134 +7,182 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import DataModels
|
import DataModels
|
||||||
|
import KeychainStorage
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct BrowseView: View {
|
public struct BrowseView: View {
|
||||||
|
|
||||||
|
// MARK: Storages
|
||||||
|
|
||||||
|
@KeychainStorage(key: .KeychainStorage.account) private var account: Account?
|
||||||
|
|
||||||
|
// MARK: States
|
||||||
|
|
||||||
|
@State private var status: ViewStatus = .loading
|
||||||
|
@State private var items: [any FileSystemItemIdentifiable] = []
|
||||||
|
@State private var stack: Stack?
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
|
private let folder: Folder
|
||||||
private let createFolder: ActionClosure
|
private let createFolder: ActionClosure
|
||||||
private let uploadFile: ActionClosure
|
private let uploadFile: ActionClosure
|
||||||
private let showProfile: ActionClosure
|
private let showProfile: ActionClosure
|
||||||
|
private let login: ActionClosure
|
||||||
|
|
||||||
|
private let getItems: GetItemsUseCase = .init()
|
||||||
|
|
||||||
// MARK: Initialisers
|
// MARK: Initialisers
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
|
folder: Folder,
|
||||||
createFolder: @escaping ActionClosure,
|
createFolder: @escaping ActionClosure,
|
||||||
uploadFile: @escaping ActionClosure,
|
uploadFile: @escaping ActionClosure,
|
||||||
showProfile: @escaping ActionClosure
|
showProfile: @escaping ActionClosure,
|
||||||
|
login: @escaping ActionClosure
|
||||||
) {
|
) {
|
||||||
|
self.folder = folder
|
||||||
self.createFolder = createFolder
|
self.createFolder = createFolder
|
||||||
self.uploadFile = uploadFile
|
self.uploadFile = uploadFile
|
||||||
self.showProfile = showProfile
|
self.showProfile = showProfile
|
||||||
|
self.login = login
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Body
|
// MARK: Body
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
List {
|
content
|
||||||
Group {
|
.navigationTitle(folder.name)
|
||||||
Group {
|
.toolbar {
|
||||||
FolderItem(name: "Some folder #1 name")
|
BrowseToolbar(
|
||||||
FolderItem(name: "Some folder #2 name")
|
createFolder: createFolder,
|
||||||
FolderItem(name: "Some folder #3 name")
|
uploadFile: uploadFile,
|
||||||
FolderItem(name: "Some folder #4 name")
|
showProfile: showProfile
|
||||||
FolderItem(name: "Some folder #5 name")
|
)
|
||||||
FolderItem(name: "Some folder #6 name")
|
}
|
||||||
FolderItem(name: "Some folder #7 name")
|
.task(id: folder) {
|
||||||
}
|
await loadItems()
|
||||||
Group {
|
}
|
||||||
DocumentItem(
|
}
|
||||||
name: "Some document #1 name",
|
}
|
||||||
lastModified: "3 months ago",
|
|
||||||
fileSize: "1,23 Mbytes"
|
// MARK: - UI
|
||||||
)
|
|
||||||
DocumentItem(
|
private extension BrowseView {
|
||||||
name: "Some document #2 name",
|
|
||||||
lastModified: "2 years ago",
|
// MARK: Properties
|
||||||
fileSize: "123 Kbytes"
|
|
||||||
)
|
@ViewBuilder var content: some View {
|
||||||
DocumentItem(
|
switch status {
|
||||||
name: "Some document #3 name",
|
case .noCredentials:
|
||||||
lastModified: "13 days ago",
|
MessageView(
|
||||||
fileSize: "12 bytes"
|
type: .noCredentials,
|
||||||
)
|
action: login
|
||||||
DocumentItem(
|
)
|
||||||
name: "Some document #4 name",
|
case .loading:
|
||||||
lastModified: "13 hours ago",
|
LoadingView()
|
||||||
fileSize: "12,3 Gbytes"
|
case .loaded:
|
||||||
)
|
List(items, id: \.id) { item in
|
||||||
DocumentItem(
|
switch item {
|
||||||
name: "Some document #5 name",
|
case is Folder:
|
||||||
lastModified: "13 minutes ago",
|
makeFolderItem(for: item)
|
||||||
fileSize: "123 Tbytes"
|
case is Document:
|
||||||
)
|
DocumentItem(item: item) {
|
||||||
DocumentItem(
|
// TODO: show the item id in a viewer...
|
||||||
name: "Some document #6 name",
|
} download: {
|
||||||
lastModified: "13 seconds ago",
|
// TODO: download the item id from the backend.
|
||||||
fileSize: "123 Tbytes"
|
} delete: {
|
||||||
)
|
// TODO: delete the item id from the backend.
|
||||||
DocumentItem(
|
}
|
||||||
name: "Some document #7 name",
|
default:
|
||||||
lastModified: "13 nanoseconds ago",
|
EmptyView()
|
||||||
fileSize: "123 Tbytes"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.swipeActions(
|
.listStyle(.inset)
|
||||||
edge: .trailing,
|
case .empty:
|
||||||
allowsFullSwipe: true
|
MessageView(
|
||||||
) {
|
type: .empty,
|
||||||
Button {
|
action: uploadFile
|
||||||
// TODO: Implement the removal of the item from the API.
|
)
|
||||||
} label: {
|
case .error:
|
||||||
Label {
|
MessageView(type: .error) {
|
||||||
Text(
|
Task {
|
||||||
"browse.swipe_action.delete_item.text",
|
await loadItems()
|
||||||
bundle: .module,
|
|
||||||
comment: "Delete item swipe action text."
|
|
||||||
)
|
|
||||||
} icon: {
|
|
||||||
Image.trash
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.tint(.red)
|
|
||||||
|
|
||||||
// TODO: allow download only if item is a file.
|
|
||||||
Button {
|
|
||||||
// TODO: Implement the downloading of the data of the item from the API into the device.
|
|
||||||
} label: {
|
|
||||||
Label {
|
|
||||||
Text(
|
|
||||||
"browse.swipe_action.download_item.text",
|
|
||||||
bundle: .module,
|
|
||||||
comment: "Download item swipe action text."
|
|
||||||
)
|
|
||||||
} icon: {
|
|
||||||
Image.download
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tint(.orange)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.inset)
|
}
|
||||||
.background(Color.red)
|
|
||||||
.navigationTitle("Folder name")
|
// MARK: Functions
|
||||||
.toolbar {
|
|
||||||
BrowseToolbar(
|
@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.
|
||||||
|
}
|
||||||
|
.navigate(
|
||||||
|
to: BrowseView(
|
||||||
|
folder: folder,
|
||||||
createFolder: createFolder,
|
createFolder: createFolder,
|
||||||
uploadFile: uploadFile,
|
uploadFile: uploadFile,
|
||||||
showProfile: showProfile
|
showProfile: showProfile,
|
||||||
|
login: login
|
||||||
|
),
|
||||||
|
tagged: .browse(folder),
|
||||||
|
in: $stack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private extension BrowseView {
|
||||||
|
func loadItems() async {
|
||||||
|
guard let account else {
|
||||||
|
status = .noCredentials
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
status = .loading
|
||||||
|
|
||||||
|
let loadedItems = try await getItems(
|
||||||
|
id: folder.id,
|
||||||
|
username: account.username,
|
||||||
|
password: account.password
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if loadedItems.isEmpty {
|
||||||
|
status = .empty
|
||||||
|
} else {
|
||||||
|
items = loadedItems
|
||||||
|
status = .loaded
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
status = .error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Image+Constants
|
// MARK: - Enumerations
|
||||||
|
|
||||||
private extension Image {
|
private extension BrowseView {
|
||||||
static let trash = Image(systemName: "trash")
|
enum ViewStatus {
|
||||||
static let download = Image(systemName: "arrow.down.doc")
|
case noCredentials
|
||||||
|
case loading
|
||||||
|
case loaded
|
||||||
|
case empty
|
||||||
|
case error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
@ -142,12 +190,17 @@ private extension Image {
|
|||||||
struct BrowseView_Previews: PreviewProvider {
|
struct BrowseView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
BrowseView {
|
BrowseView(folder: .init(
|
||||||
// ...
|
id: UUID().uuidString,
|
||||||
|
name: "Some folder name"
|
||||||
|
)) {
|
||||||
|
// create folder closure.
|
||||||
} uploadFile: {
|
} uploadFile: {
|
||||||
// ...
|
// upload file closure.
|
||||||
} showProfile: {
|
} showProfile: {
|
||||||
// ...
|
// show profile closure.
|
||||||
|
} login: {
|
||||||
|
// login closure.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
//
|
|
||||||
// Typealiases.swift
|
|
||||||
// Login
|
|
||||||
//
|
|
||||||
// Created by Javier Cicchelli on 12/12/2022.
|
|
||||||
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import DataModels
|
|
||||||
|
|
||||||
public typealias AuthenticatedClosure = (Account, User) -> Void
|
|
@ -6,10 +6,11 @@
|
|||||||
Copyright © 2022 Röck+Cöde. All rights reserved.
|
Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// LoginView
|
||||||
|
|
||||||
"login.title.text" = "My files";
|
"login.title.text" = "My files";
|
||||||
"login.text_field.username.placeholder" = "Username";
|
"login.text_field.username.placeholder" = "Username";
|
||||||
"login.text_field.password.placeholder" = "Password";
|
"login.text_field.password.placeholder" = "Password";
|
||||||
"login.button.log_in.text" = "Log in";
|
"login.button.log_in.text" = "Log in";
|
||||||
|
"login.error.authentication_failed.text" = "The provided username and/or password do not match your records.\n\nPlease confirm your credentials and try again.";
|
||||||
"login.error.authentication_failed.text" = "The given username and/or password are not correct.\nPlease re-enter your credentials and try again.";
|
"login.error.authentication_unknown.text" = "An unexpected error occurred while trying to authenticate your credentials.\n\nPlease try again at a later time.";
|
||||||
"login.error.authentication_unknown.text" = "An unexpected error occurred while trying to authenticate your credentials.\nPlease try again at a later time.";
|
|
||||||
|
@ -79,15 +79,16 @@ struct LoginForm: View {
|
|||||||
comment: "The error message received from the backend."
|
comment: "The error message received from the backend."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
.font(.body)
|
.font(.body)
|
||||||
.foregroundColor(.red)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.background(Color.primary.colorInvert())
|
.background(Color.secondary.colorInvert())
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
setClearButtonIfNeeded()
|
setClearButtonIfNeeded()
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import APIService
|
import APIService
|
||||||
|
import DataModels
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import UseCases
|
||||||
|
|
||||||
public struct LoginView: View {
|
public struct LoginView: View {
|
||||||
|
|
||||||
@ -36,7 +38,8 @@ public struct LoginView: View {
|
|||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
.padding(.top, containerTopPadding)
|
.padding(.top, containerTopPadding)
|
||||||
}
|
}
|
||||||
.background(Color.red)
|
.background(Color.primary.colorInvert())
|
||||||
|
.ignoresSafeArea()
|
||||||
.overlay(ViewHeightGeometry())
|
.overlay(ViewHeightGeometry())
|
||||||
.onPreferenceChange(ViewHeightPreferenceKey.self) { height in
|
.onPreferenceChange(ViewHeightPreferenceKey.self) { height in
|
||||||
containerTopPadding = height * 0.1
|
containerTopPadding = height * 0.1
|
||||||
@ -60,13 +63,9 @@ fileprivate extension LoginView {
|
|||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
private let getUser: GetUserUseCase
|
let authenticated: AuthenticatedClosure
|
||||||
|
|
||||||
// MARK: Initialisers
|
private let getUser: GetUserUseCase = .init()
|
||||||
|
|
||||||
init(authenticated: @escaping AuthenticatedClosure) {
|
|
||||||
self.getUser = .init(authenticated: authenticated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Body
|
// MARK: Body
|
||||||
|
|
||||||
@ -109,7 +108,7 @@ fileprivate extension LoginView {
|
|||||||
}
|
}
|
||||||
.labelStyle(.logIn)
|
.labelStyle(.logIn)
|
||||||
}
|
}
|
||||||
.tint(.orange)
|
.tint(.red)
|
||||||
.buttonStyle(.borderedProminent)
|
.buttonStyle(.borderedProminent)
|
||||||
.buttonBorderShape(.roundedRectangle(radius: 8))
|
.buttonBorderShape(.roundedRectangle(radius: 8))
|
||||||
.controlSize(.large)
|
.controlSize(.large)
|
||||||
@ -138,9 +137,15 @@ private extension LoginView.LoginContainer {
|
|||||||
guard isAuthenticating else { return }
|
guard isAuthenticating else { return }
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await getUser(
|
authenticated(
|
||||||
username: username,
|
.init(
|
||||||
password: password
|
username: username,
|
||||||
|
password: password
|
||||||
|
),
|
||||||
|
try await getUser(
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} catch APIClientError.authenticationFailed {
|
} catch APIClientError.authenticationFailed {
|
||||||
errorMessage = "login.error.authentication_failed.text"
|
errorMessage = "login.error.authentication_failed.text"
|
||||||
@ -153,6 +158,10 @@ private extension LoginView.LoginContainer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Type aliases
|
||||||
|
|
||||||
|
public typealias AuthenticatedClosure = (Account, User) -> Void
|
||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct LoginView_Previews: PreviewProvider {
|
struct LoginView_Previews: PreviewProvider {
|
||||||
|
@ -10,10 +10,9 @@
|
|||||||
"profile.sections.names.label.first_name.text" = "First name";
|
"profile.sections.names.label.first_name.text" = "First name";
|
||||||
"profile.sections.names.label.last_name.text" = "Last name";
|
"profile.sections.names.label.last_name.text" = "Last name";
|
||||||
|
|
||||||
"profile.sections.root_info.header.text" = "Root item information";
|
"profile.sections.root_info.header.text" = "Root folder";
|
||||||
"profile.sections.root_info.label.identifier.text" = "Identifier";
|
"profile.sections.root_info.label.identifier.text" = "Identifier";
|
||||||
"profile.sections.root_info.label.is_directory.text" = "Is a directory?";
|
|
||||||
"profile.sections.root_info.label.last_modified.text" = "Last modified";
|
|
||||||
"profile.sections.root_info.label.name.text" = "Name";
|
"profile.sections.root_info.label.name.text" = "Name";
|
||||||
|
"profile.sections.root_info.label.last_modified.text" = "Last modified";
|
||||||
|
|
||||||
"profile.button.log_out.text" = "Log out";
|
"profile.button.log_out.text" = "Log out";
|
||||||
|
60
Modules/Sources/Profile/UI/Components/DismissableView.swift
Normal file
60
Modules/Sources/Profile/UI/Components/DismissableView.swift
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
//
|
||||||
|
// DismissableView.swift
|
||||||
|
// Profile
|
||||||
|
//
|
||||||
|
// Created by Javier Cicchelli on 12/12/2022.
|
||||||
|
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DismissableView<Content: View>: View {
|
||||||
|
|
||||||
|
// MARK: Environments
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
// MARK: Properties
|
||||||
|
|
||||||
|
@ViewBuilder let content: Content
|
||||||
|
|
||||||
|
// MARK: Body
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
content
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Button {
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Image.close
|
||||||
|
.resizable()
|
||||||
|
.scaledToFit()
|
||||||
|
.frame(width: 32)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
|
.padding([.top, .trailing], 24)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Images+Constants
|
||||||
|
|
||||||
|
private extension Image {
|
||||||
|
static let close = Image(systemName: "xmark.circle.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Previews
|
||||||
|
|
||||||
|
struct SwiftUIView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
DismissableView {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,10 @@ import SwiftUI
|
|||||||
|
|
||||||
public struct ProfileView: View {
|
public struct ProfileView: View {
|
||||||
|
|
||||||
|
// MARK: Environments
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
// MARK: Properties
|
// MARK: Properties
|
||||||
|
|
||||||
private let user: User?
|
private let user: User?
|
||||||
@ -32,68 +36,73 @@ public struct ProfileView: View {
|
|||||||
// MARK: Body
|
// MARK: Body
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
ClearBackgroundList {
|
DismissableView {
|
||||||
Section {
|
ClearBackgroundList {
|
||||||
Image.photo
|
Section {
|
||||||
.resizable()
|
Image.photo
|
||||||
.scaledToFit()
|
.resizable()
|
||||||
.frame(width: 160)
|
.scaledToFit()
|
||||||
.frame(maxWidth: .infinity)
|
.frame(width: 160)
|
||||||
}
|
.frame(maxWidth: .infinity)
|
||||||
.listRowBackground(Color.clear)
|
|
||||||
|
|
||||||
ProfileSection(
|
|
||||||
header: "profile.sections.names.header.text",
|
|
||||||
items: [
|
|
||||||
.init(
|
|
||||||
key: "profile.sections.names.label.first_name.text",
|
|
||||||
value: stringAdapter(value: user?.profile.firstName)
|
|
||||||
),
|
|
||||||
.init(
|
|
||||||
key: "profile.sections.names.label.last_name.text",
|
|
||||||
value: stringAdapter(value: user?.profile.lastName)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
ProfileSection(
|
|
||||||
header: "profile.sections.root_info.header.text",
|
|
||||||
items: [
|
|
||||||
.init(
|
|
||||||
key: "profile.sections.root_info.label.identifier.text",
|
|
||||||
value: stringAdapter(value: user?.rootFolder.id)
|
|
||||||
),
|
|
||||||
.init(
|
|
||||||
key: "profile.sections.root_info.label.name.text",
|
|
||||||
value: stringAdapter(value: user?.rootFolder.name)
|
|
||||||
),
|
|
||||||
.init(
|
|
||||||
key: "profile.sections.root_info.label.last_modified.text",
|
|
||||||
value: dateAdapter(value: user?.rootFolder.lastModifiedAt)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
Section {
|
|
||||||
Button {
|
|
||||||
logout()
|
|
||||||
} label: {
|
|
||||||
Text(
|
|
||||||
"profile.button.log_out.text",
|
|
||||||
bundle: .module
|
|
||||||
)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
}
|
||||||
.tint(.orange)
|
.listRowBackground(Color.clear)
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.buttonBorderShape(.roundedRectangle(radius: 8))
|
Group {
|
||||||
.controlSize(.large)
|
ProfileSection(
|
||||||
|
header: "profile.sections.names.header.text",
|
||||||
|
items: [
|
||||||
|
.init(
|
||||||
|
key: "profile.sections.names.label.first_name.text",
|
||||||
|
value: stringAdapter(value: user?.profile.firstName)
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
key: "profile.sections.names.label.last_name.text",
|
||||||
|
value: stringAdapter(value: user?.profile.lastName)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
ProfileSection(
|
||||||
|
header: "profile.sections.root_info.header.text",
|
||||||
|
items: [
|
||||||
|
.init(
|
||||||
|
key: "profile.sections.root_info.label.identifier.text",
|
||||||
|
value: stringAdapter(value: user?.rootFolder.id)
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
key: "profile.sections.root_info.label.name.text",
|
||||||
|
value: stringAdapter(value: user?.rootFolder.name)
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
key: "profile.sections.root_info.label.last_modified.text",
|
||||||
|
value: dateAdapter(value: user?.rootFolder.lastModifiedAt)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.listRowBackground(Color.secondary.colorInvert())
|
||||||
|
|
||||||
|
Section {
|
||||||
|
Button {
|
||||||
|
logout()
|
||||||
|
} label: {
|
||||||
|
Text(
|
||||||
|
"profile.button.log_out.text",
|
||||||
|
bundle: .module
|
||||||
|
)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.tint(.red)
|
||||||
|
.controlSize(.large)
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.buttonBorderShape(.roundedRectangle(radius: 8))
|
||||||
|
}
|
||||||
|
.listRowBackground(Color.clear)
|
||||||
}
|
}
|
||||||
.listRowBackground(Color.clear)
|
.background(Color.primary.colorInvert())
|
||||||
}
|
}
|
||||||
.background(Color.red)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user