diff --git a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj index b5664a4..63e4ed7 100644 --- a/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj +++ b/Nextcloud Cookbook iOS Client.xcodeproj/project.pbxproj @@ -8,12 +8,26 @@ /* Begin PBXBuildFile section */ A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */; }; - A70171842AA8E71900064C43 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171832AA8E71900064C43 /* ContentView.swift */; }; + A70171842AA8E71900064C43 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171832AA8E71900064C43 /* MainView.swift */; }; A70171862AA8E71F00064C43 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A70171852AA8E71F00064C43 /* Assets.xcassets */; }; A701718A2AA8E71F00064C43 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A70171892AA8E71F00064C43 /* Preview Assets.xcassets */; }; A70171942AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171932AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientTests.swift */; }; A701719E2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A701719D2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITests.swift */; }; A70171A02AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A701719F2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift */; }; + A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171AC2AA8EF4700064C43 /* MainViewModel.swift */; }; + A70171AF2AB2116B00064C43 /* NetworkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171AE2AB2116B00064C43 /* NetworkController.swift */; }; + A70171B12AB211DF00064C43 /* CustomError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B02AB211DF00064C43 /* CustomError.swift */; }; + A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B32AB2122900064C43 /* NetworkRequests.swift */; }; + A70171B92AB399FB00064C43 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171B82AB399FB00064C43 /* Extensions.swift */; }; + A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BB2AB4983500064C43 /* CategoryCardView.swift */; }; + A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BD2AB4987900064C43 /* CategoryDetailView.swift */; }; + A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BF2AB498A900064C43 /* RecipeDetailView.swift */; }; + A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C12AB498C600064C43 /* RecipeCardView.swift */; }; + A70171C42AB4A31200064C43 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C32AB4A31200064C43 /* DataStore.swift */; }; + A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C52AB4C43A00064C43 /* DataModels.swift */; }; + A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C82AB4CBB400064C43 /* OnboardingView.swift */; }; + A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserDefaults.swift */; }; + A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CC2AB501B100064C43 /* SettingsView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,7 +50,7 @@ /* Begin PBXFileReference section */ A701717E2AA8E71900064C43 /* Nextcloud Cookbook iOS Client.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Nextcloud Cookbook iOS Client.app"; sourceTree = BUILT_PRODUCTS_DIR; }; A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientApp.swift; sourceTree = ""; }; - A70171832AA8E71900064C43 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A70171832AA8E71900064C43 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; A70171852AA8E71F00064C43 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Nextcloud_Cookbook_iOS_Client.entitlements; sourceTree = ""; }; A70171892AA8E71F00064C43 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; @@ -45,6 +59,20 @@ A70171992AA8E72000064C43 /* Nextcloud Cookbook iOS ClientUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Nextcloud Cookbook iOS ClientUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; A701719D2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientUITests.swift; sourceTree = ""; }; A701719F2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift; sourceTree = ""; }; + A70171AC2AA8EF4700064C43 /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; }; + A70171AE2AB2116B00064C43 /* NetworkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkController.swift; sourceTree = ""; }; + A70171B02AB211DF00064C43 /* CustomError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomError.swift; sourceTree = ""; }; + A70171B32AB2122900064C43 /* NetworkRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequests.swift; sourceTree = ""; }; + A70171B82AB399FB00064C43 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; + A70171BB2AB4983500064C43 /* CategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryCardView.swift; sourceTree = ""; }; + A70171BD2AB4987900064C43 /* CategoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailView.swift; sourceTree = ""; }; + A70171BF2AB498A900064C43 /* RecipeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailView.swift; sourceTree = ""; }; + A70171C12AB498C600064C43 /* RecipeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCardView.swift; sourceTree = ""; }; + A70171C32AB4A31200064C43 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = ""; }; + A70171C52AB4C43A00064C43 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = ""; }; + A70171C82AB4CBB400064C43 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = ""; }; + A70171CA2AB4CD1700064C43 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; + A70171CC2AB501B100064C43 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -96,7 +124,10 @@ isa = PBXGroup; children = ( A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */, - A70171832AA8E71900064C43 /* ContentView.swift */, + A70171C72AB4C4A100064C43 /* Data */, + A70171BA2AB4980100064C43 /* Views */, + A70171B72AB2445700064C43 /* ViewModels */, + A70171B22AB211F000064C43 /* Network */, A70171852AA8E71F00064C43 /* Assets.xcassets */, A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */, A70171882AA8E71F00064C43 /* Preview Content */, @@ -129,6 +160,49 @@ path = "Nextcloud Cookbook iOS ClientUITests"; sourceTree = ""; }; + A70171B22AB211F000064C43 /* Network */ = { + isa = PBXGroup; + children = ( + A70171B82AB399FB00064C43 /* Extensions.swift */, + A70171B32AB2122900064C43 /* NetworkRequests.swift */, + A70171AE2AB2116B00064C43 /* NetworkController.swift */, + A70171B02AB211DF00064C43 /* CustomError.swift */, + ); + path = Network; + sourceTree = ""; + }; + A70171B72AB2445700064C43 /* ViewModels */ = { + isa = PBXGroup; + children = ( + A70171AC2AA8EF4700064C43 /* MainViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + A70171BA2AB4980100064C43 /* Views */ = { + isa = PBXGroup; + children = ( + A70171832AA8E71900064C43 /* MainView.swift */, + A70171BB2AB4983500064C43 /* CategoryCardView.swift */, + A70171BD2AB4987900064C43 /* CategoryDetailView.swift */, + A70171C12AB498C600064C43 /* RecipeCardView.swift */, + A70171BF2AB498A900064C43 /* RecipeDetailView.swift */, + A70171C82AB4CBB400064C43 /* OnboardingView.swift */, + A70171CC2AB501B100064C43 /* SettingsView.swift */, + ); + path = Views; + sourceTree = ""; + }; + A70171C72AB4C4A100064C43 /* Data */ = { + isa = PBXGroup; + children = ( + A70171C32AB4A31200064C43 /* DataStore.swift */, + A70171C52AB4C43A00064C43 /* DataModels.swift */, + A70171CA2AB4CD1700064C43 /* UserDefaults.swift */, + ); + path = Data; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -259,8 +333,22 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A70171842AA8E71900064C43 /* ContentView.swift in Sources */, + A70171B12AB211DF00064C43 /* CustomError.swift in Sources */, + A70171C42AB4A31200064C43 /* DataStore.swift in Sources */, + A70171AF2AB2116B00064C43 /* NetworkController.swift in Sources */, + A70171B42AB2122900064C43 /* NetworkRequests.swift in Sources */, + A70171B92AB399FB00064C43 /* Extensions.swift in Sources */, + A70171BC2AB4983500064C43 /* CategoryCardView.swift in Sources */, + A70171BE2AB4987900064C43 /* CategoryDetailView.swift in Sources */, + A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */, + A70171C02AB498A900064C43 /* RecipeDetailView.swift in Sources */, + A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */, + A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */, + A70171842AA8E71900064C43 /* MainView.swift in Sources */, + A70171CB2AB4CD1700064C43 /* UserDefaults.swift in Sources */, A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */, + A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */, + A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -419,6 +507,8 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Cookbook; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; @@ -457,6 +547,8 @@ ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = Cookbook; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.food-and-drink"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/Contents.json index 532cd72..61e99b0 100644 --- a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,59 +1,112 @@ { "images" : [ { - "idiom" : "universal", - "platform" : "ios", + "filename" : "cookbook-20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "cookbook-20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "cookbook-29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "cookbook-29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "cookbook-40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "cookbook-40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "cookbook-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "cookbook-60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "cookbook-20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "cookbook-20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "cookbook-29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "cookbook-29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "cookbook-40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "cookbook-40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "cookbook-76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "cookbook-76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "cookbook-83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "cookbook-1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", "size" : "1024x1024" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" } ], "info" : { diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-1024.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-1024.png new file mode 100644 index 0000000..a2cfaa1 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-1024.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-20.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-20.png new file mode 100644 index 0000000..f2d43cd Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-20.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-20@2x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-20@2x.png new file mode 100644 index 0000000..0e588b4 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-20@2x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-20@3x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-20@3x.png new file mode 100644 index 0000000..bd59827 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-20@3x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-29.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-29.png new file mode 100644 index 0000000..238f089 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-29.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-29@2x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-29@2x.png new file mode 100644 index 0000000..a27977b Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-29@2x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-29@3x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-29@3x.png new file mode 100644 index 0000000..f4bf833 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-29@3x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-40.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-40.png new file mode 100644 index 0000000..0e588b4 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-40.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-40@2x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-40@2x.png new file mode 100644 index 0000000..233657b Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-40@2x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-40@3x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-40@3x.png new file mode 100644 index 0000000..9086c95 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-40@3x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-60@2x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-60@2x.png new file mode 100644 index 0000000..9086c95 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-60@2x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-60@3x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-60@3x.png new file mode 100644 index 0000000..72b3cb7 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-60@3x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-76.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-76.png new file mode 100644 index 0000000..b9ca33d Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-76.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-76@2x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-76@2x.png new file mode 100644 index 0000000..c1c71b1 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-76@2x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-83.5@2x.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-83.5@2x.png new file mode 100644 index 0000000..d4290e9 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/AppIcon.appiconset/cookbook-83.5@2x.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/CookBook.imageset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/CookBook.imageset/Contents.json new file mode 100644 index 0000000..c910fdc --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Assets.xcassets/CookBook.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "cookbook.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/CookBook.imageset/cookbook.png b/Nextcloud Cookbook iOS Client/Assets.xcassets/CookBook.imageset/cookbook.png new file mode 100644 index 0000000..e8b2f45 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/CookBook.imageset/cookbook.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/accent.colorset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/accent.colorset/Contents.json new file mode 100644 index 0000000..5512b50 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Assets.xcassets/accent.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.871", + "green" : "0.871", + "red" : "0.871" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.871", + "green" : "0.871", + "red" : "0.871" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.126", + "green" : "0.125", + "red" : "0.126" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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 new file mode 100644 index 0000000..4d0df25 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "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 new file mode 100644 index 0000000..47b6ce4 Binary files /dev/null and b/Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/logo-white.png differ diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/ncblue.colorset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/ncblue.colorset/Contents.json new file mode 100644 index 0000000..bdbd7d1 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Assets.xcassets/ncblue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xC9", + "green" : "0x82", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.788", + "green" : "0.510", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nextcloud Cookbook iOS Client/Assets.xcassets/ncdarkblue.colorset/Contents.json b/Nextcloud Cookbook iOS Client/Assets.xcassets/ncdarkblue.colorset/Contents.json new file mode 100644 index 0000000..a0ee673 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Assets.xcassets/ncdarkblue.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.374", + "green" : "0.245", + "red" : "0.106" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.374", + "green" : "0.245", + "red" : "0.106" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Nextcloud Cookbook iOS Client/ContentView.swift b/Nextcloud Cookbook iOS Client/ContentView.swift deleted file mode 100644 index 2cc8987..0000000 --- a/Nextcloud Cookbook iOS Client/ContentView.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ContentView.swift -// Nextcloud Cookbook iOS Client -// -// Created by Vincent Meilinger on 06.09.23. -// - -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") - } - .padding() - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/Nextcloud Cookbook iOS Client/Data/DataModels.swift b/Nextcloud Cookbook iOS Client/Data/DataModels.swift new file mode 100644 index 0000000..7cf31bf --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Data/DataModels.swift @@ -0,0 +1,68 @@ +// +// DataModels.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 15.09.23. +// + +import Foundation +import SwiftUI + +struct Category: Codable { + let name: String + let recipe_count: Int +} + +struct Recipe: Codable { + let name: String + let keywords: String + let dateCreated: String + let dateModified: String + let imageUrl: String + let imagePlaceholderUrl: String + let recipe_id: Int +} + +struct RecipeDetail: Codable { + let name: String + let keywords: String + let dateCreated: String + let dateModified: String + let imageUrl: String + let id: String + let prepTime: String? + let cookTime: String? + let totalTime: String? + let description: String + let url: String + let recipeYield: Int + let recipeCategory: String + let tool: [String] + let recipeIngredient: [String] + let recipeInstructions: [String] + + static func error() -> RecipeDetail { + return RecipeDetail( + name: "Error: Unable to load recipe.", + keywords: "", + dateCreated: "", + dateModified: "", + imageUrl: "", id: "", + prepTime: "", + cookTime: "", + totalTime: "", + description: "", + url: "", + recipeYield: 0, + recipeCategory: "", + tool: [], + recipeIngredient: [], + recipeInstructions: [] + ) + } +} + +struct RecipeImage { + let thumb: UIImage + let full: UIImage? +} diff --git a/Nextcloud Cookbook iOS Client/Data/DataStore.swift b/Nextcloud Cookbook iOS Client/Data/DataStore.swift new file mode 100644 index 0000000..ef31634 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Data/DataStore.swift @@ -0,0 +1,54 @@ +// +// DataController.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 15.09.23. +// + +import Foundation +import SwiftUI + +class DataStore { + private static func fileURL(appending: String) throws -> URL { + try FileManager.default.url( + for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false + ) + + .appendingPathComponent(appending) + } + + func load(fromPath path: String) async throws -> D? { + let task = Task { + let fileURL = try Self.fileURL(appending: path) + guard let data = try? Data(contentsOf: fileURL) else { + return nil + } + let storedRecipes = try JSONDecoder().decode(D.self, from: data) + return storedRecipes + } + return try await task.value + } + + func save(data: D, toPath path: String) async throws { + let task = Task { + let data = try JSONEncoder().encode(data) + let outfile = try Self.fileURL(appending: path) + try data.write(to: outfile) + } + _ = try await task.value + } + + func clearAll() { + do { + try FileManager.default.removeItem(at: Self.fileURL(appending: "")) + } catch { + print("Could not delete file, probably read-only filesystem") + } + } + +} + + diff --git a/Nextcloud Cookbook iOS Client/Data/UserDefaults.swift b/Nextcloud Cookbook iOS Client/Data/UserDefaults.swift new file mode 100644 index 0000000..3a8b8cf --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Data/UserDefaults.swift @@ -0,0 +1,43 @@ +// +// UserDefaults.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 15.09.23. +// + + +import Foundation +import Combine + +class UserSettings: ObservableObject { + @Published var username: String { + didSet { + UserDefaults.standard.set(username, forKey: "username") + } + } + + @Published var token: String { + didSet { + UserDefaults.standard.set(token, forKey: "token") + } + } + + @Published var serverAddress: String { + didSet { + UserDefaults.standard.set(serverAddress, forKey: "serverAddress") + } + } + + @Published var onboarding: Bool { + didSet { + UserDefaults.standard.set(onboarding, forKey: "onboarding") + } + } + + init() { + self.username = UserDefaults.standard.object(forKey: "username") as? String ?? "" + self.token = UserDefaults.standard.object(forKey: "token") as? String ?? "" + self.serverAddress = UserDefaults.standard.object(forKey: "serverAddress") as? String ?? "" + self.onboarding = UserDefaults.standard.object(forKey: "onboarding") as? Bool ?? true + } +} diff --git a/Nextcloud Cookbook iOS Client/Network/CustomError.swift b/Nextcloud Cookbook iOS Client/Network/CustomError.swift new file mode 100644 index 0000000..c9f6727 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Network/CustomError.swift @@ -0,0 +1,15 @@ +// +// CustomError.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 13.09.23. +// + +import Foundation + +public enum NotImplementedError: Error, CustomStringConvertible { + case notImplemented + public var description: String { + return "Function not implemented." + } +} diff --git a/Nextcloud Cookbook iOS Client/Network/Extensions.swift b/Nextcloud Cookbook iOS Client/Network/Extensions.swift new file mode 100644 index 0000000..384735c --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Network/Extensions.swift @@ -0,0 +1,34 @@ +// +// Extensions.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 14.09.23. +// + +import Foundation + +extension Formatter { + static let positional: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .positional + return formatter + }() +} + +func formatDate(duration: String) -> String { + var duration = duration + if duration.hasPrefix("PT") { duration.removeFirst(2) } + let hour, minute, second: Double + if let index = duration.firstIndex(of: "H") { + hour = Double(duration[.. Data? { + + let url = URL(string: "\(urlString)/\(path)")! + + var request = URLRequest(url: url) + + request.httpMethod = "GET" + request.setValue( + "true", + forHTTPHeaderField: "OCS-APIRequest" + ) + request.setValue( + "Basic \(authString)", + forHTTPHeaderField: "Authorization" + ) + + do { + let (data, _) = try await URLSession.shared.data(for: request) + return data + } catch { + + return nil + } + } + + func sendHTTPRequest(path: String, _ requestWrapper: RequestWrapper) async throws -> (Data?, NetworkError?) { + print("Sending \(requestWrapper.method.rawValue) request (path: \(path)) ...") + let urlStringSanitized = "\(urlString)/\(path)".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) + let url = URL(string: urlStringSanitized!)! + var request = URLRequest(url: url) + request.setValue( + "true", + forHTTPHeaderField: "OCS-APIRequest" + ) + request.setValue( + "Basic \(authString)", + forHTTPHeaderField: "Authorization" + ) + + request.setValue( + requestWrapper.accept.rawValue, + forHTTPHeaderField: "Accept" + ) + + request.httpMethod = requestWrapper.method.rawValue + + switch requestWrapper.method { + case .GET: break + case .POST, .PUT: + guard let httpBody = requestWrapper.body else { return (nil, nil) } + do { + print("Encoding request ...") + request.httpBody = try JSONEncoder().encode(httpBody) + print("Request body: \(String(data: request.httpBody ?? Data(), encoding: .utf8) ?? "nil")") + } catch { + throw error + } + case .DELETE: throw NotImplementedError.notImplemented + } + + var data: Data? = nil + var response: URLResponse? = nil + do { + (data, response) = try await URLSession.shared.data(for: request) + print("Response: ", response) + return (data, nil) + } catch { + return (nil, decodeURLResponse(response: response as? HTTPURLResponse)) + } + } + + private func decodeURLResponse(response: HTTPURLResponse?) -> NetworkError? { + guard let response = response else { + return NetworkError.unknownError + } + switch response.statusCode { + case 200...299: return (nil) + case 300...399: return (NetworkError.redirectionError) + case 400...499: return (NetworkError.clientError) + case 500...599: return (NetworkError.serverError) + case 600: return (NetworkError.invalidRequest) + default: return (NetworkError.unknownError) + } + } + + func sendDataRequest(_ request: RequestWrapper) async -> (D?, Error?) { + do { + let (data, error) = try await sendHTTPRequest(path: request.path, request) + if let data = data { + return (decodeData(data), error) + } + return (nil, error) + } catch { + print("An unknown network error occured.") + } + return (nil, NetworkError.unknownError) + } + + func sendRequest(_ request: RequestWrapper) async -> Error? { + do { + return try await sendHTTPRequest(path: request.path, request).1 + } catch { + print("An unknown network error occured.") + } + return NetworkError.unknownError + } + + private func decodeData(_ data: Data) -> D? { + let decoder = JSONDecoder() + do { + print("Decoding type ", D.self, " ...") + return try decoder.decode(D.self, from: data) + } catch (let error) { + print("DataController - decodeData(): Failed to decode data.") + print("Error: ", error) + return nil + } + } +} + + + diff --git a/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift b/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift new file mode 100644 index 0000000..a04ce12 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift @@ -0,0 +1,36 @@ +// +// NetworkRequests.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 13.09.23. +// + +import Foundation + +enum RequestMethod: String { + case GET = "GET", POST = "POST", PUT = "PUT", DELETE = "DELETE" +} + +enum RequestPath: String { + case GET_CATEGORIES = "categories" +} + +enum AcceptHeader: String { + case JSON = "application/json", IMAGE = "image/jpeg" +} + +struct RequestWrapper { + let method: RequestMethod + let path: String + let accept: AcceptHeader + let body: Codable? + + init(method: RequestMethod, path: String, body: Codable? = nil, accept: AcceptHeader = .JSON) { + self.method = method + self.path = path + self.body = body + self.accept = accept + } +} + + diff --git a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift index 4824b93..909afb3 100644 --- a/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift +++ b/Nextcloud Cookbook iOS Client/Nextcloud_Cookbook_iOS_ClientApp.swift @@ -9,9 +9,13 @@ import SwiftUI @main struct Nextcloud_Cookbook_iOS_ClientApp: App { + @StateObject var userSettings = UserSettings() var body: some Scene { WindowGroup { - ContentView() + MainView() + .fullScreenCover(isPresented: $userSettings.onboarding) { + OnboardingView(userSettings: userSettings) + } } } } diff --git a/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift new file mode 100644 index 0000000..fdc3d39 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift @@ -0,0 +1,124 @@ +// +// MainViewModel.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 06.09.23. +// + +import Foundation +import UIKit + +@MainActor class MainViewModel: ObservableObject { + @Published var categories: [Category] = [] + @Published var recipes: [String: [Recipe]] = [:] + private var recipeDetails: [Int: RecipeDetail] = [:] + private var imageCache: [Int: RecipeImage] = [:] + + let dataStore: DataStore + let networkController: NetworkController + + init() { + self.networkController = NetworkController() + self.dataStore = DataStore() + } + + func loadCategoryList(needsUpdate: Bool = false) async { + if let categoryList: [Category] = await load(localPath: "categories.data", networkPath: "categories", needsUpdate: needsUpdate) { + self.categories = categoryList + } + } + + func loadRecipeList(categoryName: String, needsUpdate: Bool = false) async { + if let recipeList: [Recipe] = await load(localPath: "category_\(categoryName).data", networkPath: "category/\(categoryName)", needsUpdate: needsUpdate) { + recipes[categoryName] = recipeList + } + } + + func loadRecipeDetail(recipeId: Int, needsUpdate: Bool = false) async -> RecipeDetail { + if !needsUpdate { + if let recipeDetail = recipeDetails[recipeId] { + return recipeDetail + } + } + if let recipeDetail: RecipeDetail = await load(localPath: "recipe\(recipeId).data", networkPath: "recipes/\(recipeId)", needsUpdate: needsUpdate) { + recipeDetails[recipeId] = recipeDetail + return recipeDetail + } + return RecipeDetail.error() + } + + func loadImage(recipeId: Int, full: Bool, needsUpdate: Bool = false) async -> UIImage? { + print("loadImage(recipeId: \(recipeId), full: \(full)") + + // Check if image is in image cache + if !needsUpdate, let recipeImage = imageCache[recipeId] { + if full { + if let fullImage = recipeImage.full { + return recipeImage.full + } + } else { + return recipeImage.thumb + } + } + + // If image is not in image cache, request from local storage + do { + let localPath = "image\(recipeId)_\(full ? "full" : "thumb")" + if !needsUpdate, let data: String = try await dataStore.load(fromPath: localPath) { + print("Image data found locally. Decoding ...") + guard let dataDecoded = Data(base64Encoded: data) else { return nil } + print("Data to UIImage ...") + let image = UIImage(data: dataDecoded) + print("Done.") + return image + } else { + // If image is not in local storage, request from server + let networkPath = "recipes/\(recipeId)/image?size=full" + let request = RequestWrapper(method: .GET, path: networkPath, accept: .IMAGE) + let (data, error): (Data?, Error?) = try await networkController.sendHTTPRequest(path: request.path, request) + guard let data = data else { + print("Error receiving or decoding data.") + print("Error Message: \n", error) + return nil + } + let image = UIImage(data: data) + if image != nil { + print("Saving image loaclly ...") + try await dataStore.save(data: data.base64EncodedString(), toPath: localPath) + } + print("Done.") + return image + } + }catch { + print("An unknown error occurred.") + } + return nil + } +} + + + + +extension MainViewModel { + private func load(localPath: String, networkPath: String, needsUpdate: Bool = false) async -> D? { + do { + if !needsUpdate, let data: D = try await dataStore.load(fromPath: localPath) { + print("Data found locally.") + return data + } else { + let request = RequestWrapper(method: .GET, path: networkPath) + let (data, error): (D?, Error?) = await networkController.sendDataRequest(request) + print(error) + if let data = data { + try await dataStore.save(data: data, toPath: localPath) + } + return data + } + }catch { + print("An unknown error occurred.") + } + return nil + } +} + + diff --git a/Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift b/Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift new file mode 100644 index 0000000..08e3782 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift @@ -0,0 +1,36 @@ +// +// CategoryCardView.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 15.09.23. +// + +import Foundation +import SwiftUI + +struct CategoryCardView: View { + @State var category: Category + + var body: some View { + ZStack { + Image("CookBook") + .aspectRatio(1, contentMode: .fit) + .overlay( + VStack { + Spacer() + Color.clear + .background( + .ultraThickMaterial + ) + .overlay( + Text(category.name) + .font(.headline) + ) + .frame(maxHeight: 30) + } + ) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding() + } + } +} diff --git a/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift b/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift new file mode 100644 index 0000000..7321fb0 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift @@ -0,0 +1,37 @@ +// +// CategoryDetailView.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 15.09.23. +// + +import Foundation +import SwiftUI + + + +struct RecipeBookView: View { + @State var categoryName: String + @ObservedObject var viewModel: MainViewModel + var body: some View { + ScrollView { + LazyVStack { + 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) + } + .buttonStyle(.plain) + } + } + } + } + .navigationTitle(categoryName) + .task { + await viewModel.loadRecipeList(categoryName: categoryName) + } + .refreshable { + await viewModel.loadRecipeList(categoryName: categoryName, needsUpdate: true) + } + } +} diff --git a/Nextcloud Cookbook iOS Client/Views/MainView.swift b/Nextcloud Cookbook iOS Client/Views/MainView.swift new file mode 100644 index 0000000..9141aba --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Views/MainView.swift @@ -0,0 +1,58 @@ +// +// ContentView.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 06.09.23. +// + +import SwiftUI + +struct MainView: View { + @StateObject var viewModel = MainViewModel() + var columns: [GridItem] = [GridItem(.adaptive(minimum: 150), spacing: 0)] + var body: some View { + NavigationStack { + 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) + } + .buttonStyle(.plain) + } + } + .padding(.horizontal) + } + .navigationTitle("CookBook") + .toolbar { + NavigationLink( destination: SettingsView()) { + Image(systemName: "gear") + } + } + } + .task { + await viewModel.loadCategoryList() + } + .refreshable { + await viewModel.loadCategoryList(needsUpdate: true) + } + } +} + +struct MainView_Previews: PreviewProvider { + static var previews: some View { + MainView() + } +} + + + + + + + + diff --git a/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift b/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift new file mode 100644 index 0000000..9eb36cc --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Views/OnboardingView.swift @@ -0,0 +1,159 @@ +// +// OnboardingView.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 15.09.23. +// + +import Foundation +import SwiftUI + +struct OnboardingView: View { + @ObservedObject var userSettings: UserSettings + @State var selectedTab: Int = 0 + + var body: some View { + TabView(selection: $selectedTab) { + WelcomeTab().tag(0) + LoginTab(userSettings: userSettings).tag(1) + } + .tabViewStyle(.page) + .background( + selectedTab == 1 ? Color("ncblue").ignoresSafeArea() : Color(uiColor: .systemBackground).ignoresSafeArea() + ) + .animation(.easeInOut, value: selectedTab) + } +} + +struct WelcomeTab: View { + var body: some View { + VStack(alignment: .center) { + Spacer() + Image("CookBook") + .resizable() + .frame(width: 120, height: 120) + .clipShape(RoundedRectangle(cornerRadius: 10)) + Text("Tank you for downloading") + .font(.headline) + Text("Nextcloud") + .font(.largeTitle) + .bold() + Text("Cookbook") + .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.") + .padding() + Spacer() + } + .padding() + .fontDesign(.rounded) + } +} + +struct LoginTab: View { + @ObservedObject var userSettings: UserSettings + + enum Field { + case server + case username + case token + } + @FocusState private var focusedField: Field? + + 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() + } + LoginLabel(text: "Server address") + LoginTextField(example: "e.g.: example.com", text: $userSettings.serverAddress) + .focused($focusedField, equals: .server) + .textContentType(.URL) + .submitLabel(.next) + .padding(.bottom) + + LoginLabel(text: "User name") + LoginTextField(example: "username", text: $userSettings.username) + .focused($focusedField, equals: .username) + .textContentType(.username) + .submitLabel(.next) + .padding(.bottom) + + + LoginLabel(text: "App Token") + LoginTextField(example: "can be generated in security settings of your nextcloud", text: $userSettings.token) + .focused($focusedField, equals: .token) + .textContentType(.password) + .submitLabel(.join) + HStack{ + Spacer() + Button { + userSettings.onboarding = false + } label: { + Text("Submit") + .foregroundColor(.white) + .font(.headline) + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.white, lineWidth: 2) + .foregroundColor(.clear) + ) + } + .padding() + Spacer() + } + Spacer() + } + .onSubmit { + switch focusedField { + case .server: + focusedField = .username + case .username: + focusedField = .token + default: + print("Attempting to log in ...") + } + } + .fontDesign(.rounded) + .padding() + } + } +} + +struct LoginLabel: View { + let text: String + var body: some View { + Text(text) + .foregroundColor(.white) + .font(.headline) + .padding(.vertical, 5) + } +} + +struct LoginTextField: View { + var example: String + @Binding var text: String + + var body: some View { + TextField(example, text: $text) + .textFieldStyle(.plain) + .textCase(.lowercase) + .foregroundColor(.white) + .accentColor(.white) + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.white, lineWidth: 2) + .foregroundColor(.clear) + ) + } +} diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift new file mode 100644 index 0000000..6ddc704 --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift @@ -0,0 +1,35 @@ +// +// RecipeCardView.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 15.09.23. +// + +import Foundation +import SwiftUI + +struct RecipeCardView: View { + @State var viewModel: MainViewModel + @State var recipe: Recipe + @State var recipeThumb: UIImage? + var body: some View { + HStack { + Image(uiImage: recipeThumb ?? UIImage(named: "CookBook")!) + .resizable() + .frame(maxWidth: 80, maxHeight: 80) + Text(recipe.name) + .font(.headline) + + Spacer() + } + .background(.ultraThickMaterial) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .padding(.horizontal) + .task { + recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, full: false) + } + .refreshable { + recipeThumb = await viewModel.loadImage(recipeId: recipe.recipe_id, full: false, needsUpdate: true) + } + } +} diff --git a/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift new file mode 100644 index 0000000..34d306e --- /dev/null +++ b/Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift @@ -0,0 +1,201 @@ +// +// RecipeDetailView.swift +// Nextcloud Cookbook iOS Client +// +// Created by Vincent Meilinger on 15.09.23. +// + +import Foundation +import SwiftUI + + +struct RecipeDetailView: View { + @ObservedObject var viewModel: MainViewModel + @State var recipe: Recipe + @State var recipeDetail: RecipeDetail? + @State var recipeImage: UIImage? + @State var showTitle: Bool = false + var body: some View { + ScrollView(showsIndicators: false) { + VStack(alignment: .leading) { + if let recipeImage = recipeImage { + Image(uiImage: recipeImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(height: 300) + .clipped() + } else { + Color.blue + .frame(height: 300) + } + + if let recipeDetail = recipeDetail { + LazyVStack (alignment: .leading) { + Divider() + Text(recipeDetail.name) + .font(.title) + .bold() + .padding() + .onDisappear { + showTitle = true + } + .onAppear { + showTitle = false + } + Divider() + RecipeYieldSection(recipeDetail: recipeDetail) + RecipeDurationSection(recipeDetail: recipeDetail) + if(!recipeDetail.recipeIngredient.isEmpty) { + RecipeIngredientSection(recipeDetail: recipeDetail) + } + if(!recipeDetail.tool.isEmpty) { + RecipeToolSection(recipeDetail: recipeDetail) + } + if(!recipeDetail.recipeInstructions.isEmpty) { + RecipeInstructionSection(recipeDetail: recipeDetail) + } + }.padding(.horizontal, 5) + + } + } + } + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(showTitle ? recipe.name : "") + .task { + recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id) + recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, full: true) + } + .refreshable { + recipeDetail = await viewModel.loadRecipeDetail(recipeId: recipe.recipe_id, needsUpdate: true) + recipeImage = await viewModel.loadImage(recipeId: recipe.recipe_id, full: true, needsUpdate: true) + } + + } +} + +struct RecipeYieldSection: View { + @State var recipeDetail: RecipeDetail + var body: some View { + HStack { + Text("Servings: \(recipeDetail.recipeYield)") + Spacer() + }.padding() + .background(Color("accent")) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } +} + +struct RecipeDurationSection: View { + @State var recipeDetail: RecipeDetail + + var body: some View { + HStack { + 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 { + VStack { + SecondaryLabel(text: "Cook time") + Text(formatDate(duration: cookTime)) + .lineLimit(1) + }.padding() + .frame(maxWidth: .infinity) + .background(Color("accent")) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + + if let totalTime = recipeDetail.totalTime { + VStack { + SecondaryLabel(text: "Total time") + Text(formatDate(duration: totalTime)) + .lineLimit(1) + }.padding() + .frame(maxWidth: .infinity) + .background(Color("accent")) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + } + } +} + + +struct RecipeIngredientSection: View { + @State var recipeDetail: RecipeDetail + var body: some View { + VStack(alignment: .leading) { + HStack { + SecondaryLabel(text: "Ingredients") + Spacer() + } + ForEach(recipeDetail.recipeIngredient, id: \.self) { ingredient in + Text("\u{2022} \(ingredient)") + .multilineTextAlignment(.leading) + .padding(4) + } + }.padding() + .frame(maxWidth: .infinity) + .background(Color("accent")) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } +} + +struct RecipeToolSection: View { + @State var recipeDetail: RecipeDetail + var body: some View { + VStack(alignment: .leading) { + HStack { + SecondaryLabel(text: "Tools") + Spacer() + } + ForEach(recipeDetail.tool, id: \.self) { tool in + Text("\u{2022} \(tool)") + .multilineTextAlignment(.leading) + .padding(4) + } + }.padding() + .frame(maxWidth: .infinity) + .background(Color("accent")) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } +} + +struct RecipeInstructionSection: View { + @State var recipeDetail: RecipeDetail + var body: some View { + VStack(alignment: .leading) { + HStack { + SecondaryLabel(text: "Instructions") + Spacer() + } + ForEach(0..