Implement saving recipes by URLs

This commit is contained in:
Kirill Kamakin
2022-11-28 21:22:24 +01:00
parent f2a4d00cf9
commit 4826478a2a
20 changed files with 305 additions and 10 deletions

View File

@@ -19,14 +19,27 @@
tools:ignore="UnusedAttribute">
<activity
android:name=".ui.activity.MainActivity"
android:windowSoftInputMode="adjustPan"
android:exported="true">
android:exported="true"
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.share.ShareRecipeActivity"
android:exported="true"
android:theme="@style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
</activity>
</application>
</manifest>

View File

@@ -8,11 +8,18 @@ import gq.kirmanak.mealient.data.baseurl.ServerVersion
import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.data.share.ParseRecipeDataSource
import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.extensions.*
import gq.kirmanak.mealient.extensions.toFullRecipeInfo
import gq.kirmanak.mealient.extensions.toRecipeSummaryInfo
import gq.kirmanak.mealient.extensions.toV0Request
import gq.kirmanak.mealient.extensions.toV1CreateRequest
import gq.kirmanak.mealient.extensions.toV1Request
import gq.kirmanak.mealient.extensions.toV1UpdateRequest
import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
import javax.inject.Singleton
@@ -24,7 +31,7 @@ class MealieDataSourceWrapper @Inject constructor(
private val v0Source: MealieDataSourceV0,
private val v1Source: MealieDataSourceV1,
private val logger: Logger,
) : AddRecipeDataSource, RecipeDataSource {
) : AddRecipeDataSource, RecipeDataSource, ParseRecipeDataSource {
override suspend fun addRecipe(
recipe: AddRecipeInfo,
@@ -64,6 +71,19 @@ class MealieDataSourceWrapper @Inject constructor(
}
}
override suspend fun parseRecipeFromURL(
parseRecipeURLInfo: ParseRecipeURLInfo,
): String = makeCall { token, url, version ->
when (version) {
ServerVersion.V0 -> {
v0Source.parseRecipeFromURL(url, token, parseRecipeURLInfo.toV0Request())
}
ServerVersion.V1 -> {
v1Source.parseRecipeFromURL(url, token, parseRecipeURLInfo.toV1Request())
}
}
}
private suspend inline fun <T> makeCall(block: (String?, String, ServerVersion) -> T): T {
val authHeader = authRepo.getAuthHeader()
val url = serverInfoRepo.requireUrl()

View File

@@ -0,0 +1,6 @@
package gq.kirmanak.mealient.data.share
interface ParseRecipeDataSource {
suspend fun parseRecipeFromURL(parseRecipeURLInfo: ParseRecipeURLInfo): String
}

View File

@@ -0,0 +1,6 @@
package gq.kirmanak.mealient.data.share
data class ParseRecipeURLInfo(
val url: String,
val includeTags: Boolean
)

View File

@@ -0,0 +1,6 @@
package gq.kirmanak.mealient.data.share
interface ShareRecipeRepo {
suspend fun saveRecipeByURL(url: CharSequence): String
}

View File

@@ -0,0 +1,18 @@
package gq.kirmanak.mealient.data.share
import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ShareRecipeRepoImpl @Inject constructor(
private val logger: Logger,
private val parseRecipeDataSource: ParseRecipeDataSource,
) : ShareRecipeRepo {
override suspend fun saveRecipeByURL(url: CharSequence): String {
logger.v { "saveRecipeByURL() called with: url = $url" }
val request = ParseRecipeURLInfo(url = url.toString(), includeTags = true)
return parseRecipeDataSource.parseRecipeFromURL(request)
}
}

View File

@@ -0,0 +1,24 @@
package gq.kirmanak.mealient.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
import gq.kirmanak.mealient.data.share.ParseRecipeDataSource
import gq.kirmanak.mealient.data.share.ShareRecipeRepo
import gq.kirmanak.mealient.data.share.ShareRecipeRepoImpl
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
interface ShareRecipeModule {
@Binds
@Singleton
fun bindShareRecipeRepo(shareRecipeRepoImpl: ShareRecipeRepoImpl): ShareRecipeRepo
@Binds
@Singleton
fun bindParseRecipeDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): ParseRecipeDataSource
}

View File

@@ -9,12 +9,32 @@ import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.data.share.ParseRecipeURLInfo
import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.datasource.v0.models.*
import gq.kirmanak.mealient.datasource.v1.models.*
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeIngredientV0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeInstructionV0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeSettingsV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeIngredientResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeInstructionResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0
import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeIngredientV1
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeInstructionV1
import gq.kirmanak.mealient.datasource.v1.models.AddRecipeSettingsV1
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeIngredientResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeInstructionResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import java.util.*
@@ -170,4 +190,13 @@ private fun AddRecipeInstructionInfo.toV1Instruction() = AddRecipeInstructionV1(
id = UUID.randomUUID().toString(),
text = text,
ingredientReferences = emptyList(),
)
)
fun ParseRecipeURLInfo.toV1Request() = ParseRecipeURLRequestV1(
url = url,
includeTags = includeTags,
)
fun ParseRecipeURLInfo.toV0Request() = ParseRecipeURLRequestV0(
url = url,
)

View File

