Better file caching and update management

This commit is contained in:
Vicnet
2023-12-14 14:11:56 +01:00
parent 899dc20e55
commit a3fc891d0a
23 changed files with 592 additions and 483 deletions

View File

@@ -11,15 +11,13 @@ import UIKit
@MainActor class MainViewModel: ObservableObject {
@AppStorage("authString") var authString = ""
@AppStorage("username") var userName = ""
@AppStorage("token") var appToken = ""
@AppStorage("serverAddress") var serverAdress = ""
@ObservedObject var userSettings = UserSettings.shared
@Published var categories: [Category] = []
@Published var recipes: [String: [Recipe]] = [:]
@Published var recipeDetails: [Int: RecipeDetail] = [:]
var recipeImages: [Int: [String: UIImage]] = [:]
var imagesNeedUpdate: [Int: [String: Bool]] = [:]
private var requestQueue: [RequestWrapper] = []
private let api: CookbookApi.Type
@@ -30,10 +28,10 @@ import UIKit
self.api = api
self.dataStore = DataStore()
if authString == "" {
let loginString = "\(userName):\(appToken)"
if userSettings.authString == "" {
let loginString = "\(userSettings.username):\(userSettings.token)"
let loginData = loginString.data(using: String.Encoding.utf8)!
authString = loginData.base64EncodedString()
userSettings.authString = loginData.base64EncodedString()
}
}
@@ -51,21 +49,26 @@ import UIKit
*/
func getCategories() async {
let (categories, _) = await api.getCategories(
from: serverAdress,
auth: authString
from: userSettings.serverAddress,
auth: userSettings.authString
)
if let categories = categories {
print("Successfully loaded categories")
self.categories = categories
print(categories)
await saveLocal(categories, path: "categories.data")
} else {
// If there's no server connection, try loading categories from local storage
print("Loading categories from store ...")
if let categories: [Category] = await loadLocal(path: "categories.data") {
self.categories = categories
print("Success!")
} else {
print("Failure!")
}
}
}
/**
Fetches recipes for a specified category from either the server or local storage.
@@ -88,14 +91,18 @@ import UIKit
return false
}
func getServer() async -> Bool {
func getServer(store: Bool = false) async -> Bool {
let (recipes, _) = await api.getCategory(
from: serverAdress,
auth: authString,
named: name
from: userSettings.serverAddress,
auth: userSettings.authString,
named: categoryString
)
if let recipes = recipes {
self.recipes[name] = recipes
if store {
await saveLocal(recipes, path: "category_\(categoryString).data")
}
//userSettings.lastUpdate = Date()
return true
}
return false
@@ -105,9 +112,9 @@ import UIKit
switch fetchMode {
case .preferLocal:
if await getLocal() { return }
if await getServer() { return }
if await getServer(store: true) { return }
case .preferServer:
if await getServer() { return }
if await getServer(store: true) { return }
if await getLocal() { return }
case .onlyLocal:
if await getLocal() { return }
@@ -116,6 +123,26 @@ import UIKit
}
}
func updateAllRecipeDetails() async {
for category in self.categories {
await updateRecipeDetails(in: category.name)
}
userSettings.lastUpdate = Date()
}
func updateRecipeDetails(in category: String) async {
guard userSettings.storeRecipes else { return }
guard let recipes = self.recipes[category] else { return }
for recipe in recipes {
if needsUpdate(lastModified: recipe.dateModified) {
print("\(recipe.name) needs an update. (last modified: \(recipe.dateModified)")
await updateRecipeDetail(id: recipe.recipe_id, withThumb: userSettings.storeThumb, withImage: userSettings.storeImages)
} else {
print("\(recipe.name) is up to date.")
}
}
}
/**
Asynchronously retrieves all recipes either from the server or the locally cached data.
@@ -129,8 +156,8 @@ import UIKit
*/
func getRecipes() async -> [Recipe] {
let (recipes, error) = await api.getRecipes(
from: serverAdress,
auth: authString
from: userSettings.serverAddress,
auth: userSettings.authString
)
if let recipes = recipes {
return recipes
@@ -162,18 +189,16 @@ import UIKit
```swift
let recipeDetail = await mainViewModel.getRecipe(id: 123)
*/
func getRecipe(id: Int, fetchMode: FetchMode) async -> RecipeDetail {
func getRecipe(id: Int, fetchMode: FetchMode) async -> RecipeDetail? {
func getLocal() async -> RecipeDetail? {
if let recipe: RecipeDetail = await loadLocal(path: "recipe\(id).data") {
return recipe
}
if let recipe: RecipeDetail = await loadLocal(path: "recipe\(id).data") { return recipe }
return nil
}
func getServer() async -> RecipeDetail? {
let (recipe, error) = await api.getRecipe(
from: serverAdress,
auth: authString,
from: userSettings.serverAddress,
auth: userSettings.authString,
id: id
)
if let recipe = recipe {
@@ -196,7 +221,7 @@ import UIKit
case .onlyServer:
if let recipe = await getServer() { return recipe }
}
return .error
return nil
}
@@ -213,26 +238,36 @@ import UIKit
```swift
await mainViewModel.downloadAllRecipes()
*/
/*
func downloadAllRecipes() async {
for category in categories {
await getCategory(named: category.name, fetchMode: .onlyServer)
guard let recipeList = recipes[category.name] else { continue }
for recipe in recipeList {
let recipeDetail = await getRecipe(id: recipe.recipe_id, fetchMode: .onlyServer)
await saveLocal(recipeDetail, path: "recipe\(recipe.recipe_id).data")
let thumbnail = await getImage(id: recipe.recipe_id, size: .THUMB, fetchMode: .onlyServer)
guard let thumbnail = thumbnail else { continue }
guard let thumbnailData = thumbnail.pngData() else { continue }
await saveLocal(thumbnailData.base64EncodedString(), path: "image\(recipe.recipe_id)_thumb")
let image = await getImage(id: recipe.recipe_id, size: .FULL, fetchMode: .onlyServer)
guard let image = image else { continue }
guard let imageData = image.pngData() else { continue }
await saveLocal(imageData.base64EncodedString(), path: "image\(recipe.recipe_id)_full")
await downloadRecipeDetail(id: recipe.recipe_id, withThumb: userSettings.storeThumb, withImage: userSettings.storeImages)
}
}
}
*/
func updateRecipeDetail(id: Int, withThumb: Bool, withImage: Bool) async {
if let recipeDetail = await getRecipe(id: id, fetchMode: .onlyServer) {
await saveLocal(recipeDetail, path: "recipe\(id).data")
}
if withThumb {
let thumbnail = await getImage(id: id, size: .THUMB, fetchMode: .onlyServer)
guard let thumbnail = thumbnail else { return }
guard let thumbnailData = thumbnail.pngData() else { return }
await saveLocal(thumbnailData.base64EncodedString(), path: "image\(id)_thumb")
}
if withImage {
let image = await getImage(id: id, size: .FULL, fetchMode: .onlyServer)
guard let image = image else { return }
guard let imageData = image.pngData() else { return }
await saveLocal(imageData.base64EncodedString(), path: "image\(id)_full")
}
}
/// Check if recipeDetail is stored locally, either in cache or on disk
@@ -240,9 +275,7 @@ import UIKit
/// - recipeId: The id of a recipe.
/// - Returns: True if the recipeDetail is stored, otherwise false
func recipeDetailExists(recipeId: Int) -> Bool {
if recipeDetails[recipeId] != nil {
return true
} else if (dataStore.recipeDetailExists(recipeId: recipeId)) {
if (dataStore.recipeDetailExists(recipeId: recipeId)) {
return true
}
return false
@@ -271,8 +304,8 @@ import UIKit
func getServer() async -> UIImage? {
let (image, _) = await api.getImage(
from: serverAdress,
auth: authString,
from: userSettings.serverAddress,
auth: userSettings.authString,
id: id,
size: size
)
@@ -282,16 +315,51 @@ import UIKit
switch fetchMode {
case .preferLocal:
if let image = await getLocal() { return image }
if let image = await getServer() { return image }
if let image = imageFromCache(id: id, size: size) {
return image
}
if !imageUpdateNeeded(id: id, size: size) { return nil }
if let image = await getLocal() {
imageToCache(id: id, size: size, image: image)
return image
}
if let image = await getServer() {
await imageToStore(id: id, size: size, image: image)
imageToCache(id: id, size: size, image: image)
return image
}
case .preferServer:
if let image = await getServer() { return image }
if let image = await getLocal() { return image }
if let image = await getServer() {
await imageToStore(id: id, size: size, image: image)
imageToCache(id: id, size: size, image: image)
return image
}
if let image = imageFromCache(id: id, size: size) {
return image
}
if let image = await getLocal() {
imageToCache(id: id, size: size, image: image)
return image
}
case .onlyLocal:
if let image = await getLocal() { return image }
if let image = imageFromCache(id: id, size: size) {
return image
}
if !imageUpdateNeeded(id: id, size: size) { return nil }
if let image = await getLocal() {
imageToCache(id: id, size: size, image: image)
return image
}
case .onlyServer:
if let image = await getServer() { return image }
if let image = imageFromCache(id: id, size: size) {
return image
}
if let image = await getServer() {
imageToCache(id: id, size: size, image: image)
return image
}
}
imagesNeedUpdate[id] = [size.rawValue: false]
return nil
}
@@ -306,15 +374,15 @@ import UIKit
```swift
let keywords = await mainViewModel.getKeywords()
*/
func getKeywords(fetchMode: FetchMode) async -> [String] {
func getLocal() async -> [String]? {
func getKeywords(fetchMode: FetchMode) async -> [RecipeKeyword] {
func getLocal() async -> [RecipeKeyword]? {
return await loadLocal(path: "keywords.data")
}
func getServer() async -> [String]? {
func getServer() async -> [RecipeKeyword]? {
let (tags, _) = await api.getTags(
from: serverAdress,
auth: authString
from: userSettings.serverAddress,
auth: userSettings.authString
)
return tags
}
@@ -322,9 +390,15 @@ import UIKit
switch fetchMode {
case .preferLocal:
if let keywords = await getLocal() { return keywords }
if let keywords = await getServer() { return keywords }
if let keywords = await getServer() {
await saveLocal(keywords, path: "keywords.data")
return keywords
}
case .preferServer:
if let keywords = await getServer() { return keywords }
if let keywords = await getServer() {
await saveLocal(keywords, path: "keywords.data")
return keywords
}
if let keywords = await getLocal() { return keywords }
case .onlyLocal:
if let keywords = await getLocal() { return keywords }
@@ -340,6 +414,8 @@ import UIKit
self.recipes = [:]
self.recipeDetails = [:]
self.requestQueue = []
self.recipeImages = [:]
self.imagesNeedUpdate = [:]
}
}
@@ -358,10 +434,10 @@ import UIKit
```swift
let requestResult = await mainViewModel.deleteRecipe(withId: 123, categoryName: "Desserts")
*/
func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert {
func deleteRecipe(withId id: Int, categoryName: String) async -> RequestAlert? {
let (error) = await api.deleteRecipe(
from: serverAdress,
auth: authString,
from: userSettings.serverAddress,
auth: userSettings.authString,
id: id
)
@@ -376,7 +452,7 @@ import UIKit
})
recipeDetails.removeValue(forKey: id)
}
return .REQUEST_SUCCESS
return nil
}
/**
@@ -392,8 +468,8 @@ import UIKit
*/
func checkServerConnection() async -> Bool {
let (categories, _) = await api.getCategories(
from: serverAdress,
auth: authString
from: userSettings.serverAddress,
auth: userSettings.authString
)
if let categories = categories {
self.categories = categories
@@ -418,25 +494,25 @@ import UIKit
```swift
let uploadResult = await mainViewModel.uploadRecipe(recipeDetail: myRecipeDetail, createNew: true)
*/
func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert {
func uploadRecipe(recipeDetail: RecipeDetail, createNew: Bool) async -> RequestAlert? {
var error: NetworkError? = nil
if createNew {
error = await api.createRecipe(
from: serverAdress,
auth: authString,
from: userSettings.serverAddress,
auth: userSettings.authString,
recipe: recipeDetail
)
} else {
error = await api.updateRecipe(
from: serverAdress,
auth: authString,
from: userSettings.serverAddress,
auth: userSettings.authString,
recipe: recipeDetail
)
}
if let error = error {
return .REQUEST_DROPPED
}
return .REQUEST_SUCCESS
return nil
}
}
@@ -472,6 +548,72 @@ extension MainViewModel {
}
return nil
}
private func imageToStore(id: Int, size: RecipeImage.RecipeImageSize, image: UIImage) async {
if let data = image.pngData() {
await saveLocal(data.base64EncodedString(), path: "image\(id)_\(size.rawValue)")
}
}
private func imageToCache(id: Int, size: RecipeImage.RecipeImageSize, image: UIImage) {
if recipeImages[id] != nil {
recipeImages[id]![size.rawValue] = image
} else {
recipeImages[id] = [size.rawValue : image]
}
if imagesNeedUpdate[id] != nil {
imagesNeedUpdate[id]![size.rawValue] = false
} else {
imagesNeedUpdate[id] = [size.rawValue: false]
}
}
private func imageFromCache(id: Int, size: RecipeImage.RecipeImageSize) -> UIImage? {
if recipeImages[id] != nil {
return recipeImages[id]![size.rawValue]
}
return nil
}
private func imageUpdateNeeded(id: Int, size: RecipeImage.RecipeImageSize) -> Bool {
if imagesNeedUpdate[id] != nil {
if imagesNeedUpdate[id]![size.rawValue] != nil {
return imagesNeedUpdate[id]![size.rawValue]!
}
}
return true
}
private func needsUpdate(lastModified: String) -> Bool {
print("=======================")
print("original date string: \(lastModified)")
// Create a DateFormatter
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
//dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set the locale to posix
// Convert the string to a Date object
if let date = dateFormatter.date(from: lastModified) {
if date < userSettings.lastUpdate {
print("No update needed. (recipe: \(dateFormatter.string(from: date)), last: \(dateFormatter.string(from: userSettings.lastUpdate))")
return false
} else {
print("Update needed. (recipe: \(dateFormatter.string(from: date)), last: \(dateFormatter.string(from: userSettings.lastUpdate))")
return true
}
}
print("String is not a date. Update needed.")
return true
}
}
extension DateFormatter {
static func utcToString(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return dateFormatter.string(from: date)
}
}