diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 20a29269b..a9afbd6ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -328,6 +328,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw" const val KEY_SUGGESTIONS_EXCLUDE_TAGS = "suggestions_exclude_tags" const val KEY_SHIKIMORI = "shikimori" + const val KEY_MAL = "mal" const val KEY_DOWNLOADS_PARALLELISM = "downloads_parallelism" const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown" const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible" diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt index 0cbd62f4a..a3399a0ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ScrobblingModule.kt @@ -9,6 +9,11 @@ import javax.inject.Singleton import okhttp3.OkHttpClient import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.scrobbling.domain.Scrobbler +import org.koitharu.kotatsu.scrobbling.mal.data.MALAuthenticator +import org.koitharu.kotatsu.scrobbling.mal.data.MALInterceptor +import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository +import org.koitharu.kotatsu.scrobbling.mal.data.MALStorage +import org.koitharu.kotatsu.scrobbling.mal.domain.MALScrobbler import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriAuthenticator import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriInterceptor import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository @@ -33,9 +38,24 @@ object ScrobblingModule { return ShikimoriRepository(okHttp, storage, database) } + @Provides + @Singleton + fun provideMALRepository( + storage: MALStorage, + database: MangaDatabase, + authenticator: MALAuthenticator, + ): MALRepository { + val okHttp = OkHttpClient.Builder().apply { + authenticator(authenticator) + addInterceptor(MALInterceptor(storage)) + }.build() + return MALRepository(okHttp, storage, database) + } + @Provides @ElementsIntoSet fun provideScrobblers( shikimoriScrobbler: ShikimoriScrobbler, - ): Set<@JvmSuppressWildcards Scrobbler> = setOf(shikimoriScrobbler) + malScrobbler: MALScrobbler, + ): Set<@JvmSuppressWildcards Scrobbler> = setOf(shikimoriScrobbler, malScrobbler) } diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/model/ScrobblerService.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/model/ScrobblerService.kt index 45038ed12..115b1becd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/model/ScrobblerService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/model/ScrobblerService.kt @@ -10,5 +10,6 @@ enum class ScrobblerService( @DrawableRes val iconResId: Int, ) { - SHIKIMORI(1, R.string.shikimori, R.drawable.ic_shikimori) -} \ No newline at end of file + SHIKIMORI(1, R.string.shikimori, R.drawable.ic_shikimori), + MAL(2, R.string.mal, R.drawable.ic_mal) +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALAuthenticator.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALAuthenticator.kt new file mode 100644 index 000000000..5bef92286 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALAuthenticator.kt @@ -0,0 +1,54 @@ +package org.koitharu.kotatsu.scrobbling.mal.data + +import kotlinx.coroutines.runBlocking +import okhttp3.Authenticator +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.network.CommonHeaders +import javax.inject.Inject +import javax.inject.Provider + +class MALAuthenticator @Inject constructor( + private val storage: MALStorage, + private val repositoryProvider: Provider, +) : Authenticator { + + override fun authenticate(route: Route?, response: Response): Request? { + val accessToken = storage.accessToken ?: return null + if (!isRequestWithAccessToken(response)) { + return null + } + synchronized(this) { + val newAccessToken = storage.accessToken ?: return null + if (accessToken != newAccessToken) { + return newRequestWithAccessToken(response.request, newAccessToken) + } + val updatedAccessToken = refreshAccessToken() ?: return null + return newRequestWithAccessToken(response.request, updatedAccessToken) + } + } + + private fun isRequestWithAccessToken(response: Response): Boolean { + val header = response.request.header(CommonHeaders.AUTHORIZATION) + return header?.startsWith("Bearer") == true + } + + private fun newRequestWithAccessToken(request: Request, accessToken: String): Request { + return request.newBuilder() + .header(CommonHeaders.AUTHORIZATION, "Bearer $accessToken") + .build() + } + + private fun refreshAccessToken(): String? = runCatching { + val repository = repositoryProvider.get() + runBlocking { repository.authorize(null) } + return storage.accessToken + }.onFailure { + if (BuildConfig.DEBUG) { + it.printStackTrace() + } + }.getOrNull() + +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt new file mode 100644 index 000000000..397877ee1 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALInterceptor.kt @@ -0,0 +1,25 @@ +package org.koitharu.kotatsu.scrobbling.mal.data + +import okhttp3.Interceptor +import okhttp3.Response +import org.koitharu.kotatsu.core.network.CommonHeaders +import java.io.IOException + +class MALInterceptor(private val storage: MALStorage) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val sourceRequest = chain.request() + val request = sourceRequest.newBuilder() + if (!sourceRequest.url.pathSegments.contains("oauth2")) { + storage.accessToken?.let { + request.header(CommonHeaders.AUTHORIZATION, "Bearer $it") + } + } + val response = chain.proceed(request.build()) + if (!response.isSuccessful && !response.isRedirect) { + throw IOException("${response.code} ${response.message}") + } + return response + } + +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt new file mode 100644 index 000000000..3dca6ecc0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALRepository.kt @@ -0,0 +1,81 @@ +package org.koitharu.kotatsu.scrobbling.mal.data + +import okhttp3.FormBody +import okhttp3.OkHttpClient +import okhttp3.Request +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.parsers.util.await +import org.koitharu.kotatsu.parsers.util.parseJson +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService +import org.koitharu.kotatsu.scrobbling.mal.data.model.MALUser +import org.koitharu.kotatsu.utils.PKCEGenerator + +private const val REDIRECT_URI = "kotatsu://mal-auth" +private const val BASE_OAUTH_URL = "https://myanimelist.net" +private const val BASE_API_URL = "https://api.myanimelist.net/v2" +private const val MANGA_PAGE_SIZE = 250 + +// af16954886b040673378423f5d62cccd + +class MALRepository( + private val okHttp: OkHttpClient, + private val storage: MALStorage, + private val db: MangaDatabase, +) { + + private var codeVerifier: String = "" + + val oauthUrl: String + get() = "${BASE_OAUTH_URL}/v1/oauth2/authorize?" + + "response_type=code" + + "&client_id=af16954886b040673378423f5d62cccd" + + "&redirect_uri=${REDIRECT_URI}" + + "&code_challenge=${getPKCEChallengeCode()}" + + "&code_challenge_method=plain" + + val isAuthorized: Boolean + get() = storage.accessToken != null + + suspend fun authorize(code: String?) { + val body = FormBody.Builder() + if (code != null) { + body.add("client_id", "af16954886b040673378423f5d62cccd") + body.add("code", code) + body.add("code_verifier", getPKCEChallengeCode()) + body.add("grant_type", "authorization_code") + } + val request = Request.Builder() + .post(body.build()) + .url("${BASE_OAUTH_URL}/v1/oauth2/token") + + val response = okHttp.newCall(request.build()).await().parseJson() + storage.accessToken = response.getString("access_token") + storage.refreshToken = response.getString("refresh_token") + } + + suspend fun loadUser(): MALUser { + val request = Request.Builder() + .get() + .url("${BASE_API_URL}/users") + val response = okHttp.newCall(request.build()).await().parseJson() + return MALUser(response).also { storage.user = it } + } + + fun getCachedUser(): MALUser? { + return storage.user + } + + suspend fun unregister(mangaId: Long) { + return db.scrobblingDao.delete(ScrobblerService.MAL.id, mangaId) + } + + fun logout() { + storage.clear() + } + + private fun getPKCEChallengeCode(): String { + codeVerifier = PKCEGenerator.generateCodeVerifier() + return codeVerifier + } + +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALStorage.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALStorage.kt new file mode 100644 index 000000000..a00af7ef0 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/MALStorage.kt @@ -0,0 +1,41 @@ +package org.koitharu.kotatsu.scrobbling.mal.data + +import android.content.Context +import androidx.core.content.edit +import dagger.hilt.android.qualifiers.ApplicationContext +import org.json.JSONObject +import org.koitharu.kotatsu.scrobbling.mal.data.model.MALUser +import javax.inject.Inject +import javax.inject.Singleton + +private const val PREF_NAME = "myanimelist" +private const val KEY_ACCESS_TOKEN = "access_token" +private const val KEY_REFRESH_TOKEN = "refresh_token" +private const val KEY_USER = "user" + +@Singleton +class MALStorage @Inject constructor(@ApplicationContext context: Context) { + + private val prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + + var accessToken: String? + get() = prefs.getString(KEY_ACCESS_TOKEN, null) + set(value) = prefs.edit { putString(KEY_ACCESS_TOKEN, value) } + + var refreshToken: String? + get() = prefs.getString(KEY_REFRESH_TOKEN, null) + set(value) = prefs.edit { putString(KEY_REFRESH_TOKEN, value) } + + var user: MALUser? + get() = prefs.getString(KEY_USER, null)?.let { + MALUser(JSONObject(it)) + } + set(value) = prefs.edit { + putString(KEY_USER, value?.toJson()?.toString()) + } + + fun clear() = prefs.edit { + clear() + } + +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/model/MALUser.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/model/MALUser.kt new file mode 100644 index 000000000..a8a4c9085 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/data/model/MALUser.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.scrobbling.mal.data.model + +import org.json.JSONObject + +class MALUser( + val id: Long, + val nickname: String, +) { + + constructor(json: JSONObject) : this( + id = json.getLong("id"), + nickname = json.getString("name"), + ) + + fun toJson() = JSONObject().apply { + put("id", id) + put("nickname", nickname) + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MALUser + + if (id != other.id) return false + if (nickname != other.nickname) return false + + return true + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + nickname.hashCode() + return result + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/domain/MALScrobbler.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/domain/MALScrobbler.kt new file mode 100644 index 000000000..6789642f8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/domain/MALScrobbler.kt @@ -0,0 +1,61 @@ +package org.koitharu.kotatsu.scrobbling.mal.domain + +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.scrobbling.domain.Scrobbler +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus +import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository +import javax.inject.Inject +import javax.inject.Singleton + +private const val RATING_MAX = 10f + +@Singleton +class MALScrobbler @Inject constructor( + private val repository: MALRepository, + db: MangaDatabase, +) : Scrobbler(db, ScrobblerService.MAL) { + + init { + statuses[ScrobblingStatus.PLANNED] = "plan_to_read" + statuses[ScrobblingStatus.READING] = "reading" + statuses[ScrobblingStatus.COMPLETED] = "completed" + statuses[ScrobblingStatus.ON_HOLD] = "on_hold" + statuses[ScrobblingStatus.DROPPED] = "dropped" + } + + override val isAvailable: Boolean + get() = repository.isAuthorized + + override suspend fun findManga(query: String, offset: Int): List { + TODO() + } + + override suspend fun linkManga(mangaId: Long, targetId: Long) { + TODO() + } + + override suspend fun scrobble(mangaId: Long, chapter: MangaChapter) { + TODO() + } + + override suspend fun updateScrobblingInfo( + mangaId: Long, + rating: Float, + status: ScrobblingStatus?, + comment: String?, + ) { + TODO() + } + + override suspend fun unregisterScrobbling(mangaId: Long) { + repository.unregister(mangaId) + } + + override suspend fun getMangaInfo(id: Long): ScrobblerMangaInfo { + TODO() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsFragment.kt new file mode 100644 index 000000000..5903cdc2e --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsFragment.kt @@ -0,0 +1,73 @@ +package org.koitharu.kotatsu.scrobbling.mal.ui + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.scrobbling.mal.data.model.MALUser +import org.koitharu.kotatsu.utils.ext.assistedViewModels +import org.koitharu.kotatsu.utils.ext.withArgs +import javax.inject.Inject + +@AndroidEntryPoint +class MALSettingsFragment : BasePreferenceFragment(R.string.mal) { + + @Inject + lateinit var viewModelFactory: MALSettingsViewModel.Factory + + private val viewModel by assistedViewModels { + viewModelFactory.create(arguments?.getString(ARG_AUTH_CODE)) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_mal) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.user.observe(viewLifecycleOwner, this::onUserChanged) + } + + override fun onPreferenceTreeClick(preference: Preference): Boolean { + return when (preference.key) { + KEY_USER -> openAuthorization() + KEY_LOGOUT -> { + viewModel.logout() + true + } + else -> super.onPreferenceTreeClick(preference) + } + } + + private fun onUserChanged(user: MALUser?) { + val pref = findPreference(KEY_USER) ?: return + pref.isSelectable = user == null + pref.title = user?.nickname ?: getString(R.string.sign_in) + findPreference(KEY_LOGOUT)?.isVisible = user != null + } + + private fun openAuthorization(): Boolean { + return runCatching { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(viewModel.authorizationUrl) + startActivity(intent) + }.isSuccess + } + + companion object { + + private const val KEY_USER = "mal_user" + private const val KEY_LOGOUT = "mal_logout" + + private const val ARG_AUTH_CODE = "auth_code" + + fun newInstance(authCode: String?) = MALSettingsFragment().withArgs(1) { + putString(ARG_AUTH_CODE, authCode) + } + } +} + diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsViewModel.kt new file mode 100644 index 000000000..03ed32c5d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/mal/ui/MALSettingsViewModel.kt @@ -0,0 +1,59 @@ +package org.koitharu.kotatsu.scrobbling.mal.ui + +import androidx.lifecycle.MutableLiveData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.Dispatchers +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository +import org.koitharu.kotatsu.scrobbling.mal.data.model.MALUser +import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository +import org.koitharu.kotatsu.scrobbling.shikimori.data.model.ShikimoriUser + +class MALSettingsViewModel @AssistedInject constructor( + private val repository: MALRepository, + @Assisted authCode: String?, +) : BaseViewModel() { + + val authorizationUrl: String + get() = repository.oauthUrl + + val user = MutableLiveData() + + init { + if (authCode != null) { + authorize(authCode) + } else { + loadUser() + } + } + + fun logout() { + launchJob(Dispatchers.Default) { + repository.logout() + user.postValue(null) + } + } + + private fun loadUser() = launchJob(Dispatchers.Default) { + val userModel = if (repository.isAuthorized) { + repository.getCachedUser()?.let(user::postValue) + repository.loadUser() + } else { + null + } + user.postValue(userModel) + } + + private fun authorize(code: String) = launchJob(Dispatchers.Default) { + repository.authorize(code) + user.postValue(repository.loadUser()) + } + + @AssistedFactory + interface Factory { + + fun create(authCode: String?): MALSettingsViewModel + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt index 151452acb..8b7228ea6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/ScrobblingSelectorBottomSheet.kt @@ -22,8 +22,8 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga import org.koitharu.kotatsu.databinding.SheetScrobblingSelectorBinding import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga -import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikiMangaSelectionDecoration -import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ShikimoriSelectorAdapter +import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ScrobblerMangaSelectionDecoration +import org.koitharu.kotatsu.scrobbling.ui.selector.adapter.ScrobblerSelectorAdapter import org.koitharu.kotatsu.utils.ext.assistedViewModels import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.requireParcelable @@ -64,8 +64,8 @@ class ScrobblingSelectorBottomSheet : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val listAdapter = ShikimoriSelectorAdapter(viewLifecycleOwner, coil, this) - val decoration = ShikiMangaSelectionDecoration(view.context) + val listAdapter = ScrobblerSelectorAdapter(viewLifecycleOwner, coil, this) + val decoration = ScrobblerMangaSelectionDecoration(view.context) with(binding.recyclerView) { adapter = listAdapter addItemDecoration(decoration) diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ShikimoriMangaAD.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerMangaAD.kt similarity index 98% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ShikimoriMangaAD.kt rename to app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerMangaAD.kt index 8ce317320..20232633e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ShikimoriMangaAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerMangaAD.kt @@ -13,7 +13,7 @@ import org.koitharu.kotatsu.utils.ext.enqueueWith import org.koitharu.kotatsu.utils.ext.newImageRequest import org.koitharu.kotatsu.utils.ext.textAndVisible -fun shikimoriMangaAD( +fun scrobblerMangaAD( lifecycleOwner: LifecycleOwner, coil: ImageLoader, clickListener: OnListItemClickListener, diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ShikiMangaSelectionDecoration.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ShikiMangaSelectionDecoration.kt rename to app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt index 3cd806a99..ced1485fa 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ShikiMangaSelectionDecoration.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerMangaSelectionDecoration.kt @@ -11,7 +11,7 @@ import org.koitharu.kotatsu.list.ui.MangaSelectionDecoration import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga import org.koitharu.kotatsu.utils.ext.getItem -class ShikiMangaSelectionDecoration(context: Context) : MangaSelectionDecoration(context) { +class ScrobblerMangaSelectionDecoration(context: Context) : MangaSelectionDecoration(context) { var checkedItemId: Long get() = selection.singleOrNull() ?: NO_ID @@ -49,4 +49,4 @@ class ShikiMangaSelectionDecoration(context: Context) : MangaSelectionDecoration draw(canvas) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ShikimoriSelectorAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerSelectorAdapter.kt similarity index 93% rename from app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ShikimoriSelectorAdapter.kt rename to app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerSelectorAdapter.kt index 90c6af56b..bb61e79ca 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ShikimoriSelectorAdapter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/ui/selector/adapter/ScrobblerSelectorAdapter.kt @@ -11,7 +11,7 @@ import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga -class ShikimoriSelectorAdapter( +class ScrobblerSelectorAdapter( lifecycleOwner: LifecycleOwner, coil: ImageLoader, clickListener: OnListItemClickListener, @@ -19,7 +19,7 @@ class ShikimoriSelectorAdapter( init { delegatesManager.addDelegate(loadingStateAD()) - .addDelegate(shikimoriMangaAD(lifecycleOwner, coil, clickListener)) + .addDelegate(scrobblerMangaAD(lifecycleOwner, coil, clickListener)) .addDelegate(loadingFooterAD()) } @@ -37,4 +37,4 @@ class ShikimoriSelectorAdapter( return Intrinsics.areEqual(oldItem, newItem) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index 595d47caa..fe7d50cdb 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.local.data.CacheDir import org.koitharu.kotatsu.local.data.LocalStorageManager +import org.koitharu.kotatsu.scrobbling.mal.data.MALRepository import org.koitharu.kotatsu.scrobbling.shikimori.data.ShikimoriRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository @@ -40,6 +41,9 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach @Inject lateinit var shikimoriRepository: ShikimoriRepository + @Inject + lateinit var malRepository: MALRepository + @Inject lateinit var cookieJar: AndroidCookieJar @@ -75,6 +79,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach override fun onResume() { super.onResume() bindShikimoriSummary() + bindMALSummary() } override fun onPreferenceTreeClick(preference: Preference): Boolean { @@ -122,6 +127,15 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach } } + AppSettings.KEY_MAL -> { + if (!malRepository.isAuthorized) { + launchMALAuth() + true + } else { + super.onPreferenceTreeClick(preference) + } + } + else -> super.onPreferenceTreeClick(preference) } } @@ -202,4 +216,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach Snackbar.make(listView, it.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() } } + + private fun bindMALSummary() { + findPreference(AppSettings.KEY_MAL)?.summary = if (malRepository.isAuthorized) { + getString(R.string.logged_in_as, malRepository.getCachedUser()?.nickname) + } else { + getString(R.string.disabled) + } + } + + private fun launchMALAuth() { + runCatching { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(malRepository.oauthUrl) + startActivity(intent) + }.onFailure { + Snackbar.make(listView, it.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() + } + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/PKCEGenerator.kt b/app/src/main/java/org/koitharu/kotatsu/utils/PKCEGenerator.kt new file mode 100644 index 000000000..eb056db8e --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/PKCEGenerator.kt @@ -0,0 +1,16 @@ +package org.koitharu.kotatsu.utils + +import android.util.Base64 +import java.security.SecureRandom + +object PKCEGenerator { + + private const val PKCE_BASE64_ENCODE_SETTINGS = Base64.NO_WRAP or Base64.NO_PADDING or Base64.URL_SAFE + + fun generateCodeVerifier(): String { + val codeVerifier = ByteArray(50) + SecureRandom().nextBytes(codeVerifier) + return Base64.encodeToString(codeVerifier, PKCE_BASE64_ENCODE_SETTINGS) + } + +} diff --git a/app/src/main/res/drawable-hdpi/ic_mal.png b/app/src/main/res/drawable-hdpi/ic_mal.png new file mode 100644 index 000000000..c46ff36d2 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_mal.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_mal.png b/app/src/main/res/drawable-mdpi/ic_mal.png new file mode 100644 index 000000000..5f2b47aad Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_mal.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_mal.png b/app/src/main/res/drawable-xhdpi/ic_mal.png new file mode 100644 index 000000000..567a0f52c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_mal.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_mal.png b/app/src/main/res/drawable-xxhdpi/ic_mal.png new file mode 100644 index 000000000..8153cc153 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_mal.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_mal.png b/app/src/main/res/drawable-xxxhdpi/ic_mal.png new file mode 100644 index 000000000..e510bab32 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_mal.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 407cb7f37..f3f92c100 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -395,4 +395,5 @@ Different languages Network is not available Turn on Wi-Fi or mobile network to read manga online + MyAnimeList diff --git a/app/src/main/res/xml/pref_history.xml b/app/src/main/res/xml/pref_history.xml index 439694206..a6ff0ab7c 100644 --- a/app/src/main/res/xml/pref_history.xml +++ b/app/src/main/res/xml/pref_history.xml @@ -24,8 +24,15 @@ + + diff --git a/app/src/main/res/xml/pref_mal.xml b/app/src/main/res/xml/pref_mal.xml new file mode 100644 index 000000000..67a9fb806 --- /dev/null +++ b/app/src/main/res/xml/pref_mal.xml @@ -0,0 +1,19 @@ + + + + + + + +