initial commit
@@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
@@ -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 = "<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 */
|
||||
|
||||
/* 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 = "<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 */
|
||||
|
||||
/* 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;
|
||||
|
||||
@@ -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" : {
|
||||
|
||||
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
21
Nextcloud Cookbook iOS Client/Assets.xcassets/CookBook.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
Nextcloud Cookbook iOS Client/Assets.xcassets/CookBook.imageset/cookbook.png
vendored
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
21
Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
Nextcloud Cookbook iOS Client/Assets.xcassets/nc-logo-white.imageset/logo-white.png
vendored
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
68
Nextcloud Cookbook iOS Client/Data/DataModels.swift
Normal 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?
|
||||
}
|
||||
54
Nextcloud Cookbook iOS Client/Data/DataStore.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
43
Nextcloud Cookbook iOS Client/Data/UserDefaults.swift
Normal 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
|
||||
}
|
||||
}
|
||||
15
Nextcloud Cookbook iOS Client/Network/CustomError.swift
Normal 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."
|
||||
}
|
||||
}
|
||||
34
Nextcloud Cookbook iOS Client/Network/Extensions.swift
Normal 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"
|
||||
}
|
||||
160
Nextcloud Cookbook iOS Client/Network/NetworkController.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
36
Nextcloud Cookbook iOS Client/Network/NetworkRequests.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
124
Nextcloud Cookbook iOS Client/ViewModels/MainViewModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
Nextcloud Cookbook iOS Client/Views/CategoryCardView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Nextcloud Cookbook iOS Client/Views/CategoryDetailView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Nextcloud Cookbook iOS Client/Views/MainView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
159
Nextcloud Cookbook iOS Client/Views/OnboardingView.swift
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
35
Nextcloud Cookbook iOS Client/Views/RecipeCardView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
201
Nextcloud Cookbook iOS Client/Views/RecipeDetailView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
55
Nextcloud Cookbook iOS Client/Views/SettingsView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||