initial commit

This commit is contained in:
Vicnet
2023-09-16 14:09:49 +02:00
parent 1e42dd4891
commit 2ebc420451
41 changed files with 1495 additions and 83 deletions

View File

@@ -8,12 +8,26 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */; }; 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 */; }; A70171862AA8E71F00064C43 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A70171852AA8E71F00064C43 /* Assets.xcassets */; };
A701718A2AA8E71F00064C43 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A70171892AA8E71F00064C43 /* Preview 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 */; }; 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 */; }; 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 */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -36,7 +50,7 @@
/* Begin PBXFileReference section */ /* 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; }; 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 = "<group>"; }; A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientApp.swift; sourceTree = "<group>"; };
A70171832AA8E71900064C43 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; A70171832AA8E71900064C43 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
A70171852AA8E71F00064C43 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; A70171852AA8E71F00064C43 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Nextcloud_Cookbook_iOS_Client.entitlements; sourceTree = "<group>"; }; A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Nextcloud_Cookbook_iOS_Client.entitlements; sourceTree = "<group>"; };
A70171892AA8E71F00064C43 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; A70171892AA8E71F00064C43 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
@@ -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; }; 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 = "<group>"; }; A701719D2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientUITests.swift; sourceTree = "<group>"; };
A701719F2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift; sourceTree = "<group>"; }; A701719F2AA8E72000064C43 /* Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nextcloud_Cookbook_iOS_ClientUITestsLaunchTests.swift; sourceTree = "<group>"; };
A70171AC2AA8EF4700064C43 /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = "<group>"; };
A70171AE2AB2116B00064C43 /* NetworkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkController.swift; sourceTree = "<group>"; };
A70171B02AB211DF00064C43 /* CustomError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomError.swift; sourceTree = "<group>"; };
A70171B32AB2122900064C43 /* NetworkRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkRequests.swift; sourceTree = "<group>"; };
A70171B82AB399FB00064C43 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
A70171BB2AB4983500064C43 /* CategoryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryCardView.swift; sourceTree = "<group>"; };
A70171BD2AB4987900064C43 /* CategoryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CategoryDetailView.swift; sourceTree = "<group>"; };
A70171BF2AB498A900064C43 /* RecipeDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeDetailView.swift; sourceTree = "<group>"; };
A70171C12AB498C600064C43 /* RecipeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCardView.swift; sourceTree = "<group>"; };
A70171C32AB4A31200064C43 /* DataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStore.swift; sourceTree = "<group>"; };
A70171C52AB4C43A00064C43 /* DataModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataModels.swift; sourceTree = "<group>"; };
A70171C82AB4CBB400064C43 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
A70171CA2AB4CD1700064C43 /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
A70171CC2AB501B100064C43 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -96,7 +124,10 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */, A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */,
A70171832AA8E71900064C43 /* ContentView.swift */, A70171C72AB4C4A100064C43 /* Data */,
A70171BA2AB4980100064C43 /* Views */,
A70171B72AB2445700064C43 /* ViewModels */,
A70171B22AB211F000064C43 /* Network */,
A70171852AA8E71F00064C43 /* Assets.xcassets */, A70171852AA8E71F00064C43 /* Assets.xcassets */,
A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */, A70171872AA8E71F00064C43 /* Nextcloud_Cookbook_iOS_Client.entitlements */,
A70171882AA8E71F00064C43 /* Preview Content */, A70171882AA8E71F00064C43 /* Preview Content */,
@@ -129,6 +160,49 @@
path = "Nextcloud Cookbook iOS ClientUITests"; path = "Nextcloud Cookbook iOS ClientUITests";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
A70171B22AB211F000064C43 /* Network */ = {
isa = PBXGroup;
children = (
A70171B82AB399FB00064C43 /* Extensions.swift */,
A70171B32AB2122900064C43 /* NetworkRequests.swift */,
A70171AE2AB2116B00064C43 /* NetworkController.swift */,
A70171B02AB211DF00064C43 /* CustomError.swift */,
);
path = Network;
sourceTree = "<group>";
};
A70171B72AB2445700064C43 /* ViewModels */ = {
isa = PBXGroup;
children = (
A70171AC2AA8EF4700064C43 /* MainViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
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 = "<group>";
};
A70171C72AB4C4A100064C43 /* Data */ = {
isa = PBXGroup;
children = (
A70171C32AB4A31200064C43 /* DataStore.swift */,
A70171C52AB4C43A00064C43 /* DataModels.swift */,
A70171CA2AB4CD1700064C43 /* UserDefaults.swift */,
);
path = Data;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -259,8 +333,22 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( 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 */, A70171822AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift in Sources */,
A70171AD2AA8EF4700064C43 /* MainViewModel.swift in Sources */,
A70171C92AB4CBB400064C43 /* OnboardingView.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -419,6 +507,8 @@
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = 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=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -457,6 +547,8 @@
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = 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=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;

View File

@@ -1,59 +1,112 @@
{ {
"images" : [ "images" : [
{ {
"idiom" : "universal", "filename" : "cookbook-20@2x.png",
"platform" : "ios", "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" "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" : { "info" : {

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -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
}
}

View File

@@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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?
}

View File

@@ -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<D: Decodable>(fromPath path: String) async throws -> D? {
let task = Task<D?, Error> {
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<D: Encodable>(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")
}
}
}

View File

@@ -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
}
}

View File

@@ -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."
}
}

View File

@@ -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[..<index]) ?? 0
duration.removeSubrange(...index)
} else { hour = 0 }
if let index = duration.firstIndex(of: "M") {
minute = Double(duration[..<index]) ?? 0
duration.removeSubrange(...index)
} else { minute = 0 }
if let index = duration.firstIndex(of: "S") {
second = Double(duration[..<index]) ?? 0
} else { second = 0 }
return Formatter.positional.string(from: hour * 3600 + minute * 60 + second) ?? "0:00"
}

View File

@@ -0,0 +1,160 @@
//
// NetworkController.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 13.09.23.
//
import Foundation
public enum NetworkError: String, Error {
case missingUrl = "Missing URL."
case parametersNil = "Parameters are nil."
case encodingFailed = "Parameter encoding failed."
case redirectionError = "Redirection error"
case clientError = "Client error"
case serverError = "Server error"
case invalidRequest = "Invalid request"
case unknownError = "Unknown error"
case dataError = "Invalid data error."
}
class NetworkController {
var userSettings: UserSettings
var authString: String
var urlString: String
let apiVersion = "1"
init() {
print("Initializing NetworkController.")
self.userSettings = UserSettings()
self.urlString = "https://\(userSettings.serverAddress)/index.php/apps/cookbook/api/v\(apiVersion)"
let loginString = "\(userSettings.username):\(userSettings.token)"
let loginData = loginString.data(using: String.Encoding.utf8)!
self.authString = loginData.base64EncodedString()
}
func fetchData(path: String) async throws -> 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<D: Decodable>(_ 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<D: Decodable>(_ 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
}
}
}

View File

@@ -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
}
}

View File

@@ -9,9 +9,13 @@ import SwiftUI
@main @main
struct Nextcloud_Cookbook_iOS_ClientApp: App { struct Nextcloud_Cookbook_iOS_ClientApp: App {
@StateObject var userSettings = UserSettings()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() MainView()
.fullScreenCover(isPresented: $userSettings.onboarding) {
OnboardingView(userSettings: userSettings)
}
} }
} }
} }

View File

@@ -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<D: Codable>(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
}
}

View File

@@ -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()
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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..<recipeDetail.recipeInstructions.count) { ix in
HStack(alignment: .top) {
Text("\(ix+1).")
Text("\(recipeDetail.recipeInstructions[ix])")
}.padding(4)
}
}.padding()
.background(Color("accent"))
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
struct SecondaryLabel: View {
let text: String
var body: some View {
Text(text)
.foregroundColor(.secondary)
.font(.headline)
.padding(.vertical, 5)
}
}

View File

@@ -0,0 +1,55 @@
//
// SettingsView.swift
// Nextcloud Cookbook iOS Client
//
// Created by Vincent Meilinger on 15.09.23.
//
import Foundation
import SwiftUI
struct SettingsView: View {
@StateObject var userSettings = UserSettings()
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack {
SettingsSection(headline: "Language", description: "Language settings coming soon.")
SettingsSection(headline: "Accent Color", description: "The accent color setting will be released in a future update.")
Button("Log out") {
print("Log out.")
userSettings.serverAddress = ""
userSettings.username = ""
userSettings.token = ""
userSettings.onboarding = true
}
.buttonStyle(.borderedProminent)
.accentColor(.red)
.padding()
Button("Clear Cache") {
print("Clear cache.")
}
.buttonStyle(.borderedProminent)
.accentColor(.red)
.padding()
}
}.navigationTitle("Settings")
}
}
struct SettingsSection: View {
@State var headline: String
@State var description: String
var body: some View {
VStack(alignment: .leading) {
Text(headline)
.font(.headline)
Text(description)
Divider()
}.padding()
}
}