@@ -0,0 +1,48 @@
package gq.kirmanak.mealient.ui.share
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.extensions.showLongToast
import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
@AndroidEntryPoint
class ShareRecipeActivity : AppCompatActivity() {
private val viewModel: ShareRecipeViewModel by viewModels()
@Inject
lateinit var logger: Logger
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" }
if (intent.action != Intent.ACTION_SEND || intent.type != "text/plain") {
logger.w { "onCreate: intent.action = ${intent.action}, intent.type = ${intent.type}" }
finish()
return
}
val url: CharSequence? = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
if (url == null) {
logger.w { "onCreate: Intent's EXTRA_TEXT was null" }
finish()
return
}
viewModel.saveOperationResult.observe(this) {
showLongToast(
if (it.isSuccess) R.string.activity_share_recipe_success_toast
else R.string.activity_share_recipe_failure_toast
)
finish()
}
viewModel.saveRecipeByURL(url)
}
}

View File

@@ -0,0 +1,37 @@
package gq.kirmanak.mealient.ui.share
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.share.ShareRecipeRepo
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ShareRecipeViewModel @Inject constructor(
private val shareRecipeRepo: ShareRecipeRepo,
private val logger: Logger,
) : ViewModel() {
private val _saveOperationResult = MutableLiveData<Result<String>>()
val saveOperationResult: LiveData<Result<String>> get() = _saveOperationResult
fun saveRecipeByURL(url: CharSequence) {
logger.v { "saveRecipeByURL() called with: url = $url" }
viewModelScope.launch {
runCatchingExceptCancel {
shareRecipeRepo.saveRecipeByURL(url)
}.onSuccess {
logger.d { "Successfully saved recipe by URL" }
_saveOperationResult.postValue(Result.success(it))
}.onFailure {
logger.e(it) { "Can't save recipe by URL" }
_saveOperationResult.postValue(Result.failure(it))
}
}
}
}

View File

@@ -51,4 +51,6 @@
<string name="search_recipes_hint">Найти рецепты</string>
<string name="view_toolbar_navigation_icon_content_description">Открыть меню навигации</string>
<string name="fragment_recipes_list_no_recipes">Нет рецептов</string>
<string name="activity_share_recipe_success_toast">Рецепт успешно сохранен.</string>
<string name="activity_share_recipe_failure_toast">Что-то пошло не так.</string>
</resources>

View File

@@ -54,4 +54,6 @@
<string name="menu_navigation_drawer_header" translatable="false">@string/app_name</string>
<string name="view_toolbar_navigation_icon_content_description">Open navigation drawer</string>
<string name="fragment_recipes_list_no_recipes">No recipes</string>
<string name="activity_share_recipe_success_toast">Recipe saved successfully.</string>
<string name="activity_share_recipe_failure_toast">Something went wrong.</string>
</resources>

View File

@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.datasource.v0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0
import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0
interface MealieDataSourceV0 {
@@ -38,4 +39,10 @@ interface MealieDataSourceV0 {
token: String?,
slug: String,
): GetRecipeResponseV0
suspend fun parseRecipeFromURL(
baseUrl: String,
token: String?,
request: ParseRecipeURLRequestV0,
): String
}

View File

@@ -3,7 +3,12 @@ package gq.kirmanak.mealient.datasource.v0
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
import gq.kirmanak.mealient.datasource.decode
import gq.kirmanak.mealient.datasource.v0.models.*
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
import gq.kirmanak.mealient.datasource.v0.models.ErrorDetailV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0
import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import retrofit2.HttpException
@@ -77,4 +82,15 @@ class MealieDataSourceV0Impl @Inject constructor(
logMethod = { "requestRecipeInfo" },
logParameters = { "baseUrl = $baseUrl, token = $token, slug = $slug" }
)
override suspend fun parseRecipeFromURL(
baseUrl: String,
token: String?,
request: ParseRecipeURLRequestV0
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", token, request) },
logMethod = { "parseRecipeFromURL" },
logParameters = { "baseUrl = $baseUrl, token = $token, request = $request" }
)
}

View File

@@ -39,4 +39,11 @@ interface MealieServiceV0 {
@Url url: String,
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
): GetRecipeResponseV0
@POST
suspend fun createRecipeFromURL(
@Url url: String,
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
@Body request: ParseRecipeURLRequestV0,
): String
}

View File

@@ -0,0 +1,9 @@
package gq.kirmanak.mealient.datasource.v0.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ParseRecipeURLRequestV0(
@SerialName("url") val url: String,
)

View File

@@ -1,6 +1,11 @@
package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.v1.models.*
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
interface MealieDataSourceV1 {
@@ -42,4 +47,10 @@ interface MealieDataSourceV1 {
token: String?,
slug: String,
): GetRecipeResponseV1
suspend fun parseRecipeFromURL(
baseUrl: String,
token: String?,
request: ParseRecipeURLRequestV1,
): String
}

View File

@@ -3,7 +3,13 @@ package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
import gq.kirmanak.mealient.datasource.decode
import gq.kirmanak.mealient.datasource.v1.models.*
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import retrofit2.HttpException
@@ -89,5 +95,16 @@ class MealieDataSourceV1Impl @Inject constructor(
logParameters = { "baseUrl = $baseUrl, token = $token, slug = $slug" }
)
override suspend fun parseRecipeFromURL(
baseUrl: String,
token: String?,
request: ParseRecipeURLRequestV1
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipeFromURL("$baseUrl/api/recipes/create-url", token, request) },
logMethod = { "parseRecipeFromURL" },
logParameters = { "baseUrl = $baseUrl, token = $token, request = $request" }
)
}

View File

@@ -46,4 +46,11 @@ interface MealieServiceV1 {
@Url url: String,
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
): GetRecipeResponseV1
@POST
suspend fun createRecipeFromURL(
@Url url: String,
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
@Body request: ParseRecipeURLRequestV1,
): String
}

View File

@@ -0,0 +1,10 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ParseRecipeURLRequestV1(
@SerialName("url") val url: String,
@SerialName("includeTags") val includeTags: Boolean
)