From ee1c0d9aedcb4eaa5ef4b50455b818c96e63b850 Mon Sep 17 00:00:00 2001 From: Vicnet <35202538+VincentMeilinger@users.noreply.github.com> Date: Sat, 30 Sep 2023 10:07:27 +0200 Subject: [PATCH] Basic Edit View and components --- .../project.pbxproj | 26 +++- .../AccentColor.colorset/Contents.json | 11 -- .../Contents.json | 0 .../nc-logo-white.imageset/Contents.json | 21 --- .../nc-logo-white.imageset/logo-white.png | Bin 14881 -> 0 bytes .../Data/DataModels.swift | 88 +++++++++--- .../Extensions/ColorExtension.swift | 18 +++ .../Extensions/DateExtension.swift | 21 +++ ...APIInterface.swift => APIController.swift} | 16 ++- .../Network/NetworkHandler.swift | 3 +- ...Nextcloud_Cookbook_iOS_Client.entitlements | 10 +- .../Nextcloud_Cookbook_iOS_ClientApp.swift | 5 +- .../ViewModels/MainViewModel.swift | 3 +- .../Views/CategoryCardView.swift | 2 +- .../Views/CategoryDetailView.swift | 4 +- .../Views/MainView.swift | 42 +++--- .../Views/OnboardingView.swift | 112 ++++++++++++--- .../Views/RecipeCardView.swift | 17 ++- .../Views/RecipeDetailView.swift | 68 ++++----- .../Views/RecipeEditView.swift | 88 ++++++++++++ .../Views/SettingsView.swift | 136 +++++++++++------- 21 files changed, 485 insertions(+), 206 deletions(-) delete mode 100644 Nextcloud Cookbook iOS Client/Assets.xcassets/AccentColor.colorset/Contents.json rename Nextcloud Cookbook iOS Client/Assets.xcassets/{accent.colorset => backgroundHighlight.colorset}/Contents.json (100%) delete mode 100644 Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/Contents.json delete mode 100644 Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/logo-white.png create mode 100644 Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift create mode 100644 Nextcloud Cookbook iOS Client/Extensions/DateExtension.swift rename Nextcloud Cookbook iOS Client/Network/{APIInterface.swift => APIController.swift} (95%) create mode 100644 Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index 79500ac..1bf0620 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -29,7 +29,10 @@ A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserDefaults.swift */; }; A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CC2AB501B100064C43 /* SettingsView.swift */; }; A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */; }; - A703226D2ABAF90D00D7C4ED /* APIInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226C2ABAF90D00D7C4ED /* APIInterface.swift */; }; + A703226D2ABAF90D00D7C4ED /* APIController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226C2ABAF90D00D7C4ED /* APIController.swift */; }; + A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */; }; + A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */; }; + A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -76,7 +79,10 @@ A70171CA2AB4CD1700064C43 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; A70171CC2AB501B100064C43 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCoderExtension.swift; sourceTree = ""; }; - A703226C2ABAF90D00D7C4ED /* APIInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIInterface.swift; sourceTree = ""; }; + A703226C2ABAF90D00D7C4ED /* APIController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIController.swift; sourceTree = ""; }; + A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorExtension.swift; sourceTree = ""; }; + A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeEditView.swift; sourceTree = ""; }; + A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -168,7 +174,7 @@ A70171B22AB211F000064C43 /* Network */ = { isa = PBXGroup; children = ( - A703226C2ABAF90D00D7C4ED /* APIInterface.swift */, + A703226C2ABAF90D00D7C4ED /* APIController.swift */, A70171B32AB2122900064C43 /* NetworkRequests.swift */, A70171AE2AB2116B00064C43 /* NetworkHandler.swift */, A70171B02AB211DF00064C43 /* CustomError.swift */, @@ -192,6 +198,7 @@ A70171BD2AB4987900064C43 /* CategoryDetailView.swift */, A70171C12AB498C600064C43 /* RecipeCardView.swift */, A70171BF2AB498A900064C43 /* RecipeDetailView.swift */, + A70D7CA02AC73CA700D53DBF /* RecipeEditView.swift */, A70171C82AB4CBB400064C43 /* OnboardingView.swift */, A70171CC2AB501B100064C43 /* SettingsView.swift */, ); @@ -212,7 +219,9 @@ isa = PBXGroup; children = ( A70171B82AB399FB00064C43 /* DateFormatterExtension.swift */, + A70D7CA22AC74B3B00D53DBF /* DateExtension.swift */, A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */, + A703226E2ABB1DD700D7C4ED /* ColorExtension.swift */, ); path = Extensions; sourceTree = ""; @@ -347,6 +356,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + A70D7CA12AC73CA800D53DBF /* RecipeEditView.swift in Sources */, + A70D7CA32AC74B3B00D53DBF /* DateExtension.swift in Sources */, A70171B12AB211DF00064C43 /* CustomError.swift in Sources */, A70171C42AB4A31200064C43 /* DataStore.swift in Sources */, A70171AF2AB2116B00064C43 /* NetworkHandler.swift in Sources */, @@ -361,10 +372,11 @@ A70171842AA8E71900064C43 /* MainView.swift in Sources */, A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */, A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */, - A703226D2ABAF90D00D7C4ED /* APIInterface.swift in Sources */, + A703226D2ABAF90D00D7C4ED /* APIController.swift in Sources */, A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */, A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */, A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */, + A703226F2ABB1DD700D7C4ED /* ColorExtension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -516,6 +528,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\""; @@ -539,7 +552,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; @@ -556,6 +569,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"Nextcloud Cookbook iOS Client/Preview Content\""; @@ -579,7 +593,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = "VincentMeilinger.Nextcloud-Cookbook-iOS-Client"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AccentColor.colorset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/Nextcloud Cookbook iOS Client/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/accent.colorset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/backgroundHighlight.colorset/Contents.json similarity index 100% rename from Nextcloud Cookbook iOS Client/Assets.xcassets/accent.colorset/Contents.json rename to Nextcloud Cookbook iOS Client/Assets.xcassets/backgroundHighlight.colorset/Contents.json diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/Contents.json deleted file mode 100644 index 4d0df25..0000000 --- a/Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "logo-white.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/logo-white.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/logo-white.png deleted file mode 100644 index 47b6ce4e8255493736b5e66f96320b720484a736..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14881 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 RecipeDetail { return RecipeDetail( @@ -69,6 +107,11 @@ struct RecipeImage { var full: UIImage? } + + + +// Login flow + struct LoginV2Request: Codable { let poll: LoginV2Poll let login: String @@ -84,3 +127,16 @@ struct LoginV2Response: Codable { let loginName: String let appPassword: String } + +struct LoginValidation: Codable { + let ocs: Ocs +} + +struct Ocs: Codable { + let meta: MetaData +} + +struct MetaData: Codable { + let status: String + let statuscode: Int +} diff --git a/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift b/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift new file mode 100644 index 0000000..dce32f7 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Extensions/ColorExtension.swift @@ -0,0 +1,18 @@ +// +// ColorExtension.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 20.09.23. +// + +import Foundation +import SwiftUI + +extension Color { + public static var nextcloudBlue: Color { + return Color("ncblue") + } + public static var backgroundHighlight: Color { + return Color("backgroundHighlight") + } +} diff --git a/Nextcloud Cookbook iOS Client/Extensions/DateExtension.swift b/Nextcloud Cookbook iOS Client/Extensions/DateExtension.swift new file mode 100644 index 0000000..77c9e19 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Extensions/DateExtension.swift @@ -0,0 +1,21 @@ +// +// DateExtension.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 29.09.23. +// + +import Foundation + +extension Date { + static var zero: Date { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm" + + if let date = dateFormatter.date(from:"00:00") { + return date + } else { + return Date() + } + } +} diff --git a/Nextcloud Cookbook iOS Client/Network/APIInterface.swift b/Nextcloud Cookbook iOS Client/Network/APIController.swift similarity index 95% rename from Nextcloud Cookbook iOS Client/Network/APIInterface.swift rename to Nextcloud Cookbook iOS Client/Network/APIController.swift index abd809f..e8598ad 100644 --- a/Nextcloud Cookbook iOS Client/Network/APIInterface.swift +++ b/Nextcloud Cookbook iOS Client/Network/APIController.swift @@ -1,5 +1,5 @@ // -// APIInterface.swift +// APIController.swift // Nextcloud Cookbook iOS Client // // Created by Vincent Meilinger on 20.09.23. @@ -7,7 +7,7 @@ import Foundation -class APIInterface { +class APIController { var userSettings: UserSettings var apiPath: String @@ -15,7 +15,7 @@ class APIInterface { let apiVersion = "1" init(userSettings: UserSettings) { - print("Initializing NetworkController.") + print("Initializing APIController.") self.userSettings = userSettings self.apiPath = "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v\(apiVersion)/" @@ -24,7 +24,11 @@ class APIInterface { let loginData = loginString.data(using: String.Encoding.utf8)! self.authString = loginData.base64EncodedString() } - +} + + + +extension APIController { func imageDataFromServer(recipeId: Int, thumb: Bool) async -> Data? { do { let request = RequestWrapper.imageRequest(path: .IMAGE(recipeId: recipeId, thumb: thumb)) @@ -43,9 +47,7 @@ class APIInterface { } return nil } -} - -extension APIInterface { + func sendDataRequest(_ request: RequestWrapper) async -> (D?, Error?) { do { let (data, error) = try await NetworkHandler.sendHTTPRequest( diff --git a/Nextcloud Cookbook iOS Client/Network/NetworkHandler.swift b/Nextcloud Cookbook iOS Client/Network/NetworkHandler.swift index e37b49f..67cd3a9 100644 --- a/Nextcloud Cookbook iOS Client/Network/NetworkHandler.swift +++ b/Nextcloud Cookbook iOS Client/Network/NetworkHandler.swift @@ -49,7 +49,7 @@ struct NetworkHandler { request.httpBody = body } - print("Request:\nMethod: \(request.httpMethod)\nHeaders: \(request.allHTTPHeaderFields)\nBody: \(request.httpBody)") + print("Request:\nMethod: \(request.httpMethod)\nPath: \(request.url?.absoluteString)\nHeaders: \(request.allHTTPHeaderFields)\nBody: \(request.httpBody)") // Wait for and return data and (decoded) response var data: Data? = nil @@ -57,6 +57,7 @@ struct NetworkHandler { do { (data, response) = try await URLSession.shared.data(for: request) print("Response: ", response) + print("Data: ", data?.description, data, String(data: data ?? Data(), encoding: .utf8)) return (data, nil) } catch { return (nil, decodeURLResponse(response: response as? HTTPURLResponse)) diff --git a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements index f2ef3ae..625af03 100644 --- a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements +++ b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_Client.entitlements @@ -2,9 +2,11 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + diff --git a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift index 3b66654..3378123 100644 --- a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift +++ b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift @@ -17,7 +17,10 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App { if userSettings.onboarding { OnboardingView(userSettings: userSettings) } else { - MainView(userSettings: userSettings, viewModel: mainViewModel) + MainView(viewModel: mainViewModel, userSettings: userSettings) + .onAppear { + mainViewModel.apiInterface = APIController(userSettings: userSettings) + } } }.transition(.slide) } diff --git a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift index 63fd4e0..fd51b07 100644 --- a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift +++ b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift @@ -7,6 +7,7 @@ import Foundation import UIKit +import SwiftUI @MainActor class MainViewModel: ObservableObject { @Published var categories: [Category] = [] @@ -15,7 +16,7 @@ import UIKit private var imageCache: [Int: RecipeImage] = [:] let dataStore: DataStore - var apiInterface: APIInterface? = nil + var apiInterface: APIController? = nil /// The path of an image in storage private var localImagePath: (Int, Bool) -> (String) = { recipeId, thumb in diff --git a/Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift b/Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift index 14ba63b..2c809ff 100644 --- a/Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift +++ b/Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift @@ -23,7 +23,7 @@ struct CategoryCardView: View { .ultraThickMaterial ) .overlay( - Text(category.name) + Text(category.name == "*" ? "Other" : category.name) .font(.headline) ) .frame(maxHeight: 25) diff --git a/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift b/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift index 64b5c9c..9d82cc7 100644 --- a/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift +++ b/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift @@ -20,14 +20,14 @@ struct RecipeBookView: View { if let recipes = viewModel.recipes[categoryName] { ForEach(recipes, id: \.recipe_id) { recipe in NavigationLink(destination: RecipeDetailView(viewModel: viewModel, recipe: recipe)) { - RecipeCardView(viewModel: viewModel, recipe: recipe, isDownloaded: viewModel.recipeDetailExists(recipeId: recipe.recipe_id)) + RecipeCardView(viewModel: viewModel, recipe: recipe) } .buttonStyle(.plain) } } } } - .navigationTitle(categoryName) + .navigationTitle(categoryName == "*" ? "Other" : categoryName) .toolbar { Menu { Button { diff --git a/Nextcloud Cookbook iOS Client/Views/MainView.swift b/Nextcloud Cookbook iOS Client/Views/MainView.swift index de64daa..2ea2bd6 100644 --- a/Nextcloud Cookbook iOS Client/Views/MainView.swift +++ b/Nextcloud Cookbook iOS Client/Views/MainView.swift @@ -10,33 +10,30 @@ import SwiftUI struct MainView: View { @ObservedObject var viewModel: MainViewModel @ObservedObject var userSettings: UserSettings - var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)] - init(userSettings: UserSettings, viewModel: MainViewModel) { - self.userSettings = userSettings - self.viewModel = viewModel - self.viewModel.apiInterface = APIInterface(userSettings: userSettings) - - } + @State var showEditView: Bool = false + var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)] var body: some View { NavigationView { ScrollView(.vertical, showsIndicators: false) { LazyVGrid(columns: columns, spacing: 0) { ForEach(viewModel.categories, id: \.name) { category in - NavigationLink( - destination: RecipeBookView( - categoryName: category.name, - viewModel: viewModel) - ) { - CategoryCardView(category: category) + if category.recipe_count != 0 { + NavigationLink( + destination: RecipeBookView( + categoryName: category.name, + viewModel: viewModel + ) + ) { + CategoryCardView(category: category) + } + .buttonStyle(.plain) } - .buttonStyle(.plain) } } - .padding(.horizontal) } - .navigationTitle("CookBook") + .navigationTitle("Cookbooks") .toolbar { Menu { Button { @@ -51,14 +48,27 @@ struct MainView: View { } } + Button { + showEditView = true + } label: { + HStack { + Text("Create new recipe") + Image(systemName: "plus.circle") + } + } } label: { Image(systemName: "ellipsis.circle") } + NavigationLink( destination: SettingsView(userSettings: userSettings, viewModel: viewModel)) { Image(systemName: "gearshape") } } + .background( + NavigationLink(destination: RecipeEditView(), isActive: $showEditView) { EmptyView() } + ) } + .tint(.nextcloudBlue) .task { await viewModel.loadCategoryList() } diff --git a/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift b/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift index 02a1e36..626ba54 100644 --- a/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift +++ b/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift @@ -19,7 +19,7 @@ struct OnboardingView: View { } .tabViewStyle(.page) .background( - selectedTab == 1 ? Color("ncblue").ignoresSafeArea() : Color(uiColor: .systemBackground).ignoresSafeArea() + selectedTab == 1 ? Color.nextcloudBlue.ignoresSafeArea() : Color(uiColor: .systemBackground).ignoresSafeArea() ) .animation(.easeInOut, value: selectedTab) } @@ -33,16 +33,13 @@ struct WelcomeTab: View { .resizable() .frame(width: 120, height: 120) .clipShape(RoundedRectangle(cornerRadius: 10)) - Text("Tank you for downloading") + Text("Tank you for downloading the") .font(.headline) - Text("Nextcloud") - .font(.largeTitle) - .bold() - Text("Cookbook") + Text("Cookbook Client") .font(.largeTitle) .bold() Spacer() - Text("This application is an open source effort and still in development. If you encounter any problems, please report them on our GitHub page.\n\nCurrently, only app token login is supported. You can create an app token in the nextcloud security settings.") + Text("This application is an open source effort and still in development. If you encounter any problems, please report them on our GitHub page.") .padding() Spacer() } @@ -54,12 +51,18 @@ struct WelcomeTab: View { struct LoginTab: View { @ObservedObject var userSettings: UserSettings + // Login flow enum LoginMethod { case v2, token } @State var selectedLoginMethod: LoginMethod = .v2 @State var loginRequest: LoginV2Request? = nil + // Login error alert + @State var showAlert: Bool = false + @State var alertMessage: String = "Error: Could not connect to server." + + // TextField handling enum Field { case server case username @@ -70,15 +73,7 @@ struct LoginTab: View { var body: some View { ScrollView(showsIndicators: false) { VStack(alignment: .leading) { - HStack { - Spacer() - Image("nc-logo-white") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxHeight: 150) - .padding() - Spacer() - } + Spacer() Picker("Login Method", selection: $selectedLoginMethod) { Text("Nextcloud Login").tag(LoginMethod.v2) Text("App Token Login").tag(LoginMethod.token) @@ -109,7 +104,11 @@ struct LoginTab: View { HStack{ Spacer() Button { - userSettings.onboarding = false + Task { + if await loginCheck(nextcloudLogin: false) { + userSettings.onboarding = false + } + } } label: { Text("Submit") .foregroundColor(.white) @@ -138,20 +137,43 @@ struct LoginTab: View { await sendLoginV2Request() if let loginRequest = loginRequest { await UIApplication.shared.open(URL(string: loginRequest.login)!) + } else { + alertMessage = "Unable to reach server. Please check your server address and internet connection." + showAlert = true } } } - Text("Submitting will open a web browser. Please follow the login instructions provided there.\nAfter a successfull login, return to this application and press 'Validate'.") + Text("Entering the server address will open a web browser. Please follow the login instructions provided there. If the browser does not open, click the link 'Open in browser'\nAfter a successfull login, return to this application and press 'Validate'.") .font(.subheadline) .padding(.bottom) - .tint(.white) + .foregroundStyle(.white) + Button { + Task { + await sendLoginV2Request() + if let loginRequest = loginRequest { + await UIApplication.shared.open(URL(string: loginRequest.login)!) + } else { + alertMessage = "Unable to reach server. Please check your server address and internet connection." + showAlert = true + } + } + } label: { + Text("Open in browser") + .foregroundColor(.white) + .font(.headline) + } + HStack{ Spacer() Button { // fetch login v2 response Task { - guard let res = await fetchLoginV2Response() else { return } + guard let res = await fetchLoginV2Response() else { + alertMessage = "Login failed. Please login via the browser and try again." + showAlert = true + return + } print("Login successfull for user \(res.loginName)!") userSettings.username = res.loginName userSettings.token = res.appPassword @@ -187,6 +209,9 @@ struct LoginTab: View { } .fontDesign(.rounded) .padding() + .alert(alertMessage, isPresented: $showAlert) { + Button("Ok", role: .cancel) { } + } } } @@ -250,6 +275,53 @@ struct LoginTab: View { print("Could not decode.") return nil } + + func loginCheck(nextcloudLogin: Bool) async -> Bool { + if userSettings.serverAddress == "" { + alertMessage = "Please enter a server address!" + showAlert = true + return false + } else if !nextcloudLogin && (userSettings.username == "" || userSettings.token == "") { + alertMessage = "Please enter a user name and app token!" + showAlert = true + return false + } + let headerFields = [ + HeaderField.ocsRequest(value: true), + ] + let request = RequestWrapper.customRequest( + method: .GET, + path: .CATEGORIES, + headerFields: headerFields, + authenticate: true + ) + + var (data, error): (Data?, Error?) = (nil, nil) + do { + let loginString = "\(userSettings.username):\(userSettings.token)" + let loginData = loginString.data(using: String.Encoding.utf8)! + let authString = loginData.base64EncodedString() + (data, error) = try await NetworkHandler.sendHTTPRequest( + request, + hostPath: "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v1/", + authString: authString + ) + } catch { + print("Error: ", error) + } + guard let data = data else { + alertMessage = "Login failed. Please check your inputs." + showAlert = true + return false + } + if let testRequest: [Category] = JSONDecoder.safeDecode(data) { + print("validationResponse: \(testRequest)") + return true + } + alertMessage = "Login failed. Please check your inputs and internet connection." + showAlert = true + return false + } } struct LoginLabel: View { diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift index e947ba6..bcb019d 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift @@ -12,7 +12,7 @@ struct RecipeCardView: View { @State var viewModel: MainViewModel @State var recipe: Recipe @State var recipeThumb: UIImage? - @State var isDownloaded: Bool + @State var isDownloaded: Bool? = nil var body: some View { HStack { @@ -25,18 +25,21 @@ struct RecipeCardView: View { .font(.headline) Spacer() - VStack { - Image(systemName: isDownloaded ? "checkmark.icloud" : "icloud.and.arrow.down") - .foregroundColor(.secondary) - .padding() - Spacer() + if let isDownloaded = isDownloaded { + VStack { + Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down") + .foregroundColor(.secondary) + .padding() + Spacer() + } } } - .background(.ultraThickMaterial) + .background(Color.backgroundHighlight) .clipShape(RoundedRectangle(cornerRadius: 10)) .padding(.horizontal) .task { recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true) + self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id) } .refreshable { recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: true, needsUpdate: true) diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift index ffdd72b..8d3e0b5 100644 --- a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift +++ b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift @@ -15,35 +15,41 @@ struct RecipeDetailView: View { @State var recipeDetail: RecipeDetail? @State var recipeImage: UIImage? @State var showTitle: Bool = false + @State var isDownloaded: Bool? = nil var body: some View { ScrollView(showsIndicators: false) { VStack(alignment: .leading) { if let recipeImage = recipeImage { Image(uiImage: recipeImage) .resizable() - .aspectRatio(contentMode: .fill) - .frame(height: 300) + .scaledToFill() + .frame(maxHeight: 300) .clipped() - } else { - Color("ncblue") - .frame(height: 150) } if let recipeDetail = recipeDetail { LazyVStack (alignment: .leading) { Divider() - Text(recipeDetail.name) - .font(.title) - .bold() - .padding() - .onDisappear { - showTitle = true - } - .onAppear { - showTitle = false + HStack { + Text(recipeDetail.name) + .font(.title) + .bold() + .padding() + .onDisappear { + showTitle = true + } + .onAppear { + showTitle = false + } + + if let isDownloaded = isDownloaded { + Spacer() + Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down") + .foregroundColor(.secondary) + .padding() } + } Divider() - RecipeYieldSection(recipeDetail: recipeDetail) RecipeDurationSection(recipeDetail: recipeDetail) LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) { if(!recipeDetail.recipeIngredient.isEmpty) { @@ -59,13 +65,14 @@ struct RecipeDetailView: View { }.padding(.horizontal, 5) } - } + }.animation(.easeInOut, value: recipeImage) } .navigationBarTitleDisplayMode(.inline) .navigationTitle(showTitle ? recipe.name : "") .task { recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id) recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, thumb: false) + self.isDownloaded = viewModel.recipeDetailExists(recipeId: recipe.recipe_id) } .refreshable { recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true) @@ -75,31 +82,18 @@ struct RecipeDetailView: View { } } -struct RecipeYieldSection: View { - @State var recipeDetail: RecipeDetail - var body: some View { - HStack { - Text("Servings: \(recipeDetail.recipeYield)") - Spacer() - }.padding() - - } -} struct RecipeDurationSection: View { @State var recipeDetail: RecipeDetail var body: some View { - HStack { + HStack(alignment: .center) { if let prepTime = recipeDetail.prepTime { VStack { SecondaryLabel(text: "Prep time") Text(formatDate(duration: prepTime)) .lineLimit(1) }.padding() - .frame(maxWidth: .infinity) - .background(Color("accent")) - .clipShape(RoundedRectangle(cornerRadius: 10)) } if let cookTime = recipeDetail.cookTime { @@ -108,9 +102,6 @@ struct RecipeDurationSection: View { Text(formatDate(duration: cookTime)) .lineLimit(1) }.padding() - .frame(maxWidth: .infinity) - .background(Color("accent")) - .clipShape(RoundedRectangle(cornerRadius: 10)) } if let totalTime = recipeDetail.totalTime { @@ -119,9 +110,6 @@ struct RecipeDurationSection: View { Text(formatDate(duration: totalTime)) .lineLimit(1) }.padding() - .frame(maxWidth: .infinity) - .background(Color("accent")) - .clipShape(RoundedRectangle(cornerRadius: 10)) } } } @@ -134,7 +122,13 @@ struct RecipeIngredientSection: View { VStack(alignment: .leading) { Divider() HStack { - SecondaryLabel(text: "Ingredients") + if recipeDetail.recipeYield == 0 { + SecondaryLabel(text: "Ingredients") + } else if recipeDetail.recipeYield == 1 { + SecondaryLabel(text: "Ingredients per serving") + } else { + SecondaryLabel(text: "Ingredients for \(recipeDetail.recipeYield) servings") + } Spacer() } ForEach(recipeDetail.recipeIngredient, id: \.self) { ingredient in diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift new file mode 100644 index 0000000..a4668e4 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Views/RecipeEditView.swift @@ -0,0 +1,88 @@ +// +// RecipeEditView.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 29.09.23. +// + +import Foundation +import SwiftUI + + +struct RecipeEditView: View { + @State var recipe: RecipeDetail + + @State var times = [Date.zero, Date.zero, Date.zero] + + init(recipe: RecipeDetail? = nil) { + + self.recipe = recipe ?? RecipeDetail() + } + + var body: some View { + Form { + TextField("Title", text: $recipe.name) + Section() { + DatePicker("Prep time:", selection: $times[0], displayedComponents: .hourAndMinute) + DatePicker("Cook time:", selection: $times[1], displayedComponents: .hourAndMinute) + DatePicker("Total time:", selection: $times[2], displayedComponents: .hourAndMinute) + } + + Section() { + + List { + ForEach(recipe.recipeInstructions.indices, id: \.self) { ix in + HStack(alignment: .top) { + Text("\(ix+1).") + TextEditor(text: $recipe.recipeInstructions[ix]) + .multilineTextAlignment(.leading) + } + } + .onMove { indexSet, offset in + recipe.recipeInstructions.move(fromOffsets: indexSet, toOffset: offset) + } + .onDelete { indexSet in + recipe.recipeInstructions.remove(atOffsets: indexSet) + } + } + HStack { + Spacer() + Text("Add instruction") + Button() { + recipe.recipeInstructions.append("") + } label: { + Image(systemName: "plus.circle.fill") + } + } + } header: { + HStack { + Text("Ingredients") + Spacer() + EditButton() + } + } + } + } +} + + +struct TimePicker: View { + @Binding var hours: Int + @Binding var minutes: Int + + var body: some View { + HStack { + Picker("", selection: $hours){ + ForEach(0..<99, id: \.self) { i in + Text("\(i) hours").tag(i) + } + }.pickerStyle(.wheel) + Picker("", selection: $minutes){ + ForEach(0..<60, id: \.self) { i in + Text("\(i) min").tag(i) + } + }.pickerStyle(.wheel) + } + .padding(.horizontal) + } +} diff --git a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift index c65b695..7664b31 100644 --- a/Nextcloud Cookbook iOS Client/Views/SettingsView.swift +++ b/Nextcloud Cookbook iOS Client/Views/SettingsView.swift @@ -8,72 +8,98 @@ import Foundation import SwiftUI +fileprivate enum SettingsAlert { + case LOG_OUT, + DELETE_CACHE, + NONE + + func getTitle() -> String { + switch self { + case .LOG_OUT: return "Log out" + case .DELETE_CACHE: return "Delete local data" + default: return "Please confirm your action." + } + } + + func getMessage() -> String { + switch self { + case .LOG_OUT: return "Are you sure that you want to log out of your account?" + case .DELETE_CACHE: return "Are you sure that you want to delete the downloaded recipes? This action will not affect any recipes stored on your server." + default: return "" + } + } +} + struct SettingsView: View { @ObservedObject var userSettings: UserSettings @ObservedObject var viewModel: MainViewModel + @State fileprivate var alertType: SettingsAlert = .NONE + @State var showAlert: Bool = false + var body: some View { - List { - SettingsSection(title: "Language", description: "Language settings coming soon.") - SettingsSection(title: "Accent Color", description: "The accent color setting will be released in a future update.") - SettingsSection(title: "Log out", description: "Log out of your Nextcloud account in this app. Your recipes will be removed from local storage.") - { + Form { + Section() { + Link("Visit the GitHub page", destination: URL(string: "https://github.com/VincentMeilinger/Nextcloud-Cookbook-iOS")!) + } header: { + Text("About") + } footer: { + Text("If you are interested in contributing to this project or simply wish to review its source code, we encourage you to visit the GitHub repository for this application.") + } + + Section() { + Link("Get support", destination: URL(string: "https://vincentmeilinger.github.io/Nextcloud-Cookbook-Client-Support/")!) + } header: { + Text("Support") + } footer: { + Text("If you have any inquiries, feedback, or require assistance, please refer to the support page for contact information.") + } + + Section() { Button("Log out") { print("Log out.") - userSettings.serverAddress = "" - userSettings.username = "" - userSettings.token = "" - userSettings.onboarding = true + alertType = .LOG_OUT + showAlert = true + } - .buttonStyle(.borderedProminent) - .accentColor(.red) - .padding() - } - - SettingsSection(title: "Clear local data", description: "Your recipes will be removed from local storage.") - { - Button("Clear Cache") { + .tint(.red) + + Button("Delete local data.") { print("Clear cache.") - viewModel.deleteAllData() + alertType = .DELETE_CACHE + showAlert = true } - .buttonStyle(.borderedProminent) - .accentColor(.red) - .padding() + .tint(.red) + + } header: { + Text("Danger Zone") } - - }.navigationTitle("Settings") - } -} - - -struct SettingsSection: View { - let title: String - let description: String - @ViewBuilder let content: () -> Content - - init(title: String, description: String, content: @escaping () -> Content) { - self.title = title - self.description = description - self.content = content - } - - init(title: String, description: String) where Content == EmptyView { - self.title = title - self.description = description - self.content = { EmptyView() } - } - - var body: some View { - HStack { - VStack(alignment: .leading) { - Text(title) - .font(.headline) - Text(description) - .font(.caption) - }.padding() - Spacer() - content() } - + .navigationTitle("Settings") + .alert(alertType.getTitle(), isPresented: $showAlert) { + Button("Cancel", role: .cancel) { } + if alertType == .LOG_OUT { + Button("Log out", role: .destructive) { logOut() } + } else if alertType == .DELETE_CACHE { + Button("Delete", role: .destructive) { deleteCache() } + } + } message: { + Text(alertType.getMessage()) + } + } + + func logOut() { + userSettings.serverAddress = "" + userSettings.username = "" + userSettings.token = "" + viewModel.deleteAllData() + userSettings.onboarding = true + } + + func deleteCache() { + //viewModel.deleteAllData() } } + + +