WIP - Complete App refactoring
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 56;
|
objectVersion = 70;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
A70171C02AB498A900064C43 /* RecipeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BF2AB498A900064C43 /* RecipeView.swift */; };
|
A70171C02AB498A900064C43 /* RecipeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171BF2AB498A900064C43 /* RecipeView.swift */; };
|
||||||
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C12AB498C600064C43 /* RecipeCardView.swift */; };
|
A70171C22AB498C600064C43 /* RecipeCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C12AB498C600064C43 /* RecipeCardView.swift */; };
|
||||||
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C32AB4A31200064C43 /* DataStore.swift */; };
|
A70171C42AB4A31200064C43 /* DataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C32AB4A31200064C43 /* DataStore.swift */; };
|
||||||
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171C52AB4C43A00064C43 /* DataModels.swift */; };
|
|
||||||
A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserSettings.swift */; };
|
A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CA2AB4CD1700064C43 /* UserSettings.swift */; };
|
||||||
A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CC2AB501B100064C43 /* SettingsView.swift */; };
|
A70171CD2AB501B100064C43 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70171CC2AB501B100064C43 /* SettingsView.swift */; };
|
||||||
A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */; };
|
A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */; };
|
||||||
@@ -54,17 +53,24 @@
|
|||||||
A977D0DE2B600300009783A9 /* SearchTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0DD2B600300009783A9 /* SearchTabView.swift */; };
|
A977D0DE2B600300009783A9 /* SearchTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0DD2B600300009783A9 /* SearchTabView.swift */; };
|
||||||
A977D0E02B600318009783A9 /* RecipeTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0DF2B600318009783A9 /* RecipeTabView.swift */; };
|
A977D0E02B600318009783A9 /* RecipeTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0DF2B600318009783A9 /* RecipeTabView.swift */; };
|
||||||
A977D0E22B60034E009783A9 /* GroceryListTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0E12B60034E009783A9 /* GroceryListTabView.swift */; };
|
A977D0E22B60034E009783A9 /* GroceryListTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977D0E12B60034E009783A9 /* GroceryListTabView.swift */; };
|
||||||
A97B4D322B80B3E900EC1A88 /* RecipeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */; };
|
A97B4D322B80B3E900EC1A88 /* CookbookModelsV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D312B80B3E900EC1A88 /* CookbookModelsV1.swift */; };
|
||||||
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D342B80B82A00EC1A88 /* ShareView.swift */; };
|
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97B4D342B80B82A00EC1A88 /* ShareView.swift */; };
|
||||||
A9805BED2BAAC70E003B7231 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */; };
|
A9805BED2BAAC70E003B7231 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */; };
|
||||||
|
A98F931E2C07B07400E34359 /* CookbookState.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98F931D2C07B07400E34359 /* CookbookState.swift */; };
|
||||||
|
A99A2D4E2BEFBC0900402B36 /* CookbookLoginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A2D4D2BEFBC0900402B36 /* CookbookLoginModels.swift */; };
|
||||||
|
A99A2D502BEFC44000402B36 /* CookbookProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99A2D4F2BEFC44000402B36 /* CookbookProtocols.swift */; };
|
||||||
A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */; };
|
A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */; };
|
||||||
A9BBB38E2B8E44B3002DA7FF /* BottomClipper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */; };
|
A9BBB38E2B8E44B3002DA7FF /* BottomClipper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */; };
|
||||||
A9BBB3902B91BE31002DA7FF /* ObservableRecipeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38F2B91BE31002DA7FF /* ObservableRecipeDetail.swift */; };
|
A9BBB3902B91BE31002DA7FF /* Recipe.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9BBB38F2B91BE31002DA7FF /* Recipe.swift */; };
|
||||||
A9CA6CEF2B4C086100F78AB5 /* RecipeExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */; };
|
A9CA6CEF2B4C086100F78AB5 /* RecipeExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */; };
|
||||||
A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */ = {isa = PBXBuildFile; productRef = A9CA6CF52B4C63F200F78AB5 /* TPPDF */; };
|
A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */ = {isa = PBXBuildFile; productRef = A9CA6CF52B4C63F200F78AB5 /* TPPDF */; };
|
||||||
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D89AAF2B4FE97800F49D92 /* TimerView.swift */; };
|
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D89AAF2B4FE97800F49D92 /* TimerView.swift */; };
|
||||||
A9D8F9052B99F3E5009BACAE /* RecipeImportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */; };
|
A9D8F9052B99F3E5009BACAE /* RecipeImportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */; };
|
||||||
A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A2A2BE7799F00206866 /* JsonAny.swift */; };
|
A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A2A2BE7799F00206866 /* JsonAny.swift */; };
|
||||||
|
A9E78A2D2BE8E3AF00206866 /* DataInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A2C2BE8E3AF00206866 /* DataInterface.swift */; };
|
||||||
|
A9E78A322BEA770600206866 /* NextcloudDataInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A312BEA770600206866 /* NextcloudDataInterface.swift */; };
|
||||||
|
A9E78A342BEA773900206866 /* LocalDataInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9E78A332BEA773900206866 /* LocalDataInterface.swift */; };
|
||||||
|
A9E78A372BEA839100206866 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A9E78A362BEA839100206866 /* KeychainSwift */; };
|
||||||
A9FA2AB62B5079B200A43702 /* alarm_sound_0.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */; };
|
A9FA2AB62B5079B200A43702 /* alarm_sound_0.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
@@ -104,7 +110,6 @@
|
|||||||
A70171BF2AB498A900064C43 /* RecipeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeView.swift; sourceTree = "<group>"; };
|
A70171BF2AB498A900064C43 /* RecipeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeView.swift; sourceTree = "<group>"; };
|
||||||
A70171C12AB498C600064C43 /* RecipeCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeCardView.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>"; };
|
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>"; };
|
|
||||||
A70171CA2AB4CD1700064C43 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; };
|
A70171CA2AB4CD1700064C43 /* UserSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettings.swift; sourceTree = "<group>"; };
|
||||||
A70171CC2AB501B100064C43 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
A70171CC2AB501B100064C43 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCoderExtension.swift; sourceTree = "<group>"; };
|
A70322692ABAF49800D7C4ED /* JSONCoderExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONCoderExtension.swift; sourceTree = "<group>"; };
|
||||||
@@ -136,25 +141,36 @@
|
|||||||
A977D0DD2B600300009783A9 /* SearchTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTabView.swift; sourceTree = "<group>"; };
|
A977D0DD2B600300009783A9 /* SearchTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTabView.swift; sourceTree = "<group>"; };
|
||||||
A977D0DF2B600318009783A9 /* RecipeTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeTabView.swift; sourceTree = "<group>"; };
|
A977D0DF2B600318009783A9 /* RecipeTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeTabView.swift; sourceTree = "<group>"; };
|
||||||
A977D0E12B60034E009783A9 /* GroceryListTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryListTabView.swift; sourceTree = "<group>"; };
|
A977D0E12B60034E009783A9 /* GroceryListTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroceryListTabView.swift; sourceTree = "<group>"; };
|
||||||
A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeModels.swift; sourceTree = "<group>"; };
|
A97B4D312B80B3E900EC1A88 /* CookbookModelsV1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookbookModelsV1.swift; sourceTree = "<group>"; };
|
||||||
A97B4D342B80B82A00EC1A88 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; };
|
A97B4D342B80B82A00EC1A88 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = "<group>"; };
|
||||||
A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = "<group>"; };
|
A9805BEC2BAAC70E003B7231 /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = "<group>"; };
|
||||||
|
A98F931D2C07B07400E34359 /* CookbookState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookbookState.swift; sourceTree = "<group>"; };
|
||||||
|
A99A2D4D2BEFBC0900402B36 /* CookbookLoginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookbookLoginModels.swift; sourceTree = "<group>"; };
|
||||||
|
A99A2D4F2BEFC44000402B36 /* CookbookProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookbookProtocols.swift; sourceTree = "<group>"; };
|
||||||
A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeaderView.swift; sourceTree = "<group>"; };
|
A9BBB38B2B8D3B0C002DA7FF /* ParallaxHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeaderView.swift; sourceTree = "<group>"; };
|
||||||
A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomClipper.swift; sourceTree = "<group>"; };
|
A9BBB38D2B8E44B3002DA7FF /* BottomClipper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomClipper.swift; sourceTree = "<group>"; };
|
||||||
A9BBB38F2B91BE31002DA7FF /* ObservableRecipeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableRecipeDetail.swift; sourceTree = "<group>"; };
|
A9BBB38F2B91BE31002DA7FF /* Recipe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recipe.swift; sourceTree = "<group>"; };
|
||||||
A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeExporter.swift; sourceTree = "<group>"; };
|
A9CA6CEE2B4C086100F78AB5 /* RecipeExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeExporter.swift; sourceTree = "<group>"; };
|
||||||
A9D89AAF2B4FE97800F49D92 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = "<group>"; };
|
A9D89AAF2B4FE97800F49D92 /* TimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimerView.swift; sourceTree = "<group>"; };
|
||||||
A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeImportSection.swift; sourceTree = "<group>"; };
|
A9D8F9042B99F3E4009BACAE /* RecipeImportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipeImportSection.swift; sourceTree = "<group>"; };
|
||||||
A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Nextcloud-Cookbook-iOS-Client-Info.plist"; sourceTree = SOURCE_ROOT; };
|
A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Nextcloud-Cookbook-iOS-Client-Info.plist"; sourceTree = SOURCE_ROOT; };
|
||||||
A9E78A2A2BE7799F00206866 /* JsonAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonAny.swift; sourceTree = "<group>"; };
|
A9E78A2A2BE7799F00206866 /* JsonAny.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonAny.swift; sourceTree = "<group>"; };
|
||||||
|
A9E78A2C2BE8E3AF00206866 /* DataInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataInterface.swift; sourceTree = "<group>"; };
|
||||||
|
A9E78A312BEA770600206866 /* NextcloudDataInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextcloudDataInterface.swift; sourceTree = "<group>"; };
|
||||||
|
A9E78A332BEA773900206866 /* LocalDataInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalDataInterface.swift; sourceTree = "<group>"; };
|
||||||
A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm_sound_0.mp3; sourceTree = "<group>"; };
|
A9FA2AB52B5079B200A43702 /* alarm_sound_0.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm_sound_0.mp3; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
A9C34A722D390E69006EEB66 /* Account */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Account; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
A701717B2AA8E71900064C43 /* Frameworks */ = {
|
A701717B2AA8E71900064C43 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
A9E78A372BEA839100206866 /* KeychainSwift in Frameworks */,
|
||||||
A74D33BE2AF82AAE00D06555 /* SwiftSoup in Frameworks */,
|
A74D33BE2AF82AAE00D06555 /* SwiftSoup in Frameworks */,
|
||||||
A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */,
|
A9CA6CF62B4C63F200F78AB5 /* TPPDF in Frameworks */,
|
||||||
);
|
);
|
||||||
@@ -202,6 +218,8 @@
|
|||||||
A70171802AA8E71900064C43 /* Nextcloud Cookbook iOS Client */ = {
|
A70171802AA8E71900064C43 /* Nextcloud Cookbook iOS Client */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A9C34A722D390E69006EEB66 /* Account */,
|
||||||
|
A9E78A2E2BEA726A00206866 /* Persistence */,
|
||||||
A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */,
|
A9DA25D42B82096B0061FC2B /* Nextcloud-Cookbook-iOS-Client-Info.plist */,
|
||||||
A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */,
|
A70171812AA8E71900064C43 /* Nextcloud_Cookbook_iOS_ClientApp.swift */,
|
||||||
A70171AC2AA8EF4700064C43 /* AppState.swift */,
|
A70171AC2AA8EF4700064C43 /* AppState.swift */,
|
||||||
@@ -209,7 +227,7 @@
|
|||||||
A70171BA2AB4980100064C43 /* Views */,
|
A70171BA2AB4980100064C43 /* Views */,
|
||||||
A70171B72AB2445700064C43 /* Models */,
|
A70171B72AB2445700064C43 /* Models */,
|
||||||
A97B4D332B80B51700EC1A88 /* Util */,
|
A97B4D332B80B51700EC1A88 /* Util */,
|
||||||
A70171B22AB211F000064C43 /* Network */,
|
A70171B22AB211F000064C43 /* Networking */,
|
||||||
A781E75F2AF8228100452F6F /* RecipeImport */,
|
A781E75F2AF8228100452F6F /* RecipeImport */,
|
||||||
A9CA6CED2B4C084100F78AB5 /* RecipeExport */,
|
A9CA6CED2B4C084100F78AB5 /* RecipeExport */,
|
||||||
A703226B2ABAF60D00D7C4ED /* Extensions */,
|
A703226B2ABAF60D00D7C4ED /* Extensions */,
|
||||||
@@ -246,16 +264,16 @@
|
|||||||
path = "Nextcloud Cookbook iOS ClientUITests";
|
path = "Nextcloud Cookbook iOS ClientUITests";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
A70171B22AB211F000064C43 /* Network */ = {
|
A70171B22AB211F000064C43 /* Networking */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A79AA8EA2B062E15007D25F2 /* ApiRequest.swift */,
|
A79AA8EA2B062E15007D25F2 /* ApiRequest.swift */,
|
||||||
A79AA8E72B062DB6007D25F2 /* CookbookApi */,
|
|
||||||
A79AA8EE2B063B33007D25F2 /* NextcloudApi */,
|
|
||||||
A70171B32AB2122900064C43 /* NetworkUtils.swift */,
|
A70171B32AB2122900064C43 /* NetworkUtils.swift */,
|
||||||
A70171B02AB211DF00064C43 /* NetworkError.swift */,
|
A70171B02AB211DF00064C43 /* NetworkError.swift */,
|
||||||
|
A79AA8E72B062DB6007D25F2 /* CookbookApi */,
|
||||||
|
A79AA8EE2B063B33007D25F2 /* NextcloudApi */,
|
||||||
);
|
);
|
||||||
path = Network;
|
path = Networking;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
A70171B72AB2445700064C43 /* Models */ = {
|
A70171B72AB2445700064C43 /* Models */ = {
|
||||||
@@ -284,9 +302,7 @@
|
|||||||
children = (
|
children = (
|
||||||
A70171C32AB4A31200064C43 /* DataStore.swift */,
|
A70171C32AB4A31200064C43 /* DataStore.swift */,
|
||||||
A70171CA2AB4CD1700064C43 /* UserSettings.swift */,
|
A70171CA2AB4CD1700064C43 /* UserSettings.swift */,
|
||||||
A70171C52AB4C43A00064C43 /* DataModels.swift */,
|
A9BBB38F2B91BE31002DA7FF /* Recipe.swift */,
|
||||||
A97B4D312B80B3E900EC1A88 /* RecipeModels.swift */,
|
|
||||||
A9BBB38F2B91BE31002DA7FF /* ObservableRecipeDetail.swift */,
|
|
||||||
);
|
);
|
||||||
path = Data;
|
path = Data;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -321,7 +337,10 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A79AA8E32B02A961007D25F2 /* CookbookApi.swift */,
|
A79AA8E32B02A961007D25F2 /* CookbookApi.swift */,
|
||||||
|
A99A2D4F2BEFC44000402B36 /* CookbookProtocols.swift */,
|
||||||
A79AA8E82B062DD1007D25F2 /* CookbookApiV1.swift */,
|
A79AA8E82B062DD1007D25F2 /* CookbookApiV1.swift */,
|
||||||
|
A99A2D4D2BEFBC0900402B36 /* CookbookLoginModels.swift */,
|
||||||
|
A97B4D312B80B3E900EC1A88 /* CookbookModelsV1.swift */,
|
||||||
);
|
);
|
||||||
path = CookbookApi;
|
path = CookbookApi;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -382,6 +401,16 @@
|
|||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A98F931F2C07BA4F00E34359 /* Interfaces */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A9E78A2C2BE8E3AF00206866 /* DataInterface.swift */,
|
||||||
|
A9E78A332BEA773900206866 /* LocalDataInterface.swift */,
|
||||||
|
A9E78A312BEA770600206866 /* NextcloudDataInterface.swift */,
|
||||||
|
);
|
||||||
|
path = Interfaces;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A9C3BE502B630E3900562C79 /* Recipes */ = {
|
A9C3BE502B630E3900562C79 /* Recipes */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -413,6 +442,15 @@
|
|||||||
path = RecipeExport;
|
path = RecipeExport;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
A9E78A2E2BEA726A00206866 /* Persistence */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
A98F931D2C07B07400E34359 /* CookbookState.swift */,
|
||||||
|
A98F931F2C07BA4F00E34359 /* Interfaces */,
|
||||||
|
);
|
||||||
|
path = Persistence;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
A9FA2AB42B50798800A43702 /* Resources */ = {
|
A9FA2AB42B50798800A43702 /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -436,10 +474,14 @@
|
|||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
A9C34A722D390E69006EEB66 /* Account */,
|
||||||
|
);
|
||||||
name = "Nextcloud Cookbook iOS Client";
|
name = "Nextcloud Cookbook iOS Client";
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
A74D33BD2AF82AAE00D06555 /* SwiftSoup */,
|
A74D33BD2AF82AAE00D06555 /* SwiftSoup */,
|
||||||
A9CA6CF52B4C63F200F78AB5 /* TPPDF */,
|
A9CA6CF52B4C63F200F78AB5 /* TPPDF */,
|
||||||
|
A9E78A362BEA839100206866 /* KeychainSwift */,
|
||||||
);
|
);
|
||||||
productName = "Nextcloud Cookbook iOS Client";
|
productName = "Nextcloud Cookbook iOS Client";
|
||||||
productReference = A701717E2AA8E71900064C43 /* Nextcloud Cookbook iOS Client.app */;
|
productReference = A701717E2AA8E71900064C43 /* Nextcloud Cookbook iOS Client.app */;
|
||||||
@@ -519,6 +561,7 @@
|
|||||||
packageReferences = (
|
packageReferences = (
|
||||||
A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
A74D33BC2AF82AAE00D06555 /* XCRemoteSwiftPackageReference "SwiftSoup" */,
|
||||||
A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */,
|
A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */,
|
||||||
|
A9E78A352BEA839100206866 /* XCRemoteSwiftPackageReference "keychain-swift" */,
|
||||||
);
|
);
|
||||||
productRefGroup = A701717F2AA8E71900064C43 /* Products */;
|
productRefGroup = A701717F2AA8E71900064C43 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -569,6 +612,7 @@
|
|||||||
A97506192B920EC200E86029 /* RecipeIngredientSection.swift in Sources */,
|
A97506192B920EC200E86029 /* RecipeIngredientSection.swift in Sources */,
|
||||||
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */,
|
A97B4D352B80B82A00EC1A88 /* ShareView.swift in Sources */,
|
||||||
A975061F2B920FFC00E86029 /* RecipeToolSection.swift in Sources */,
|
A975061F2B920FFC00E86029 /* RecipeToolSection.swift in Sources */,
|
||||||
|
A9E78A322BEA770600206866 /* NextcloudDataInterface.swift in Sources */,
|
||||||
A9805BED2BAAC70E003B7231 /* NumberFormatter.swift in Sources */,
|
A9805BED2BAAC70E003B7231 /* NumberFormatter.swift in Sources */,
|
||||||
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */,
|
A9D89AB02B4FE97800F49D92 /* TimerView.swift in Sources */,
|
||||||
A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */,
|
A9BBB38C2B8D3B0C002DA7FF /* ParallaxHeaderView.swift in Sources */,
|
||||||
@@ -585,17 +629,20 @@
|
|||||||
A975061D2B920FCC00E86029 /* RecipeInstructionSection.swift in Sources */,
|
A975061D2B920FCC00E86029 /* RecipeInstructionSection.swift in Sources */,
|
||||||
A787B0782B2B1E6400C2DF1B /* DateExtension.swift in Sources */,
|
A787B0782B2B1E6400C2DF1B /* DateExtension.swift in Sources */,
|
||||||
A79AA8E92B062DD1007D25F2 /* CookbookApiV1.swift in Sources */,
|
A79AA8E92B062DD1007D25F2 /* CookbookApiV1.swift in Sources */,
|
||||||
|
A9E78A2D2BE8E3AF00206866 /* DataInterface.swift in Sources */,
|
||||||
A7CD3FD22B2C546A00D764AD /* CollapsibleView.swift in Sources */,
|
A7CD3FD22B2C546A00D764AD /* CollapsibleView.swift in Sources */,
|
||||||
A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */,
|
A9E78A2B2BE7799F00206866 /* JsonAny.swift in Sources */,
|
||||||
A9BBB3902B91BE31002DA7FF /* ObservableRecipeDetail.swift in Sources */,
|
A9BBB3902B91BE31002DA7FF /* Recipe.swift in Sources */,
|
||||||
|
A98F931E2C07B07400E34359 /* CookbookState.swift in Sources */,
|
||||||
|
A99A2D4E2BEFBC0900402B36 /* CookbookLoginModels.swift in Sources */,
|
||||||
A97506212B92104700E86029 /* RecipeMetadataSection.swift in Sources */,
|
A97506212B92104700E86029 /* RecipeMetadataSection.swift in Sources */,
|
||||||
A70171B42AB2122900064C43 /* NetworkUtils.swift in Sources */,
|
A70171B42AB2122900064C43 /* NetworkUtils.swift in Sources */,
|
||||||
A97B4D322B80B3E900EC1A88 /* RecipeModels.swift in Sources */,
|
A97B4D322B80B3E900EC1A88 /* CookbookModelsV1.swift in Sources */,
|
||||||
A70171BE2AB4987900064C43 /* RecipeListView.swift in Sources */,
|
A70171BE2AB4987900064C43 /* RecipeListView.swift in Sources */,
|
||||||
A70171C62AB4C43A00064C43 /* DataModels.swift in Sources */,
|
|
||||||
A79AA8EB2B062E15007D25F2 /* ApiRequest.swift in Sources */,
|
A79AA8EB2B062E15007D25F2 /* ApiRequest.swift in Sources */,
|
||||||
A7F3F8E82ACBFC760076C227 /* RecipeKeywordSection.swift in Sources */,
|
A7F3F8E82ACBFC760076C227 /* RecipeKeywordSection.swift in Sources */,
|
||||||
A79AA8E02AFF80E3007D25F2 /* DurationComponents.swift in Sources */,
|
A79AA8E02AFF80E3007D25F2 /* DurationComponents.swift in Sources */,
|
||||||
|
A9E78A342BEA773900206866 /* LocalDataInterface.swift in Sources */,
|
||||||
A70171C02AB498A900064C43 /* RecipeView.swift in Sources */,
|
A70171C02AB498A900064C43 /* RecipeView.swift in Sources */,
|
||||||
A79AA8E42B02A962007D25F2 /* CookbookApi.swift in Sources */,
|
A79AA8E42B02A962007D25F2 /* CookbookApi.swift in Sources */,
|
||||||
A975061B2B920F9F00E86029 /* RecipeNutritionSection.swift in Sources */,
|
A975061B2B920F9F00E86029 /* RecipeNutritionSection.swift in Sources */,
|
||||||
@@ -605,6 +652,7 @@
|
|||||||
A70171842AA8E71900064C43 /* MainView.swift in Sources */,
|
A70171842AA8E71900064C43 /* MainView.swift in Sources */,
|
||||||
A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */,
|
A70171CB2AB4CD1700064C43 /* UserSettings.swift in Sources */,
|
||||||
A977D0DE2B600300009783A9 /* SearchTabView.swift in Sources */,
|
A977D0DE2B600300009783A9 /* SearchTabView.swift in Sources */,
|
||||||
|
A99A2D502BEFC44000402B36 /* CookbookProtocols.swift in Sources */,
|
||||||
A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */,
|
A703226A2ABAF49800D7C4ED /* JSONCoderExtension.swift in Sources */,
|
||||||
A79AA8ED2B063AD5007D25F2 /* NextcloudApi.swift in Sources */,
|
A79AA8ED2B063AD5007D25F2 /* NextcloudApi.swift in Sources */,
|
||||||
A97506132B920D9F00E86029 /* RecipeDurationSection.swift in Sources */,
|
A97506132B920D9F00E86029 /* RecipeDurationSection.swift in Sources */,
|
||||||
@@ -793,7 +841,7 @@
|
|||||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
@@ -837,7 +885,7 @@
|
|||||||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.1;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
MACOSX_DEPLOYMENT_TARGET = 14.0;
|
||||||
@@ -1005,6 +1053,14 @@
|
|||||||
minimumVersion = 2.4.1;
|
minimumVersion = 2.4.1;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
A9E78A352BEA839100206866 /* XCRemoteSwiftPackageReference "keychain-swift" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/evgenyneu/keychain-swift.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 24.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
@@ -1018,6 +1074,11 @@
|
|||||||
package = A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */;
|
package = A9CA6CF42B4C63F200F78AB5 /* XCRemoteSwiftPackageReference "TPPDF" */;
|
||||||
productName = TPPDF;
|
productName = TPPDF;
|
||||||
};
|
};
|
||||||
|
A9E78A362BEA839100206866 /* KeychainSwift */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = A9E78A352BEA839100206866 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||||
|
productName = KeychainSwift;
|
||||||
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = A70171762AA8E71900064C43 /* Project object */;
|
rootObject = A70171762AA8E71900064C43 /* Project object */;
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"originHash" : "4b59f87688d89ebd5be92449f747ea79123f0d90515aea6b92e218b4860a3ef3",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "keychain-swift",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/evgenyneu/keychain-swift.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "5e1b02b6a9dac2a759a1d5dbc175c86bd192a608",
|
||||||
|
"version" : "24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swiftsoup",
|
"identity" : "swiftsoup",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -19,5 +29,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 2
|
"version" : 3
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -6,3 +6,40 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@Observable
|
||||||
|
class AccountManager {
|
||||||
|
var accounts: [any Account] = []
|
||||||
|
var authTokens: [String: String] = [:]
|
||||||
|
|
||||||
|
/// Save account as JSON.
|
||||||
|
func save(account: any Account, authToken: String?) async throws {
|
||||||
|
account.saveTokenToKeychain(authToken!)
|
||||||
|
let data = try JSONEncoder().encode(account)
|
||||||
|
|
||||||
|
let accountDir = account.accountType.rawValue + "/" + account.id.uuidString + "/account.json"
|
||||||
|
await DataStore.shared.save(data: data, toPath: accountDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load accounts from JSON files.
|
||||||
|
func loadAccounts() async throws {
|
||||||
|
// Read data from file or user defaults
|
||||||
|
for accountType in AccountType.allCases {
|
||||||
|
// List all account UUIDs under the /accountType directory
|
||||||
|
let accountUUIDs = DataStore.shared.listAllFolders(dir: accountType.rawValue + "/")
|
||||||
|
|
||||||
|
// Decode each account and fetch the authToken
|
||||||
|
for accountUUID in accountUUIDs {
|
||||||
|
do {
|
||||||
|
guard let account = try await DataStore.shared.loadDynamic(fromPath: accountType.rawValue + "/" + accountUUID + "/account.json", type: accountType.accountType) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
authTokens[accountUUID] = (account as! any Account).getTokenFromKeychain() ?? ""
|
||||||
|
self.accounts.append(account as! (any Account))
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,3 +6,44 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import KeychainSwift
|
||||||
|
|
||||||
|
|
||||||
|
enum AccountType: String, Codable, CaseIterable {
|
||||||
|
case cookbook = "cookbook"
|
||||||
|
case local = "local"
|
||||||
|
|
||||||
|
var accountType: any Decodable.Type {
|
||||||
|
switch self {
|
||||||
|
case .cookbook: return CookbookAccount.self
|
||||||
|
case .local: return LocalAccount.self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol Account: Codable, Identifiable {
|
||||||
|
/// A unique identifier for this account
|
||||||
|
var id: UUID { get }
|
||||||
|
|
||||||
|
/// A name for the account that can be displayed in the UI
|
||||||
|
var displayName: String { get }
|
||||||
|
|
||||||
|
/// For differentiating account types when decoding
|
||||||
|
var accountType: AccountType { get }
|
||||||
|
|
||||||
|
/// Base endpoint URL
|
||||||
|
var baseURL: URL { get }
|
||||||
|
|
||||||
|
/// Account username
|
||||||
|
var username: String { get }
|
||||||
|
|
||||||
|
/// For storing/retrieving tokens from Keychain
|
||||||
|
func saveTokenToKeychain(_ token: String)
|
||||||
|
func getTokenFromKeychain() -> String?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// CookbookV1Account.swift
|
// CookbookAccount.swift
|
||||||
// Nextcloud Cookbook iOS Client
|
// Nextcloud Cookbook iOS Client
|
||||||
//
|
//
|
||||||
// Created by Vincent Meilinger on 24.01.25.
|
// Created by Vincent Meilinger on 24.01.25.
|
||||||
@@ -10,9 +10,10 @@ import KeychainSwift
|
|||||||
|
|
||||||
|
|
||||||
struct CookbookAccount: Account {
|
struct CookbookAccount: Account {
|
||||||
|
let accountType: AccountType = .cookbook
|
||||||
|
|
||||||
let id: UUID
|
let id: UUID
|
||||||
var displayName: String = "Nextcloud Cookbook Account"
|
var displayName: String = "Nextcloud Cookbook Account"
|
||||||
let accountType: AccountType = .cookbook
|
|
||||||
|
|
||||||
let baseURL: URL
|
let baseURL: URL
|
||||||
let username: String
|
let username: String
|
||||||
|
|||||||
@@ -6,3 +6,23 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
struct LocalAccount: Account {
|
||||||
|
let id: UUID
|
||||||
|
var displayName: String = "Local Account"
|
||||||
|
var accountType: AccountType = .local
|
||||||
|
|
||||||
|
let baseURL: URL = URL(filePath: "")!
|
||||||
|
let username: String = ""
|
||||||
|
|
||||||
|
/// Keychain convenience
|
||||||
|
func saveTokenToKeychain(_ token: String) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTokenFromKeychain() -> String? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
/*
|
||||||
@MainActor class AppState: ObservableObject {
|
@MainActor class AppState: ObservableObject {
|
||||||
@Published var categories: [Category] = []
|
@Published var categories: [Category] = []
|
||||||
@Published var recipes: [String: [Recipe]] = [:]
|
@Published var recipes: [String: [CookbookApiRecipeV1]] = [:]
|
||||||
@Published var recipeDetails: [Int: RecipeDetail] = [:]
|
@Published var recipeDetails: [Int: CookbookApiRecipeDetailV1] = [:]
|
||||||
@Published var timers: [String: RecipeTimer] = [:]
|
@Published var timers: [String: RecipeTimer] = [:]
|
||||||
var recipeImages: [Int: [String: UIImage]] = [:]
|
var recipeImages: [Int: [String: UIImage]] = [:]
|
||||||
var imagesNeedUpdate: [Int: [String: Bool]] = [:]
|
var imagesNeedUpdate: [Int: [String: Bool]] = [:]
|
||||||
@@ -86,7 +86,7 @@ import UIKit
|
|||||||
func getCategory(named name: String, fetchMode: FetchMode) async {
|
func getCategory(named name: String, fetchMode: FetchMode) async {
|
||||||
print("getCategory(\(name), fetchMode: \(fetchMode))")
|
print("getCategory(\(name), fetchMode: \(fetchMode))")
|
||||||
func getLocal() async -> Bool {
|
func getLocal() async -> Bool {
|
||||||
if let recipes: [Recipe] = await loadLocal(path: "category_\(categoryString).data") {
|
if let recipes: [CookbookApiRecipeV1] = await loadLocal(path: "category_\(categoryString).data") {
|
||||||
self.recipes[name] = recipes
|
self.recipes[name] = recipes
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ import UIKit
|
|||||||
```swift
|
```swift
|
||||||
let recipes = await mainViewModel.getRecipes()
|
let recipes = await mainViewModel.getRecipes()
|
||||||
*/
|
*/
|
||||||
func getRecipes() async -> [Recipe] {
|
func getRecipes() async -> [CookbookApiRecipeV1] {
|
||||||
let (recipes, error) = await cookbookApi.getRecipes(
|
let (recipes, error) = await cookbookApi.getRecipes(
|
||||||
auth: UserSettings.shared.authString
|
auth: UserSettings.shared.authString
|
||||||
)
|
)
|
||||||
@@ -168,7 +168,7 @@ import UIKit
|
|||||||
} else if let error = error {
|
} else if let error = error {
|
||||||
print(error)
|
print(error)
|
||||||
}
|
}
|
||||||
var allRecipes: [Recipe] = []
|
var allRecipes: [CookbookApiRecipeV1] = []
|
||||||
for category in categories {
|
for category in categories {
|
||||||
if let recipeArray = self.recipes[category.name] {
|
if let recipeArray = self.recipes[category.name] {
|
||||||
allRecipes.append(contentsOf: recipeArray)
|
allRecipes.append(contentsOf: recipeArray)
|
||||||
@@ -193,13 +193,13 @@ import UIKit
|
|||||||
```swift
|
```swift
|
||||||
let recipeDetail = await mainViewModel.getRecipe(id: 123)
|
let recipeDetail = await mainViewModel.getRecipe(id: 123)
|
||||||
*/
|
*/
|
||||||
func getRecipe(id: Int, fetchMode: FetchMode, save: Bool = false) async -> RecipeDetail? {
|
func getRecipe(id: Int, fetchMode: FetchMode, save: Bool = false) async -> CookbookApiRecipeDetailV1? {
|
||||||
func getLocal() async -> RecipeDetail? {
|
func getLocal() async -> CookbookApiRecipeDetailV1? {
|
||||||
if let recipe: RecipeDetail = await loadLocal(path: "recipe\(id).data") { return recipe }
|
if let recipe: CookbookApiRecipeDetailV1 = await loadLocal(path: "recipe\(id).data") { return recipe }
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getServer() async -> RecipeDetail? {
|
func getServer() async -> CookbookApiRecipeDetailV1? {
|
||||||
let (recipe, error) = await cookbookApi.getRecipe(
|
let (recipe, error) = await cookbookApi.getRecipe(
|
||||||
auth: UserSettings.shared.authString,
|
auth: UserSettings.shared.authString,
|
||||||
id: id
|
id: id
|
||||||
@@ -483,7 +483,7 @@ import UIKit
|
|||||||
```swift
|
```swift
|
||||||
let uploadResult = await mainViewModel.uploadRecipe(recipeDetail: myRecipeDetail, createNew: true)
|
let uploadResult = await mainViewModel.uploadRecipe(recipeDetail: myRecipeDetail, createNew: true)
|
||||||
*/
|
*/
|
||||||
func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert? {
|
func uploadRecipe(recipeDetail: CookbookApiRecipeDetailV1, createNew: Bool) async -> RequestAlert? {
|
||||||
var error: NetworkError? = nil
|
var error: NetworkError? = nil
|
||||||
if createNew {
|
if createNew {
|
||||||
error = await cookbookApi.createRecipe(
|
error = await cookbookApi.createRecipe(
|
||||||
@@ -502,7 +502,7 @@ import UIKit
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func importRecipe(url: String) async -> (RecipeDetail?, RequestAlert?) {
|
func importRecipe(url: String) async -> (CookbookApiRecipeDetailV1?, RequestAlert?) {
|
||||||
guard let data = JSONEncoder.safeEncode(RecipeImportRequest(url: url)) else { return (nil, .REQUEST_DROPPED) }
|
guard let data = JSONEncoder.safeEncode(RecipeImportRequest(url: url)) else { return (nil, .REQUEST_DROPPED) }
|
||||||
let (recipeDetail, error) = await cookbookApi.importRecipe(
|
let (recipeDetail, error) = await cookbookApi.importRecipe(
|
||||||
auth: UserSettings.shared.authString,
|
auth: UserSettings.shared.authString,
|
||||||
@@ -633,3 +633,4 @@ extension AppState {
|
|||||||
timers.removeValue(forKey: recipeId)
|
timers.removeValue(forKey: recipeId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -37,12 +37,21 @@ class DataStore {
|
|||||||
guard let data = try? Data(contentsOf: fileURL) else {
|
guard let data = try? Data(contentsOf: fileURL) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let storedRecipes = try JSONDecoder().decode(D.self, from: data)
|
let decodedData = try JSONDecoder().decode(D.self, from: data)
|
||||||
return storedRecipes
|
return decodedData
|
||||||
}
|
}
|
||||||
return try await task.value
|
return try await task.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadDynamic(fromPath path: String, type: Decodable.Type) async throws -> Any? {
|
||||||
|
let fileURL = try Self.fileURL(appending: path)
|
||||||
|
guard let data = try? Data(contentsOf: fileURL) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let decoded = try JSONDecoder().decode(type, from: data)
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
func save<D: Encodable>(data: D, toPath path: String) async {
|
func save<D: Encodable>(data: D, toPath path: String) async {
|
||||||
let task = Task {
|
let task = Task {
|
||||||
let data = try JSONEncoder().encode(data)
|
let data = try JSONEncoder().encode(data)
|
||||||
@@ -69,6 +78,27 @@ class DataStore {
|
|||||||
return fileManager.fileExists(atPath: folderPath + filePath)
|
return fileManager.fileExists(atPath: folderPath + filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listAllFolders(dir: String) -> [String] {
|
||||||
|
guard let baseURL = try? Self.fileURL() else {
|
||||||
|
print("Failed to retrieve documents directory.")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetURL = baseURL.appendingPathComponent(dir)
|
||||||
|
|
||||||
|
do {
|
||||||
|
let contents = try fileManager.contentsOfDirectory(at: targetURL, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles])
|
||||||
|
|
||||||
|
let folders = contents.filter { url in
|
||||||
|
(try? url.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
|
||||||
|
}
|
||||||
|
return folders.map { $0.lastPathComponent }
|
||||||
|
} catch {
|
||||||
|
print("Error listing folders in \(dir): \(error)")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func clearAll() -> Bool {
|
func clearAll() -> Bool {
|
||||||
print("Attempting to delete all data ...")
|
print("Attempting to delete all data ...")
|
||||||
guard let folderPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first?.path() else { return false }
|
guard let folderPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first?.path() else { return false }
|
||||||
|
|||||||
@@ -7,18 +7,183 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
class ObservableRecipeDetail: ObservableObject {
|
// MARK: - Recipe Model
|
||||||
|
|
||||||
|
@Model
|
||||||
|
class RecipeImage {
|
||||||
|
enum RecipeImageSize {
|
||||||
|
case THUMB, FULL
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageData: Data?
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var image: UIImage? {
|
||||||
|
guard let imageData else { return nil }
|
||||||
|
return UIImage(data: imageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(imageData: Data? = nil) {
|
||||||
|
self.imageData = imageData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Model
|
||||||
|
class RecipeThumbnail {
|
||||||
|
var thumbnailData: Data?
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var thumbnail: UIImage? {
|
||||||
|
guard let thumbnailData else { return nil }
|
||||||
|
return UIImage(data: thumbnailData)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(thumbnailData: Data? = nil) {
|
||||||
|
self.thumbnailData = thumbnailData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Model
|
||||||
|
class Recipe {
|
||||||
|
var id: String
|
||||||
|
var name: String
|
||||||
|
var keywords: [String]
|
||||||
|
@Attribute(.externalStorage) var image: RecipeImage?
|
||||||
|
var thumbnail: RecipeThumbnail?
|
||||||
|
var dateCreated: String? = nil
|
||||||
|
var dateModified: String? = nil
|
||||||
|
var prepTime: String
|
||||||
|
var cookTime: String
|
||||||
|
var totalTime: String
|
||||||
|
var recipeDescription: String
|
||||||
|
var url: String?
|
||||||
|
var yield: Int
|
||||||
|
var category: String
|
||||||
|
var tools: [String]
|
||||||
|
var ingredients: [String]
|
||||||
|
var instructions: [String]
|
||||||
|
var nutrition: [String:String]
|
||||||
|
|
||||||
|
// Additional functionality
|
||||||
|
@Transient
|
||||||
|
var ingredientMultiplier: Double = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
keywords: [String],
|
||||||
|
dateCreated: String? = nil,
|
||||||
|
dateModified: String? = nil,
|
||||||
|
prepTime: String,
|
||||||
|
cookTime: String,
|
||||||
|
totalTime: String,
|
||||||
|
recipeDescription: String,
|
||||||
|
url: String? = nil,
|
||||||
|
yield: Int,
|
||||||
|
category: String,
|
||||||
|
tools: [String],
|
||||||
|
ingredients: [String],
|
||||||
|
instructions: [String],
|
||||||
|
nutrition: [String : String],
|
||||||
|
ingredientMultiplier: Double
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.name = name
|
||||||
|
self.keywords = keywords
|
||||||
|
self.dateCreated = dateCreated
|
||||||
|
self.dateModified = dateModified
|
||||||
|
self.prepTime = prepTime
|
||||||
|
self.cookTime = cookTime
|
||||||
|
self.totalTime = totalTime
|
||||||
|
self.recipeDescription = recipeDescription
|
||||||
|
self.url = url
|
||||||
|
self.yield = yield
|
||||||
|
self.category = category
|
||||||
|
self.tools = tools
|
||||||
|
self.ingredients = ingredients
|
||||||
|
self.instructions = instructions
|
||||||
|
self.nutrition = nutrition
|
||||||
|
self.ingredientMultiplier = ingredientMultiplier
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try container.decode(String.self, forKey: .id)
|
||||||
|
name = try container.decode(String.self, forKey: .name)
|
||||||
|
keywords = try container.decode([String].self, forKey: .keywords)
|
||||||
|
dateCreated = try container.decodeIfPresent(String.self, forKey: .dateCreated)
|
||||||
|
dateModified = try container.decodeIfPresent(String.self, forKey: .dateModified)
|
||||||
|
prepTime = try container.decode(String.self, forKey: .prepTime)
|
||||||
|
cookTime = try container.decode(String.self, forKey: .cookTime)
|
||||||
|
totalTime = try container.decode(String.self, forKey: .totalTime)
|
||||||
|
recipeDescription = try container.decode(String.self, forKey: .recipeDescription)
|
||||||
|
url = try container.decodeIfPresent(String.self, forKey: .url)
|
||||||
|
yield = try container.decode(Int.self, forKey: .yield)
|
||||||
|
category = try container.decode(String.self, forKey: .category)
|
||||||
|
tools = try container.decode([String].self, forKey: .tools)
|
||||||
|
ingredients = try container.decode([String].self, forKey: .ingredients)
|
||||||
|
instructions = try container.decode([String].self, forKey: .instructions)
|
||||||
|
nutrition = try container.decode([String: String].self, forKey: .nutrition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Recipe: Codable {
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id, name, keywords, dateCreated, dateModified, prepTime, cookTime, totalTime, recipeDescription, url, yield, category, tools, ingredients, instructions, nutrition
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(id, forKey: .id)
|
||||||
|
try container.encode(name, forKey: .name)
|
||||||
|
try container.encode(keywords, forKey: .keywords)
|
||||||
|
try container.encode(dateCreated, forKey: .dateCreated)
|
||||||
|
try container.encode(dateModified, forKey: .dateModified)
|
||||||
|
try container.encode(prepTime, forKey: .prepTime)
|
||||||
|
try container.encode(cookTime, forKey: .cookTime)
|
||||||
|
try container.encode(totalTime, forKey: .totalTime)
|
||||||
|
try container.encode(recipeDescription, forKey: .recipeDescription)
|
||||||
|
try container.encode(url, forKey: .url)
|
||||||
|
try container.encode(yield, forKey: .yield)
|
||||||
|
try container.encode(category, forKey: .category)
|
||||||
|
try container.encode(tools, forKey: .tools)
|
||||||
|
try container.encode(ingredients, forKey: .ingredients)
|
||||||
|
try container.encode(instructions, forKey: .instructions)
|
||||||
|
try container.encode(nutrition, forKey: .nutrition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// MARK: - Recipe Stub
|
||||||
|
|
||||||
|
struct RecipeStub: Codable, Hashable, Identifiable {
|
||||||
|
let id: String
|
||||||
|
let name: String
|
||||||
|
let keywords: String?
|
||||||
|
let dateCreated: String?
|
||||||
|
let dateModified: String?
|
||||||
|
let thumbnailPath: String?
|
||||||
|
var storedLocally: Bool = false
|
||||||
|
var lastUpdated: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Recipe
|
||||||
|
/*
|
||||||
|
class Recipe: ObservableObject {
|
||||||
// Cookbook recipe detail fields
|
// Cookbook recipe detail fields
|
||||||
var id: String
|
var id: String
|
||||||
@Published var name: String
|
@Published var name: String
|
||||||
@Published var keywords: [String]
|
@Published var keywords: [String]
|
||||||
@Published var imageUrl: String
|
@Published var imageUrl: String?
|
||||||
|
var dateCreated: String? = nil
|
||||||
|
var dateModified: String? = nil
|
||||||
@Published var prepTime: DurationComponents
|
@Published var prepTime: DurationComponents
|
||||||
@Published var cookTime: DurationComponents
|
@Published var cookTime: DurationComponents
|
||||||
@Published var totalTime: DurationComponents
|
@Published var totalTime: DurationComponents
|
||||||
@Published var description: String
|
@Published var description: String
|
||||||
@Published var url: String
|
@Published var url: String?
|
||||||
@Published var recipeYield: Int
|
@Published var recipeYield: Int
|
||||||
@Published var recipeCategory: String
|
@Published var recipeCategory: String
|
||||||
@Published var tool: [String]
|
@Published var tool: [String]
|
||||||
@@ -51,7 +216,7 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
ingredientMultiplier = 1
|
ingredientMultiplier = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ recipeDetail: RecipeDetail) {
|
init(_ recipeDetail: CookbookApiRecipeDetailV1) {
|
||||||
id = recipeDetail.id
|
id = recipeDetail.id
|
||||||
name = recipeDetail.name
|
name = recipeDetail.name
|
||||||
keywords = recipeDetail.keywords.isEmpty ? [] : recipeDetail.keywords.components(separatedBy: ",")
|
keywords = recipeDetail.keywords.isEmpty ? [] : recipeDetail.keywords.components(separatedBy: ",")
|
||||||
@@ -71,8 +236,70 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
ingredientMultiplier = Double(recipeDetail.recipeYield == 0 ? 1 : recipeDetail.recipeYield)
|
ingredientMultiplier = Double(recipeDetail.recipeYield == 0 ? 1 : recipeDetail.recipeYield)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toRecipeDetail() -> RecipeDetail {
|
init(
|
||||||
return RecipeDetail(
|
name: String,
|
||||||
|
keywords: [String],
|
||||||
|
dateCreated: String?,
|
||||||
|
dateModified: String?,
|
||||||
|
imageUrl: String?,
|
||||||
|
id: String,
|
||||||
|
prepTime: DurationComponents? = nil,
|
||||||
|
cookTime: DurationComponents? = nil,
|
||||||
|
totalTime: DurationComponents? = nil,
|
||||||
|
description: String,
|
||||||
|
url: String?,
|
||||||
|
recipeYield: Int,
|
||||||
|
recipeCategory: String,
|
||||||
|
tool: [String],
|
||||||
|
recipeIngredient: [String],
|
||||||
|
recipeInstructions: [String],
|
||||||
|
nutrition: [String:String]
|
||||||
|
) {
|
||||||
|
self.name = name
|
||||||
|
self.keywords = keywords
|
||||||
|
self.dateCreated = dateCreated
|
||||||
|
self.dateModified = dateModified
|
||||||
|
self.imageUrl = imageUrl
|
||||||
|
self.id = id
|
||||||
|
self.prepTime = prepTime ?? DurationComponents()
|
||||||
|
self.cookTime = cookTime ?? DurationComponents()
|
||||||
|
self.totalTime = totalTime ?? DurationComponents()
|
||||||
|
self.description = description
|
||||||
|
self.url = url
|
||||||
|
self.recipeYield = recipeYield
|
||||||
|
self.recipeCategory = recipeCategory
|
||||||
|
self.tool = tool
|
||||||
|
self.recipeIngredient = recipeIngredient
|
||||||
|
self.recipeInstructions = recipeInstructions
|
||||||
|
self.nutrition = nutrition
|
||||||
|
|
||||||
|
ingredientMultiplier = Double(recipeYield == 0 ? 1 : recipeYield)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try container.decode(String.self, forKey: .id)
|
||||||
|
_name = Published(initialValue: try container.decode(String.self, forKey: .name))
|
||||||
|
_keywords = Published(initialValue: try container.decode([String].self, forKey: .keywords))
|
||||||
|
_imageUrl = Published(initialValue: try container.decodeIfPresent(String.self, forKey: .imageUrl))
|
||||||
|
dateCreated = try container.decodeIfPresent(String.self, forKey: .dateCreated)
|
||||||
|
dateModified = try container.decodeIfPresent(String.self, forKey: .dateModified)
|
||||||
|
_prepTime = Published(initialValue: try container.decode(DurationComponents.self, forKey: .prepTime))
|
||||||
|
_cookTime = Published(initialValue: try container.decode(DurationComponents.self, forKey: .cookTime))
|
||||||
|
_totalTime = Published(initialValue: try container.decode(DurationComponents.self, forKey: .totalTime))
|
||||||
|
_description = Published(initialValue: try container.decode(String.self, forKey: .description))
|
||||||
|
_url = Published(initialValue: try container.decodeIfPresent(String.self, forKey: .url))
|
||||||
|
_recipeYield = Published(initialValue: try container.decode(Int.self, forKey: .recipeYield))
|
||||||
|
_recipeCategory = Published(initialValue: try container.decode(String.self, forKey: .recipeCategory))
|
||||||
|
_tool = Published(initialValue: try container.decode([String].self, forKey: .tool))
|
||||||
|
_recipeIngredient = Published(initialValue: try container.decode([String].self, forKey: .recipeIngredient))
|
||||||
|
_recipeInstructions = Published(initialValue: try container.decode([String].self, forKey: .recipeInstructions))
|
||||||
|
_nutrition = Published(initialValue: try container.decode([String: String].self, forKey: .nutrition))
|
||||||
|
_ingredientMultiplier = Published(initialValue: try container.decode(Double.self, forKey: .ingredientMultiplier))
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRecipeDetail() -> CookbookApiRecipeDetailV1 {
|
||||||
|
return CookbookApiRecipeDetailV1(
|
||||||
name: self.name,
|
name: self.name,
|
||||||
keywords: self.keywords.joined(separator: ","),
|
keywords: self.keywords.joined(separator: ","),
|
||||||
dateCreated: "",
|
dateCreated: "",
|
||||||
@@ -98,14 +325,14 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
return AttributedString(ingredient)
|
return AttributedString(ingredient)
|
||||||
}
|
}
|
||||||
// Match mixed fractions first
|
// Match mixed fractions first
|
||||||
var matches = ObservableRecipeDetail.matchPatternAndMultiply(
|
var matches = Recipe.matchPatternAndMultiply(
|
||||||
.mixedFraction,
|
.mixedFraction,
|
||||||
in: ingredient,
|
in: ingredient,
|
||||||
multFactor: factor
|
multFactor: factor
|
||||||
)
|
)
|
||||||
// Then match fractions, exclude mixed fraction ranges
|
// Then match fractions, exclude mixed fraction ranges
|
||||||
matches.append(contentsOf:
|
matches.append(contentsOf:
|
||||||
ObservableRecipeDetail.matchPatternAndMultiply(
|
Recipe.matchPatternAndMultiply(
|
||||||
.fraction,
|
.fraction,
|
||||||
in: ingredient,
|
in: ingredient,
|
||||||
multFactor: factor,
|
multFactor: factor,
|
||||||
@@ -114,7 +341,7 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
)
|
)
|
||||||
// Match numbers at last, exclude all prior matches
|
// Match numbers at last, exclude all prior matches
|
||||||
matches.append(contentsOf:
|
matches.append(contentsOf:
|
||||||
ObservableRecipeDetail.matchPatternAndMultiply(
|
Recipe.matchPatternAndMultiply(
|
||||||
.number,
|
.number,
|
||||||
in: ingredient,
|
in: ingredient,
|
||||||
multFactor: factor,
|
multFactor: factor,
|
||||||
@@ -221,6 +448,34 @@ class ObservableRecipeDetail: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Recipe: Codable {
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id, name, keywords, imageUrl, dateCreated, dateModified, prepTime, cookTime, totalTime, description, url, recipeYield, recipeCategory, tool, recipeIngredient, recipeInstructions, nutrition, ingredientMultiplier
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(id, forKey: .id)
|
||||||
|
try container.encode(name, forKey: .name)
|
||||||
|
try container.encode(keywords, forKey: .keywords)
|
||||||
|
try container.encode(imageUrl, forKey: .imageUrl)
|
||||||
|
try container.encode(dateCreated, forKey: .dateCreated)
|
||||||
|
try container.encode(dateModified, forKey: .dateModified)
|
||||||
|
try container.encode(prepTime, forKey: .prepTime)
|
||||||
|
try container.encode(cookTime, forKey: .cookTime)
|
||||||
|
try container.encode(totalTime, forKey: .totalTime)
|
||||||
|
try container.encode(description, forKey: .description)
|
||||||
|
try container.encode(url, forKey: .url)
|
||||||
|
try container.encode(recipeYield, forKey: .recipeYield)
|
||||||
|
try container.encode(recipeCategory, forKey: .recipeCategory)
|
||||||
|
try container.encode(tool, forKey: .tool)
|
||||||
|
try container.encode(recipeIngredient, forKey: .recipeIngredient)
|
||||||
|
try container.encode(recipeInstructions, forKey: .recipeInstructions)
|
||||||
|
try container.encode(nutrition, forKey: .nutrition)
|
||||||
|
try container.encode(ingredientMultiplier, forKey: .ingredientMultiplier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum RegexPattern: String, CaseIterable, Identifiable {
|
enum RegexPattern: String, CaseIterable, Identifiable {
|
||||||
case mixedFraction, fraction, number
|
case mixedFraction, fraction, number
|
||||||
|
|
||||||
@@ -249,3 +504,4 @@ enum RegexPattern: String, CaseIterable, Identifiable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"(%lld)" : {
|
"(%lld)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -135,6 +136,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"%@: %@" : {
|
"%@: %@" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -285,6 +287,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"%lld Serving(s)" : {
|
"%lld Serving(s)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -397,6 +400,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"A simple-to-use PDF builder for Swift. Used for generating recipe PDF documents." : {
|
"A simple-to-use PDF builder for Swift. Used for generating recipe PDF documents." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -419,6 +423,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"About" : {
|
"About" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -441,6 +446,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Acknowledgements" : {
|
"Acknowledgements" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -508,6 +514,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add cooking steps for fellow chefs to follow." : {
|
"Add cooking steps for fellow chefs to follow." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -530,6 +537,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add groceries to this list by either using the button next to an ingredient list in a recipe, or by swiping right on individual ingredients of a recipe." : {
|
"Add groceries to this list by either using the button next to an ingredient list in a recipe, or by swiping right on individual ingredients of a recipe." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -552,6 +560,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Add new recipe" : {
|
"Add new recipe" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -574,6 +583,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"An HTML parsing and web scraping library for Swift. Used for importing schema.org recipes from websites." : {
|
"An HTML parsing and web scraping library for Swift. Used for importing schema.org recipes from websites." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -641,6 +651,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"App Token Login" : {
|
"App Token Login" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -753,6 +764,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Category" : {
|
"Category" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -821,6 +833,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Choose" : {
|
"Choose" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -841,8 +854,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Client error" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Comma (e.g. 1,42)" : {
|
"Comma (e.g. 1,42)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -865,6 +882,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Configure what is stored on your device." : {
|
"Configure what is stored on your device." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -887,6 +905,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Configure which sections in your recipes are expanded by default." : {
|
"Configure which sections in your recipes are expanded by default." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -909,6 +928,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Connected to server." : {
|
"Connected to server." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -953,6 +973,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Cookbook Client" : {
|
"Cookbook Client" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -975,6 +996,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Cookbooks" : {
|
"Cookbooks" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1041,6 +1063,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Copy Link" : {
|
"Copy Link" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1085,6 +1108,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Created: %@" : {
|
"Created: %@" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1105,8 +1129,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Data decoding failed." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Decimal number format" : {
|
"Decimal number format" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1127,6 +1155,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Decoding Error" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Delete" : {
|
"Delete" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1151,6 +1182,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Delete local data" : {
|
"Delete local data" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1196,6 +1228,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Delete Recipe" : {
|
"Delete Recipe" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1240,6 +1273,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Deleting local data will not affect the recipe data stored on your server." : {
|
"Deleting local data will not affect the recipe data stored on your server." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1284,6 +1318,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Description" : {
|
"Description" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1328,6 +1363,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Downloads" : {
|
"Downloads" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1395,6 +1431,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"e.g.: example.com" : {
|
"e.g.: example.com" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1417,6 +1454,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Edit" : {
|
"Edit" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1437,6 +1475,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Encoding Error" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Error" : {
|
"Error" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -1483,6 +1524,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Expand information section" : {
|
"Expand information section" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1505,6 +1547,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Expand keyword section" : {
|
"Expand keyword section" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1527,6 +1570,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Expand nutrition section" : {
|
"Expand nutrition section" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1595,6 +1639,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Fraction" : {
|
"Fraction" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1617,6 +1662,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"General" : {
|
"General" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1639,6 +1685,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Get support" : {
|
"Get support" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1661,6 +1708,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Grocery List" : {
|
"Grocery List" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1683,6 +1731,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Hours" : {
|
"Hours" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1705,6 +1754,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"If 'Same as Device' is selected and your device language is not supported yet, this option will default to english." : {
|
"If 'Same as Device' is selected and your device language is not supported yet, this option will default to english." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1727,6 +1777,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"If the login button does not open your browser, use the 'Copy Link' button and paste the link in your browser manually." : {
|
"If the login button does not open your browser, use the 'Copy Link' button and paste the link in your browser manually." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1749,6 +1800,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"If you are interested in contributing to this project or simply wish to review its source code, we encourage you to visit the GitHub repository for this application." : {
|
"If you are interested in contributing to this project or simply wish to review its source code, we encourage you to visit the GitHub repository for this application." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1771,6 +1823,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"If you have any inquiries, feedback, or require assistance, please refer to the support page for contact information." : {
|
"If you have any inquiries, feedback, or require assistance, please refer to the support page for contact information." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1793,6 +1846,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Import" : {
|
"Import" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1815,6 +1869,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Import Recipe" : {
|
"Import Recipe" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1837,6 +1892,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Ingredient" : {
|
"Ingredient" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1859,6 +1915,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Ingredients" : {
|
"Ingredients" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1927,6 +1984,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Instruction" : {
|
"Instruction" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1949,6 +2007,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Instructions" : {
|
"Instructions" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1969,8 +2028,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Invalid data error." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Invalid request" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Keep screen awake when viewing recipes" : {
|
"Keep screen awake when viewing recipes" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1993,6 +2059,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Keywords" : {
|
"Keywords" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2015,6 +2082,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Language" : {
|
"Language" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2037,6 +2105,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Last modified: %@" : {
|
"Last modified: %@" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2059,6 +2128,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Last updated: %@" : {
|
"Last updated: %@" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2081,6 +2151,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"List your tools here. 🍴" : {
|
"List your tools here. 🍴" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2103,6 +2174,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Log out" : {
|
"Log out" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2125,6 +2197,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Login" : {
|
"Login" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2169,6 +2242,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Login Method" : {
|
"Login Method" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2191,6 +2265,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Make sure to enter the server address in the form 'example.com', or \n'<server address>:<port>'\n when a non-standard port is used." : {
|
"Make sure to enter the server address in the form 'example.com', or \n'<server address>:<port>'\n when a non-standard port is used." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2213,6 +2288,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Marked ingredients could not be adjusted!" : {
|
"Marked ingredients could not be adjusted!" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2235,6 +2311,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Minutes" : {
|
"Minutes" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2323,8 +2400,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Missing URL." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Mixed fraction" : {
|
"Mixed fraction" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2347,6 +2428,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"More information" : {
|
"More information" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2389,6 +2471,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"New" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"New recipe" : {
|
"New recipe" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
@@ -2436,6 +2521,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Nextcloud Login" : {
|
"Nextcloud Login" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2458,6 +2544,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"No keywords." : {
|
"No keywords." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2480,6 +2567,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"No nutritional information." : {
|
"No nutritional information." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2502,6 +2590,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"None" : {
|
"None" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2524,6 +2613,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Number" : {
|
"Number" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2546,6 +2636,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Nutrition" : {
|
"Nutrition" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2568,6 +2659,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Nutrition (%@)" : {
|
"Nutrition (%@)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2590,6 +2682,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Offline recipes" : {
|
"Offline recipes" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2634,6 +2727,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Other" : {
|
"Other" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2654,6 +2748,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Parameter encoding failed." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Parameters are nil." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Parsing error" : {
|
"Parsing error" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2678,6 +2778,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Paste the url of a recipe you would like to import in the above, and we will try to fill in the fields for you. This feature does not work with every website. If your favourite website is not supported, feel free to reach out for help. You can find the contact details in the app settings." : {
|
"Paste the url of a recipe you would like to import in the above, and we will try to fill in the fields for you. This feature does not work with every website. If your favourite website is not supported, feel free to reach out for help. You can find the contact details in the app settings." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2788,6 +2889,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Point (e.g. 1.42)" : {
|
"Point (e.g. 1.42)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2810,6 +2912,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Preparation" : {
|
"Preparation" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2900,6 +3003,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Recipe Name" : {
|
"Recipe Name" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2944,6 +3048,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Recipes" : {
|
"Recipes" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2964,8 +3069,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Redirection error" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Refresh" : {
|
"Refresh" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -2988,6 +3097,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Refresh all" : {
|
"Refresh all" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3055,6 +3165,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Search" : {
|
"Search" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3077,6 +3188,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Search recipe" : {
|
"Search recipe" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3099,6 +3211,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Search recipes/keywords" : {
|
"Search recipes/keywords" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3121,6 +3234,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Select a default cookbook" : {
|
"Select a default cookbook" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3143,6 +3257,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Select Keywords" : {
|
"Select Keywords" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3165,6 +3280,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Selected keywords:" : {
|
"Selected keywords:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3185,6 +3301,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Server error" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Serving size" : {
|
"Serving size" : {
|
||||||
"comment" : "Serving size",
|
"comment" : "Serving size",
|
||||||
@@ -3210,6 +3329,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Servings" : {
|
"Servings" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3344,6 +3464,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Share Recipe" : {
|
"Share Recipe" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3366,6 +3487,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Show help" : {
|
"Show help" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3411,6 +3533,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Start by adding your first ingredient! 🥬" : {
|
"Start by adding your first ingredient! 🥬" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3433,6 +3556,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Store recipe images locally" : {
|
"Store recipe images locally" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3455,6 +3579,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Store recipe thumbnails locally" : {
|
"Store recipe thumbnails locally" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3477,6 +3602,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Submit" : {
|
"Submit" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3544,6 +3670,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Support" : {
|
"Support" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3566,6 +3693,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SwiftSoup" : {
|
"SwiftSoup" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3588,6 +3716,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Thank you for downloading" : {
|
"Thank you for downloading" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3610,6 +3739,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"The 'Login' button will open a web browser. Please follow the login instructions provided there.\nAfter a successful login, return to this application and press 'Validate'." : {
|
"The 'Login' button will open a web browser. Please follow the login instructions provided there.\nAfter a successful login, return to this application and press 'Validate'." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3655,6 +3785,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"The selected cookbook will open on app launch by default." : {
|
"The selected cookbook will open on app launch by default." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3677,6 +3808,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"There are no recipes in this cookbook!" : {
|
"There are no recipes in this cookbook!" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3744,6 +3876,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"This application is an open source effort. If you're interested in suggesting or contributing new features, or you encounter any problems, please use the support link or visit the GitHub repository in the app settings." : {
|
"This application is an open source effort. If you're interested in suggesting or contributing new features, or you encounter any problems, please use the support link or visit the GitHub repository in the app settings." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3766,6 +3899,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"This setting will take effect after the app is restarted. It affects the adjustment of ingredient quantities." : {
|
"This setting will take effect after the app is restarted. It affects the adjustment of ingredient quantities." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3833,6 +3967,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Tool" : {
|
"Tool" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3855,6 +3990,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Tools" : {
|
"Tools" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3900,6 +4036,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Total time" : {
|
"Total time" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3922,6 +4059,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"TPPDF" : {
|
"TPPDF" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -3989,6 +4127,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Unable to connect to server." : {
|
"Unable to connect to server." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4009,6 +4148,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Unable to decode recipe data." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Unable to encode recipe data." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Unable to load recipe." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Unable to load website content. Please check your internet connection." : {
|
"Unable to load website content. Please check your internet connection." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4031,6 +4179,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Unable to save recipe." : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Unable to upload your recipe. Please check your internet connection." : {
|
"Unable to upload your recipe. Please check your internet connection." : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -4053,6 +4204,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Unknown error" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Unsaturated fat content" : {
|
"Unsaturated fat content" : {
|
||||||
"comment" : "Unsaturated fat content",
|
"comment" : "Unsaturated fat content",
|
||||||
@@ -4101,6 +4255,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Upload Changes" : {
|
"Upload Changes" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4123,6 +4278,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Upload Recipe" : {
|
"Upload Recipe" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4145,6 +4301,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"URL (e.g. example.com/recipe)" : {
|
"URL (e.g. example.com/recipe)" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4167,6 +4324,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"URL:" : {
|
"URL:" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4189,6 +4347,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Username: %@" : {
|
"Username: %@" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4211,6 +4370,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Validate" : {
|
"Validate" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4233,6 +4393,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Visit the GitHub page" : {
|
"Visit the GitHub page" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4255,6 +4416,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"You're all set for cooking 🍓" : {
|
"You're all set for cooking 🍓" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4277,6 +4439,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Your grocery list is stored locally and therefore not synchronized across your devices." : {
|
"Your grocery list is stored locally and therefore not synchronized across your devices." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
/*
|
||||||
@MainActor class RecipeEditViewModel: ObservableObject {
|
@MainActor class RecipeEditViewModel: ObservableObject {
|
||||||
@ObservedObject var mainViewModel: AppState
|
@ObservedObject var mainViewModel: AppState
|
||||||
@Published var recipe: RecipeDetail = RecipeDetail()
|
@Published var recipe: CookbookApiRecipeDetailV1 = CookbookApiRecipeDetailV1()
|
||||||
|
|
||||||
@Published var prepDuration: DurationComponents = DurationComponents()
|
@Published var prepDuration: DurationComponents = DurationComponents()
|
||||||
@Published var cookDuration: DurationComponents = DurationComponents()
|
@Published var cookDuration: DurationComponents = DurationComponents()
|
||||||
@@ -34,7 +34,7 @@ import SwiftUI
|
|||||||
self.uploadNew = uploadNew
|
self.uploadNew = uploadNew
|
||||||
}
|
}
|
||||||
|
|
||||||
init(mainViewModel: AppState, recipeDetail: RecipeDetail, uploadNew: Bool) {
|
init(mainViewModel: AppState, recipeDetail: CookbookApiRecipeDetailV1, uploadNew: Bool) {
|
||||||
self.mainViewModel = mainViewModel
|
self.mainViewModel = mainViewModel
|
||||||
self.recipe = recipeDetail
|
self.recipe = recipeDetail
|
||||||
self.uploadNew = uploadNew
|
self.uploadNew = uploadNew
|
||||||
@@ -135,3 +135,4 @@ import SwiftUI
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ protocol CookbookApi {
|
|||||||
static func importRecipe(
|
static func importRecipe(
|
||||||
auth: String,
|
auth: String,
|
||||||
data: Data
|
data: Data
|
||||||
) async -> (RecipeDetail?, NetworkError?)
|
) async -> (Recipe?, NetworkError?)
|
||||||
|
|
||||||
/// Get either the full image or a thumbnail sized version.
|
/// Get either the full image or a thumbnail sized version.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -42,7 +42,7 @@ protocol CookbookApi {
|
|||||||
/// - Returns: The image of the recipe with the specified id. A NetworkError if the request fails, otherwise nil.
|
/// - Returns: The image of the recipe with the specified id. A NetworkError if the request fails, otherwise nil.
|
||||||
static func getImage(
|
static func getImage(
|
||||||
auth: String,
|
auth: String,
|
||||||
id: Int,
|
id: String,
|
||||||
size: RecipeImage.RecipeImageSize
|
size: RecipeImage.RecipeImageSize
|
||||||
) async -> (UIImage?, NetworkError?)
|
) async -> (UIImage?, NetworkError?)
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ protocol CookbookApi {
|
|||||||
/// - Returns: A list of all recipes.
|
/// - Returns: A list of all recipes.
|
||||||
static func getRecipes(
|
static func getRecipes(
|
||||||
auth: String
|
auth: String
|
||||||
) async -> ([Recipe]?, NetworkError?)
|
) async -> ([RecipeStub]?, NetworkError?)
|
||||||
|
|
||||||
/// Create a new recipe.
|
/// Create a new recipe.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -60,7 +60,7 @@ protocol CookbookApi {
|
|||||||
/// - Returns: A NetworkError if the request fails. Nil otherwise.
|
/// - Returns: A NetworkError if the request fails. Nil otherwise.
|
||||||
static func createRecipe(
|
static func createRecipe(
|
||||||
auth: String,
|
auth: String,
|
||||||
recipe: RecipeDetail
|
recipe: Recipe
|
||||||
) async -> (NetworkError?)
|
) async -> (NetworkError?)
|
||||||
|
|
||||||
/// Get the recipe with the specified id.
|
/// Get the recipe with the specified id.
|
||||||
@@ -69,8 +69,9 @@ protocol CookbookApi {
|
|||||||
/// - id: The recipe id.
|
/// - id: The recipe id.
|
||||||
/// - Returns: The recipe if it exists. A NetworkError if the request fails.
|
/// - Returns: The recipe if it exists. A NetworkError if the request fails.
|
||||||
static func getRecipe(
|
static func getRecipe(
|
||||||
auth: String, id: Int
|
auth: String,
|
||||||
) async -> (RecipeDetail?, NetworkError?)
|
id: String
|
||||||
|
) async -> (Recipe?, NetworkError?)
|
||||||
|
|
||||||
/// Update an existing recipe with new entries.
|
/// Update an existing recipe with new entries.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -79,7 +80,7 @@ protocol CookbookApi {
|
|||||||
/// - Returns: A NetworkError if the request fails. Nil otherwise.
|
/// - Returns: A NetworkError if the request fails. Nil otherwise.
|
||||||
static func updateRecipe(
|
static func updateRecipe(
|
||||||
auth: String,
|
auth: String,
|
||||||
recipe: RecipeDetail
|
recipe: Recipe
|
||||||
) async -> (NetworkError?)
|
) async -> (NetworkError?)
|
||||||
|
|
||||||
/// Delete the recipe with the specified id.
|
/// Delete the recipe with the specified id.
|
||||||
@@ -89,7 +90,7 @@ protocol CookbookApi {
|
|||||||
/// - Returns: A NetworkError if the request fails. Nil otherwise.
|
/// - Returns: A NetworkError if the request fails. Nil otherwise.
|
||||||
static func deleteRecipe(
|
static func deleteRecipe(
|
||||||
auth: String,
|
auth: String,
|
||||||
id: Int
|
id: String
|
||||||
) async -> (NetworkError?)
|
) async -> (NetworkError?)
|
||||||
|
|
||||||
/// Get all categories.
|
/// Get all categories.
|
||||||
@@ -108,7 +109,7 @@ protocol CookbookApi {
|
|||||||
static func getCategory(
|
static func getCategory(
|
||||||
auth: String,
|
auth: String,
|
||||||
named categoryName: String
|
named categoryName: String
|
||||||
) async -> ([Recipe]?, NetworkError?)
|
) async -> ([RecipeStub]?, NetworkError?)
|
||||||
|
|
||||||
/// Rename an existing category.
|
/// Rename an existing category.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -138,7 +139,7 @@ protocol CookbookApi {
|
|||||||
static func getRecipesTagged(
|
static func getRecipesTagged(
|
||||||
auth: String,
|
auth: String,
|
||||||
keyword: String
|
keyword: String
|
||||||
) async -> ([Recipe]?, NetworkError?)
|
) async -> ([RecipeStub]?, NetworkError?)
|
||||||
|
|
||||||
/// Get the servers api version.
|
/// Get the servers api version.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -176,3 +177,4 @@ protocol CookbookApi {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import UIKit
|
|||||||
class CookbookApiV1: CookbookApi {
|
class CookbookApiV1: CookbookApi {
|
||||||
static let basePath: String = "/index.php/apps/cookbook/api/v1"
|
static let basePath: String = "/index.php/apps/cookbook/api/v1"
|
||||||
|
|
||||||
static func importRecipe(auth: String, data: Data) async -> (RecipeDetail?, NetworkError?) {
|
static func importRecipe(auth: String, data: Data) async -> (Recipe?, NetworkError?) {
|
||||||
let request = ApiRequest(
|
let request = ApiRequest(
|
||||||
path: basePath + "/import",
|
path: basePath + "/import",
|
||||||
method: .POST,
|
method: .POST,
|
||||||
@@ -22,10 +22,12 @@ class CookbookApiV1: CookbookApi {
|
|||||||
|
|
||||||
let (data, error) = await request.send()
|
let (data, error) = await request.send()
|
||||||
guard let data = data else { return (nil, error) }
|
guard let data = data else { return (nil, error) }
|
||||||
return (JSONDecoder.safeDecode(data), nil)
|
let recipe: CookbookApiRecipeDetailV1? = JSONDecoder.safeDecode(data)
|
||||||
|
return (recipe?.toRecipe(), error)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getImage(auth: String, id: Int, size: RecipeImage.RecipeImageSize) async -> (UIImage?, NetworkError?) {
|
static func getImage(auth: String, id: String, size: RecipeImage.RecipeImageSize) async -> (UIImage?, NetworkError?) {
|
||||||
|
guard let id = Int(id) else {return (nil, .unknownError)}
|
||||||
let imageSize = (size == .FULL ? "full" : "thumb")
|
let imageSize = (size == .FULL ? "full" : "thumb")
|
||||||
let request = ApiRequest(
|
let request = ApiRequest(
|
||||||
path: basePath + "/recipes/\(id)/image?size=\(imageSize)",
|
path: basePath + "/recipes/\(id)/image?size=\(imageSize)",
|
||||||
@@ -39,7 +41,7 @@ class CookbookApiV1: CookbookApi {
|
|||||||
return (UIImage(data: data), error)
|
return (UIImage(data: data), error)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getRecipes(auth: String) async -> ([Recipe]?, NetworkError?) {
|
static func getRecipes(auth: String) async -> ([RecipeStub]?, NetworkError?) {
|
||||||
let request = ApiRequest(
|
let request = ApiRequest(
|
||||||
path: basePath + "/recipes",
|
path: basePath + "/recipes",
|
||||||
method: .GET,
|
method: .GET,
|
||||||
@@ -50,10 +52,12 @@ class CookbookApiV1: CookbookApi {
|
|||||||
let (data, error) = await request.send()
|
let (data, error) = await request.send()
|
||||||
guard let data = data else { return (nil, error) }
|
guard let data = data else { return (nil, error) }
|
||||||
print("\n\nRECIPE: ", String(data: data, encoding: .utf8))
|
print("\n\nRECIPE: ", String(data: data, encoding: .utf8))
|
||||||
return (JSONDecoder.safeDecode(data), nil)
|
let recipes: [CookbookApiRecipeV1]? = JSONDecoder.safeDecode(data)
|
||||||
|
return (recipes?.map({ recipe in recipe.toRecipeStub() }), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func createRecipe(auth: String, recipe: RecipeDetail) async -> (NetworkError?) {
|
static func createRecipe(auth: String, recipe: Recipe) async -> (NetworkError?) {
|
||||||
|
let recipe = CookbookApiRecipeDetailV1.fromRecipe(recipe)
|
||||||
guard let recipeData = JSONEncoder.safeEncode(recipe) else {
|
guard let recipeData = JSONEncoder.safeEncode(recipe) else {
|
||||||
return .dataError
|
return .dataError
|
||||||
}
|
}
|
||||||
@@ -81,7 +85,8 @@ class CookbookApiV1: CookbookApi {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getRecipe(auth: String, id: Int) async -> (RecipeDetail?, NetworkError?) {
|
static func getRecipe(auth: String, id: String) async -> (Recipe?, NetworkError?) {
|
||||||
|
guard let id = Int(id) else {return (nil, .unknownError)}
|
||||||
let request = ApiRequest(
|
let request = ApiRequest(
|
||||||
path: basePath + "/recipes/\(id)",
|
path: basePath + "/recipes/\(id)",
|
||||||
method: .GET,
|
method: .GET,
|
||||||
@@ -91,10 +96,13 @@ class CookbookApiV1: CookbookApi {
|
|||||||
|
|
||||||
let (data, error) = await request.send()
|
let (data, error) = await request.send()
|
||||||
guard let data = data else { return (nil, error) }
|
guard let data = data else { return (nil, error) }
|
||||||
return (JSONDecoder.safeDecode(data), nil)
|
|
||||||
|
let recipe: CookbookApiRecipeDetailV1? = JSONDecoder.safeDecode(data)
|
||||||
|
return (recipe?.toRecipe(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func updateRecipe(auth: String, recipe: RecipeDetail) async -> (NetworkError?) {
|
static func updateRecipe(auth: String, recipe: Recipe) async -> (NetworkError?) {
|
||||||
|
let cookbookRecipe = CookbookApiRecipeDetailV1.fromRecipe(recipe)
|
||||||
guard let recipeData = JSONEncoder.safeEncode(recipe) else {
|
guard let recipeData = JSONEncoder.safeEncode(recipe) else {
|
||||||
return .dataError
|
return .dataError
|
||||||
}
|
}
|
||||||
@@ -121,7 +129,8 @@ class CookbookApiV1: CookbookApi {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
static func deleteRecipe(auth: String, id: Int) async -> (NetworkError?) {
|
static func deleteRecipe(auth: String, id: String) async -> (NetworkError?) {
|
||||||
|
guard let id = Int(id) else {return .unknownError}
|
||||||
let request = ApiRequest(
|
let request = ApiRequest(
|
||||||
path: basePath + "/recipes/\(id)",
|
path: basePath + "/recipes/\(id)",
|
||||||
method: .DELETE,
|
method: .DELETE,
|
||||||
@@ -147,7 +156,7 @@ class CookbookApiV1: CookbookApi {
|
|||||||
return (JSONDecoder.safeDecode(data), nil)
|
return (JSONDecoder.safeDecode(data), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getCategory(auth: String, named categoryName: String) async -> ([Recipe]?, NetworkError?) {
|
static func getCategory(auth: String, named categoryName: String) async -> ([RecipeStub]?, NetworkError?) {
|
||||||
let request = ApiRequest(
|
let request = ApiRequest(
|
||||||
path: basePath + "/category/\(categoryName)",
|
path: basePath + "/category/\(categoryName)",
|
||||||
method: .GET,
|
method: .GET,
|
||||||
@@ -157,7 +166,8 @@ class CookbookApiV1: CookbookApi {
|
|||||||
|
|
||||||
let (data, error) = await request.send()
|
let (data, error) = await request.send()
|
||||||
guard let data = data else { return (nil, error) }
|
guard let data = data else { return (nil, error) }
|
||||||
return (JSONDecoder.safeDecode(data), nil)
|
let recipes: [CookbookApiRecipeV1]? = JSONDecoder.safeDecode(data)
|
||||||
|
return (recipes?.map({ recipe in recipe.toRecipeStub() }), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func renameCategory(auth: String, named categoryName: String, newName: String) async -> (NetworkError?) {
|
static func renameCategory(auth: String, named categoryName: String, newName: String) async -> (NetworkError?) {
|
||||||
@@ -186,7 +196,7 @@ class CookbookApiV1: CookbookApi {
|
|||||||
return (JSONDecoder.safeDecode(data), nil)
|
return (JSONDecoder.safeDecode(data), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getRecipesTagged(auth: String, keyword: String) async -> ([Recipe]?, NetworkError?) {
|
static func getRecipesTagged(auth: String, keyword: String) async -> ([RecipeStub]?, NetworkError?) {
|
||||||
let request = ApiRequest(
|
let request = ApiRequest(
|
||||||
path: basePath + "/tags/\(keyword)",
|
path: basePath + "/tags/\(keyword)",
|
||||||
method: .GET,
|
method: .GET,
|
||||||
@@ -196,7 +206,8 @@ class CookbookApiV1: CookbookApi {
|
|||||||
|
|
||||||
let (data, error) = await request.send()
|
let (data, error) = await request.send()
|
||||||
guard let data = data else { return (nil, error) }
|
guard let data = data else { return (nil, error) }
|
||||||
return (JSONDecoder.safeDecode(data), nil)
|
let recipes: [CookbookApiRecipeV1]? = JSONDecoder.safeDecode(data)
|
||||||
|
return (recipes?.map({ recipe in recipe.toRecipeStub() }), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func getApiVersion(auth: String) async -> (NetworkError?) {
|
static func getApiVersion(auth: String) async -> (NetworkError?) {
|
||||||
@@ -215,3 +226,4 @@ class CookbookApiV1: CookbookApi {
|
|||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// Models.swift
|
// CookbookLoginModels.swift
|
||||||
// Nextcloud Cookbook iOS Client
|
// Nextcloud Cookbook iOS Client
|
||||||
//
|
//
|
||||||
// Created by Vincent Meilinger on 11.05.24.
|
// Created by Vincent Meilinger on 11.05.24.
|
||||||
@@ -9,10 +9,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Login Models
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Login flow
|
|
||||||
|
|
||||||
struct LoginV2Request: Codable {
|
struct LoginV2Request: Codable {
|
||||||
let poll: LoginV2Poll
|
let poll: LoginV2Poll
|
||||||
|
|||||||
@@ -8,8 +8,18 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
struct Category: Codable, Identifiable, Hashable {
|
||||||
|
var id: String { name }
|
||||||
|
let name: String
|
||||||
|
let recipe_count: Int
|
||||||
|
|
||||||
struct CookbookApiRecipeV1: Codable {
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case name, recipe_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CookbookApiRecipeV1: CookbookApiRecipe, Codable, Identifiable, Hashable {
|
||||||
|
var id: String { name + String(recipe_id) }
|
||||||
let name: String
|
let name: String
|
||||||
let keywords: String?
|
let keywords: String?
|
||||||
let dateCreated: String?
|
let dateCreated: String?
|
||||||
@@ -24,15 +34,22 @@ struct CookbookApiRecipeV1: Codable {
|
|||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case name, keywords, dateCreated, dateModified, imageUrl, imagePlaceholderUrl, recipe_id
|
case name, keywords, dateCreated, dateModified, imageUrl, imagePlaceholderUrl, recipe_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toRecipeStub() -> RecipeStub {
|
||||||
|
return RecipeStub(
|
||||||
|
id: String(recipe_id),
|
||||||
|
name: name,
|
||||||
|
keywords: keywords,
|
||||||
|
dateCreated: dateCreated,
|
||||||
|
dateModified: dateModified,
|
||||||
|
thumbnailPath: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension CookbookApiRecipeV1: Identifiable, Hashable {
|
|
||||||
var id: String { name }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
struct CookbookApiRecipeDetailV1: CookbookApiRecipeDetail {
|
||||||
struct CookbookApiRecipeDetailV1: Codable {
|
|
||||||
var name: String
|
var name: String
|
||||||
var keywords: String
|
var keywords: String
|
||||||
var dateCreated: String?
|
var dateCreated: String?
|
||||||
@@ -51,7 +68,7 @@ struct CookbookApiRecipeDetailV1: Codable {
|
|||||||
var recipeInstructions: [String]
|
var recipeInstructions: [String]
|
||||||
var nutrition: [String:String]
|
var nutrition: [String:String]
|
||||||
|
|
||||||
init(name: String, keywords: String, dateCreated: String, dateModified: String, imageUrl: String, id: String, prepTime: String? = nil, cookTime: String? = nil, totalTime: String? = nil, description: String, url: String, recipeYield: Int, recipeCategory: String, tool: [String], recipeIngredient: [String], recipeInstructions: [String], nutrition: [String:String]) {
|
init(name: String, keywords: String, dateCreated: String?, dateModified: String?, imageUrl: String?, id: String, prepTime: String? = nil, cookTime: String? = nil, totalTime: String? = nil, description: String, url: String?, recipeYield: Int, recipeCategory: String, tool: [String], recipeIngredient: [String], recipeInstructions: [String], nutrition: [String:String]) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.keywords = keywords
|
self.keywords = keywords
|
||||||
self.dateCreated = dateCreated
|
self.dateCreated = dateCreated
|
||||||
@@ -117,6 +134,47 @@ struct CookbookApiRecipeDetailV1: Codable {
|
|||||||
|
|
||||||
nutrition = try container.decode(Dictionary<String, JSONAny>.self, forKey: .nutrition).mapValues { String(describing: $0.value) }
|
nutrition = try container.decode(Dictionary<String, JSONAny>.self, forKey: .nutrition).mapValues { String(describing: $0.value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toRecipe() -> Recipe {
|
||||||
|
return Recipe(
|
||||||
|
id: self.id,
|
||||||
|
name: self.name,
|
||||||
|
keywords: keywords.components(separatedBy: ","),
|
||||||
|
dateCreated: self.dateCreated,
|
||||||
|
dateModified: self.dateModified,
|
||||||
|
prepTime: self.prepTime ?? "",
|
||||||
|
cookTime: self.cookTime ?? "",
|
||||||
|
totalTime: self.totalTime ?? "",
|
||||||
|
recipeDescription: self.description,
|
||||||
|
url: self.url,
|
||||||
|
yield: self.recipeYield,
|
||||||
|
category: self.recipeCategory,
|
||||||
|
tools: self.tool,
|
||||||
|
ingredients: self.recipeIngredient,
|
||||||
|
instructions: self.recipeInstructions,
|
||||||
|
nutrition: self.nutrition,
|
||||||
|
ingredientMultiplier: 1.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func fromRecipe(_ recipe: Recipe) -> any CookbookApiRecipeDetail {
|
||||||
|
return CookbookApiRecipeDetailV1(
|
||||||
|
name: recipe.name,
|
||||||
|
keywords: recipe.keywords.joined(separator: ","),
|
||||||
|
dateCreated: recipe.dateCreated,
|
||||||
|
dateModified: recipe.dateModified,
|
||||||
|
imageUrl: "",
|
||||||
|
id: recipe.id,
|
||||||
|
description: recipe.recipeDescription,
|
||||||
|
url: recipe.url,
|
||||||
|
recipeYield: recipe.yield,
|
||||||
|
recipeCategory: recipe.category,
|
||||||
|
tool: recipe.tools,
|
||||||
|
recipeIngredient: recipe.ingredients,
|
||||||
|
recipeInstructions: recipe.instructions,
|
||||||
|
nutrition: recipe.nutrition
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -155,7 +213,7 @@ extension CookbookApiRecipeDetailV1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
struct RecipeImage {
|
struct RecipeImage {
|
||||||
enum RecipeImageSize: String {
|
enum RecipeImageSize: String {
|
||||||
case THUMB="thumb", FULL="full"
|
case THUMB="thumb", FULL="full"
|
||||||
@@ -164,7 +222,7 @@ struct RecipeImage {
|
|||||||
var thumb: UIImage?
|
var thumb: UIImage?
|
||||||
var full: UIImage?
|
var full: UIImage?
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
struct RecipeKeyword: Codable {
|
struct RecipeKeyword: Codable {
|
||||||
let name: String
|
let name: String
|
||||||
@@ -244,3 +302,4 @@ enum Nutrition: CaseIterable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,13 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
|
protocol CookbookApiRecipe {
|
||||||
|
func toRecipeStub() -> RecipeStub
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol CookbookApiRecipeDetail: Codable {
|
||||||
|
func toRecipe() -> Recipe
|
||||||
|
static func fromRecipe(_ recipe: Recipe) -> CookbookApiRecipeDetail
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,18 +6,53 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
public enum NetworkError: String, Error {
|
|
||||||
case missingUrl = "Missing URL."
|
public enum NetworkError: UserAlert {
|
||||||
case parametersNil = "Parameters are nil."
|
case missingUrl
|
||||||
case encodingFailed = "Parameter encoding failed."
|
case parametersNil
|
||||||
case decodingFailed = "Data decoding failed."
|
case encodingFailed
|
||||||
case redirectionError = "Redirection error"
|
case decodingFailed
|
||||||
case clientError = "Client error"
|
case redirectionError
|
||||||
case serverError = "Server error"
|
case clientError
|
||||||
case invalidRequest = "Invalid request"
|
case serverError
|
||||||
case unknownError = "Unknown error"
|
case invalidRequest
|
||||||
case dataError = "Invalid data error."
|
case unknownError
|
||||||
|
case dataError
|
||||||
|
|
||||||
|
var localizedTitle: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .missingUrl:
|
||||||
|
"Missing URL."
|
||||||
|
case .parametersNil:
|
||||||
|
"Parameters are nil."
|
||||||
|
case .encodingFailed:
|
||||||
|
"Parameter encoding failed."
|
||||||
|
case .decodingFailed:
|
||||||
|
"Data decoding 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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var localizedDescription: LocalizedStringKey {
|
||||||
|
return "" // TODO: Add description
|
||||||
|
}
|
||||||
|
|
||||||
|
var alertButtons: [AlertButton] {
|
||||||
|
return [.OK]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Nextcloud_Cookbook_iOS_ClientApp: App {
|
struct Nextcloud_Cookbook_iOS_ClientApp: App {
|
||||||
@@ -18,9 +17,11 @@ struct Nextcloud_Cookbook_iOS_ClientApp: App {
|
|||||||
WindowGroup {
|
WindowGroup {
|
||||||
ZStack {
|
ZStack {
|
||||||
if onboarding {
|
if onboarding {
|
||||||
OnboardingView()
|
//OnboardingView()
|
||||||
|
EmptyView()
|
||||||
} else {
|
} else {
|
||||||
MainView()
|
MainView()
|
||||||
|
.modelContainer(for: Recipe.self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.transition(.slide)
|
.transition(.slide)
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
//
|
|
||||||
// Account.swift
|
|
||||||
// Nextcloud Cookbook iOS Client
|
|
||||||
//
|
|
||||||
// Created by Vincent Meilinger on 24.01.25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// AccountState.swift
|
// CookbookState.swift
|
||||||
// Nextcloud Cookbook iOS Client
|
// Nextcloud Cookbook iOS Client
|
||||||
//
|
//
|
||||||
// Created by Vincent Meilinger on 29.05.24.
|
// Created by Vincent Meilinger on 29.05.24.
|
||||||
@@ -7,13 +7,10 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
/*
|
||||||
|
|
||||||
@Observable
|
@Observable
|
||||||
class CookbookState {
|
class CookbookState {
|
||||||
let id = UUID()
|
|
||||||
let account: Account? = nil
|
|
||||||
|
|
||||||
/// Caches recipe categories.
|
/// Caches recipe categories.
|
||||||
var categories: [Category] = []
|
var categories: [Category] = []
|
||||||
|
|
||||||
@@ -33,212 +30,193 @@ class CookbookState {
|
|||||||
var keywords: [RecipeKeyword] = []
|
var keywords: [RecipeKeyword] = []
|
||||||
|
|
||||||
/// Read and write interfaces.
|
/// Read and write interfaces.
|
||||||
var readLocal: ReadInterface
|
var localReadInterface: ReadInterface
|
||||||
var writeLocal: WriteInterface
|
var localWriteInterface: WriteInterface
|
||||||
var readRemote: [ReadInterface]?
|
var remoteReadInterface: ReadInterface?
|
||||||
var writeRemote: [WriteInterface]?
|
var remoteWriteInterface: WriteInterface?
|
||||||
|
|
||||||
var localOnly: Bool = false
|
|
||||||
|
|
||||||
/// UI state variables
|
/// UI state variables
|
||||||
var selectedCategory: Category? = nil
|
var selectedCategory: Category? = nil
|
||||||
var selectedRecipe: RecipeStub? = nil
|
var selectedRecipeStub: RecipeStub? = nil
|
||||||
var navigationPath: NavigationPath = NavigationPath()
|
var showSettings: Bool = false
|
||||||
|
var showGroceries: Bool = false
|
||||||
|
|
||||||
/// Grocery List
|
/// Grocery List
|
||||||
var groceryList = GroceryList()
|
var groceryList = GroceryList()
|
||||||
|
|
||||||
init(
|
init(
|
||||||
readLocal: ReadInterface,
|
localReadInterface: ReadInterface,
|
||||||
writeLocal: WriteInterface,
|
localWriteInterface: WriteInterface,
|
||||||
readRemote: [ReadInterface] = [],
|
remoteReadInterface: ReadInterface? = nil,
|
||||||
writeRemote: [WriteInterface] = []
|
remoteWriteInterface: WriteInterface? = nil
|
||||||
|
|
||||||
) {
|
) {
|
||||||
self.readLocal = readLocal
|
self.localReadInterface = localReadInterface
|
||||||
self.writeLocal = writeLocal
|
self.localWriteInterface = localWriteInterface
|
||||||
self.readRemote = readRemote
|
self.remoteReadInterface = remoteReadInterface
|
||||||
self.writeRemote = writeRemote
|
self.remoteWriteInterface = remoteWriteInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let accountLoader = AccountLoader()
|
||||||
|
rI, wI = accountLoader.load
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension CookbookState {
|
extension CookbookState {
|
||||||
func removeRecipe(_ id: String) {
|
func loadCategories(remoteFirst: Bool = false) async {
|
||||||
for key in recipeStubs.keys {
|
if remoteFirst {
|
||||||
recipeStubs[key]?.removeAll(where: { $0.id == id })
|
if let remoteReadInterface, let categories = await remoteReadInterface.getCategories() {
|
||||||
}
|
|
||||||
recipes.removeValue(forKey: id)
|
|
||||||
thumbnails.removeValue(forKey: id)
|
|
||||||
images.removeValue(forKey: id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func imgToCache(_ image: UIImage, id: String, size: RecipeImage.RecipeImageSize) {
|
|
||||||
if size == .THUMB {
|
|
||||||
thumbnails[id] = image
|
|
||||||
} else {
|
|
||||||
images[id] = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func imgFromCache(id: String, size: RecipeImage.RecipeImageSize) -> UIImage? {
|
|
||||||
if size == .THUMB {
|
|
||||||
return thumbnails[id]
|
|
||||||
} else {
|
|
||||||
return images[id]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CookbookState: ReadInterface {
|
|
||||||
func getImage(id: String, size: RecipeImage.RecipeImageSize) async -> UIImage? {
|
|
||||||
if let image = imgFromCache(id: id, size: size) {
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
|
|
||||||
if !localOnly, let readRemote {
|
|
||||||
if let image = await readRemote.getImage(id: id, size: size) {
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let image = await readLocal.getImage(id: id, size: size) {
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRecipeStubs() async -> [RecipeStub]? {
|
|
||||||
if !localOnly, let readRemote {
|
|
||||||
if let stubs = await readRemote.getRecipeStubs() {
|
|
||||||
return stubs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if categories.isEmpty {
|
|
||||||
self.categories = await readLocal.getCategories() ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
for category in self.categories {
|
|
||||||
self.recipeStubs[category.name] = await readLocal.getRecipeStubsForCategory(named: category.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.recipeStubs.flatMap({_, val in val})
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRecipe(id: String) async -> Recipe? {
|
|
||||||
if let recipe = self.recipes[id] {
|
|
||||||
return recipe
|
|
||||||
}
|
|
||||||
|
|
||||||
if !localOnly, let readRemote {
|
|
||||||
if let recipe = await readRemote.getRecipe(id: id) {
|
|
||||||
return recipe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await readLocal.getRecipe(id: id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCategories() async -> [Category]? {
|
|
||||||
if !localOnly, let readRemote {
|
|
||||||
if let categories = await readRemote.getCategories() {
|
|
||||||
self.categories = categories
|
self.categories = categories
|
||||||
return categories
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let categories = await localReadInterface.getCategories() {
|
||||||
|
self.categories = categories
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let categories = await localReadInterface.getCategories() {
|
||||||
|
self.categories = categories
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let remoteReadInterface else { return }
|
||||||
|
if let categories = await remoteReadInterface.getCategories() {
|
||||||
|
self.categories = categories
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.categories.isEmpty, let categories = await readLocal.getCategories() {
|
|
||||||
self.categories = categories
|
|
||||||
return categories
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.categories
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipeStubsForCategory(named categoryName: String) async -> [RecipeStub]? {
|
func loadRecipeStubs(category: String, remoteFirst: Bool = false) async {
|
||||||
if let stubs = self.recipeStubs[categoryName] {
|
if remoteFirst {
|
||||||
return stubs
|
if let remoteReadInterface, let stubs = await remoteReadInterface.getRecipeStubs() {
|
||||||
}
|
self.recipeStubs[category] = stubs
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !localOnly, let readRemote {
|
if let stubs = await localReadInterface.getRecipeStubs() {
|
||||||
if let stubs = await readRemote.getRecipeStubsForCategory(named: categoryName) {
|
self.recipeStubs[category] = stubs
|
||||||
self.recipeStubs[categoryName] = stubs
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let stubs = await localReadInterface.getRecipeStubs() {
|
||||||
|
self.recipeStubs[category] = stubs
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let remoteReadInterface else { return }
|
||||||
|
if let stubs = await remoteReadInterface.getRecipeStubs() {
|
||||||
|
self.recipeStubs[category] = stubs
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let stubs = await readLocal.getRecipeStubsForCategory(named: categoryName) {
|
|
||||||
self.recipeStubs[categoryName] = stubs
|
|
||||||
return stubs
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTags() async -> [RecipeKeyword]? {
|
func loadKeywords(remoteFirst: Bool = false) async {
|
||||||
if !keywords.isEmpty {
|
if remoteFirst {
|
||||||
return keywords
|
if let remoteReadInterface, let keywords = await remoteReadInterface.getTags() {
|
||||||
}
|
self.keywords = keywords
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !localOnly, let readRemote {
|
if let keywords = await localReadInterface.getTags() {
|
||||||
if let tags = await readRemote.getTags() {
|
self.keywords = keywords
|
||||||
self.keywords = tags
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let keywords = await localReadInterface.getTags() {
|
||||||
|
self.keywords = keywords
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let remoteReadInterface else { return }
|
||||||
|
if let keywords = await remoteReadInterface.getTags() {
|
||||||
|
self.keywords = keywords
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await readLocal.getTags()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipesTagged(keyword: String) async -> [RecipeStub]? {
|
func loadRecipe(id: String, remoteFirst: Bool = false) async {
|
||||||
if !localOnly, let readRemote {
|
if remoteFirst {
|
||||||
if let stubs = await readRemote.getRecipesTagged(keyword: keyword) {
|
if let remoteReadInterface, let recipe = await remoteReadInterface.getRecipe(id: id) {
|
||||||
return stubs
|
self.recipes[id] = recipe
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let recipe = await localReadInterface.getRecipe(id: id) {
|
||||||
|
self.recipes[id] = recipe
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let recipe = await localReadInterface.getRecipe(id: id) {
|
||||||
|
self.recipes[id] = recipe
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let remoteReadInterface else { return }
|
||||||
|
if let recipe = await remoteReadInterface.getRecipe(id: id) {
|
||||||
|
self.recipes[id] = recipe
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await getRecipeStubs()?.filter({ recipe in
|
|
||||||
recipe.keywords?.contains(keyword.lowercased()) ?? false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CookbookState: WriteInterface {
|
|
||||||
func postImage(id: String, image: UIImage, size: RecipeImage.RecipeImageSize) async -> ((any UserAlert)?) {
|
|
||||||
let _ = await writeLocal.postImage(id: id, image: image, size: size)
|
|
||||||
let _ = await writeRemote?.postImage(id: id, image: image, size: size)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func postRecipe(recipe: Recipe) async -> ((any UserAlert)?) {
|
|
||||||
let _ = await writeLocal.postRecipe(recipe: recipe)
|
|
||||||
let _ = await writeRemote?.postRecipe(recipe: recipe)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateRecipe(recipe: Recipe) async -> ((any UserAlert)?) {
|
|
||||||
let _ = await writeLocal.updateRecipe(recipe: recipe)
|
|
||||||
let _ = await writeRemote?.updateRecipe(recipe: recipe)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteRecipe(id: String) async -> ((any UserAlert)?) {
|
|
||||||
let _ = await writeLocal.deleteRecipe(id: id)
|
|
||||||
let _ = await writeRemote?.deleteRecipe(id: id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func renameCategory(named categoryName: String, newName: String) async -> ((any UserAlert)?) {
|
|
||||||
let _ = await writeLocal.renameCategory(named: categoryName, newName: newName)
|
|
||||||
let _ = await writeRemote?.renameCategory(named: categoryName, newName: newName)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
extension AccountState: Hashable, Identifiable {
|
|
||||||
static func == (lhs: AccountState, rhs: AccountState) -> Bool {
|
class AccountLoader {
|
||||||
lhs.id == rhs.id
|
func initInterfaces() async -> [ReadInterface & WriteInterface] {
|
||||||
|
let accounts = await self.loadAccounts("accounts.data")
|
||||||
|
|
||||||
|
if accounts.isEmpty && UserSettings.shared.serverAddress != "" {
|
||||||
|
print("Creating new Account from legacy Cookbook client account.")
|
||||||
|
let auth = Authentication(
|
||||||
|
baseUrl: UserSettings.shared.serverAddress,
|
||||||
|
user: UserSettings.shared.username,
|
||||||
|
token: UserSettings.shared.authString
|
||||||
|
)
|
||||||
|
let authKey = "legacyNextcloud"
|
||||||
|
let legacyAccount = Account(
|
||||||
|
id: UUID(),
|
||||||
|
name: "Nextcloud",
|
||||||
|
type: .nextcloud,
|
||||||
|
apiVersion: "1.0",
|
||||||
|
authKey: authKey
|
||||||
|
)
|
||||||
|
await saveAccounts([legacyAccount], "accounts.data")
|
||||||
|
legacyAccount.storeAuth(auth)
|
||||||
|
|
||||||
|
let interface = NextcloudDataInterface(auth: auth, version: legacyAccount.apiVersion)
|
||||||
|
return [interface]
|
||||||
|
} else {
|
||||||
|
print("Recovering existing accounts.")
|
||||||
|
var interfaces: [ReadInterface & WriteInterface] = []
|
||||||
|
for account in accounts {
|
||||||
|
if let interface: CookbookInterface = account.getInterface() {
|
||||||
|
interfaces.append(interface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interfaces
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func loadAccounts(_ path: String) async -> [Account] {
|
||||||
hasher.combine(id)
|
do {
|
||||||
|
return try await DataStore.shared.load(fromPath: path) ?? []
|
||||||
|
} catch (let error) {
|
||||||
|
print(error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveAccounts(_ accounts: [Account], _ path: String) async {
|
||||||
|
await DataStore.shared.save(data: accounts, toPath: path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import KeychainSwift
|
import KeychainSwift
|
||||||
|
|
||||||
|
/*
|
||||||
protocol CookbookInterface {
|
protocol CookbookInterface {
|
||||||
/// A unique id of the interface. Used to associate recipes to their respective accounts.
|
/// A unique id of the interface. Used to associate recipes to their respective accounts.
|
||||||
var id: String { get }
|
var id: String { get }
|
||||||
@@ -24,12 +24,12 @@ protocol ReadInterface {
|
|||||||
func getImage(
|
func getImage(
|
||||||
id: String,
|
id: String,
|
||||||
size: RecipeImage.RecipeImageSize
|
size: RecipeImage.RecipeImageSize
|
||||||
) async -> (UIImage?, UserAlert?)
|
) async -> UIImage?
|
||||||
|
|
||||||
/// Get all recipe stubs.
|
/// Get all recipe stubs.
|
||||||
/// - Returns: A list of all recipes.
|
/// - Returns: A list of all recipes.
|
||||||
func getRecipeStubs(
|
func getRecipeStubs(
|
||||||
) async -> ([RecipeStub]?, UserAlert?)
|
) async -> [RecipeStub]?
|
||||||
|
|
||||||
/// Get the recipe with the specified id.
|
/// Get the recipe with the specified id.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -37,12 +37,12 @@ protocol ReadInterface {
|
|||||||
/// - Returns: The recipe if it exists. A UserAlert if the request fails.
|
/// - Returns: The recipe if it exists. A UserAlert if the request fails.
|
||||||
func getRecipe(
|
func getRecipe(
|
||||||
id: String
|
id: String
|
||||||
) async -> (Recipe?, UserAlert?)
|
) async -> Recipe?
|
||||||
|
|
||||||
/// Get all categories.
|
/// Get all categories.
|
||||||
/// - Returns: A list of categories. A UserAlert if the request fails.
|
/// - Returns: A list of categories. A UserAlert if the request fails.
|
||||||
func getCategories(
|
func getCategories(
|
||||||
) async -> ([Category]?, UserAlert?)
|
) async -> [Category]?
|
||||||
|
|
||||||
/// Get all recipes of a specified category.
|
/// Get all recipes of a specified category.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -50,12 +50,12 @@ protocol ReadInterface {
|
|||||||
/// - Returns: A list of recipes. A UserAlert if the request fails.
|
/// - Returns: A list of recipes. A UserAlert if the request fails.
|
||||||
func getRecipeStubsForCategory(
|
func getRecipeStubsForCategory(
|
||||||
named categoryName: String
|
named categoryName: String
|
||||||
) async -> ([RecipeStub]?, UserAlert?)
|
) async -> [RecipeStub]?
|
||||||
|
|
||||||
/// Get all keywords/tags.
|
/// Get all keywords/tags.
|
||||||
/// - Returns: A list of tag strings. A UserAlert if the request fails.
|
/// - Returns: A list of tag strings. A UserAlert if the request fails.
|
||||||
func getTags(
|
func getTags(
|
||||||
) async -> ([RecipeKeyword]?, UserAlert?)
|
) async -> [RecipeKeyword]?
|
||||||
|
|
||||||
/// Get all recipes tagged with the specified keyword.
|
/// Get all recipes tagged with the specified keyword.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
@@ -63,7 +63,7 @@ protocol ReadInterface {
|
|||||||
/// - Returns: A list of recipes tagged with the specified keyword. A UserAlert if the request fails.
|
/// - Returns: A list of recipes tagged with the specified keyword. A UserAlert if the request fails.
|
||||||
func getRecipesTagged(
|
func getRecipesTagged(
|
||||||
keyword: String
|
keyword: String
|
||||||
) async -> ([RecipeStub]?, UserAlert?)
|
) async -> [RecipeStub]?
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol WriteInterface {
|
protocol WriteInterface {
|
||||||
@@ -111,3 +111,4 @@ protocol WriteInterface {
|
|||||||
newName: String
|
newName: String
|
||||||
) async -> (UserAlert?)
|
) async -> (UserAlert?)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
/*
|
||||||
|
|
||||||
class LocalDataInterface: CookbookInterface {
|
class LocalDataInterface: CookbookInterface {
|
||||||
var id: String
|
var id: String
|
||||||
@@ -49,49 +49,36 @@ class LocalDataInterface: CookbookInterface {
|
|||||||
// MARK: - Local Read Interface
|
// MARK: - Local Read Interface
|
||||||
|
|
||||||
extension LocalDataInterface: ReadInterface {
|
extension LocalDataInterface: ReadInterface {
|
||||||
|
func getImage(id: String, size: RecipeImage.RecipeImageSize) async -> UIImage? {
|
||||||
func getImage(id: String, size: RecipeImage.RecipeImageSize) async -> (UIImage?, (any UserAlert)?) {
|
|
||||||
guard let data: String = await load(path: .image(id: id, size: size)) else {
|
guard let data: String = await load(path: .image(id: id, size: size)) else {
|
||||||
return (nil, PersistenceAlert.LOAD_FAILED)
|
return nil
|
||||||
}
|
}
|
||||||
guard let dataDecoded = Data(base64Encoded: data) else { return (nil, PersistenceAlert.DECODING_FAILED) }
|
guard let dataDecoded = Data(base64Encoded: data) else { return nil }
|
||||||
if let image = UIImage(data: dataDecoded) {
|
return UIImage(data: dataDecoded)
|
||||||
return (image, nil)
|
|
||||||
}
|
|
||||||
return (nil, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipeStubs() async -> ([RecipeStub]?, (any UserAlert)?) {
|
func getRecipeStubs() async -> [RecipeStub]? {
|
||||||
return (nil, PersistenceAlert.LOAD_FAILED)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipe(id: String) async -> (Recipe?, (any UserAlert)?) {
|
func getRecipe(id: String) async -> Recipe? {
|
||||||
if let recipe: Recipe? = await load(path: LocalDataPath.recipe(id: id)) {
|
return await load(path: LocalDataPath.recipe(id: id))
|
||||||
return (recipe, nil)
|
|
||||||
}
|
|
||||||
return (nil, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCategories() async -> ([Category]?, (any UserAlert)?) {
|
func getCategories() async -> [Category]? {
|
||||||
return (await load(path: LocalDataPath.categories), nil)
|
return await load(path: LocalDataPath.categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipeStubsForCategory(named categoryName: String) async -> ([RecipeStub]?, (any UserAlert)?) {
|
func getRecipeStubsForCategory(named categoryName: String) async -> [RecipeStub]? {
|
||||||
if let stubs: [RecipeStub] = await load(path: .recipeStubs(category: categoryName)) {
|
return await load(path: .recipeStubs(category: categoryName))
|
||||||
return (stubs, nil)
|
|
||||||
}
|
|
||||||
return (nil, PersistenceAlert.LOAD_FAILED)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTags() async -> ([RecipeKeyword]?, (any UserAlert)?) {
|
func getTags() async -> [RecipeKeyword]? {
|
||||||
if let keywords: [RecipeKeyword] = await load(path: .keywords) {
|
return await load(path: .keywords)
|
||||||
return (keywords, nil)
|
|
||||||
}
|
|
||||||
return (nil, PersistenceAlert.LOAD_FAILED)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipesTagged(keyword: String) async -> ([RecipeStub]?, (any UserAlert)?) {
|
func getRecipesTagged(keyword: String) async -> [RecipeStub]? {
|
||||||
return (nil, PersistenceAlert.LOAD_FAILED)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -109,6 +96,7 @@ extension LocalDataInterface: WriteInterface {
|
|||||||
path: LocalDataPath.image(id: id, size: size)
|
path: LocalDataPath.image(id: id, size: size)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func postRecipe(recipe: Recipe) async -> ((any UserAlert)?) {
|
func postRecipe(recipe: Recipe) async -> ((any UserAlert)?) {
|
||||||
@@ -129,8 +117,9 @@ extension LocalDataInterface: WriteInterface {
|
|||||||
guard let stubs: [RecipeStub] = await load(path: .recipeStubs(category: categoryName)) else {
|
guard let stubs: [RecipeStub] = await load(path: .recipeStubs(category: categoryName)) else {
|
||||||
return PersistenceAlert.LOAD_FAILED
|
return PersistenceAlert.LOAD_FAILED
|
||||||
}
|
}
|
||||||
await delete(path: .recipeStubs(category: categoryName))
|
|
||||||
await save(stubs, path: .recipeStubs(category: newName))
|
await save(stubs, path: .recipeStubs(category: newName))
|
||||||
|
await delete(path: .recipeStubs(category: categoryName))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -159,3 +148,4 @@ extension LocalDataInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
/*
|
||||||
class NextcloudDataInterface: CookbookInterface {
|
class NextcloudDataInterface: CookbookInterface {
|
||||||
var id: String
|
var id: String
|
||||||
|
|
||||||
@@ -31,39 +31,35 @@ class NextcloudDataInterface: CookbookInterface {
|
|||||||
// MARK: - Nextcloud Read Interface
|
// MARK: - Nextcloud Read Interface
|
||||||
extension NextcloudDataInterface: ReadInterface {
|
extension NextcloudDataInterface: ReadInterface {
|
||||||
|
|
||||||
func getImage(id: String, size: RecipeImage.RecipeImageSize) async -> (UIImage?, UserAlert?) {
|
func getImage(id: String, size: RecipeImage.RecipeImageSize) async -> UIImage? {
|
||||||
let (image, error) = await api.getImage(auth: auth.token, id: id, size: size)
|
return await api.getImage(auth: auth.token, id: id, size: size).0
|
||||||
if let image {
|
|
||||||
return (image, nil)
|
|
||||||
}
|
|
||||||
return (nil, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipeStubs() async -> ([RecipeStub]?, UserAlert?) {
|
func getRecipeStubs() async -> [RecipeStub]? {
|
||||||
return await api.getRecipes(auth: auth.token)
|
return await api.getRecipes(auth: auth.token).0
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipe(id: String) async -> (Recipe?, UserAlert?) {
|
func getRecipe(id: String) async -> Recipe?{
|
||||||
return await api.getRecipe(auth: auth.token, id: id)
|
return await api.getRecipe(auth: auth.token, id: id).0
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCategories() async -> ([Category]?, UserAlert?) {
|
func getCategories() async -> [Category]? {
|
||||||
return await api.getCategories(auth: auth.token)
|
return await api.getCategories(auth: auth.token).0
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipeStubsForCategory(named categoryName: String) async -> ([RecipeStub]?, UserAlert?) {
|
func getRecipeStubsForCategory(named categoryName: String) async -> [RecipeStub]? {
|
||||||
return await api.getCategory(
|
return await api.getCategory(
|
||||||
auth: UserSettings.shared.authString,
|
auth: UserSettings.shared.authString,
|
||||||
named: categoryName
|
named: categoryName
|
||||||
)
|
).0
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTags() async -> ([RecipeKeyword]?, (any UserAlert)?) {
|
func getTags() async -> [RecipeKeyword]? {
|
||||||
return await api.getTags(auth: auth.token)
|
return await api.getTags(auth: auth.token).0
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRecipesTagged(keyword: String) async -> ([RecipeStub]?, UserAlert?) {
|
func getRecipesTagged(keyword: String) async -> [RecipeStub]? {
|
||||||
return await api.getRecipesTagged(auth: auth.token, keyword: keyword)
|
return await api.getRecipesTagged(auth: auth.token, keyword: keyword).0
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -93,3 +89,4 @@ extension NextcloudDataInterface: WriteInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import SwiftUI
|
|||||||
|
|
||||||
class RecipeExporter {
|
class RecipeExporter {
|
||||||
|
|
||||||
func createPDF(recipe: RecipeDetail, image: UIImage?) -> URL? {
|
func createPDF(recipe: CookbookApiRecipeDetailV1, image: UIImage?) -> URL? {
|
||||||
let document = PDFDocument(format: .a4)
|
let document = PDFDocument(format: .a4)
|
||||||
|
|
||||||
let titleStyle = PDFTextStyle(name: "title", font: UIFont.boldSystemFont(ofSize: 18), color: .black)
|
let titleStyle = PDFTextStyle(name: "title", font: UIFont.boldSystemFont(ofSize: 18), color: .black)
|
||||||
@@ -82,7 +82,7 @@ class RecipeExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createText(recipe: RecipeDetail) -> String {
|
func createText(recipe: CookbookApiRecipeDetailV1) -> String {
|
||||||
var recipeString = ""
|
var recipeString = ""
|
||||||
recipeString.append("☛ " + recipe.name + "\n")
|
recipeString.append("☛ " + recipe.name + "\n")
|
||||||
recipeString.append(recipe.description + "\n\n")
|
recipeString.append(recipe.description + "\n\n")
|
||||||
@@ -99,7 +99,7 @@ class RecipeExporter {
|
|||||||
return recipeString
|
return recipeString
|
||||||
}
|
}
|
||||||
|
|
||||||
func createJson(recipe: RecipeDetail) -> Data? {
|
func createJson(recipe: CookbookApiRecipeDetailV1) -> Data? {
|
||||||
return JSONEncoder.safeEncode(recipe)
|
return JSONEncoder.safeEncode(recipe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import SwiftUI
|
|||||||
|
|
||||||
|
|
||||||
class RecipeScraper {
|
class RecipeScraper {
|
||||||
func scrape(url: String) async throws -> (RecipeDetail?, RecipeImportAlert?) {
|
func scrape(url: String) async throws -> (CookbookApiRecipeDetailV1?, RecipeImportAlert?) {
|
||||||
var contents: String? = nil
|
var contents: String? = nil
|
||||||
if let url = URL(string: url) {
|
if let url = URL(string: url) {
|
||||||
do {
|
do {
|
||||||
@@ -77,9 +77,9 @@ class RecipeScraper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getRecipe(fromDict recipe: Dictionary<String, Any>) -> RecipeDetail? {
|
private func getRecipe(fromDict recipe: Dictionary<String, Any>) -> CookbookApiRecipeDetailV1? {
|
||||||
|
|
||||||
var recipeDetail = RecipeDetail()
|
var recipeDetail = CookbookApiRecipeDetailV1()
|
||||||
recipeDetail.name = recipe["name"] as? String ?? "New Recipe"
|
recipeDetail.name = recipe["name"] as? String ?? "New Recipe"
|
||||||
recipeDetail.recipeCategory = recipe["recipeCategory"] as? String ?? ""
|
recipeDetail.recipeCategory = recipe["recipeCategory"] as? String ?? ""
|
||||||
recipeDetail.keywords = joinedStringForKey("keywords", dict: recipe)
|
recipeDetail.keywords = joinedStringForKey("keywords", dict: recipe)
|
||||||
|
|||||||
@@ -146,3 +146,33 @@ enum RequestAlert: UserAlert {
|
|||||||
return [.OK]
|
return [.OK]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum PersistenceAlert: UserAlert {
|
||||||
|
case DECODING_FAILED,
|
||||||
|
ENCODING_FAILED,
|
||||||
|
SAVE_FAILED,
|
||||||
|
LOAD_FAILED
|
||||||
|
|
||||||
|
var localizedDescription: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .DECODING_FAILED: return "Unable to decode recipe data."
|
||||||
|
case .ENCODING_FAILED: return "Unable to encode recipe data."
|
||||||
|
case .SAVE_FAILED: return "Unable to save recipe."
|
||||||
|
case .LOAD_FAILED: return "Unable to load recipe."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var localizedTitle: LocalizedStringKey {
|
||||||
|
switch self {
|
||||||
|
case .DECODING_FAILED: return "Decoding Error"
|
||||||
|
case .ENCODING_FAILED: return "Encoding Error"
|
||||||
|
case .SAVE_FAILED: return "Error"
|
||||||
|
case .LOAD_FAILED: return "Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var alertButtons: [AlertButton] {
|
||||||
|
return [.OK]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,7 +37,28 @@ class DurationComponents: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ hours: Int, _ min: Int, _ sec: Int = 0) {
|
||||||
|
self.hourComponent = hours
|
||||||
|
self.minuteComponent = min
|
||||||
|
self.secondComponent = sec
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
let durationString = try container.decode(String.self)
|
||||||
|
let hourRegex = /([0-9]{1,2})H/
|
||||||
|
let minuteRegex = /([0-9]{1,2})M/
|
||||||
|
if let match = durationString.firstMatch(of: hourRegex) {
|
||||||
|
self.hourComponent = Int(match.1) ?? 0
|
||||||
|
}
|
||||||
|
if let match = durationString.firstMatch(of: minuteRegex) {
|
||||||
|
self.minuteComponent = Int(match.1) ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var displayString: String {
|
var displayString: String {
|
||||||
if hourComponent != 0 && minuteComponent != 0 {
|
if hourComponent != 0 && minuteComponent != 0 {
|
||||||
@@ -144,3 +165,11 @@ class DurationComponents: ObservableObject {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension DurationComponents: Codable {
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.singleValueContainer()
|
||||||
|
let durationString = toPTString()
|
||||||
|
try container.encode(durationString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,68 +6,177 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import SwiftData
|
||||||
|
|
||||||
struct MainView: View {
|
struct MainView: View {
|
||||||
@StateObject var appState = AppState()
|
//@State var cookbookState: CookbookState = CookbookState()
|
||||||
@StateObject var groceryList = GroceryList()
|
@Environment(\.modelContext) var modelContext
|
||||||
|
@Query var recipes: [Recipe] = []
|
||||||
|
|
||||||
// Tab ViewModels
|
|
||||||
@StateObject var recipeViewModel = RecipeTabView.ViewModel()
|
|
||||||
@StateObject var searchViewModel = SearchTabView.ViewModel()
|
|
||||||
|
|
||||||
enum Tab {
|
|
||||||
case recipes, search, groceryList
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
VStack {
|
||||||
RecipeTabView()
|
List {
|
||||||
.environmentObject(recipeViewModel)
|
ForEach(recipes) { recipe in
|
||||||
.environmentObject(appState)
|
Text(recipe.name)
|
||||||
.environmentObject(groceryList)
|
|
||||||
.tabItem {
|
|
||||||
Label("Recipes", systemImage: "book.closed.fill")
|
|
||||||
}
|
|
||||||
.tag(Tab.recipes)
|
|
||||||
|
|
||||||
SearchTabView()
|
|
||||||
.environmentObject(searchViewModel)
|
|
||||||
.environmentObject(appState)
|
|
||||||
.environmentObject(groceryList)
|
|
||||||
.tabItem {
|
|
||||||
Label("Search", systemImage: "magnifyingglass")
|
|
||||||
}
|
|
||||||
.tag(Tab.search)
|
|
||||||
|
|
||||||
GroceryListTabView()
|
|
||||||
.environmentObject(groceryList)
|
|
||||||
.tabItem {
|
|
||||||
if #available(iOS 17.0, *) {
|
|
||||||
Label("Grocery List", systemImage: "storefront")
|
|
||||||
} else {
|
|
||||||
Label("Grocery List", systemImage: "heart.text.square")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tag(Tab.groceryList)
|
|
||||||
}
|
|
||||||
.task {
|
|
||||||
recipeViewModel.presentLoadingIndicator = true
|
|
||||||
await appState.getCategories()
|
|
||||||
await appState.updateAllRecipeDetails()
|
|
||||||
|
|
||||||
// Open detail view for default category
|
|
||||||
if UserSettings.shared.defaultCategory != "" {
|
|
||||||
if let cat = appState.categories.first(where: { c in
|
|
||||||
if c.name == UserSettings.shared.defaultCategory {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}) {
|
|
||||||
recipeViewModel.selectedCategory = cat
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await groceryList.load()
|
Button("New") {
|
||||||
recipeViewModel.presentLoadingIndicator = false
|
let recipe = Recipe(id: UUID().uuidString, name: "Neues Rezept", keywords: [], prepTime: "", cookTime: "", totalTime: "", recipeDescription: "", yield: 0, category: "", tools: [], ingredients: [], instructions: [], nutrition: [:], ingredientMultiplier: 0)
|
||||||
|
modelContext.insert(recipe)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
/*NavigationSplitView {
|
||||||
|
VStack {
|
||||||
|
List(selection: $cookbookState.selectedCategory) {
|
||||||
|
ForEach(cookbookState.categories) { category in
|
||||||
|
Text(category.name)
|
||||||
|
.tag(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.onAppear {
|
||||||
|
Task {
|
||||||
|
await cookbookState.loadCategories()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} content: {
|
||||||
|
if let selectedCategory = cookbookState.selectedCategory {
|
||||||
|
List(selection: $cookbookState.selectedRecipeStub) {
|
||||||
|
ForEach(cookbookState.recipeStubs[selectedCategory.name] ?? [], id: \.id) { recipeStub in
|
||||||
|
Text(recipeStub.title)
|
||||||
|
.tag(recipeStub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
Task {
|
||||||
|
await cookbookState.loadRecipeStubs(category: selectedCategory.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Please select a category.")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
} detail: {
|
||||||
|
if let selectedRecipe = cookbookState.selectedRecipe {
|
||||||
|
if let recipe = cookbookState.recipes[selectedRecipe.id] {
|
||||||
|
RecipeView(recipe: recipe)
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
|
.onAppear {
|
||||||
|
Task {
|
||||||
|
await cookbookState.loadRecipe(id: selectedRecipe.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("Please select a recipe.")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .bottomBar) {
|
||||||
|
Button(action: {
|
||||||
|
cookbookState.showGroceries = true
|
||||||
|
}) {
|
||||||
|
Label("Grocery List", systemImage: "cart")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .topBarLeading) {
|
||||||
|
Button(action: {
|
||||||
|
cookbookState.showSettings = true
|
||||||
|
}) {
|
||||||
|
Label("Settings", systemImage: "gearshape")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*struct CategoryListView: View {
|
||||||
|
@Bindable var cookbookState: CookbookState
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List(cookbookState.selectedAccountState.categories) { category in
|
||||||
|
NavigationLink {
|
||||||
|
RecipeListView(
|
||||||
|
cookbookState: cookbookState,
|
||||||
|
selectedCategory: category.name,
|
||||||
|
showEditView: .constant(false)
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
if cookbookState.selectedAccountState.selectedCategory != nil &&
|
||||||
|
category.name == cookbookState.selectedAccountState.selectedCategory?.name {
|
||||||
|
Image(systemName: "book")
|
||||||
|
} else {
|
||||||
|
Image(systemName: "book.closed.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
if category.name == "*" {
|
||||||
|
Text("Other")
|
||||||
|
.font(.system(size: 20, weight: .medium, design: .default))
|
||||||
|
} else {
|
||||||
|
Text(category.name)
|
||||||
|
.font(.system(size: 20, weight: .medium, design: .default))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
Text("\(category.recipe_count)")
|
||||||
|
.font(.system(size: 15, weight: .bold, design: .default))
|
||||||
|
.foregroundStyle(Color.background)
|
||||||
|
.frame(width: 25, height: 25, alignment: .center)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
|
.background {
|
||||||
|
Circle()
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
}
|
||||||
|
}.padding(7)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
/*struct CategoryListView: View {
|
||||||
|
@State var state: CookbookState
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List(selection: $state.categoryListSelection) {
|
||||||
|
ForEach(state.categories) { category in
|
||||||
|
NavigationLink(value: category) {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
if state.categoryListSelection != nil &&
|
||||||
|
category.name == state.categoryListSelection {
|
||||||
|
Image(systemName: "book")
|
||||||
|
} else {
|
||||||
|
Image(systemName: "book.closed.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
if category.name == "*" {
|
||||||
|
Text("Other")
|
||||||
|
.font(.system(size: 20, weight: .medium, design: .default))
|
||||||
|
} else {
|
||||||
|
Text(category.name)
|
||||||
|
.font(.system(size: 20, weight: .medium, design: .default))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
Text("\(category.recipe_count)")
|
||||||
|
.font(.system(size: 15, weight: .bold, design: .default))
|
||||||
|
.foregroundStyle(Color.background)
|
||||||
|
.frame(width: 25, height: 25, alignment: .center)
|
||||||
|
.minimumScaleFactor(0.5)
|
||||||
|
.background {
|
||||||
|
Circle()
|
||||||
|
.foregroundStyle(Color.secondary)
|
||||||
|
}
|
||||||
|
}.padding(7)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
/*
|
||||||
struct OnboardingView: View {
|
struct OnboardingView: View {
|
||||||
@State var selectedTab: Int = 0
|
@State var selectedTab: Int = 0
|
||||||
|
|
||||||
@@ -244,3 +244,4 @@ struct ServerAddressField_Preview: PreviewProvider {
|
|||||||
.background(Color.nextcloudBlue)
|
.background(Color.nextcloudBlue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
struct TokenLoginView: View {
|
struct TokenLoginView: View {
|
||||||
@Binding var showAlert: Bool
|
@Binding var showAlert: Bool
|
||||||
@Binding var alertMessage: String
|
@Binding var alertMessage: String
|
||||||
@@ -105,3 +105,4 @@ struct TokenLoginView: View {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import WebKit
|
import WebKit
|
||||||
|
/*
|
||||||
enum V2LoginStage: LoginStage {
|
enum V2LoginStage: LoginStage {
|
||||||
case login, validate
|
case login, validate
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ struct V2LoginView: View {
|
|||||||
Task {
|
Task {
|
||||||
let error = await sendLoginV2Request()
|
let error = await sendLoginV2Request()
|
||||||
if let error = error {
|
if let error = error {
|
||||||
alertMessage = "A network error occured (\(error.rawValue))."
|
alertMessage = "A network error occured (\(error.localizedDescription))."
|
||||||
showAlert = true
|
showAlert = true
|
||||||
}
|
}
|
||||||
if let loginRequest = loginRequest {
|
if let loginRequest = loginRequest {
|
||||||
@@ -157,7 +157,7 @@ struct V2LoginView: View {
|
|||||||
|
|
||||||
func checkLogin(response: LoginV2Response?, error: NetworkError?) {
|
func checkLogin(response: LoginV2Response?, error: NetworkError?) {
|
||||||
if let error = error {
|
if let error = error {
|
||||||
alertMessage = "Login failed. Please login via the browser and try again. (\(error.rawValue))"
|
alertMessage = "Login failed. Please login via the browser and try again. (\(error.localizedDescription))"
|
||||||
showAlert = true
|
showAlert = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -209,3 +209,4 @@ struct WebView: UIViewRepresentable {
|
|||||||
uiView.load(request)
|
uiView.load(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
/*
|
||||||
struct RecipeCardView: View {
|
struct RecipeCardView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@State var recipe: Recipe
|
@State var recipe: CookbookApiRecipeV1
|
||||||
@State var recipeThumb: UIImage?
|
@State var recipeThumb: UIImage?
|
||||||
@State var isDownloaded: Bool? = nil
|
@State var isDownloaded: Bool? = nil
|
||||||
|
|
||||||
@@ -69,3 +70,63 @@ struct RecipeCardView: View {
|
|||||||
.frame(height: 80)
|
.frame(height: 80)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
struct RecipeCardView: View {
|
||||||
|
@State var state: AccountState
|
||||||
|
@State var recipe: RecipeStub
|
||||||
|
@State var recipeThumb: UIImage?
|
||||||
|
@State var isDownloaded: Bool? = nil
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
if let recipeThumb = recipeThumb {
|
||||||
|
Image(uiImage: recipeThumb)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: 80, height: 80)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 17))
|
||||||
|
} else {
|
||||||
|
Image(systemName: "square.text.square")
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.foregroundStyle(Color.white)
|
||||||
|
.padding(10)
|
||||||
|
.background(Color("ncblue"))
|
||||||
|
.frame(width: 80, height: 80)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 17))
|
||||||
|
}
|
||||||
|
Text(recipe.name)
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.leading, 4)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
if let isDownloaded = isDownloaded {
|
||||||
|
VStack {
|
||||||
|
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color.backgroundHighlight)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 17))
|
||||||
|
.task {
|
||||||
|
recipeThumb = await state.getImage(
|
||||||
|
id: recipe.id,
|
||||||
|
size: .THUMB
|
||||||
|
)
|
||||||
|
|
||||||
|
isDownloaded = recipe.storedLocally
|
||||||
|
}
|
||||||
|
.refreshable {
|
||||||
|
recipeThumb = await state.getImage(
|
||||||
|
id: recipe.id,
|
||||||
|
size: .THUMB
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.frame(height: 80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
struct RecipeListView: View {
|
struct RecipeListView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@EnvironmentObject var groceryList: GroceryList
|
@EnvironmentObject var groceryList: GroceryList
|
||||||
@State var categoryName: String
|
@State var categoryName: String
|
||||||
@State var searchText: String = ""
|
@State var searchText: String = ""
|
||||||
@Binding var showEditView: Bool
|
@Binding var showEditView: Bool
|
||||||
@State var selectedRecipe: Recipe? = nil
|
@State var selectedRecipe: CookbookApiRecipeV1? = nil
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
@@ -56,7 +56,7 @@ struct RecipeListView: View {
|
|||||||
.searchable(text: $searchText, prompt: "Search recipes/keywords")
|
.searchable(text: $searchText, prompt: "Search recipes/keywords")
|
||||||
.navigationTitle(categoryName == "*" ? String(localized: "Other") : categoryName)
|
.navigationTitle(categoryName == "*" ? String(localized: "Other") : categoryName)
|
||||||
|
|
||||||
.navigationDestination(for: Recipe.self) { recipe in
|
.navigationDestination(for: CookbookApiRecipeV1.self) { recipe in
|
||||||
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
|
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
|
||||||
.environmentObject(appState)
|
.environmentObject(appState)
|
||||||
.environmentObject(groceryList)
|
.environmentObject(groceryList)
|
||||||
@@ -85,7 +85,7 @@ struct RecipeListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func recipesFiltered() -> [Recipe] {
|
func recipesFiltered() -> [CookbookApiRecipeV1] {
|
||||||
guard let recipes = appState.recipes[categoryName] else { return [] }
|
guard let recipes = appState.recipes[categoryName] else { return [] }
|
||||||
guard searchText != "" else { return recipes }
|
guard searchText != "" else { return recipes }
|
||||||
return recipes.filter { recipe in
|
return recipes.filter { recipe in
|
||||||
@@ -94,3 +94,86 @@ struct RecipeListView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
|
||||||
|
struct RecipeListView: View {
|
||||||
|
@Bindable var cookbookState: CookbookState
|
||||||
|
@State var selectedCategory: String
|
||||||
|
@State var searchText: String = ""
|
||||||
|
@Binding var showEditView: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
let recipes = recipesFiltered()
|
||||||
|
if !recipes.isEmpty {
|
||||||
|
|
||||||
|
List(recipesFiltered(), selection: $cookbookState.selectedAccountState.selectedRecipe) { recipe in
|
||||||
|
RecipeCardView(state: cookbookState.selectedAccountState, recipe: recipe)
|
||||||
|
.shadow(radius: 2)
|
||||||
|
.background(
|
||||||
|
NavigationLink {
|
||||||
|
RecipeView(viewModel: RecipeView.ViewModel(recipeStub: recipe))
|
||||||
|
.environment(cookbookState)
|
||||||
|
} label: {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.opacity(0)
|
||||||
|
)
|
||||||
|
.frame(height: 85)
|
||||||
|
.listRowInsets(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))
|
||||||
|
.listRowSeparatorTint(.clear)
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
VStack {
|
||||||
|
Text("There are no recipes in this cookbook!")
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
let _ = await cookbookState.selectedAccountState.getCategories()
|
||||||
|
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(named: selectedCategory)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("Refresh")
|
||||||
|
.bold()
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
}.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.searchable(text: $searchText, prompt: "Search recipes/keywords")
|
||||||
|
.navigationTitle(selectedCategory == "*" ? String(localized: "Other") : selectedCategory)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Button {
|
||||||
|
print("Add new recipe")
|
||||||
|
showEditView = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(
|
||||||
|
named: selectedCategory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.refreshable {
|
||||||
|
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(
|
||||||
|
named: selectedCategory
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func recipesFiltered() -> [RecipeStub] {
|
||||||
|
guard let recipes = cookbookState.selectedAccountState.recipeStubs[selectedCategory] else { return [] }
|
||||||
|
guard searchText != "" else { return recipes }
|
||||||
|
return recipes.filter { recipe in
|
||||||
|
recipe.name.lowercased().contains(searchText.lowercased()) || // check name for occurence of search term
|
||||||
|
(recipe.keywords != nil && recipe.keywords!.lowercased().contains(searchText.lowercased())) // check keywords for search term
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
/*
|
||||||
struct RecipeView: View {
|
struct RecipeView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@@ -164,7 +164,7 @@ struct RecipeView: View {
|
|||||||
let recipeDetail = await appState.getRecipe(
|
let recipeDetail = await appState.getRecipe(
|
||||||
id: viewModel.recipe.recipe_id,
|
id: viewModel.recipe.recipe_id,
|
||||||
fetchMode: UserSettings.shared.storeRecipes ? .preferLocal : .onlyServer
|
fetchMode: UserSettings.shared.storeRecipes ? .preferLocal : .onlyServer
|
||||||
) ?? RecipeDetail.error
|
) ?? CookbookApiRecipeDetailV1.error
|
||||||
viewModel.setupView(recipeDetail: recipeDetail)
|
viewModel.setupView(recipeDetail: recipeDetail)
|
||||||
|
|
||||||
// Show download badge
|
// Show download badge
|
||||||
@@ -182,7 +182,7 @@ struct RecipeView: View {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Prepare view for a new recipe
|
// Prepare view for a new recipe
|
||||||
viewModel.setupView(recipeDetail: RecipeDetail())
|
viewModel.setupView(recipeDetail: CookbookApiRecipeDetailV1())
|
||||||
viewModel.editMode = true
|
viewModel.editMode = true
|
||||||
viewModel.isDownloaded = false
|
viewModel.isDownloaded = false
|
||||||
}
|
}
|
||||||
@@ -231,8 +231,8 @@ struct RecipeView: View {
|
|||||||
// MARK: - RecipeView ViewModel
|
// MARK: - RecipeView ViewModel
|
||||||
|
|
||||||
class ViewModel: ObservableObject {
|
class ViewModel: ObservableObject {
|
||||||
@Published var observableRecipeDetail: ObservableRecipeDetail = ObservableRecipeDetail()
|
@Published var observableRecipeDetail: Recipe = Recipe()
|
||||||
@Published var recipeDetail: RecipeDetail = RecipeDetail.error
|
@Published var recipeDetail: CookbookApiRecipeDetailV1 = CookbookApiRecipeDetailV1.error
|
||||||
@Published var recipeImage: UIImage? = nil
|
@Published var recipeImage: UIImage? = nil
|
||||||
@Published var editMode: Bool = false
|
@Published var editMode: Bool = false
|
||||||
@Published var showTitle: Bool = false
|
@Published var showTitle: Bool = false
|
||||||
@@ -244,7 +244,7 @@ struct RecipeView: View {
|
|||||||
@Published var presentIngredientEditView: Bool = false
|
@Published var presentIngredientEditView: Bool = false
|
||||||
@Published var presentToolEditView: Bool = false
|
@Published var presentToolEditView: Bool = false
|
||||||
|
|
||||||
var recipe: Recipe
|
var recipe: CookbookApiRecipeV1
|
||||||
var sharedURL: URL? = nil
|
var sharedURL: URL? = nil
|
||||||
var newRecipe: Bool = false
|
var newRecipe: Bool = false
|
||||||
|
|
||||||
@@ -254,13 +254,13 @@ struct RecipeView: View {
|
|||||||
var alertAction: () async -> () = { }
|
var alertAction: () async -> () = { }
|
||||||
|
|
||||||
// Initializers
|
// Initializers
|
||||||
init(recipe: Recipe) {
|
init(recipe: CookbookApiRecipeV1) {
|
||||||
self.recipe = recipe
|
self.recipe = recipe
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.newRecipe = true
|
self.newRecipe = true
|
||||||
self.recipe = Recipe(
|
self.recipe = CookbookApiRecipeV1(
|
||||||
name: String(localized: "New Recipe"),
|
name: String(localized: "New Recipe"),
|
||||||
keywords: "",
|
keywords: "",
|
||||||
dateCreated: "",
|
dateCreated: "",
|
||||||
@@ -271,9 +271,9 @@ struct RecipeView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// View setup
|
// View setup
|
||||||
func setupView(recipeDetail: RecipeDetail) {
|
func setupView(recipeDetail: CookbookApiRecipeDetailV1) {
|
||||||
self.recipeDetail = recipeDetail
|
self.recipeDetail = recipeDetail
|
||||||
self.observableRecipeDetail = ObservableRecipeDetail(recipeDetail)
|
self.observableRecipeDetail = Recipe(recipeDetail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentAlert(_ type: UserAlert, action: @escaping () async -> () = {}) {
|
func presentAlert(_ type: UserAlert, action: @escaping () async -> () = {}) {
|
||||||
@@ -458,4 +458,434 @@ struct RecipeViewToolBar: ToolbarContent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
struct RecipeView: View {
|
||||||
|
@Environment(CookbookState.self) var cookbookState
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State var viewModel: ViewModel
|
||||||
|
@GestureState private var dragOffset = CGSize.zero
|
||||||
|
|
||||||
|
var imageHeight: CGFloat {
|
||||||
|
if let image = viewModel.recipeImage {
|
||||||
|
return image.size.height < 350 ? image.size.height : 350
|
||||||
|
}
|
||||||
|
return 200
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CoordinateSpaces {
|
||||||
|
case scrollView
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ScrollView(showsIndicators: false) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ParallaxHeader(
|
||||||
|
coordinateSpace: CoordinateSpaces.scrollView,
|
||||||
|
defaultHeight: imageHeight
|
||||||
|
) {
|
||||||
|
if let recipeImage = viewModel.recipeImage {
|
||||||
|
Image(uiImage: recipeImage)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(maxHeight: imageHeight + 200)
|
||||||
|
.clipped()
|
||||||
|
} else {
|
||||||
|
Rectangle()
|
||||||
|
.frame(height: 400)
|
||||||
|
.foregroundStyle(
|
||||||
|
LinearGradient(
|
||||||
|
gradient: Gradient(colors: [.ncGradientDark, .ncGradientLight]),
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if viewModel.editMode {
|
||||||
|
RecipeImportSection(viewModel: viewModel, importRecipe: importRecipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
if viewModel.editMode {
|
||||||
|
RecipeMetadataSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
EditableText(text: $viewModel.recipe.name, editMode: $viewModel.editMode, titleKey: "Recipe Name")
|
||||||
|
.font(.title)
|
||||||
|
.bold()
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
if let isDownloaded = viewModel.isDownloaded {
|
||||||
|
Image(systemName: isDownloaded ? "checkmark.circle" : "icloud.and.arrow.down")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}.padding([.top, .horizontal])
|
||||||
|
|
||||||
|
if viewModel.recipe.description != "" || viewModel.editMode {
|
||||||
|
EditableText(text: $viewModel.recipe.description, editMode: $viewModel.editMode, titleKey: "Description", lineLimit: 0...5, axis: .vertical)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.top, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipe Body Section
|
||||||
|
RecipeDurationSection(viewModel: viewModel)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
||||||
|
if(!viewModel.recipe.recipeIngredient.isEmpty || viewModel.editMode) {
|
||||||
|
RecipeIngredientSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
if(!viewModel.recipe.recipeInstructions.isEmpty || viewModel.editMode) {
|
||||||
|
RecipeInstructionSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
if(!viewModel.recipe.tool.isEmpty || viewModel.editMode) {
|
||||||
|
RecipeToolSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
RecipeNutritionSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !viewModel.editMode {
|
||||||
|
Divider()
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 400), alignment: .top)]) {
|
||||||
|
RecipeKeywordSection(viewModel: viewModel)
|
||||||
|
MoreInformationSection(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 5)
|
||||||
|
.background(Rectangle().foregroundStyle(.background).shadow(radius: 5).mask(Rectangle().padding(.top, -20)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.coordinateSpace(name: CoordinateSpaces.scrollView)
|
||||||
|
.ignoresSafeArea(.container, edges: .top)
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar(.visible, for: .navigationBar)
|
||||||
|
//.toolbarTitleDisplayMode(.inline)
|
||||||
|
.navigationTitle(viewModel.showTitle ? viewModel.recipe.name : "")
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
RecipeViewToolBar(viewModel: viewModel)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $viewModel.presentShareSheet) {
|
||||||
|
ShareView(recipeDetail: viewModel.recipe.toRecipeDetail(),
|
||||||
|
recipeImage: viewModel.recipeImage,
|
||||||
|
presentShareSheet: $viewModel.presentShareSheet)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $viewModel.presentInstructionEditView) {
|
||||||
|
EditableListView(
|
||||||
|
isPresented: $viewModel.presentInstructionEditView,
|
||||||
|
items: $viewModel.recipe.recipeInstructions,
|
||||||
|
title: "Instructions",
|
||||||
|
emptyListText: "Add cooking steps for fellow chefs to follow.",
|
||||||
|
titleKey: "Instruction",
|
||||||
|
lineLimit: 0...10,
|
||||||
|
axis: .vertical)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $viewModel.presentIngredientEditView) {
|
||||||
|
EditableListView(
|
||||||
|
isPresented: $viewModel.presentIngredientEditView,
|
||||||
|
items: $viewModel.recipe.recipeIngredient,
|
||||||
|
title: "Ingredients",
|
||||||
|
emptyListText: "Start by adding your first ingredient! 🥬",
|
||||||
|
titleKey: "Ingredient",
|
||||||
|
lineLimit: 0...1,
|
||||||
|
axis: .horizontal)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $viewModel.presentToolEditView) {
|
||||||
|
EditableListView(
|
||||||
|
isPresented: $viewModel.presentToolEditView,
|
||||||
|
items: $viewModel.recipe.tool,
|
||||||
|
title: "Tools",
|
||||||
|
emptyListText: "List your tools here. 🍴",
|
||||||
|
titleKey: "Tool",
|
||||||
|
lineLimit: 0...1,
|
||||||
|
axis: .horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
.task {
|
||||||
|
// Load recipe detail
|
||||||
|
if let recipeStub = viewModel.recipeStub {
|
||||||
|
// For existing recipes, load the recipeDetail and image
|
||||||
|
let recipe = await cookbookState.selectedAccountState.getRecipe(
|
||||||
|
id: recipeStub.id
|
||||||
|
) ?? Recipe()
|
||||||
|
viewModel.recipe = recipe
|
||||||
|
|
||||||
|
// Show download badge
|
||||||
|
/*if viewModel.recipeStub!.storedLocally == nil {
|
||||||
|
viewModel.recipeStub?.storedLocally = cookbookState.selectedAccountState.recipeDetailExists(
|
||||||
|
recipeId: viewModel.recipe.recipe_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModel.isDownloaded = viewModel.recipeStub!.storedLocally
|
||||||
|
*/
|
||||||
|
// Load recipe image
|
||||||
|
viewModel.recipeImage = await cookbookState.selectedAccountState.getImage(
|
||||||
|
id: recipeStub.id,
|
||||||
|
size: .FULL
|
||||||
|
)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Prepare view for a new recipe
|
||||||
|
viewModel.editMode = true
|
||||||
|
viewModel.isDownloaded = false
|
||||||
|
viewModel.recipe = Recipe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert(viewModel.alertType.localizedTitle, isPresented: $viewModel.presentAlert) {
|
||||||
|
ForEach(viewModel.alertType.alertButtons) { buttonType in
|
||||||
|
if buttonType == .OK {
|
||||||
|
Button(AlertButton.OK.rawValue, role: .cancel) {
|
||||||
|
Task {
|
||||||
|
await viewModel.alertAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if buttonType == .CANCEL {
|
||||||
|
Button(AlertButton.CANCEL.rawValue, role: .cancel) { }
|
||||||
|
} else if buttonType == .DELETE {
|
||||||
|
Button(AlertButton.DELETE.rawValue, role: .destructive) {
|
||||||
|
Task {
|
||||||
|
await viewModel.alertAction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} message: {
|
||||||
|
Text(viewModel.alertType.localizedDescription)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if UserSettings.shared.keepScreenAwake {
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = false
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.editMode) { newValue in
|
||||||
|
if newValue && cookbookState.selectedAccountState.keywords.isEmpty {
|
||||||
|
Task {
|
||||||
|
if let keywords = await cookbookState.selectedAccountState.getTags()?.sorted(by: { a, b in
|
||||||
|
a.recipe_count > b.recipe_count
|
||||||
|
}) {
|
||||||
|
cookbookState.selectedAccountState.keywords = keywords
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - RecipeView ViewModel
|
||||||
|
|
||||||
|
@Observable class ViewModel {
|
||||||
|
var recipeImage: UIImage? = nil
|
||||||
|
var editMode: Bool = false
|
||||||
|
var showTitle: Bool = false
|
||||||
|
var isDownloaded: Bool? = nil
|
||||||
|
var importUrl: String = ""
|
||||||
|
|
||||||
|
var presentShareSheet: Bool = false
|
||||||
|
var presentInstructionEditView: Bool = false
|
||||||
|
var presentIngredientEditView: Bool = false
|
||||||
|
var presentToolEditView: Bool = false
|
||||||
|
|
||||||
|
var recipeStub: RecipeStub? = nil
|
||||||
|
var recipe: Recipe = Recipe()
|
||||||
|
var sharedURL: URL? = nil
|
||||||
|
var newRecipe: Bool = false
|
||||||
|
|
||||||
|
// Alerts
|
||||||
|
var presentAlert = false
|
||||||
|
var alertType: UserAlert = RecipeAlert.GENERIC
|
||||||
|
var alertAction: () async -> () = { }
|
||||||
|
|
||||||
|
// Initializers
|
||||||
|
init(recipeStub: RecipeStub) {
|
||||||
|
self.recipeStub = recipeStub
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.newRecipe = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentAlert(_ type: UserAlert, action: @escaping () async -> () = {}) {
|
||||||
|
alertType = type
|
||||||
|
alertAction = action
|
||||||
|
presentAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
extension RecipeView {
|
||||||
|
func importRecipe(from url: String) async -> UserAlert? {
|
||||||
|
/*let (scrapedRecipe, error) = await appState.importRecipe(url: url)
|
||||||
|
if let scrapedRecipe = scrapedRecipe {
|
||||||
|
viewModel.setupView(recipeDetail: scrapedRecipe)
|
||||||
|
return nil
|
||||||
|
}*/
|
||||||
|
|
||||||
|
do {
|
||||||
|
let (scrapedRecipe, error) = try await RecipeScraper().scrape(url: url)
|
||||||
|
if let scrapedRecipe = scrapedRecipe {
|
||||||
|
viewModel.recipe = scrapedRecipe.toRecipe()
|
||||||
|
}
|
||||||
|
if let error = error {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error")
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Tool Bar
|
||||||
|
|
||||||
|
|
||||||
|
struct RecipeViewToolBar: ToolbarContent {
|
||||||
|
@Environment(CookbookState.self) var cookbookState
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
@State var viewModel: RecipeView.ViewModel
|
||||||
|
|
||||||
|
|
||||||
|
var body: some ToolbarContent {
|
||||||
|
if viewModel.editMode {
|
||||||
|
ToolbarItemGroup(placement: .topBarLeading) {
|
||||||
|
Button("Cancel") {
|
||||||
|
viewModel.editMode = false
|
||||||
|
if viewModel.newRecipe {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !viewModel.newRecipe {
|
||||||
|
Menu {
|
||||||
|
Button(role: .destructive) {
|
||||||
|
viewModel.presentAlert(
|
||||||
|
RecipeAlert.CONFIRM_DELETE,
|
||||||
|
action: {
|
||||||
|
await handleDelete()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} label: {
|
||||||
|
Label("Delete Recipe", systemImage: "trash")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis.circle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await handleUpload()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if viewModel.newRecipe {
|
||||||
|
Text("Upload Recipe")
|
||||||
|
} else {
|
||||||
|
Text("Upload Changes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ToolbarItem(placement: .topBarTrailing) {
|
||||||
|
Menu {
|
||||||
|
Button {
|
||||||
|
viewModel.editMode = true
|
||||||
|
} label: {
|
||||||
|
Label("Edit", systemImage: "pencil")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
print("Sharing recipe ...")
|
||||||
|
viewModel.presentShareSheet = true
|
||||||
|
} label: {
|
||||||
|
Label("Share Recipe", systemImage: "square.and.arrow.up")
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "ellipsis.circle")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUpload() async {
|
||||||
|
if viewModel.newRecipe {
|
||||||
|
print("Uploading new recipe.")
|
||||||
|
if let recipeValidationError = recipeValid() {
|
||||||
|
viewModel.presentAlert(recipeValidationError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let alert = await cookbookState.selectedAccountState.postRecipe(recipe: viewModel.recipe) {
|
||||||
|
viewModel.presentAlert(alert)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print("Uploading changed recipe.")
|
||||||
|
|
||||||
|
if let alert = await cookbookState.selectedAccountState.updateRecipe(recipe: viewModel.recipe) {
|
||||||
|
viewModel.presentAlert(alert)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = await cookbookState.selectedAccountState.getCategories()
|
||||||
|
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(named: viewModel.recipe.recipeCategory)
|
||||||
|
let _ = await cookbookState.selectedAccountState.getRecipe(id: viewModel.recipe.id)
|
||||||
|
viewModel.editMode = false
|
||||||
|
viewModel.presentAlert(RecipeAlert.UPLOAD_SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDelete() async {
|
||||||
|
let category = viewModel.recipe.recipeCategory
|
||||||
|
if let alert = await cookbookState.selectedAccountState.deleteRecipe(id: viewModel.recipe.id) {
|
||||||
|
viewModel.presentAlert(alert)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = await cookbookState.selectedAccountState.getCategories()
|
||||||
|
let _ = await cookbookState.selectedAccountState.getRecipeStubsForCategory(named: category)
|
||||||
|
viewModel.presentAlert(RecipeAlert.DELETE_SUCCESS)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
func recipeValid() -> RecipeAlert? {
|
||||||
|
// Check if the recipe has a name
|
||||||
|
if viewModel.recipe.name.replacingOccurrences(of: " ", with: "") == "" {
|
||||||
|
return RecipeAlert.NO_TITLE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the recipe has a unique name
|
||||||
|
for recipeList in cookbookState.selectedAccountState.recipeStubs.values {
|
||||||
|
for r in recipeList {
|
||||||
|
if r.name
|
||||||
|
.replacingOccurrences(of: " ", with: "")
|
||||||
|
.lowercased() ==
|
||||||
|
viewModel.recipe.name
|
||||||
|
.replacingOccurrences(of: " ", with: "")
|
||||||
|
.lowercased()
|
||||||
|
{
|
||||||
|
return RecipeAlert.DUPLICATE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,17 +9,17 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - RecipeView Duration Section
|
// MARK: - RecipeView Duration Section
|
||||||
|
/*
|
||||||
struct RecipeDurationSection: View {
|
struct RecipeDurationSection: View {
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@State var viewModel: RecipeView.ViewModel
|
||||||
@State var presentPopover: Bool = false
|
@State var presentPopover: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 200, maximum: .infinity), alignment: .leading)]) {
|
||||||
DurationView(time: viewModel.observableRecipeDetail.prepTime, title: LocalizedStringKey("Preparation"))
|
DurationView(time: viewModel.recipe.prepTime, title: LocalizedStringKey("Preparation"))
|
||||||
DurationView(time: viewModel.observableRecipeDetail.cookTime, title: LocalizedStringKey("Cooking"))
|
DurationView(time: viewModel.recipe.cookTime, title: LocalizedStringKey("Cooking"))
|
||||||
DurationView(time: viewModel.observableRecipeDetail.totalTime, title: LocalizedStringKey("Total time"))
|
DurationView(time: viewModel.recipe.totalTime, title: LocalizedStringKey("Total time"))
|
||||||
}
|
}
|
||||||
if viewModel.editMode {
|
if viewModel.editMode {
|
||||||
Button {
|
Button {
|
||||||
@@ -34,9 +34,9 @@ struct RecipeDurationSection: View {
|
|||||||
.padding()
|
.padding()
|
||||||
.popover(isPresented: $presentPopover) {
|
.popover(isPresented: $presentPopover) {
|
||||||
EditableDurationView(
|
EditableDurationView(
|
||||||
prepTime: viewModel.observableRecipeDetail.prepTime,
|
prepTime: viewModel.recipe.prepTime,
|
||||||
cookTime: viewModel.observableRecipeDetail.cookTime,
|
cookTime: viewModel.recipe.cookTime,
|
||||||
totalTime: viewModel.observableRecipeDetail.totalTime
|
totalTime: viewModel.recipe.totalTime
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,10 +94,10 @@ fileprivate struct EditableDurationView: View {
|
|||||||
TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent)
|
TimePickerView(selectedHour: $totalTime.hourComponent, selectedMinute: $totalTime.minuteComponent)
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.onChange(of: prepTime.hourComponent) { _ in updateTotalTime() }
|
.onChange(of: prepTime.hourComponent) { updateTotalTime() }
|
||||||
.onChange(of: prepTime.minuteComponent) { _ in updateTotalTime() }
|
.onChange(of: prepTime.minuteComponent) { updateTotalTime() }
|
||||||
.onChange(of: cookTime.hourComponent) { _ in updateTotalTime() }
|
.onChange(of: cookTime.hourComponent) { updateTotalTime() }
|
||||||
.onChange(of: cookTime.minuteComponent) { _ in updateTotalTime() }
|
.onChange(of: cookTime.minuteComponent) { updateTotalTime() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,3 +142,5 @@ fileprivate struct TimePickerView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import SwiftUI
|
|||||||
|
|
||||||
|
|
||||||
// MARK: - RecipeView Import Section
|
// MARK: - RecipeView Import Section
|
||||||
|
/*
|
||||||
struct RecipeImportSection: View {
|
struct RecipeImportSection: View {
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@State var viewModel: RecipeView.ViewModel
|
||||||
var importRecipe: (String) async -> UserAlert?
|
var importRecipe: (String) async -> UserAlert?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -49,4 +49,4 @@ struct RecipeImportSection: View {
|
|||||||
.padding(.top, 5)
|
.padding(.top, 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,23 +9,23 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - RecipeView Ingredients Section
|
// MARK: - RecipeView Ingredients Section
|
||||||
|
/*
|
||||||
struct RecipeIngredientSection: View {
|
struct RecipeIngredientSection: View {
|
||||||
@EnvironmentObject var groceryList: GroceryList
|
@Environment(CookbookState.self) var cookbookState
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@State var viewModel: RecipeView.ViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
if groceryList.containsRecipe(viewModel.observableRecipeDetail.id) {
|
if cookbookState.groceryList.containsRecipe(viewModel.recipe.id) {
|
||||||
groceryList.deleteGroceryRecipe(viewModel.observableRecipeDetail.id)
|
cookbookState.groceryList.deleteGroceryRecipe(viewModel.recipe.id)
|
||||||
} else {
|
} else {
|
||||||
groceryList.addItems(
|
cookbookState.groceryList.addItems(
|
||||||
viewModel.observableRecipeDetail.recipeIngredient,
|
viewModel.recipe.recipeIngredient,
|
||||||
toRecipe: viewModel.observableRecipeDetail.id,
|
toRecipe: viewModel.recipe.id,
|
||||||
recipeName: viewModel.observableRecipeDetail.name
|
recipeName: viewModel.recipe.name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,26 +45,26 @@ struct RecipeIngredientSection: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.bold()
|
.bold()
|
||||||
|
|
||||||
ServingPickerView(selectedServingSize: $viewModel.observableRecipeDetail.ingredientMultiplier)
|
ServingPickerView(selectedServingSize: $viewModel.recipe.ingredientMultiplier)
|
||||||
}
|
}
|
||||||
|
|
||||||
ForEach(0..<viewModel.observableRecipeDetail.recipeIngredient.count, id: \.self) { ix in
|
ForEach(0..<viewModel.recipe.recipeIngredient.count, id: \.self) { ix in
|
||||||
IngredientListItem(
|
IngredientListItem(
|
||||||
ingredient: $viewModel.observableRecipeDetail.recipeIngredient[ix],
|
ingredient: $viewModel.recipe.recipeIngredient[ix],
|
||||||
servings: $viewModel.observableRecipeDetail.ingredientMultiplier,
|
servings: $viewModel.recipe.ingredientMultiplier,
|
||||||
recipeYield: Double(viewModel.observableRecipeDetail.recipeYield),
|
recipeYield: Double(viewModel.recipe.recipeYield),
|
||||||
recipeId: viewModel.observableRecipeDetail.id
|
recipeId: viewModel.recipe.id
|
||||||
) {
|
) {
|
||||||
groceryList.addItem(
|
cookbookState.groceryList.addItem(
|
||||||
viewModel.observableRecipeDetail.recipeIngredient[ix],
|
viewModel.recipe.recipeIngredient[ix],
|
||||||
toRecipe: viewModel.observableRecipeDetail.id,
|
toRecipe: viewModel.recipe.id,
|
||||||
recipeName: viewModel.observableRecipeDetail.name
|
recipeName: viewModel.recipe.name
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.padding(4)
|
.padding(4)
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.observableRecipeDetail.ingredientMultiplier != Double(viewModel.observableRecipeDetail.recipeYield) {
|
if viewModel.recipe.ingredientMultiplier != Double(viewModel.recipe.recipeYield) {
|
||||||
HStack() {
|
HStack() {
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
@@ -83,14 +83,14 @@ struct RecipeIngredientSection: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
.animation(.easeInOut, value: viewModel.observableRecipeDetail.ingredientMultiplier)
|
.animation(.easeInOut, value: viewModel.recipe.ingredientMultiplier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - RecipeIngredientSection List Item
|
// MARK: - RecipeIngredientSection List Item
|
||||||
|
|
||||||
fileprivate struct IngredientListItem: View {
|
fileprivate struct IngredientListItem: View {
|
||||||
@EnvironmentObject var groceryList: GroceryList
|
@Environment(CookbookState.self) var cookbookState
|
||||||
@Binding var ingredient: String
|
@Binding var ingredient: String
|
||||||
@Binding var servings: Double
|
@Binding var servings: Double
|
||||||
@State var recipeYield: Double
|
@State var recipeYield: Double
|
||||||
@@ -110,7 +110,7 @@ fileprivate struct IngredientListItem: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
if groceryList.containsItem(at: recipeId, item: ingredient) {
|
if cookbookState.groceryList.containsItem(at: recipeId, item: ingredient) {
|
||||||
if #available(iOS 17.0, *) {
|
if #available(iOS 17.0, *) {
|
||||||
Image(systemName: "storefront")
|
Image(systemName: "storefront")
|
||||||
.foregroundStyle(Color.green)
|
.foregroundStyle(Color.green)
|
||||||
@@ -140,11 +140,11 @@ fileprivate struct IngredientListItem: View {
|
|||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.onChange(of: servings) { newServings in
|
.onChange(of: servings) { _, newServings in
|
||||||
if recipeYield == 0 {
|
if recipeYield == 0 {
|
||||||
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings)
|
modifiedIngredient = Recipe.adjustIngredient(ingredient, by: newServings)
|
||||||
} else {
|
} else {
|
||||||
modifiedIngredient = ObservableRecipeDetail.adjustIngredient(ingredient, by: newServings/recipeYield)
|
modifiedIngredient = Recipe.adjustIngredient(ingredient, by: newServings/recipeYield)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
.foregroundStyle(isSelected ? Color.secondary : Color.primary)
|
||||||
@@ -168,8 +168,8 @@ fileprivate struct IngredientListItem: View {
|
|||||||
.onEnded { gesture in
|
.onEnded { gesture in
|
||||||
withAnimation {
|
withAnimation {
|
||||||
if dragOffset > maxDragDistance * 0.3 { // Swipe threshold
|
if dragOffset > maxDragDistance * 0.3 { // Swipe threshold
|
||||||
if groceryList.containsItem(at: recipeId, item: ingredient) {
|
if cookbookState.groceryList.containsItem(at: recipeId, item: ingredient) {
|
||||||
groceryList.deleteItem(ingredient, fromRecipe: recipeId)
|
cookbookState.groceryList.deleteItem(ingredient, fromRecipe: recipeId)
|
||||||
} else {
|
} else {
|
||||||
addToGroceryListAction()
|
addToGroceryListAction()
|
||||||
}
|
}
|
||||||
@@ -209,9 +209,12 @@ struct ServingPickerView: View {
|
|||||||
.bold()
|
.bold()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: selectedServingSize) { newValue in
|
.onChange(of: selectedServingSize) { _, newValue in
|
||||||
if newValue < 0 { selectedServingSize = 0 }
|
if newValue < 0 { selectedServingSize = 0 }
|
||||||
else if newValue > 100 { selectedServingSize = 100 }
|
else if newValue > 100 { selectedServingSize = 100 }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - RecipeView Instructions Section
|
// MARK: - RecipeView Instructions Section
|
||||||
|
/*
|
||||||
struct RecipeInstructionSection: View {
|
struct RecipeInstructionSection: View {
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@State var viewModel: RecipeView.ViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
@@ -19,8 +19,8 @@ struct RecipeInstructionSection: View {
|
|||||||
SecondaryLabel(text: LocalizedStringKey("Instructions"))
|
SecondaryLabel(text: LocalizedStringKey("Instructions"))
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
ForEach(viewModel.observableRecipeDetail.recipeInstructions.indices, id: \.self) { ix in
|
ForEach(viewModel.recipe.recipeInstructions.indices, id: \.self) { ix in
|
||||||
RecipeInstructionListItem(instruction: $viewModel.observableRecipeDetail.recipeInstructions[ix], index: ix+1)
|
RecipeInstructionListItem(instruction: $viewModel.recipe.recipeInstructions[ix], index: ix+1)
|
||||||
}
|
}
|
||||||
if viewModel.editMode {
|
if viewModel.editMode {
|
||||||
Button {
|
Button {
|
||||||
@@ -56,4 +56,4 @@ fileprivate struct RecipeInstructionListItem: View {
|
|||||||
.animation(.easeInOut, value: isSelected)
|
.animation(.easeInOut, value: isSelected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,16 +9,16 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - RecipeView Keyword Section
|
// MARK: - RecipeView Keyword Section
|
||||||
|
/*
|
||||||
struct RecipeKeywordSection: View {
|
struct RecipeKeywordSection: View {
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@State var viewModel: RecipeView.ViewModel
|
||||||
let columns: [GridItem] = [ GridItem(.flexible(minimum: 50, maximum: 200), spacing: 5) ]
|
let columns: [GridItem] = [ GridItem(.flexible(minimum: 50, maximum: 200), spacing: 5) ]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandKeywordSection) {
|
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandKeywordSection) {
|
||||||
Group {
|
Group {
|
||||||
if !viewModel.observableRecipeDetail.keywords.isEmpty && !viewModel.editMode {
|
if !viewModel.recipe.keywords.isEmpty && !viewModel.editMode {
|
||||||
RecipeListSection(list: $viewModel.observableRecipeDetail.keywords)
|
RecipeListSection(list: $viewModel.recipe.keywords)
|
||||||
} else {
|
} else {
|
||||||
Text(LocalizedStringKey("No keywords."))
|
Text(LocalizedStringKey("No keywords."))
|
||||||
}
|
}
|
||||||
@@ -189,3 +189,4 @@ struct KeywordPickerView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - Recipe Metadata Section
|
// MARK: - Recipe Metadata Section
|
||||||
|
/*
|
||||||
struct RecipeMetadataSection: View {
|
struct RecipeMetadataSection: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@Environment(CookbookState.self) var cookbookState
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@State var viewModel: RecipeView.ViewModel
|
||||||
|
|
||||||
@State var keywords: [RecipeKeyword] = []
|
@State var keywords: [RecipeKeyword] = []
|
||||||
var categories: [String] {
|
var categories: [String] {
|
||||||
appState.categories.map({ category in category.name })
|
cookbookState.selectedAccountState.categories.map({ category in category.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
@State var presentKeywordSheet: Bool = false
|
@State var presentKeywordSheet: Bool = false
|
||||||
@@ -28,11 +28,11 @@ struct RecipeMetadataSection: View {
|
|||||||
// Category
|
// Category
|
||||||
SecondaryLabel(text: "Category")
|
SecondaryLabel(text: "Category")
|
||||||
HStack {
|
HStack {
|
||||||
TextField("Category", text: $viewModel.observableRecipeDetail.recipeCategory)
|
TextField("Category", text: $viewModel.recipe.recipeCategory)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
|
|
||||||
Picker("Choose", selection: $viewModel.observableRecipeDetail.recipeCategory) {
|
Picker("Choose", selection: $viewModel.recipe.recipeCategory) {
|
||||||
Text("").tag("")
|
Text("").tag("")
|
||||||
ForEach(categories, id: \.self) { item in
|
ForEach(categories, id: \.self) { item in
|
||||||
Text(item)
|
Text(item)
|
||||||
@@ -45,10 +45,10 @@ struct RecipeMetadataSection: View {
|
|||||||
// Keywords
|
// Keywords
|
||||||
SecondaryLabel(text: "Keywords")
|
SecondaryLabel(text: "Keywords")
|
||||||
|
|
||||||
if !viewModel.observableRecipeDetail.keywords.isEmpty {
|
if !viewModel.recipe.keywords.isEmpty {
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(viewModel.observableRecipeDetail.keywords, id: \.self) { keyword in
|
ForEach(viewModel.recipe.keywords, id: \.self) { keyword in
|
||||||
Text(keyword)
|
Text(keyword)
|
||||||
.padding(5)
|
.padding(5)
|
||||||
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
|
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
|
||||||
@@ -70,11 +70,11 @@ struct RecipeMetadataSection: View {
|
|||||||
Button {
|
Button {
|
||||||
presentServingsPopover.toggle()
|
presentServingsPopover.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
Text("\(viewModel.observableRecipeDetail.recipeYield) Serving(s)")
|
Text("\(viewModel.recipe.recipeYield) Serving(s)")
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
.popover(isPresented: $presentServingsPopover) {
|
.popover(isPresented: $presentServingsPopover) {
|
||||||
PickerPopoverView(isPresented: $presentServingsPopover, value: $viewModel.observableRecipeDetail.recipeYield, items: 1..<99, title: "Servings", titleKey: "Servings")
|
PickerPopoverView(isPresented: $presentServingsPopover, value: $viewModel.recipe.recipeYield, items: 1..<99, title: "Servings", titleKey: "Servings")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ struct RecipeMetadataSection: View {
|
|||||||
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
|
.background(RoundedRectangle(cornerRadius: 20).foregroundStyle(Color.primary.opacity(0.1)))
|
||||||
.padding([.horizontal, .bottom], 5)
|
.padding([.horizontal, .bottom], 5)
|
||||||
.sheet(isPresented: $presentKeywordSheet) {
|
.sheet(isPresented: $presentKeywordSheet) {
|
||||||
KeywordPickerView(title: "Keywords", searchSuggestions: appState.allKeywords, selection: $viewModel.observableRecipeDetail.keywords)
|
KeywordPickerView(title: "Keywords", searchSuggestions: cookbookState.selectedAccountState.keywords, selection: $viewModel.recipe.keywords)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,22 +126,22 @@ fileprivate struct PickerPopoverView<Item: Hashable & CustomStringConvertible, C
|
|||||||
// MARK: - RecipeView More Information Section
|
// MARK: - RecipeView More Information Section
|
||||||
|
|
||||||
struct MoreInformationSection: View {
|
struct MoreInformationSection: View {
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@State var viewModel: RecipeView.ViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandInfoSection) {
|
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandInfoSection) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if let dateCreated = viewModel.recipeDetail.dateCreated {
|
if let dateCreated = viewModel.recipe.dateCreated {
|
||||||
Text("Created: \(Date.convertISOStringToLocalString(isoDateString: dateCreated) ?? "")")
|
Text("Created: \(Date.convertISOStringToLocalString(isoDateString: dateCreated) ?? "")")
|
||||||
}
|
}
|
||||||
if let dateModified = viewModel.recipeDetail.dateModified {
|
if let dateModified = viewModel.recipe.dateModified {
|
||||||
Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: dateModified) ?? "")")
|
Text("Last modified: \(Date.convertISOStringToLocalString(isoDateString: dateModified) ?? "")")
|
||||||
}
|
}
|
||||||
if viewModel.observableRecipeDetail.url != "", let url = URL(string: viewModel.observableRecipeDetail.url) {
|
if viewModel.recipe.url != "", let url = URL(string: viewModel.recipe.url ?? "") {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Text("URL:")
|
Text("URL:")
|
||||||
Link(destination: url) {
|
Link(destination: url) {
|
||||||
Text(viewModel.observableRecipeDetail.url)
|
Text(viewModel.recipe.url ?? "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,3 +157,5 @@ struct MoreInformationSection: View {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - RecipeView Nutrition Section
|
// MARK: - RecipeView Nutrition Section
|
||||||
|
/*
|
||||||
struct RecipeNutritionSection: View {
|
struct RecipeNutritionSection: View {
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@State var viewModel: RecipeView.ViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandNutritionSection) {
|
CollapsibleView(titleColor: .secondary, isCollapsed: !UserSettings.shared.expandNutritionSection) {
|
||||||
@@ -28,7 +28,7 @@ struct RecipeNutritionSection: View {
|
|||||||
} else if !nutritionEmpty() {
|
} else if !nutritionEmpty() {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
ForEach(Nutrition.allCases, id: \.self) { nutrition in
|
ForEach(Nutrition.allCases, id: \.self) { nutrition in
|
||||||
if let value = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey], nutrition.dictKey != Nutrition.servingSize.dictKey {
|
if let value = viewModel.recipe.nutrition[nutrition.dictKey], nutrition.dictKey != Nutrition.servingSize.dictKey {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
Text("\(nutrition.localizedDescription): \(value)")
|
Text("\(nutrition.localizedDescription): \(value)")
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
@@ -43,7 +43,7 @@ struct RecipeNutritionSection: View {
|
|||||||
}
|
}
|
||||||
} title: {
|
} title: {
|
||||||
HStack {
|
HStack {
|
||||||
if let servingSize = viewModel.observableRecipeDetail.nutrition["servingSize"] {
|
if let servingSize = viewModel.recipe.nutrition["servingSize"] {
|
||||||
SecondaryLabel(text: "Nutrition (\(servingSize))")
|
SecondaryLabel(text: "Nutrition (\(servingSize))")
|
||||||
} else {
|
} else {
|
||||||
SecondaryLabel(text: LocalizedStringKey("Nutrition"))
|
SecondaryLabel(text: LocalizedStringKey("Nutrition"))
|
||||||
@@ -56,17 +56,19 @@ struct RecipeNutritionSection: View {
|
|||||||
|
|
||||||
func binding(for key: String) -> Binding<String> {
|
func binding(for key: String) -> Binding<String> {
|
||||||
Binding(
|
Binding(
|
||||||
get: { viewModel.observableRecipeDetail.nutrition[key, default: ""] },
|
get: { viewModel.recipe.nutrition[key, default: ""] },
|
||||||
set: { viewModel.observableRecipeDetail.nutrition[key] = $0 }
|
set: { viewModel.recipe.nutrition[key] = $0 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func nutritionEmpty() -> Bool {
|
func nutritionEmpty() -> Bool {
|
||||||
for nutrition in Nutrition.allCases {
|
for nutrition in Nutrition.allCases {
|
||||||
if let value = viewModel.observableRecipeDetail.nutrition[nutrition.dictKey] {
|
if let value = viewModel.recipe.nutrition[nutrition.dictKey] {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - RecipeView Tool Section
|
// MARK: - RecipeView Tool Section
|
||||||
|
/*
|
||||||
struct RecipeToolSection: View {
|
struct RecipeToolSection: View {
|
||||||
@ObservedObject var viewModel: RecipeView.ViewModel
|
@State var viewModel: RecipeView.ViewModel
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
@@ -20,7 +20,7 @@ struct RecipeToolSection: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|
||||||
RecipeListSection(list: $viewModel.observableRecipeDetail.tool)
|
RecipeListSection(list: $viewModel.recipe.tool)
|
||||||
|
|
||||||
if viewModel.editMode {
|
if viewModel.editMode {
|
||||||
Button {
|
Button {
|
||||||
@@ -35,3 +35,5 @@ struct RecipeToolSection: View {
|
|||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SwiftUI
|
|||||||
|
|
||||||
|
|
||||||
struct ShareView: View {
|
struct ShareView: View {
|
||||||
@State var recipeDetail: RecipeDetail
|
@State var recipeDetail: CookbookApiRecipeDetailV1
|
||||||
@State var recipeImage: UIImage?
|
@State var recipeImage: UIImage?
|
||||||
@Binding var presentShareSheet: Bool
|
@Binding var presentShareSheet: Bool
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
/*
|
||||||
struct CollapsibleView<C: View, T: View>: View {
|
struct CollapsibleView<C: View, T: View>: View {
|
||||||
@State var titleColor: Color = .white
|
@State var titleColor: Color = .white
|
||||||
@State var isCollapsed: Bool = true
|
@State var isCollapsed: Bool = true
|
||||||
@@ -48,3 +48,4 @@ struct CollapsibleView<C: View, T: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
var body: some View {
|
||||||
|
Text("Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*struct SettingsView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@ObservedObject var userSettings = UserSettings.shared
|
@ObservedObject var userSettings = UserSettings.shared
|
||||||
@ObservedObject var viewModel = ViewModel()
|
@ObservedObject var viewModel = ViewModel()
|
||||||
@@ -248,3 +252,4 @@ extension SettingsView {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -8,36 +8,34 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
/*
|
||||||
struct GroceryListTabView: View {
|
struct GroceryListTabView: View {
|
||||||
@EnvironmentObject var groceryList: GroceryList
|
@Environment(CookbookState.self) var cookbookState
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
if groceryList.groceryDict.isEmpty {
|
if cookbookState.groceryList.groceryDict.isEmpty {
|
||||||
EmptyGroceryListView()
|
EmptyGroceryListView()
|
||||||
} else {
|
} else {
|
||||||
List {
|
List {
|
||||||
ForEach(groceryList.groceryDict.keys.sorted(), id: \.self) { key in
|
ForEach(cookbookState.groceryList.groceryDict.keys.sorted(), id: \.self) { key in
|
||||||
Section {
|
Section {
|
||||||
ForEach(groceryList.groceryDict[key]!.items) { item in
|
ForEach(cookbookState.groceryList.groceryDict[key]!.items) { item in
|
||||||
GroceryListItemView(item: item, toggleAction: {
|
GroceryListItemView(item: item, toggleAction: {
|
||||||
groceryList.toggleItemChecked(item)
|
cookbookState.groceryList.toggleItemChecked(item)
|
||||||
groceryList.objectWillChange.send()
|
|
||||||
}, deleteAction: {
|
}, deleteAction: {
|
||||||
groceryList.deleteItem(item.name, fromRecipe: key)
|
|
||||||
withAnimation {
|
withAnimation {
|
||||||
groceryList.objectWillChange.send()
|
cookbookState.groceryList.deleteItem(item.name, fromRecipe: key)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
HStack {
|
HStack {
|
||||||
Text(groceryList.groceryDict[key]!.name)
|
Text(cookbookState.groceryList.groceryDict[key]!.name)
|
||||||
.foregroundStyle(Color.nextcloudBlue)
|
.foregroundStyle(Color.nextcloudBlue)
|
||||||
Spacer()
|
Spacer()
|
||||||
Button {
|
Button {
|
||||||
groceryList.deleteGroceryRecipe(key)
|
cookbookState.groceryList.deleteGroceryRecipe(key)
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "trash")
|
Image(systemName: "trash")
|
||||||
.foregroundStyle(Color.nextcloudBlue)
|
.foregroundStyle(Color.nextcloudBlue)
|
||||||
@@ -51,7 +49,7 @@ struct GroceryListTabView: View {
|
|||||||
.navigationTitle("Grocery List")
|
.navigationTitle("Grocery List")
|
||||||
.toolbar {
|
.toolbar {
|
||||||
Button {
|
Button {
|
||||||
groceryList.deleteAll()
|
cookbookState.groceryList.deleteAll()
|
||||||
} label: {
|
} label: {
|
||||||
Text("Delete")
|
Text("Delete")
|
||||||
.foregroundStyle(Color.nextcloudBlue)
|
.foregroundStyle(Color.nextcloudBlue)
|
||||||
@@ -143,25 +141,22 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
@MainActor class GroceryList: ObservableObject {
|
@Observable class GroceryList {
|
||||||
let dataStore: DataStore = DataStore()
|
let dataStore: DataStore = DataStore()
|
||||||
@Published var groceryDict: [String: GroceryRecipe] = [:]
|
var groceryDict: [String: GroceryRecipe] = [:]
|
||||||
@Published var sortBySimilarity: Bool = false
|
var sortBySimilarity: Bool = false
|
||||||
|
|
||||||
|
|
||||||
func addItem(_ itemName: String, toRecipe recipeId: String, recipeName: String? = nil, saveGroceryDict: Bool = true) {
|
func addItem(_ itemName: String, toRecipe recipeId: String, recipeName: String? = nil, saveGroceryDict: Bool = true) {
|
||||||
print("Adding item of recipe \(String(describing: recipeName))")
|
print("Adding item of recipe \(String(describing: recipeName))")
|
||||||
DispatchQueue.main.async {
|
if self.groceryDict[recipeId] != nil {
|
||||||
if self.groceryDict[recipeId] != nil {
|
self.groceryDict[recipeId]?.items.append(GroceryRecipeItem(itemName))
|
||||||
self.groceryDict[recipeId]?.items.append(GroceryRecipeItem(itemName))
|
} else {
|
||||||
} else {
|
let newRecipe = GroceryRecipe(name: recipeName ?? "-", items: [GroceryRecipeItem(itemName)])
|
||||||
let newRecipe = GroceryRecipe(name: recipeName ?? "-", items: [GroceryRecipeItem(itemName)])
|
self.groceryDict[recipeId] = newRecipe
|
||||||
self.groceryDict[recipeId] = newRecipe
|
}
|
||||||
}
|
if saveGroceryDict {
|
||||||
if saveGroceryDict {
|
self.save()
|
||||||
self.save()
|
|
||||||
self.objectWillChange.send()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +165,6 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
addItem(item, toRecipe: recipeId, recipeName: recipeName, saveGroceryDict: false)
|
addItem(item, toRecipe: recipeId, recipeName: recipeName, saveGroceryDict: false)
|
||||||
}
|
}
|
||||||
save()
|
save()
|
||||||
objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
|
func deleteItem(_ itemName: String, fromRecipe recipeId: String) {
|
||||||
@@ -182,14 +176,12 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
groceryDict.removeValue(forKey: recipeId)
|
groceryDict.removeValue(forKey: recipeId)
|
||||||
}
|
}
|
||||||
save()
|
save()
|
||||||
objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteGroceryRecipe(_ recipeId: String) {
|
func deleteGroceryRecipe(_ recipeId: String) {
|
||||||
print("Deleting grocery recipe with id \(recipeId)")
|
print("Deleting grocery recipe with id \(recipeId)")
|
||||||
groceryDict.removeValue(forKey: recipeId)
|
groceryDict.removeValue(forKey: recipeId)
|
||||||
save()
|
save()
|
||||||
objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteAll() {
|
func deleteAll() {
|
||||||
@@ -234,4 +226,4 @@ class GroceryRecipeItem: Identifiable, Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
/*
|
||||||
struct RecipeTabView: View {
|
struct RecipeTabView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@EnvironmentObject var groceryList: GroceryList
|
@EnvironmentObject var groceryList: GroceryList
|
||||||
@@ -179,3 +179,5 @@ fileprivate struct RecipeTabViewToolBar: ToolbarContent {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
/*
|
||||||
struct SearchTabView: View {
|
struct SearchTabView: View {
|
||||||
@EnvironmentObject var viewModel: SearchTabView.ViewModel
|
@EnvironmentObject var viewModel: SearchTabView.ViewModel
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@@ -30,7 +30,7 @@ struct SearchTabView: View {
|
|||||||
.listRowSeparatorTint(.clear)
|
.listRowSeparatorTint(.clear)
|
||||||
}
|
}
|
||||||
.listStyle(.plain)
|
.listStyle(.plain)
|
||||||
.navigationDestination(for: Recipe.self) { recipe in
|
.navigationDestination(for: CookbookApiRecipeV1.self) { recipe in
|
||||||
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
|
RecipeView(viewModel: RecipeView.ViewModel(recipe: recipe))
|
||||||
}
|
}
|
||||||
.searchable(text: $viewModel.searchText, prompt: "Search recipes/keywords")
|
.searchable(text: $viewModel.searchText, prompt: "Search recipes/keywords")
|
||||||
@@ -48,7 +48,7 @@ struct SearchTabView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ViewModel: ObservableObject {
|
class ViewModel: ObservableObject {
|
||||||
@Published var allRecipes: [Recipe] = []
|
@Published var allRecipes: [CookbookApiRecipeV1] = []
|
||||||
@Published var searchText: String = ""
|
@Published var searchText: String = ""
|
||||||
@Published var searchMode: SearchMode = .name
|
@Published var searchMode: SearchMode = .name
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ struct SearchTabView: View {
|
|||||||
case name = "Name & Keywords", ingredient = "Ingredients"
|
case name = "Name & Keywords", ingredient = "Ingredients"
|
||||||
}
|
}
|
||||||
|
|
||||||
func recipesFiltered() -> [Recipe] {
|
func recipesFiltered() -> [CookbookApiRecipeV1] {
|
||||||
if searchMode == .name {
|
if searchMode == .name {
|
||||||
guard searchText != "" else { return allRecipes }
|
guard searchText != "" else { return allRecipes }
|
||||||
return allRecipes.filter { recipe in
|
return allRecipes.filter { recipe in
|
||||||
@@ -72,3 +72,4 @@ struct SearchTabView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user