| @ -4,10 +4,13 @@ import android.util.Log | ||||
| import com.github.kittinunf.fuel.Fuel | ||||
| import com.github.kittinunf.fuel.core.FuelError | ||||
| import com.github.kittinunf.fuel.core.Parameters | ||||
| import com.github.kittinunf.fuel.core.extensions.jsonBody | ||||
| import com.github.kittinunf.fuel.json.FuelJson | ||||
| import com.github.kittinunf.fuel.json.responseJson | ||||
| import com.github.kittinunf.result.Result | ||||
| import io.ktor.client.* | ||||
| import io.ktor.client.request.* | ||||
| import io.ktor.client.statement.* | ||||
| import io.ktor.http.* | ||||
| import kotlinx.coroutines.* | ||||
| import kotlinx.serialization.decodeFromString | ||||
| import kotlinx.serialization.json.Json | ||||
| @ -20,7 +23,9 @@ import org.mosad.teapod.util.concatenate | ||||
| private val json = Json { ignoreUnknownKeys = true } | ||||
|  | ||||
| object Crunchyroll { | ||||
|     private val TAG = javaClass.name | ||||
|  | ||||
|     private val client = HttpClient() | ||||
|     private const val baseUrl = "https://beta-api.crunchyroll.com" | ||||
|  | ||||
|     private var accessToken = "" | ||||
| @ -80,7 +85,7 @@ object Crunchyroll { | ||||
| //            println("response: $response") | ||||
| //            println("response: $result") | ||||
|  | ||||
|             Log.i(javaClass.name, "login complete with code ${response.statusCode}") | ||||
|             Log.i(TAG, "login complete with code ${response.statusCode}") | ||||
|             success = (response.statusCode == 200) | ||||
|         } | ||||
|  | ||||
| @ -119,16 +124,46 @@ object Crunchyroll { | ||||
|     private suspend fun requestPost( | ||||
|         endpoint: String, | ||||
|         params: Parameters = listOf(), | ||||
|         body: String | ||||
|         requestBody: String | ||||
|     ) = coroutineScope { | ||||
|         val path = "$baseUrl$endpoint" | ||||
|         if (System.currentTimeMillis() > tokenValidUntil) refreshToken() | ||||
|  | ||||
|         withContext(Dispatchers.IO) { | ||||
|             Fuel.post(path, params) | ||||
|                 .header("Authorization", "$tokenType $accessToken") | ||||
|                 .jsonBody(body) | ||||
|                 .response() // without a response, crunchy doesn't accept the request | ||||
|             val response: HttpResponse = client.request(path) { | ||||
|                 method = HttpMethod.Post | ||||
|                 body = requestBody | ||||
|                 header("Authorization", "$tokenType $accessToken") | ||||
|                 contentType(ContentType.Application.Json) | ||||
|                 params.forEach { | ||||
|                     parameter(it.first, it.second) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Log.i(TAG, "Response status: ${response.status}") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private suspend fun requestPatch( | ||||
|         endpoint: String, | ||||
|         params: Parameters = listOf(), | ||||
|         requestBody: String | ||||
|     ) = coroutineScope { | ||||
|         val path = "$baseUrl$endpoint" | ||||
|         if (System.currentTimeMillis() > tokenValidUntil) refreshToken() | ||||
|  | ||||
|         withContext(Dispatchers.IO) { | ||||
|             val response: HttpResponse = client.request(path) { | ||||
|                 method = HttpMethod.Patch | ||||
|                 body = requestBody | ||||
|                 header("Authorization", "$tokenType $accessToken") | ||||
|                 contentType(ContentType.Application.Json) | ||||
|                 params.forEach { | ||||
|                     parameter(it.first, it.second) | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             Log.i(TAG, "Response status: ${response.status}") | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -494,4 +529,13 @@ object Crunchyroll { | ||||
|         } ?: NoneProfile | ||||
|     } | ||||
|  | ||||
|     suspend fun postPrefSubLanguage(languageTag: String) { | ||||
|         val profileEndpoint = "/accounts/v1/me/profile" | ||||
|         val json = buildJsonObject { | ||||
|             put("preferred_content_subtitle_language", languageTag) | ||||
|         } | ||||
|  | ||||
|         requestPatch(profileEndpoint, requestBody = json.toString()) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @ -14,6 +14,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder | ||||
| import kotlinx.coroutines.Deferred | ||||
| import kotlinx.coroutines.async | ||||
| import kotlinx.coroutines.launch | ||||
| import kotlinx.coroutines.runBlocking | ||||
| import org.mosad.teapod.BuildConfig | ||||
| import org.mosad.teapod.R | ||||
| import org.mosad.teapod.databinding.FragmentAccountBinding | ||||
| @ -26,12 +27,13 @@ import org.mosad.teapod.ui.activity.main.MainActivity | ||||
| import org.mosad.teapod.ui.components.LoginDialog | ||||
| import org.mosad.teapod.util.DataTypes.Theme | ||||
| import org.mosad.teapod.util.showFragment | ||||
| import org.mosad.teapod.util.toDisplayString | ||||
| import java.util.* | ||||
|  | ||||
| class AccountFragment : Fragment() { | ||||
|  | ||||
|     private lateinit var binding: FragmentAccountBinding | ||||
|     private val profile: Deferred<Profile> = lifecycleScope.async { | ||||
|     private var profile: Deferred<Profile> = lifecycleScope.async { | ||||
|         Crunchyroll.profile() | ||||
|     } | ||||
|  | ||||
| @ -107,6 +109,7 @@ class AccountFragment : Fragment() { | ||||
|             //startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(AoDParser.getSubscriptionUrl()))) | ||||
|         } | ||||
|  | ||||
|  | ||||
|         binding.linearSettingsContentLanguage.setOnClickListener { | ||||
|             showContentLanguageSelection() | ||||
|         } | ||||
| @ -161,24 +164,45 @@ class AccountFragment : Fragment() { | ||||
|     } | ||||
|  | ||||
|     private fun showContentLanguageSelection() { | ||||
|         // we should be able to use the index of supportedLocals for language selection, items is GUI only | ||||
|         val items = supportedLocals.map { | ||||
|             if (it.displayLanguage.isNotEmpty() && it.displayCountry.isNotEmpty()) { | ||||
|                 "${it.displayLanguage} (${it.displayCountry})" | ||||
|             } else if (it.displayCountry.isNotEmpty()) { | ||||
|                 it.displayLanguage | ||||
|             } else { | ||||
|                 getString(R.string.settings_content_language_none) | ||||
|             } | ||||
|             it.toDisplayString(getString(R.string.settings_content_language_none)) | ||||
|         }.toTypedArray() | ||||
|  | ||||
|         var initialSelection: Int | ||||
|         // profile should be completed here, therefore blocking | ||||
|         runBlocking { | ||||
|             initialSelection = supportedLocals.indexOf(Locale.forLanguageTag( | ||||
|                 profile.await().preferredContentSubtitleLanguage)) | ||||
|             if (initialSelection < 0) initialSelection = supportedLocals.lastIndex | ||||
|         } | ||||
|  | ||||
|         MaterialAlertDialogBuilder(requireContext()) | ||||
|             .setTitle(R.string.settings_content_language) | ||||
|             .setSingleChoiceItems(items, 0){ _, which -> | ||||
|                 // TODO | ||||
|             .setSingleChoiceItems(items, initialSelection){ dialog, which -> | ||||
|                 updatePrefContentLanguage(supportedLocals[which].toLanguageTag()) | ||||
|                 dialog.dismiss() | ||||
|             } | ||||
|             .show() | ||||
|     } | ||||
|  | ||||
|     @kotlinx.coroutines.ExperimentalCoroutinesApi | ||||
|     private fun updatePrefContentLanguage(languageTag: String) { | ||||
|         lifecycleScope.launch { | ||||
|             Crunchyroll.postPrefSubLanguage(languageTag) | ||||
|  | ||||
|         }.invokeOnCompletion { | ||||
|             // update profile since the language selection might have changed | ||||
|             profile = lifecycleScope.async { Crunchyroll.profile() } | ||||
|             profile.invokeOnCompletion { | ||||
|                 // update language once loading profile is completed | ||||
|                 binding.textSettingsContentLanguageDesc.text = Locale.forLanguageTag( | ||||
|                     profile.getCompleted().preferredContentSubtitleLanguage | ||||
|                 ).displayLanguage | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun showThemeDialog() { | ||||
|         val items = arrayOf( | ||||
|             resources.getString(R.string.theme_light), | ||||
|  | ||||
| @ -3,8 +3,8 @@ package org.mosad.teapod.util | ||||
| import android.widget.TextView | ||||
| import org.mosad.teapod.parser.crunchyroll.Collection | ||||
| import org.mosad.teapod.parser.crunchyroll.ContinueWatchingItem | ||||
| import org.mosad.teapod.parser.crunchyroll.ContinueWatchingList | ||||
| import org.mosad.teapod.parser.crunchyroll.Item | ||||
| import java.util.* | ||||
|  | ||||
| fun TextView.setDrawableTop(drawable: Int) { | ||||
|     this.setCompoundDrawablesWithIntrinsicBounds(0, drawable, 0, 0) | ||||
| @ -27,3 +27,13 @@ fun Collection<ContinueWatchingItem>.toItemMediaList(): List<ItemMedia> { | ||||
|         ItemMedia(it.panel.episodeMetadata.seriesId, it.panel.title, it.panel.images.thumbnail[0][0].source) | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun Locale.toDisplayString(fallback: String): String { | ||||
|     return if (this.displayLanguage.isNotEmpty() && this.displayCountry.isNotEmpty()) { | ||||
|         "${this.displayLanguage} (${this.displayCountry})" | ||||
|     } else if (this.displayCountry.isNotEmpty()) { | ||||
|         this.displayLanguage | ||||
|     } else { | ||||
|         fallback | ||||
|     } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user