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 Profile
|
||||
import SwiftUI
|
||||
import UseCases
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@ -24,49 +25,94 @@ struct ContentView: View {
|
||||
@State private var user: User?
|
||||
@State private var showSheet: SheetView?
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let getUser: GetUserUseCase = .init()
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
BrowseView {
|
||||
// ...
|
||||
} uploadFile: {
|
||||
// ...
|
||||
} showProfile: {
|
||||
showSheet = .profile
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
shouldShowLogin()
|
||||
}
|
||||
.onChange(of: account) { _ in
|
||||
shouldShowLogin()
|
||||
Container(user: user) {
|
||||
// TODO: create a new folder
|
||||
} uploadFile: {
|
||||
// TODO: upload a new file
|
||||
} showProfile: {
|
||||
showSheet = .profile
|
||||
} login: {
|
||||
showSheet = .login
|
||||
}
|
||||
.sheet(item: $showSheet) { sheet in
|
||||
switch sheet {
|
||||
case .login:
|
||||
LoginView {
|
||||
user = $1
|
||||
account = $0
|
||||
user = $1
|
||||
}
|
||||
case .profile:
|
||||
ProfileView(user: user) {
|
||||
user = 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
|
||||
|
||||
private extension ContentView {
|
||||
func shouldShowLogin() {
|
||||
showSheet = account == nil
|
||||
? .login
|
||||
: nil
|
||||
func loadUserOrLogin() async {
|
||||
guard let account else {
|
||||
showSheet = .login
|
||||
return
|
||||
}
|
||||
|
||||
showSheet = nil
|
||||
user = try? await getUser(
|
||||
username: account.username,
|
||||
password: account.password
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,8 @@ let package = Package(
|
||||
"APIService",
|
||||
"DataModels",
|
||||
"Dependencies",
|
||||
"KeychainStorage"
|
||||
"KeychainStorage",
|
||||
"UseCases"
|
||||
]
|
||||
),
|
||||
],
|
||||
@ -39,6 +40,16 @@ let package = Package(
|
||||
"KeychainAccess"
|
||||
]
|
||||
),
|
||||
.target(
|
||||
name: "UseCases",
|
||||
dependencies: [
|
||||
"Cores",
|
||||
"APIService",
|
||||
"DataModels",
|
||||
"Dependencies",
|
||||
"KeychainStorage"
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "APIServiceTests",
|
||||
dependencies: ["APIService"]
|
||||
|
@ -7,25 +7,26 @@
|
||||
//
|
||||
|
||||
import APIService
|
||||
import DataModels
|
||||
import DependencyInjection
|
||||
import Dependencies
|
||||
|
||||
struct GetUserUseCase {
|
||||
public struct GetUserUseCase {
|
||||
|
||||
// MARK: Dependencies
|
||||
|
||||
@Dependency(\.apiService) private var apiService
|
||||
|
||||
// MARK: Properties
|
||||
// MARK: Initialisers
|
||||
|
||||
let authenticated: AuthenticatedClosure
|
||||
public init() {}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
func callAsFunction(
|
||||
public func callAsFunction(
|
||||
username: String,
|
||||
password: String
|
||||
) async throws {
|
||||
) async throws -> User {
|
||||
let me = try await apiService.getUser(
|
||||
credentials: .init(
|
||||
username: username,
|
||||
@ -33,21 +34,15 @@ struct GetUserUseCase {
|
||||
)
|
||||
)
|
||||
|
||||
authenticated(
|
||||
.init(
|
||||
username: username,
|
||||
password: password
|
||||
return .init(
|
||||
profile: .init(
|
||||
firstName: me.firstName,
|
||||
lastName: me.lastName
|
||||
),
|
||||
.init(
|
||||
profile: .init(
|
||||
firstName: me.firstName,
|
||||
lastName: me.lastName
|
||||
),
|
||||
rootFolder: .init(
|
||||
id: me.rootItem.id,
|
||||
name: me.rootItem.name,
|
||||
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.
|
||||
*/
|
||||
|
||||
// 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.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";
|
||||
|
||||
"browse.swipe_action.delete_item.text" = "Delete item";
|
||||
"browse.swipe_action.download_item.text" = "Download item";
|
||||
|
@ -6,42 +6,86 @@
|
||||
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import DataModels
|
||||
import SwiftUI
|
||||
|
||||
struct DocumentItem: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let name: String
|
||||
let lastModified: String
|
||||
let fileSize: String
|
||||
let item: FileSystemItem
|
||||
let select: ActionClosure
|
||||
let download: ActionClosure
|
||||
let delete: ActionClosure
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
Image.document
|
||||
.icon(size: 32)
|
||||
.foregroundColor(.red)
|
||||
|
||||
VStack {
|
||||
Text(name)
|
||||
.itemName()
|
||||
Button {
|
||||
select()
|
||||
} label: {
|
||||
HStack(spacing: 16) {
|
||||
Image.document
|
||||
.icon(size: 32)
|
||||
.foregroundColor(.red)
|
||||
|
||||
HStack {
|
||||
Text(lastModified)
|
||||
VStack(spacing: 8) {
|
||||
Text(item.name)
|
||||
.itemName()
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(fileSize)
|
||||
HStack {
|
||||
Text("lastModified")
|
||||
|
||||
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
|
||||
@ -54,18 +98,34 @@ private extension Image {
|
||||
|
||||
struct DocumentItem_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DocumentItem(
|
||||
DocumentItem(item: Document(
|
||||
id: "1234567890",
|
||||
name: "Some document name goes in here...",
|
||||
lastModified: "Some few hours ago",
|
||||
fileSize: "23,5 Mbytes"
|
||||
)
|
||||
contentType: "some content type",
|
||||
size: .random(in: 1 ... 100),
|
||||
lastModifiedAt: .now
|
||||
)) {
|
||||
// select closure.
|
||||
} download: {
|
||||
// download closure.
|
||||
} delete: {
|
||||
// delete closure.
|
||||
}
|
||||
.previewDisplayName("Document item")
|
||||
|
||||
DocumentItem(
|
||||
DocumentItem(item: Document(
|
||||
id: "1234567890",
|
||||
name: "Some very, extremely long document name goes in here...",
|
||||
lastModified: "Yesterday",
|
||||
fileSize: "235,6 Kbytes"
|
||||
)
|
||||
contentType: "some content type",
|
||||
size: .random(in: 1 ... 100),
|
||||
lastModifiedAt: .now
|
||||
)) {
|
||||
// select closure.
|
||||
} download: {
|
||||
// download closure.
|
||||
} delete: {
|
||||
// delete closure.
|
||||
}
|
||||
.previewDisplayName("Document item with long name")
|
||||
}
|
||||
}
|
||||
|
@ -6,31 +6,56 @@
|
||||
// Copyright © 2022 Röck+Cöde. All rights reserved.
|
||||
//
|
||||
|
||||
import DataModels
|
||||
import SwiftUI
|
||||
|
||||
struct FolderItem: View {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
let name: String
|
||||
let item: FileSystemItem
|
||||
let select: ActionClosure
|
||||
let delete: ActionClosure
|
||||
|
||||
// MARK: Body
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
Image.folder
|
||||
.icon(size: 32)
|
||||
.foregroundColor(.red)
|
||||
|
||||
Text(name)
|
||||
.itemName()
|
||||
|
||||
Image.chevronRight
|
||||
.icon(size: 16)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.headline)
|
||||
Button {
|
||||
select()
|
||||
} label: {
|
||||
HStack(spacing: 16) {
|
||||
Image.folder
|
||||
.icon(size: 32)
|
||||
.foregroundColor(.red)
|
||||
|
||||
Text(item.name)
|
||||
.itemName()
|
||||
|
||||
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 {
|
||||
static var previews: some View {
|
||||
FolderItem(name: "Some folder name goes in here...")
|
||||
.previewDisplayName("Folder item")
|
||||
FolderItem(item: Folder(
|
||||
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...")
|
||||
.previewDisplayName("Folder item with long name")
|
||||
FolderItem(item: Folder(
|
||||
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 KeychainStorage
|
||||
import SwiftUI
|
||||
|
||||
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
|
||||
|
||||
private let folder: Folder
|
||||
private let createFolder: ActionClosure
|
||||
private let uploadFile: ActionClosure
|
||||
private let showProfile: ActionClosure
|
||||
private let login: ActionClosure
|
||||
|
||||
private let getItems: GetItemsUseCase = .init()
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
public init(
|
||||
folder: Folder,
|
||||
createFolder: @escaping ActionClosure,
|
||||
uploadFile: @escaping ActionClosure,
|
||||
showProfile: @escaping ActionClosure
|
||||
showProfile: @escaping ActionClosure,
|
||||
login: @escaping ActionClosure
|
||||
) {
|
||||
self.folder = folder
|
||||
self.createFolder = createFolder
|
||||
self.uploadFile = uploadFile
|
||||
self.showProfile = showProfile
|
||||
self.login = login
|
||||
}
|
||||
|
||||
// MARK: Body
|
||||
|
||||
public var body: some View {
|
||||
List {
|
||||
Group {
|
||||
Group {
|
||||
FolderItem(name: "Some folder #1 name")
|
||||
FolderItem(name: "Some folder #2 name")
|
||||
FolderItem(name: "Some folder #3 name")
|
||||
FolderItem(name: "Some folder #4 name")
|
||||
FolderItem(name: "Some folder #5 name")
|
||||
FolderItem(name: "Some folder #6 name")
|
||||
FolderItem(name: "Some folder #7 name")
|
||||
}
|
||||
Group {
|
||||
DocumentItem(
|
||||
name: "Some document #1 name",
|
||||
lastModified: "3 months ago",
|
||||
fileSize: "1,23 Mbytes"
|
||||
)
|
||||
DocumentItem(
|
||||
name: "Some document #2 name",
|
||||
lastModified: "2 years ago",
|
||||
fileSize: "123 Kbytes"
|
||||
)
|
||||
DocumentItem(
|
||||
name: "Some document #3 name",
|
||||
lastModified: "13 days ago",
|
||||
fileSize: "12 bytes"
|
||||
)
|
||||
DocumentItem(
|
||||
name: "Some document #4 name",
|
||||
lastModified: "13 hours ago",
|
||||
fileSize: "12,3 Gbytes"
|
||||
)
|
||||
DocumentItem(
|
||||
name: "Some document #5 name",
|
||||
lastModified: "13 minutes ago",
|
||||
fileSize: "123 Tbytes"
|
||||
)
|
||||
DocumentItem(
|
||||
name: "Some document #6 name",
|
||||
lastModified: "13 seconds ago",
|
||||
fileSize: "123 Tbytes"
|
||||
)
|
||||
DocumentItem(
|
||||
name: "Some document #7 name",
|
||||
lastModified: "13 nanoseconds ago",
|
||||
fileSize: "123 Tbytes"
|
||||
)
|
||||
content
|
||||
.navigationTitle(folder.name)
|
||||
.toolbar {
|
||||
BrowseToolbar(
|
||||
createFolder: createFolder,
|
||||
uploadFile: uploadFile,
|
||||
showProfile: showProfile
|
||||
)
|
||||
}
|
||||
.task(id: folder) {
|
||||
await loadItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UI
|
||||
|
||||
private extension BrowseView {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
@ViewBuilder var content: some View {
|
||||
switch status {
|
||||
case .noCredentials:
|
||||
MessageView(
|
||||
type: .noCredentials,
|
||||
action: login
|
||||
)
|
||||
case .loading:
|
||||
LoadingView()
|
||||
case .loaded:
|
||||
List(items, id: \.id) { item in
|
||||
switch item {
|
||||
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.
|
||||
}
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.swipeActions(
|
||||
edge: .trailing,
|
||||
allowsFullSwipe: true
|
||||
) {
|
||||
Button {
|
||||
// TODO: Implement the removal of the item from the API.
|
||||
} label: {
|
||||
Label {
|
||||
Text(
|
||||
"browse.swipe_action.delete_item.text",
|
||||
bundle: .module,
|
||||
comment: "Delete item swipe action text."
|
||||
)
|
||||
} icon: {
|
||||
Image.trash
|
||||
}
|
||||
.listStyle(.inset)
|
||||
case .empty:
|
||||
MessageView(
|
||||
type: .empty,
|
||||
action: uploadFile
|
||||
)
|
||||
case .error:
|
||||
MessageView(type: .error) {
|
||||
Task {
|
||||
await loadItems()
|
||||
}
|
||||
.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")
|
||||
.toolbar {
|
||||
BrowseToolbar(
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
|
||||
@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,
|
||||
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 {
|
||||
static let trash = Image(systemName: "trash")
|
||||
static let download = Image(systemName: "arrow.down.doc")
|
||||
private extension BrowseView {
|
||||
enum ViewStatus {
|
||||
case noCredentials
|
||||
case loading
|
||||
case loaded
|
||||
case empty
|
||||
case error
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
@ -142,12 +190,17 @@ private extension Image {
|
||||
struct BrowseView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NavigationView {
|
||||
BrowseView {
|
||||
// ...
|
||||
BrowseView(folder: .init(
|
||||
id: UUID().uuidString,
|
||||
name: "Some folder name"
|
||||
)) {
|
||||
// create folder closure.
|
||||
} uploadFile: {
|
||||
// ...
|
||||
// upload file closure.
|
||||
} 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.
|
||||
*/
|
||||
|
||||
// LoginView
|
||||
|
||||
"login.title.text" = "My files";
|
||||
"login.text_field.username.placeholder" = "Username";
|
||||
"login.text_field.password.placeholder" = "Password";
|
||||
"login.button.log_in.text" = "Log in";
|
||||
|
||||
"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.\nPlease try again at a later time.";
|
||||
"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_unknown.text" = "An unexpected error occurred while trying to authenticate your credentials.\n\nPlease try again at a later time.";
|
||||
|
@ -79,15 +79,16 @@ struct LoginForm: View {
|
||||
comment: "The error message received from the backend."
|
||||
)
|
||||
)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.body)
|
||||
.foregroundColor(.red)
|
||||
.frame(maxWidth: .infinity)
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(16)
|
||||
.background(Color.primary.colorInvert())
|
||||
.background(Color.secondary.colorInvert())
|
||||
.cornerRadius(8)
|
||||
.onAppear {
|
||||
setClearButtonIfNeeded()
|
||||
|
@ -7,7 +7,9 @@
|
||||
//
|
||||
|
||||
import APIService
|
||||
import DataModels
|
||||
import SwiftUI
|
||||
import UseCases
|
||||
|
||||
public struct LoginView: View {
|
||||
|
||||
@ -36,7 +38,8 @@ public struct LoginView: View {
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, containerTopPadding)
|
||||
}
|
||||
.background(Color.red)
|
||||
.background(Color.primary.colorInvert())
|
||||
.ignoresSafeArea()
|
||||
.overlay(ViewHeightGeometry())
|
||||
.onPreferenceChange(ViewHeightPreferenceKey.self) { height in
|
||||
containerTopPadding = height * 0.1
|
||||
@ -60,13 +63,9 @@ fileprivate extension LoginView {
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let getUser: GetUserUseCase
|
||||
let authenticated: AuthenticatedClosure
|
||||
|
||||
// MARK: Initialisers
|
||||
|
||||
init(authenticated: @escaping AuthenticatedClosure) {
|
||||
self.getUser = .init(authenticated: authenticated)
|
||||
}
|
||||
private let getUser: GetUserUseCase = .init()
|
||||
|
||||
// MARK: Body
|
||||
|
||||
@ -109,7 +108,7 @@ fileprivate extension LoginView {
|
||||
}
|
||||
.labelStyle(.logIn)
|
||||
}
|
||||
.tint(.orange)
|
||||
.tint(.red)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.buttonBorderShape(.roundedRectangle(radius: 8))
|
||||
.controlSize(.large)
|
||||
@ -138,9 +137,15 @@ private extension LoginView.LoginContainer {
|
||||
guard isAuthenticating else { return }
|
||||
|
||||
do {
|
||||
try await getUser(
|
||||
username: username,
|
||||
password: password
|
||||
authenticated(
|
||||
.init(
|
||||
username: username,
|
||||
password: password
|
||||
),
|
||||
try await getUser(
|
||||
username: username,
|
||||
password: password
|
||||
)
|
||||
)
|
||||
} catch APIClientError.authenticationFailed {
|
||||
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
|
||||
|
||||
struct LoginView_Previews: PreviewProvider {
|
||||
|
@ -10,10 +10,9 @@
|
||||
"profile.sections.names.label.first_name.text" = "First 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.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.last_modified.text" = "Last modified";
|
||||
|
||||
"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 {
|
||||
|
||||
// MARK: Environments
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
// MARK: Properties
|
||||
|
||||
private let user: User?
|
||||
@ -32,68 +36,73 @@ public struct ProfileView: View {
|
||||
// MARK: Body
|
||||
|
||||
public var body: some View {
|
||||
ClearBackgroundList {
|
||||
Section {
|
||||
Image.photo
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.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)
|
||||
DismissableView {
|
||||
ClearBackgroundList {
|
||||
Section {
|
||||
Image.photo
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 160)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.tint(.orange)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.buttonBorderShape(.roundedRectangle(radius: 8))
|
||||
.controlSize(.large)
|
||||
.listRowBackground(Color.clear)
|
||||
|
||||
Group {
|
||||
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