From 3d50979d4b893a59b3a2ed0fc12b1f9701e843d1 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 12 Dec 2022 21:30:16 +0100 Subject: [PATCH 01/32] Implemented the dismiss button in the ProfileView view for the Profile module. --- .../Resources/en.lproj/Localizable.strings | 5 +- .../Profile/UI/Views/ProfileView.swift | 140 ++++++++++-------- 2 files changed, 83 insertions(+), 62 deletions(-) diff --git a/Modules/Sources/Profile/Resources/en.lproj/Localizable.strings b/Modules/Sources/Profile/Resources/en.lproj/Localizable.strings index 683966f..50c09c5 100644 --- a/Modules/Sources/Profile/Resources/en.lproj/Localizable.strings +++ b/Modules/Sources/Profile/Resources/en.lproj/Localizable.strings @@ -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"; diff --git a/Modules/Sources/Profile/UI/Views/ProfileView.swift b/Modules/Sources/Profile/UI/Views/ProfileView.swift index 0633db2..5b7ef6b 100644 --- a/Modules/Sources/Profile/UI/Views/ProfileView.swift +++ b/Modules/Sources/Profile/UI/Views/ProfileView.swift @@ -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,85 @@ 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) + ZStack { + 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) + + 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) + .buttonStyle(.borderedProminent) + .buttonBorderShape(.roundedRectangle(radius: 8)) + .controlSize(.large) + } + .listRowBackground(Color.clear) } - .listRowBackground(Color.clear) + .background(Color.red) + + VStack { + Button { + dismiss() + } label: { + Image.close + .resizable() + .frame(width: 32, height: 32) + .foregroundColor(.secondary) + } + + Spacer() + } + .frame(maxWidth: .infinity, alignment: .trailing) + .padding([.top, .trailing], 24) } - .background(Color.red) } } @@ -101,6 +122,7 @@ public struct ProfileView: View { // MARK: - Images+Constants private extension Image { + static let close = Image(systemName: "xmark.circle.fill") static let photo = Image(systemName: "person.crop.circle.fill") } From f65ecf556aee3fa0bbf6a8d4f3ae5da13262d62f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 12 Dec 2022 21:42:47 +0100 Subject: [PATCH 02/32] Implemented the DismissableView component for the Profile module. --- .../UI/Components/DismissableView.swift | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Modules/Sources/Profile/UI/Components/DismissableView.swift diff --git a/Modules/Sources/Profile/UI/Components/DismissableView.swift b/Modules/Sources/Profile/UI/Components/DismissableView.swift new file mode 100644 index 0000000..ecfc07f --- /dev/null +++ b/Modules/Sources/Profile/UI/Components/DismissableView.swift @@ -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: 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() + } + } +} From ceb18a907cdfdb0f7a8083f56b121049f874f128 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 12 Dec 2022 21:43:24 +0100 Subject: [PATCH 03/32] Integrated the DismissableView component into the ProfileView view for the Profile module. --- .../Sources/Profile/UI/Views/ProfileView.swift | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/Modules/Sources/Profile/UI/Views/ProfileView.swift b/Modules/Sources/Profile/UI/Views/ProfileView.swift index 5b7ef6b..1536a35 100644 --- a/Modules/Sources/Profile/UI/Views/ProfileView.swift +++ b/Modules/Sources/Profile/UI/Views/ProfileView.swift @@ -36,7 +36,7 @@ public struct ProfileView: View { // MARK: Body public var body: some View { - ZStack { + DismissableView { ClearBackgroundList { Section { Image.photo @@ -99,21 +99,6 @@ public struct ProfileView: View { .listRowBackground(Color.clear) } .background(Color.red) - - VStack { - Button { - dismiss() - } label: { - Image.close - .resizable() - .frame(width: 32, height: 32) - .foregroundColor(.secondary) - } - - Spacer() - } - .frame(maxWidth: .infinity, alignment: .trailing) - .padding([.top, .trailing], 24) } } @@ -122,7 +107,6 @@ public struct ProfileView: View { // MARK: - Images+Constants private extension Image { - static let close = Image(systemName: "xmark.circle.fill") static let photo = Image(systemName: "person.crop.circle.fill") } From e055f41b92ec9f7bb0d11f9eb83c14f1a2b8e2d8 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 12 Dec 2022 23:04:17 +0100 Subject: [PATCH 04/32] Defined the UseCases target for the Libraries package. --- Libraries/Package.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Libraries/Package.swift b/Libraries/Package.swift index f4e096b..802ba69 100644 --- a/Libraries/Package.swift +++ b/Libraries/Package.swift @@ -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"] From ebee2ddcc04ad5a4579bfee2110725e51d0dc278 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 12 Dec 2022 23:06:56 +0100 Subject: [PATCH 05/32] Moved the GetUserUseCase use case to the UseCases target for the Libraries package. --- .../UseCases}/Use Cases/GetUserUseCase.swift | 33 ++++++++----------- .../Login/Logic/Defines/Typealiases.swift | 11 ------- 2 files changed, 14 insertions(+), 30 deletions(-) rename {Modules/Sources/Login/Logic => Libraries/Sources/UseCases}/Use Cases/GetUserUseCase.swift (50%) delete mode 100644 Modules/Sources/Login/Logic/Defines/Typealiases.swift diff --git a/Modules/Sources/Login/Logic/Use Cases/GetUserUseCase.swift b/Libraries/Sources/UseCases/Use Cases/GetUserUseCase.swift similarity index 50% rename from Modules/Sources/Login/Logic/Use Cases/GetUserUseCase.swift rename to Libraries/Sources/UseCases/Use Cases/GetUserUseCase.swift index f49fa92..e06aeae 100644 --- a/Modules/Sources/Login/Logic/Use Cases/GetUserUseCase.swift +++ b/Libraries/Sources/UseCases/Use Cases/GetUserUseCase.swift @@ -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 ) ) } diff --git a/Modules/Sources/Login/Logic/Defines/Typealiases.swift b/Modules/Sources/Login/Logic/Defines/Typealiases.swift deleted file mode 100644 index b65b046..0000000 --- a/Modules/Sources/Login/Logic/Defines/Typealiases.swift +++ /dev/null @@ -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 From 07ffd2bf806f6a20f8bbddeb1e084320380dca19 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 12 Dec 2022 23:53:21 +0100 Subject: [PATCH 06/32] Integrated the GetUserUseCase use case into the LoginView view for the Login module. --- .../Sources/Login/UI/Views/LoginView.swift | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/Modules/Sources/Login/UI/Views/LoginView.swift b/Modules/Sources/Login/UI/Views/LoginView.swift index 6dbb018..16fac56 100644 --- a/Modules/Sources/Login/UI/Views/LoginView.swift +++ b/Modules/Sources/Login/UI/Views/LoginView.swift @@ -7,7 +7,9 @@ // import APIService +import DataModels import SwiftUI +import UseCases public struct LoginView: View { @@ -60,13 +62,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 @@ -138,9 +136,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 +157,10 @@ private extension LoginView.LoginContainer { } +// MARK: - Type aliases + +public typealias AuthenticatedClosure = (Account, User) -> Void + // MARK: - Previews struct LoginView_Previews: PreviewProvider { From c1c25c356d91ea14e511526ef738573182e554b5 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 13 Dec 2022 00:03:26 +0100 Subject: [PATCH 07/32] Integrated the GetUserUseCase use case into the ContentView view for the BeReal app target. --- BeReal/UI/Views/ContentView.swift | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift index aa429f0..94b4f2a 100644 --- a/BeReal/UI/Views/ContentView.swift +++ b/BeReal/UI/Views/ContentView.swift @@ -12,6 +12,7 @@ import Login import KeychainStorage import Profile import SwiftUI +import UseCases struct ContentView: View { @@ -24,6 +25,10 @@ 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 { @@ -36,12 +41,6 @@ struct ContentView: View { showSheet = .profile } } - .onAppear { - shouldShowLogin() - } - .onChange(of: account) { _ in - shouldShowLogin() - } .sheet(item: $showSheet) { sheet in switch sheet { case .login: @@ -56,6 +55,9 @@ struct ContentView: View { } } } + .task(id: account) { + await loadUserOrLogin() + } } } @@ -63,10 +65,17 @@ struct ContentView: View { // 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 + ) } } From 6478a57bbaecd107048005ef6fd3cc408b2a08b2 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 13 Dec 2022 00:20:23 +0100 Subject: [PATCH 08/32] Defined the Folder model and integrated it to the BrowseView view for the Browse module. --- BeReal/UI/Views/ContentView.swift | 5 +++- Modules/Sources/Browse/UI/Models/Folder.swift | 26 +++++++++++++++++++ .../Sources/Browse/UI/Views/BrowseView.swift | 10 +++++-- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 Modules/Sources/Browse/UI/Models/Folder.swift diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift index 94b4f2a..8a840d9 100644 --- a/BeReal/UI/Views/ContentView.swift +++ b/BeReal/UI/Views/ContentView.swift @@ -33,7 +33,10 @@ struct ContentView: View { var body: some View { NavigationView { - BrowseView { + BrowseView(folder: .init( + id: user?.rootFolder.id, + name: user?.rootFolder.name + )) { // ... } uploadFile: { // ... diff --git a/Modules/Sources/Browse/UI/Models/Folder.swift b/Modules/Sources/Browse/UI/Models/Folder.swift new file mode 100644 index 0000000..1c2596b --- /dev/null +++ b/Modules/Sources/Browse/UI/Models/Folder.swift @@ -0,0 +1,26 @@ +// +// 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? = nil, + name: String? = nil + ) { + self.id = id ?? "-" + self.name = name ?? "-" + } + +} diff --git a/Modules/Sources/Browse/UI/Views/BrowseView.swift b/Modules/Sources/Browse/UI/Views/BrowseView.swift index 2ee78a4..b13754a 100644 --- a/Modules/Sources/Browse/UI/Views/BrowseView.swift +++ b/Modules/Sources/Browse/UI/Views/BrowseView.swift @@ -13,6 +13,7 @@ public struct BrowseView: View { // MARK: Properties + private let folder: Folder private let createFolder: ActionClosure private let uploadFile: ActionClosure private let showProfile: ActionClosure @@ -20,10 +21,12 @@ public struct BrowseView: View { // MARK: Initialisers public init( + folder: Folder, createFolder: @escaping ActionClosure, uploadFile: @escaping ActionClosure, showProfile: @escaping ActionClosure ) { + self.folder = folder self.createFolder = createFolder self.uploadFile = uploadFile self.showProfile = showProfile @@ -119,7 +122,7 @@ public struct BrowseView: View { } .listStyle(.inset) .background(Color.red) - .navigationTitle("Folder name") + .navigationTitle(folder.name) .toolbar { BrowseToolbar( createFolder: createFolder, @@ -142,7 +145,10 @@ private extension Image { struct BrowseView_Previews: PreviewProvider { static var previews: some View { NavigationView { - BrowseView { + BrowseView(folder: .init( + id: UUID().uuidString, + name: "Some folder name" + )) { // ... } uploadFile: { // ... From d0371c6e4855ae122100d947aadcad2ad58809a1 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 13 Dec 2022 02:02:25 +0100 Subject: [PATCH 09/32] Defined the Model protocol and implemented the File and Folder models for the Browse module. --- .../Sources/Browse/Logic/Models/File.swift | 30 +++++++++++++++++++ .../Browse/{UI => Logic}/Models/Folder.swift | 12 +++++--- .../Browse/Logic/Protocols/Model.swift | 16 ++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 Modules/Sources/Browse/Logic/Models/File.swift rename Modules/Sources/Browse/{UI => Logic}/Models/Folder.swift (67%) create mode 100644 Modules/Sources/Browse/Logic/Protocols/Model.swift diff --git a/Modules/Sources/Browse/Logic/Models/File.swift b/Modules/Sources/Browse/Logic/Models/File.swift new file mode 100644 index 0000000..e4a5cf3 --- /dev/null +++ b/Modules/Sources/Browse/Logic/Models/File.swift @@ -0,0 +1,30 @@ +// +// File.swift +// Browse +// +// Created by Javier Cicchelli on 13/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +struct File { + + // 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: - ModelIdentifiable + +extension File: ModelIdentifiable {} diff --git a/Modules/Sources/Browse/UI/Models/Folder.swift b/Modules/Sources/Browse/Logic/Models/Folder.swift similarity index 67% rename from Modules/Sources/Browse/UI/Models/Folder.swift rename to Modules/Sources/Browse/Logic/Models/Folder.swift index 1c2596b..55054ef 100644 --- a/Modules/Sources/Browse/UI/Models/Folder.swift +++ b/Modules/Sources/Browse/Logic/Models/Folder.swift @@ -16,11 +16,15 @@ public struct Folder { // MARK: Initialisers public init( - id: String? = nil, - name: String? = nil + id: String, + name: String ) { - self.id = id ?? "-" - self.name = name ?? "-" + self.id = id + self.name = name } } + +// MARK: - ModelIdentifiable + +extension Folder: ModelIdentifiable {} diff --git a/Modules/Sources/Browse/Logic/Protocols/Model.swift b/Modules/Sources/Browse/Logic/Protocols/Model.swift new file mode 100644 index 0000000..30accae --- /dev/null +++ b/Modules/Sources/Browse/Logic/Protocols/Model.swift @@ -0,0 +1,16 @@ +// +// Model.swift +// Browse +// +// Created by Javier Cicchelli on 13/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +protocol Model { + var id: String { get } + var name: String { get } +} + +// MARK: - Type aliases + +typealias ModelIdentifiable = Model & Identifiable From 08edb6b7ec903a9429e62255af32026b2f4d8fae Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 13 Dec 2022 02:03:38 +0100 Subject: [PATCH 10/32] Implemented the GetItemsUseCase use case for the Browse module. --- .../Logic/Use Cases/GetItemsUseCase.swift | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift diff --git a/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift b/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift new file mode 100644 index 0000000..af82e03 --- /dev/null +++ b/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift @@ -0,0 +1,50 @@ +// +// 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 ModelIdentifiable] { + let items = try await apiService.getItems( + id: id, + credentials: .init( + username: username, + password: password + ) + ) + + return items + .compactMap { item -> any ModelIdentifiable in + if item.isDirectory { + return Folder( + id: item.id, + name: item.name + ) + } else { + return File( + id: item.id, + name: item.name + ) + } + } + } + +} From 0ba14e85fc35dff8797c72d7850a175488fb0f64 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Tue, 13 Dec 2022 12:09:08 +0100 Subject: [PATCH 11/32] Work done so far on the ContentView and the BrowseView views. --- BeReal/UI/Views/ContentView.swift | 52 ++++- .../Sources/Browse/Logic/Models/Folder.swift | 4 + .../Sources/Browse/UI/Views/BrowseView.swift | 204 +++++++++++------- 3 files changed, 169 insertions(+), 91 deletions(-) diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift index 8a840d9..07830f8 100644 --- a/BeReal/UI/Views/ContentView.swift +++ b/BeReal/UI/Views/ContentView.swift @@ -32,17 +32,12 @@ struct ContentView: View { // MARK: Body var body: some View { - NavigationView { - BrowseView(folder: .init( - id: user?.rootFolder.id, - name: user?.rootFolder.name - )) { - // ... - } uploadFile: { - // ... - } showProfile: { - showSheet = .profile - } + Container(user: $user) { + // TODO: create a new folder + } uploadFile: { + // TODO: upload a new file + } showProfile: { + showSheet = .profile } .sheet(item: $showSheet) { sheet in switch sheet { @@ -65,6 +60,41 @@ struct ContentView: View { } +// MARK: - Views + +private extension ContentView { + struct Container: View { + + // MARK: Properties + + @Binding var user: User? + let createFolder: ActionClosure + let uploadFile: ActionClosure + let showProfile: 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 + ) + } + } else { + EmptyView() + } + } + + } +} + // MARK: - Helpers private extension ContentView { diff --git a/Modules/Sources/Browse/Logic/Models/Folder.swift b/Modules/Sources/Browse/Logic/Models/Folder.swift index 55054ef..557c9f2 100644 --- a/Modules/Sources/Browse/Logic/Models/Folder.swift +++ b/Modules/Sources/Browse/Logic/Models/Folder.swift @@ -28,3 +28,7 @@ public struct Folder { // MARK: - ModelIdentifiable extension Folder: ModelIdentifiable {} + +// MARK: - Equatable + +extension Folder: Equatable {} diff --git a/Modules/Sources/Browse/UI/Views/BrowseView.swift b/Modules/Sources/Browse/UI/Views/BrowseView.swift index b13754a..1c6764a 100644 --- a/Modules/Sources/Browse/UI/Views/BrowseView.swift +++ b/Modules/Sources/Browse/UI/Views/BrowseView.swift @@ -7,10 +7,19 @@ // 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 items: [any ModelIdentifiable] = [] + // MARK: Properties private let folder: Folder @@ -18,6 +27,8 @@ public struct BrowseView: View { private let uploadFile: ActionClosure private let showProfile: ActionClosure + private let getItems: GetItemsUseCase = .init() + // MARK: Initialisers public init( @@ -36,92 +47,102 @@ public struct BrowseView: View { 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 { + ForEach(items, id: \.id) { item in + if let folder = item as? Folder { + FolderItem(name: folder.name) + } else if let file = item as? File { 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" + name: file.name, + lastModified: "-", + fileSize: "-" ) } } - .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 - } - } - .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) - } +// 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" +// ) +// } +// } +// .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 +// } +// } +// .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( @@ -130,6 +151,29 @@ public struct BrowseView: View { showProfile: showProfile ) } + .task(id: folder) { + await getItemsOrStop() + } + } +} + +// MARK: - Helpers + +private extension BrowseView { + func getItemsOrStop() async { + guard let account else { return } + + do { + items = try await getItems( + id: folder.id, + username: account.username, + password: account.password + ) + } catch let error { + print(folder) + print(error) + // TODO: Handle the error. + } } } From d5f7d5030fa69a765030e7fe9ed90b96d49d4e6e Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 00:11:24 +0100 Subject: [PATCH 12/32] Fixed some data loading issues in the ContentView view for the app target., --- BeReal/UI/Views/ContentView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift index 07830f8..e0c3f70 100644 --- a/BeReal/UI/Views/ContentView.swift +++ b/BeReal/UI/Views/ContentView.swift @@ -32,7 +32,7 @@ struct ContentView: View { // MARK: Body var body: some View { - Container(user: $user) { + Container(user: user) { // TODO: create a new folder } uploadFile: { // TODO: upload a new file @@ -43,13 +43,13 @@ struct ContentView: View { switch sheet { case .login: LoginView { - user = $1 account = $0 + user = $1 } case .profile: ProfileView(user: user) { - user = nil account = nil + user = nil } } } @@ -67,7 +67,7 @@ private extension ContentView { // MARK: Properties - @Binding var user: User? + let user: User? let createFolder: ActionClosure let uploadFile: ActionClosure let showProfile: ActionClosure From 265ebc8f259c763ea854d37b67f70fbb9c225b76 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 00:41:44 +0100 Subject: [PATCH 13/32] Defined the ItemIdClosure typealias for the Browse module. --- Modules/Sources/Browse/Logic/Defines/Typealiases.swift | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Modules/Sources/Browse/Logic/Defines/Typealiases.swift diff --git a/Modules/Sources/Browse/Logic/Defines/Typealiases.swift b/Modules/Sources/Browse/Logic/Defines/Typealiases.swift new file mode 100644 index 0000000..c7f48d8 --- /dev/null +++ b/Modules/Sources/Browse/Logic/Defines/Typealiases.swift @@ -0,0 +1,9 @@ +// +// Typealiases.swift +// Browse +// +// Created by Javier Cicchelli on 14/12/2022. +// Copyright © 2022 Röck+Cöde. All rights reserved. +// + +typealias ItemIdClosure = (String) -> Void From 5c402f8958e7b5e9fec0f8d2d2270ff403ed159f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 00:42:25 +0100 Subject: [PATCH 14/32] Defined the "trash" and "download" images in the Image+Constants extension for the Browse module. --- .../Browse/UI/Extensions/Image+Constants.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Modules/Sources/Browse/UI/Extensions/Image+Constants.swift diff --git a/Modules/Sources/Browse/UI/Extensions/Image+Constants.swift b/Modules/Sources/Browse/UI/Extensions/Image+Constants.swift new file mode 100644 index 0000000..adebaa7 --- /dev/null +++ b/Modules/Sources/Browse/UI/Extensions/Image+Constants.swift @@ -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") +} From 02c6626d9425fd16912c5fbd97fdcb4f22e93241 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 00:44:01 +0100 Subject: [PATCH 15/32] Improved the FolderItem component to for the Browse module to support the folder swipe actions. --- .../Browse/UI/Components/FolderItem.swift | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/Modules/Sources/Browse/UI/Components/FolderItem.swift b/Modules/Sources/Browse/UI/Components/FolderItem.swift index fd89e77..6e0b87b 100644 --- a/Modules/Sources/Browse/UI/Components/FolderItem.swift +++ b/Modules/Sources/Browse/UI/Components/FolderItem.swift @@ -12,7 +12,8 @@ struct FolderItem: View { // MARK: Properties - let name: String + let item: Model + let delete: ItemIdClosure // MARK: Body @@ -22,7 +23,7 @@ struct FolderItem: View { .icon(size: 32) .foregroundColor(.red) - Text(name) + Text(item.name) .itemName() Image.chevronRight @@ -31,6 +32,24 @@ struct FolderItem: View { .font(.headline) } .padding(.vertical, 8) + .swipeActions( + edge: .trailing, + allowsFullSwipe: true + ) { + Button { + delete(item.id) + } label: { + Label { + Text( + "browse.swipe_action.delete_item.text", + bundle: .module + ) + } icon: { + Image.trash + } + } + .tint(.red) + } } } @@ -46,10 +65,20 @@ 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..." + )) { _ in + // delete closure with item id. + } + .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..." + )) { _ in + // delete closire with item id. + } + .previewDisplayName("Folder item with long name") } } From 627698e1bf07ddd7e08e2186361c895d668f5555 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 01:10:45 +0100 Subject: [PATCH 16/32] Renamed the File model for the Browse module as Document. --- .../Browse/Logic/Models/Document.swift | 41 +++++++++++++++++++ .../Sources/Browse/Logic/Models/File.swift | 30 -------------- 2 files changed, 41 insertions(+), 30 deletions(-) create mode 100644 Modules/Sources/Browse/Logic/Models/Document.swift delete mode 100644 Modules/Sources/Browse/Logic/Models/File.swift diff --git a/Modules/Sources/Browse/Logic/Models/Document.swift b/Modules/Sources/Browse/Logic/Models/Document.swift new file mode 100644 index 0000000..74bb69f --- /dev/null +++ b/Modules/Sources/Browse/Logic/Models/Document.swift @@ -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: FileSystemIdIdentifiable {} diff --git a/Modules/Sources/Browse/Logic/Models/File.swift b/Modules/Sources/Browse/Logic/Models/File.swift deleted file mode 100644 index e4a5cf3..0000000 --- a/Modules/Sources/Browse/Logic/Models/File.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// File.swift -// Browse -// -// Created by Javier Cicchelli on 13/12/2022. -// Copyright © 2022 Röck+Cöde. All rights reserved. -// - -struct File { - - // 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: - ModelIdentifiable - -extension File: ModelIdentifiable {} From 9a3b9049c7d83ae27620b4b991ac33914c77900f Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 01:12:27 +0100 Subject: [PATCH 17/32] Renamed the Model and ModelIdentifiable protocols for the Browse module as FileSystemItem and FileSystemItemIdentifiable respectively. --- Modules/Sources/Browse/Logic/Models/Folder.swift | 4 ++-- .../Protocols/{Model.swift => FileSystemItem.swift} | 6 +++--- .../Browse/Logic/Use Cases/GetItemsUseCase.swift | 11 +++++++---- Modules/Sources/Browse/UI/Components/FolderItem.swift | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) rename Modules/Sources/Browse/Logic/Protocols/{Model.swift => FileSystemItem.swift} (63%) diff --git a/Modules/Sources/Browse/Logic/Models/Folder.swift b/Modules/Sources/Browse/Logic/Models/Folder.swift index 557c9f2..7c879b9 100644 --- a/Modules/Sources/Browse/Logic/Models/Folder.swift +++ b/Modules/Sources/Browse/Logic/Models/Folder.swift @@ -25,9 +25,9 @@ public struct Folder { } -// MARK: - ModelIdentifiable +// MARK: - FileSystemIdIdentifiable -extension Folder: ModelIdentifiable {} +extension Folder: FileSystemIdIdentifiable {} // MARK: - Equatable diff --git a/Modules/Sources/Browse/Logic/Protocols/Model.swift b/Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift similarity index 63% rename from Modules/Sources/Browse/Logic/Protocols/Model.swift rename to Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift index 30accae..ffbb1e7 100644 --- a/Modules/Sources/Browse/Logic/Protocols/Model.swift +++ b/Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift @@ -1,16 +1,16 @@ // -// Model.swift +// FileSystemItem.swift // Browse // // Created by Javier Cicchelli on 13/12/2022. // Copyright © 2022 Röck+Cöde. All rights reserved. // -protocol Model { +protocol FileSystemItem { var id: String { get } var name: String { get } } // MARK: - Type aliases -typealias ModelIdentifiable = Model & Identifiable +typealias FileSystemIdIdentifiable = FileSystemItem & Identifiable diff --git a/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift b/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift index af82e03..7f14d9f 100644 --- a/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift +++ b/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift @@ -22,7 +22,7 @@ struct GetItemsUseCase { id: String, username: String, password: String - ) async throws -> [any ModelIdentifiable] { + ) async throws -> [any FileSystemIdIdentifiable] { let items = try await apiService.getItems( id: id, credentials: .init( @@ -32,16 +32,19 @@ struct GetItemsUseCase { ) return items - .compactMap { item -> any ModelIdentifiable in + .compactMap { item -> any FileSystemIdIdentifiable in if item.isDirectory { return Folder( id: item.id, name: item.name ) } else { - return File( + return Document( id: item.id, - name: item.name + name: item.name, + contentType: item.contentType ?? "-", + size: item.size ?? 0, + lastModifiedAt: item.lastModifiedAt ) } } diff --git a/Modules/Sources/Browse/UI/Components/FolderItem.swift b/Modules/Sources/Browse/UI/Components/FolderItem.swift index 6e0b87b..e1a114e 100644 --- a/Modules/Sources/Browse/UI/Components/FolderItem.swift +++ b/Modules/Sources/Browse/UI/Components/FolderItem.swift @@ -12,7 +12,7 @@ struct FolderItem: View { // MARK: Properties - let item: Model + let item: FileSystemItem let delete: ItemIdClosure // MARK: Body From c7f5d0db400689889d168a1ebfe0a68288219749 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 01:17:26 +0100 Subject: [PATCH 18/32] Improved the DocumentItem component to for the Browse module to support the folder swipe actions. --- .../Browse/UI/Components/DocumentItem.swift | 84 +++++++++++++++---- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/Modules/Sources/Browse/UI/Components/DocumentItem.swift b/Modules/Sources/Browse/UI/Components/DocumentItem.swift index 11ee561..3d586fd 100644 --- a/Modules/Sources/Browse/UI/Components/DocumentItem.swift +++ b/Modules/Sources/Browse/UI/Components/DocumentItem.swift @@ -12,10 +12,10 @@ struct DocumentItem: View { // MARK: Properties - let name: String - let lastModified: String - let fileSize: String - + let item: FileSystemItem + let download: ItemIdClosure + let delete: ItemIdClosure + // MARK: Body var body: some View { @@ -24,26 +24,64 @@ struct DocumentItem: View { .icon(size: 32) .foregroundColor(.red) - VStack { - Text(name) + VStack(spacing: 8) { + Text(item.name) .itemName() HStack { - Text(lastModified) + Text("lastModified") Spacer() - Text(fileSize) + Text("fileSize") } .font(.subheadline) .foregroundColor(.secondary) } } - .padding(.vertical, 8) + .padding(.vertical, 4) + .swipeActions( + edge: .trailing, + allowsFullSwipe: true + ) { + Button { + delete(item.id) + } label: { + Label { + Text( + "browse.swipe_action.delete_item.text", + bundle: .module + ) + } icon: { + Image.trash + } + } + .tint(.red) + + Button { + download(item.id) + } label: { + Label { + Text( + "browse.swipe_action.download_item.text", + bundle: .module + ) + } icon: { + Image.download + } + } + .tint(.orange) + } } } +// MARK: - Helpers + +private extension DocumentItem { + var document: Document? { item as? Document } +} + // MARK: - Image+Constants private extension Image { @@ -54,18 +92,30 @@ 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 + )) { _ in + // download closure with item id. + } delete: { _ in + // delete closure with item id. + } .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 + )) { _ in + // download closure with item id. + } delete: { _ in + // delete closure with item id. + } .previewDisplayName("Document item with long name") } } From 37490ba3d7dba3f52efff6ff60609121ac287aad Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 01:21:20 +0100 Subject: [PATCH 19/32] Integrated the FolderItem and the DocumentItem components into the BrowseView view for the Browse module. --- .../Sources/Browse/UI/Views/BrowseView.swift | 121 +++--------------- 1 file changed, 17 insertions(+), 104 deletions(-) diff --git a/Modules/Sources/Browse/UI/Views/BrowseView.swift b/Modules/Sources/Browse/UI/Views/BrowseView.swift index 1c6764a..65de97b 100644 --- a/Modules/Sources/Browse/UI/Views/BrowseView.swift +++ b/Modules/Sources/Browse/UI/Views/BrowseView.swift @@ -18,7 +18,7 @@ public struct BrowseView: View { // MARK: States - @State private var items: [any ModelIdentifiable] = [] + @State private var items: [any FileSystemIdIdentifiable] = [] // MARK: Properties @@ -48,99 +48,21 @@ public struct BrowseView: View { public var body: some View { List { ForEach(items, id: \.id) { item in - if let folder = item as? Folder { - FolderItem(name: folder.name) - } else if let file = item as? File { - DocumentItem( - name: file.name, - lastModified: "-", - fileSize: "-" - ) + switch item { + case is Folder: + FolderItem(item: item) { id in + // TODO: delete the item id from the backend. + } + case is Document: + DocumentItem(item: item) { id in + // TODO: download the item id from the backend. + } delete: { id in + // TODO: delete the item id from the backend. + } + default: + EmptyView() } } -// 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" -// ) -// } -// } -// .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 -// } -// } -// .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) .navigationTitle(folder.name) @@ -162,28 +84,19 @@ public struct BrowseView: View { private extension BrowseView { func getItemsOrStop() async { guard let account else { return } - + do { items = try await getItems( id: folder.id, username: account.username, password: account.password ) - } catch let error { - print(folder) - print(error) - // TODO: Handle the error. + } catch { + // TODO: handle the error appropriately. } } } -// MARK: - Image+Constants - -private extension Image { - static let trash = Image(systemName: "trash") - static let download = Image(systemName: "arrow.down.doc") -} - // MARK: - Previews struct BrowseView_Previews: PreviewProvider { From 98db94d18033e00d7cdaab70beb184fd7a201ed3 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 01:41:10 +0100 Subject: [PATCH 20/32] Fixed the naming of the FileSystemIdIdentifiable protocol for the Browse module as FileSystemItemIdentifiable. --- Modules/Sources/Browse/Logic/Models/Document.swift | 2 +- Modules/Sources/Browse/Logic/Models/Folder.swift | 2 +- Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift | 2 +- Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/Sources/Browse/Logic/Models/Document.swift b/Modules/Sources/Browse/Logic/Models/Document.swift index 74bb69f..46df5eb 100644 --- a/Modules/Sources/Browse/Logic/Models/Document.swift +++ b/Modules/Sources/Browse/Logic/Models/Document.swift @@ -38,4 +38,4 @@ struct Document { // MARK: - FileSystemIdIdentifiable -extension Document: FileSystemIdIdentifiable {} +extension Document: FileSystemItemIdentifiable {} diff --git a/Modules/Sources/Browse/Logic/Models/Folder.swift b/Modules/Sources/Browse/Logic/Models/Folder.swift index 7c879b9..b089e36 100644 --- a/Modules/Sources/Browse/Logic/Models/Folder.swift +++ b/Modules/Sources/Browse/Logic/Models/Folder.swift @@ -27,7 +27,7 @@ public struct Folder { // MARK: - FileSystemIdIdentifiable -extension Folder: FileSystemIdIdentifiable {} +extension Folder: FileSystemItemIdentifiable {} // MARK: - Equatable diff --git a/Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift b/Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift index ffbb1e7..d4f4d04 100644 --- a/Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift +++ b/Modules/Sources/Browse/Logic/Protocols/FileSystemItem.swift @@ -13,4 +13,4 @@ protocol FileSystemItem { // MARK: - Type aliases -typealias FileSystemIdIdentifiable = FileSystemItem & Identifiable +typealias FileSystemItemIdentifiable = FileSystemItem & Identifiable & Hashable diff --git a/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift b/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift index 7f14d9f..bbd219f 100644 --- a/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift +++ b/Modules/Sources/Browse/Logic/Use Cases/GetItemsUseCase.swift @@ -22,7 +22,7 @@ struct GetItemsUseCase { id: String, username: String, password: String - ) async throws -> [any FileSystemIdIdentifiable] { + ) async throws -> [any FileSystemItemIdentifiable] { let items = try await apiService.getItems( id: id, credentials: .init( @@ -32,7 +32,7 @@ struct GetItemsUseCase { ) return items - .compactMap { item -> any FileSystemIdIdentifiable in + .compactMap { item -> any FileSystemItemIdentifiable in if item.isDirectory { return Folder( id: item.id, From 0ecc4810fa0d8552a2ed43e595123e81c47440b9 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 01:43:31 +0100 Subject: [PATCH 21/32] Improved the FolderItem and the DocumentItem components to support the item selection. --- .../Browse/UI/Components/DocumentItem.swift | 47 +++++++++++-------- .../Browse/UI/Components/FolderItem.swift | 37 +++++++++------ .../Sources/Browse/UI/Views/BrowseView.swift | 34 +++++++------- 3 files changed, 69 insertions(+), 49 deletions(-) diff --git a/Modules/Sources/Browse/UI/Components/DocumentItem.swift b/Modules/Sources/Browse/UI/Components/DocumentItem.swift index 3d586fd..7d30db0 100644 --- a/Modules/Sources/Browse/UI/Components/DocumentItem.swift +++ b/Modules/Sources/Browse/UI/Components/DocumentItem.swift @@ -13,33 +13,38 @@ struct DocumentItem: View { // MARK: Properties let item: FileSystemItem + let select: ItemIdClosure let download: ItemIdClosure let delete: ItemIdClosure - + // MARK: Body var body: some View { - HStack(spacing: 16) { - Image.document - .icon(size: 32) - .foregroundColor(.red) - - VStack(spacing: 8) { - Text(item.name) - .itemName() + Button { + select(item.id) + } 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) } - .padding(.vertical, 4) .swipeActions( edge: .trailing, allowsFullSwipe: true @@ -57,7 +62,7 @@ struct DocumentItem: View { } } .tint(.red) - + Button { download(item.id) } label: { @@ -73,7 +78,7 @@ struct DocumentItem: View { .tint(.orange) } } - + } // MARK: - Helpers @@ -99,6 +104,8 @@ struct DocumentItem_Previews: PreviewProvider { size: .random(in: 1 ... 100), lastModifiedAt: .now )) { _ in + // select closure with item id. + } download: { _ in // download closure with item id. } delete: { _ in // delete closure with item id. @@ -112,6 +119,8 @@ struct DocumentItem_Previews: PreviewProvider { size: .random(in: 1 ... 100), lastModifiedAt: .now )) { _ in + // select closure with item id. + } download: { _ in // download closure with item id. } delete: { _ in // delete closure with item id. diff --git a/Modules/Sources/Browse/UI/Components/FolderItem.swift b/Modules/Sources/Browse/UI/Components/FolderItem.swift index e1a114e..bce7697 100644 --- a/Modules/Sources/Browse/UI/Components/FolderItem.swift +++ b/Modules/Sources/Browse/UI/Components/FolderItem.swift @@ -13,25 +13,30 @@ struct FolderItem: View { // MARK: Properties let item: FileSystemItem + let select: ItemIdClosure let delete: ItemIdClosure // MARK: Body var body: some View { - HStack(spacing: 16) { - Image.folder - .icon(size: 32) - .foregroundColor(.red) - - Text(item.name) - .itemName() - - Image.chevronRight - .icon(size: 16) - .foregroundColor(.secondary) - .font(.headline) + Button { + select(item.id) + } 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) } - .padding(.vertical, 8) .swipeActions( edge: .trailing, allowsFullSwipe: true @@ -69,6 +74,8 @@ struct BrowseItem_Previews: PreviewProvider { id: "1234567890", name: "Some folder name goes in here..." )) { _ in + // select closure with item id. + } delete: { _ in // delete closure with item id. } .previewDisplayName("Folder item") @@ -77,7 +84,9 @@ struct BrowseItem_Previews: PreviewProvider { id: "1234567890", name: "Some very, extremely long folder name goes in here..." )) { _ in - // delete closire with item id. + // select closure with item id. + } delete: { _ in + // delete closure with item id. } .previewDisplayName("Folder item with long name") } diff --git a/Modules/Sources/Browse/UI/Views/BrowseView.swift b/Modules/Sources/Browse/UI/Views/BrowseView.swift index 65de97b..6e0d9b1 100644 --- a/Modules/Sources/Browse/UI/Views/BrowseView.swift +++ b/Modules/Sources/Browse/UI/Views/BrowseView.swift @@ -18,7 +18,7 @@ public struct BrowseView: View { // MARK: States - @State private var items: [any FileSystemIdIdentifiable] = [] + @State private var items: [any FileSystemItemIdentifiable] = [] // MARK: Properties @@ -46,22 +46,24 @@ public struct BrowseView: View { // MARK: Body public var body: some View { - List { - ForEach(items, id: \.id) { item in - switch item { - case is Folder: - FolderItem(item: item) { id in - // TODO: delete the item id from the backend. - } - case is Document: - DocumentItem(item: item) { id in - // TODO: download the item id from the backend. - } delete: { id in - // TODO: delete the item id from the backend. - } - default: - EmptyView() + List(items, id: \.id) { item in + switch item { + case is Folder: + FolderItem(item: item) { id in + // TODO: browse to the item id in another view. + } delete: { id in + // TODO: delete the item id from the backend. } + case is Document: + DocumentItem(item: item) { id in + // TODO: show the item id in a viewer... + } download: { id in + // TODO: download the item id from the backend. + } delete: { id in + // TODO: delete the item id from the backend. + } + default: + EmptyView() } } .listStyle(.inset) From fc9f650a082d2490b1d735edea888906085a7697 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 23:57:20 +0100 Subject: [PATCH 22/32] Removed the ItemIdClosure definition from the Browse module as it is not needed anymore. --- .../Browse/Logic/Defines/Typealiases.swift | 9 ----- .../Browse/UI/Components/DocumentItem.swift | 37 ++++++++++--------- .../Browse/UI/Components/FolderItem.swift | 25 +++++++------ 3 files changed, 32 insertions(+), 39 deletions(-) delete mode 100644 Modules/Sources/Browse/Logic/Defines/Typealiases.swift diff --git a/Modules/Sources/Browse/Logic/Defines/Typealiases.swift b/Modules/Sources/Browse/Logic/Defines/Typealiases.swift deleted file mode 100644 index c7f48d8..0000000 --- a/Modules/Sources/Browse/Logic/Defines/Typealiases.swift +++ /dev/null @@ -1,9 +0,0 @@ -// -// Typealiases.swift -// Browse -// -// Created by Javier Cicchelli on 14/12/2022. -// Copyright © 2022 Röck+Cöde. All rights reserved. -// - -typealias ItemIdClosure = (String) -> Void diff --git a/Modules/Sources/Browse/UI/Components/DocumentItem.swift b/Modules/Sources/Browse/UI/Components/DocumentItem.swift index 7d30db0..7a6ae70 100644 --- a/Modules/Sources/Browse/UI/Components/DocumentItem.swift +++ b/Modules/Sources/Browse/UI/Components/DocumentItem.swift @@ -6,6 +6,7 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // +import DataModels import SwiftUI struct DocumentItem: View { @@ -13,15 +14,15 @@ struct DocumentItem: View { // MARK: Properties let item: FileSystemItem - let select: ItemIdClosure - let download: ItemIdClosure - let delete: ItemIdClosure + let select: ActionClosure + let download: ActionClosure + let delete: ActionClosure // MARK: Body var body: some View { Button { - select(item.id) + select() } label: { HStack(spacing: 16) { Image.document @@ -50,7 +51,7 @@ struct DocumentItem: View { allowsFullSwipe: true ) { Button { - delete(item.id) + delete() } label: { Label { Text( @@ -64,7 +65,7 @@ struct DocumentItem: View { .tint(.red) Button { - download(item.id) + download() } label: { Label { Text( @@ -103,12 +104,12 @@ struct DocumentItem_Previews: PreviewProvider { contentType: "some content type", size: .random(in: 1 ... 100), lastModifiedAt: .now - )) { _ in - // select closure with item id. - } download: { _ in - // download closure with item id. - } delete: { _ in - // delete closure with item id. + )) { + // select closure. + } download: { + // download closure. + } delete: { + // delete closure. } .previewDisplayName("Document item") @@ -118,12 +119,12 @@ struct DocumentItem_Previews: PreviewProvider { contentType: "some content type", size: .random(in: 1 ... 100), lastModifiedAt: .now - )) { _ in - // select closure with item id. - } download: { _ in - // download closure with item id. - } delete: { _ in - // delete closure with item id. + )) { + // select closure. + } download: { + // download closure. + } delete: { + // delete closure. } .previewDisplayName("Document item with long name") } diff --git a/Modules/Sources/Browse/UI/Components/FolderItem.swift b/Modules/Sources/Browse/UI/Components/FolderItem.swift index bce7697..40733f4 100644 --- a/Modules/Sources/Browse/UI/Components/FolderItem.swift +++ b/Modules/Sources/Browse/UI/Components/FolderItem.swift @@ -6,6 +6,7 @@ // Copyright © 2022 Röck+Cöde. All rights reserved. // +import DataModels import SwiftUI struct FolderItem: View { @@ -13,14 +14,14 @@ struct FolderItem: View { // MARK: Properties let item: FileSystemItem - let select: ItemIdClosure - let delete: ItemIdClosure + let select: ActionClosure + let delete: ActionClosure // MARK: Body var body: some View { Button { - select(item.id) + select() } label: { HStack(spacing: 16) { Image.folder @@ -42,7 +43,7 @@ struct FolderItem: View { allowsFullSwipe: true ) { Button { - delete(item.id) + delete() } label: { Label { Text( @@ -73,20 +74,20 @@ struct BrowseItem_Previews: PreviewProvider { FolderItem(item: Folder( id: "1234567890", name: "Some folder name goes in here..." - )) { _ in - // select closure with item id. - } delete: { _ in - // delete closure with item id. + )) { + // select closure. + } delete: { + // delete closure. } .previewDisplayName("Folder item") FolderItem(item: Folder( id: "1234567890", name: "Some very, extremely long folder name goes in here..." - )) { _ in - // select closure with item id. - } delete: { _ in - // delete closure with item id. + )) { + // select closure. + } delete: { + // delete closure. } .previewDisplayName("Folder item with long name") } From 9b74420ee454f9a574f7c36499e493476f3db629 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 23:59:00 +0100 Subject: [PATCH 23/32] Moved the GetUserUseCase use case around a bit. --- .../Sources/UseCases/{Use Cases => Users}/GetUserUseCase.swift | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Libraries/Sources/UseCases/{Use Cases => Users}/GetUserUseCase.swift (100%) diff --git a/Libraries/Sources/UseCases/Use Cases/GetUserUseCase.swift b/Libraries/Sources/UseCases/Users/GetUserUseCase.swift similarity index 100% rename from Libraries/Sources/UseCases/Use Cases/GetUserUseCase.swift rename to Libraries/Sources/UseCases/Users/GetUserUseCase.swift From 12104f90dd88d6922d306a9ef9f175c76d779ecd Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Wed, 14 Dec 2022 23:59:33 +0100 Subject: [PATCH 24/32] Implemented the StackNavigationViewModifier view modifier for the Browse module. --- .../StackNavigationViewModifier.swift | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Modules/Sources/Browse/UI/View Modifiers/StackNavigationViewModifier.swift diff --git a/Modules/Sources/Browse/UI/View Modifiers/StackNavigationViewModifier.swift b/Modules/Sources/Browse/UI/View Modifiers/StackNavigationViewModifier.swift new file mode 100644 index 0000000..72c3c12 --- /dev/null +++ b/Modules/Sources/Browse/UI/View Modifiers/StackNavigationViewModifier.swift @@ -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: 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() + } + ) + } + +} From a393449bee534c8e9c445fe6b180d89f48d066ce Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 15 Dec 2022 00:03:10 +0100 Subject: [PATCH 25/32] Integrated the StackNavigationViewModifier view modifier by the "navigate(to:tagged:in:)" function in the View+ViewModifiers extension for the Browse module. --- .../UI/Extensions/View+ViewModifiers.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Modules/Sources/Browse/UI/Extensions/View+ViewModifiers.swift diff --git a/Modules/Sources/Browse/UI/Extensions/View+ViewModifiers.swift b/Modules/Sources/Browse/UI/Extensions/View+ViewModifiers.swift new file mode 100644 index 0000000..7b199c9 --- /dev/null +++ b/Modules/Sources/Browse/UI/Extensions/View+ViewModifiers.swift @@ -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 + ) -> some View { + modifier(StackNavigationViewModifier( + tag: tag, + stack: stack, + destination: { destination } + )) + } +} From 0f0773992ff31b0be07df7ab47541feea01f81ea Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 15 Dec 2022 00:15:13 +0100 Subject: [PATCH 26/32] Defined the Stack enumeration for the Browse module. --- .../Browse/UI/Enumerations/Stack.swift | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 Modules/Sources/Browse/UI/Enumerations/Stack.swift diff --git a/Modules/Sources/Browse/UI/Enumerations/Stack.swift b/Modules/Sources/Browse/UI/Enumerations/Stack.swift new file mode 100644 index 0000000..d43cd4a --- /dev/null +++ b/Modules/Sources/Browse/UI/Enumerations/Stack.swift @@ -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 = "-" + } +} From 58a73fa6378083fd5df0e8971882f257d3a7b706 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 15 Dec 2022 00:16:59 +0100 Subject: [PATCH 27/32] Implemented the stack navigation of the folder items in the BrowseView view for the Browse module. --- .../Sources/Browse/UI/Views/BrowseView.swift | 42 +++++++++++++++---- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/Modules/Sources/Browse/UI/Views/BrowseView.swift b/Modules/Sources/Browse/UI/Views/BrowseView.swift index 6e0d9b1..ab7dcfe 100644 --- a/Modules/Sources/Browse/UI/Views/BrowseView.swift +++ b/Modules/Sources/Browse/UI/Views/BrowseView.swift @@ -19,6 +19,7 @@ public struct BrowseView: View { // MARK: States @State private var items: [any FileSystemItemIdentifiable] = [] + @State private var stack: Stack? // MARK: Properties @@ -49,17 +50,13 @@ public struct BrowseView: View { List(items, id: \.id) { item in switch item { case is Folder: - FolderItem(item: item) { id in - // TODO: browse to the item id in another view. - } delete: { id in - // TODO: delete the item id from the backend. - } + makeFolderItem(for: item) case is Document: - DocumentItem(item: item) { id in + DocumentItem(item: item) { // TODO: show the item id in a viewer... - } download: { id in + } download: { // TODO: download the item id from the backend. - } delete: { id in + } delete: { // TODO: delete the item id from the backend. } default: @@ -80,6 +77,35 @@ public struct BrowseView: View { } } } + +// MARK: - UI + +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. + } + .navigate( + to: BrowseView( + folder: folder, + createFolder: createFolder, + uploadFile: uploadFile, + showProfile: showProfile + ), + tagged: .browse(folder), + in: $stack + ) + } +} // MARK: - Helpers From 3d21e0f78a092b5ea848ed1e108793940708939c Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 15 Dec 2022 01:07:40 +0100 Subject: [PATCH 28/32] Implemented the LoadingView component for the Browse module. --- .../Resources/en.lproj/Localizable.strings | 7 ++++ .../Browse/UI/Components/LoadingView.swift | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 Modules/Sources/Browse/UI/Components/LoadingView.swift diff --git a/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings b/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings index 2724f95..7eaa1c0 100644 --- a/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings +++ b/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings @@ -6,9 +6,16 @@ Copyright © 2022 Röck+Cöde. All rights reserved. */ +// Loading + +"loading.loading_data.text" = "Loading data\nfrom the API..."; + +// Browse + "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"; diff --git a/Modules/Sources/Browse/UI/Components/LoadingView.swift b/Modules/Sources/Browse/UI/Components/LoadingView.swift new file mode 100644 index 0000000..d57ec8d --- /dev/null +++ b/Modules/Sources/Browse/UI/Components/LoadingView.swift @@ -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() + } +} From 3e70186959d915587e0c62a01a4faca06ca241f0 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 15 Dec 2022 01:51:21 +0100 Subject: [PATCH 29/32] Implemented the MessageView component for the Browse module. --- .../Resources/en.lproj/Localizable.strings | 16 ++- .../Browse/UI/Components/MessageView.swift | 134 ++++++++++++++++++ 2 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 Modules/Sources/Browse/UI/Components/MessageView.swift diff --git a/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings b/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings index 7eaa1c0..d6f8df1 100644 --- a/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings +++ b/Modules/Sources/Browse/Resources/en.lproj/Localizable.strings @@ -6,11 +6,23 @@ Copyright © 2022 Röck+Cöde. All rights reserved. */ -// Loading +// LoadingView "loading.loading_data.text" = "Loading data\nfrom the API..."; -// Browse +// 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"; diff --git a/Modules/Sources/Browse/UI/Components/MessageView.swift b/Modules/Sources/Browse/UI/Components/MessageView.swift new file mode 100644 index 0000000..eaa6356 --- /dev/null +++ b/Modules/Sources/Browse/UI/Components/MessageView.swift @@ -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") + } +} From c735bd0381671600a3d5ecb32bd6ee0100e8b18d Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 15 Dec 2022 02:13:14 +0100 Subject: [PATCH 30/32] Implemented the support for the view status in the BrowseView view for the Browse module. --- BeReal/UI/Views/ContentView.swift | 6 +- .../Sources/Browse/UI/Views/BrowseView.swift | 134 +++++++++++++----- 2 files changed, 103 insertions(+), 37 deletions(-) diff --git a/BeReal/UI/Views/ContentView.swift b/BeReal/UI/Views/ContentView.swift index e0c3f70..a3b8a2c 100644 --- a/BeReal/UI/Views/ContentView.swift +++ b/BeReal/UI/Views/ContentView.swift @@ -38,6 +38,8 @@ struct ContentView: View { // TODO: upload a new file } showProfile: { showSheet = .profile + } login: { + showSheet = .login } .sheet(item: $showSheet) { sheet in switch sheet { @@ -71,6 +73,7 @@ private extension ContentView { let createFolder: ActionClosure let uploadFile: ActionClosure let showProfile: ActionClosure + let login: ActionClosure // MARK: Body @@ -84,7 +87,8 @@ private extension ContentView { ), createFolder: createFolder, uploadFile: uploadFile, - showProfile: showProfile + showProfile: showProfile, + login: login ) } } else { diff --git a/Modules/Sources/Browse/UI/Views/BrowseView.swift b/Modules/Sources/Browse/UI/Views/BrowseView.swift index ab7dcfe..030c0ed 100644 --- a/Modules/Sources/Browse/UI/Views/BrowseView.swift +++ b/Modules/Sources/Browse/UI/Views/BrowseView.swift @@ -18,6 +18,7 @@ public struct BrowseView: View { // MARK: States + @State private var status: ViewStatus = .loading @State private var items: [any FileSystemItemIdentifiable] = [] @State private var stack: Stack? @@ -27,6 +28,7 @@ public struct BrowseView: View { private let createFolder: ActionClosure private let uploadFile: ActionClosure private let showProfile: ActionClosure + private let login: ActionClosure private let getItems: GetItemsUseCase = .init() @@ -36,51 +38,83 @@ public struct BrowseView: View { 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(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() + content + .navigationTitle(folder.name) + .toolbar { + BrowseToolbar( + createFolder: createFolder, + uploadFile: uploadFile, + showProfile: showProfile + ) + } + .task(id: folder) { + await loadItems() } - } - .listStyle(.inset) - .navigationTitle(folder.name) - .toolbar { - BrowseToolbar( - createFolder: createFolder, - uploadFile: uploadFile, - showProfile: showProfile - ) - } - .task(id: folder) { - await getItemsOrStop() - } } } // 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() + } + } + .listStyle(.inset) + case .empty: + MessageView( + type: .empty, + action: uploadFile + ) + case .error: + MessageView(type: .error) { + Task { + await loadItems() + } + } + } + } + + // MARK: Functions + @ViewBuilder func makeFolderItem( for item: any FileSystemItemIdentifiable ) -> some View { @@ -99,32 +133,58 @@ private extension BrowseView { folder: folder, createFolder: createFolder, uploadFile: uploadFile, - showProfile: showProfile + showProfile: showProfile, + login: login ), tagged: .browse(folder), in: $stack ) } + } // MARK: - Helpers private extension BrowseView { - func getItemsOrStop() async { - guard let account else { return } + func loadItems() async { + guard let account else { + status = .noCredentials + return + } do { - items = try await getItems( + 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 { - // TODO: handle the error appropriately. + status = .error } } } +// MARK: - Enumerations + +private extension BrowseView { + enum ViewStatus { + case noCredentials + case loading + case loaded + case empty + case error + } +} + // MARK: - Previews struct BrowseView_Previews: PreviewProvider { @@ -134,11 +194,13 @@ struct BrowseView_Previews: PreviewProvider { id: UUID().uuidString, name: "Some folder name" )) { - // ... + // create folder closure. } uploadFile: { - // ... + // upload file closure. } showProfile: { - // ... + // show profile closure. + } login: { + // login closure. } } } From 6286086d2cfd011e3549d7838764462b34ea4ad7 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 15 Dec 2022 02:23:12 +0100 Subject: [PATCH 31/32] Tweaked the UI and localisations in the LoginView view for the Login module. --- .../Sources/Login/Resources/en.lproj/Localizable.strings | 7 ++++--- Modules/Sources/Login/UI/Components/LoginForm.swift | 7 ++++--- Modules/Sources/Login/UI/Views/LoginView.swift | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Modules/Sources/Login/Resources/en.lproj/Localizable.strings b/Modules/Sources/Login/Resources/en.lproj/Localizable.strings index f571afc..38fe90b 100644 --- a/Modules/Sources/Login/Resources/en.lproj/Localizable.strings +++ b/Modules/Sources/Login/Resources/en.lproj/Localizable.strings @@ -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."; diff --git a/Modules/Sources/Login/UI/Components/LoginForm.swift b/Modules/Sources/Login/UI/Components/LoginForm.swift index 5bb0711..fe2d598 100644 --- a/Modules/Sources/Login/UI/Components/LoginForm.swift +++ b/Modules/Sources/Login/UI/Components/LoginForm.swift @@ -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() diff --git a/Modules/Sources/Login/UI/Views/LoginView.swift b/Modules/Sources/Login/UI/Views/LoginView.swift index 16fac56..a88dade 100644 --- a/Modules/Sources/Login/UI/Views/LoginView.swift +++ b/Modules/Sources/Login/UI/Views/LoginView.swift @@ -38,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 @@ -107,7 +108,7 @@ fileprivate extension LoginView { } .labelStyle(.logIn) } - .tint(.orange) + .tint(.red) .buttonStyle(.borderedProminent) .buttonBorderShape(.roundedRectangle(radius: 8)) .controlSize(.large) From 1e513e688ccfdb7e1b419c31233459874f093355 Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Thu, 15 Dec 2022 02:29:59 +0100 Subject: [PATCH 32/32] Tweaked the UI in the ProfileView view for the Profile module. --- .../Profile/UI/Views/ProfileView.swift | 71 ++++++++++--------- 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/Modules/Sources/Profile/UI/Views/ProfileView.swift b/Modules/Sources/Profile/UI/Views/ProfileView.swift index 1536a35..e508eb7 100644 --- a/Modules/Sources/Profile/UI/Views/ProfileView.swift +++ b/Modules/Sources/Profile/UI/Views/ProfileView.swift @@ -47,37 +47,40 @@ public struct ProfileView: View { } .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) - ) - ] - ) + 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 { @@ -91,14 +94,14 @@ public struct ProfileView: View { .foregroundColor(.primary) .frame(maxWidth: .infinity) } - .tint(.orange) + .tint(.red) + .controlSize(.large) .buttonStyle(.borderedProminent) .buttonBorderShape(.roundedRectangle(radius: 8)) - .controlSize(.large) } .listRowBackground(Color.clear) } - .background(Color.red) + .background(Color.primary.colorInvert()) } }