mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-10 01:55:06 +01:00
Update to MAL's new API + Refactoring
To Arkon: no, I'm not hogging your clientId Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
parent
77a0244373
commit
05e3437f49
@ -95,7 +95,18 @@
|
|||||||
android:theme="@style/FilePickerTheme" />
|
android:theme="@style/FilePickerTheme" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
android:name=".ui.setting.track.MyAnimeListLoginActivity"
|
||||||
android:configChanges="uiMode|orientation|screenSize" />
|
android:label="MyAnimeList">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data
|
||||||
|
android:host="myanimelist-auth"
|
||||||
|
android:scheme="tachiyomij2k" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||||
android:label="Anilist">
|
android:label="Anilist">
|
||||||
|
@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.util.chapter.ChapterFilter
|
import eu.kanade.tachiyomi.util.chapter.ChapterFilter
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
import uy.kohesive.injekt.api.InjektRegistrar
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
import uy.kohesive.injekt.api.addSingleton
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
@ -49,6 +50,8 @@ class AppModule(val app: Application) : InjektModule {
|
|||||||
|
|
||||||
addSingletonFactory { Gson() }
|
addSingletonFactory { Gson() }
|
||||||
|
|
||||||
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
addSingletonFactory { ChapterFilter() }
|
addSingletonFactory { ChapterFilter() }
|
||||||
|
|
||||||
// Asynchronously init expensive components for a faster cold start
|
// Asynchronously init expensive components for a faster cold start
|
||||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
import eu.kanade.tachiyomi.data.updater.UpdaterJob
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -84,10 +85,15 @@ object Migrations {
|
|||||||
if (oldVersion < 66) {
|
if (oldVersion < 66) {
|
||||||
LibraryPresenter.updateCustoms()
|
LibraryPresenter.updateCustoms()
|
||||||
}
|
}
|
||||||
if (oldVersion < 67) {
|
if (oldVersion < 68) {
|
||||||
// Force MAL log out due to login flow change
|
// Force MAL log out due to login flow change
|
||||||
|
// v67: switched from scraping to WebView
|
||||||
|
// v68: switched from WebView to OAuth
|
||||||
val trackManager = Injekt.get<TrackManager>()
|
val trackManager = Injekt.get<TrackManager>()
|
||||||
trackManager.myAnimeList.logout()
|
if (trackManager.myAnimeList.isLogged) {
|
||||||
|
trackManager.myAnimeList.logout()
|
||||||
|
context.toast(R.string.myanimelist_relogin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -150,7 +150,7 @@ class PreferencesHelper(val context: Context) {
|
|||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun trackToken(sync: TrackService) = rxPrefs.getString(Keys.trackToken(sync.id), "")
|
fun trackToken(sync: TrackService) = flowPrefs.getString(Keys.trackToken(sync.id), "")
|
||||||
|
|
||||||
fun anilistScoreType() = rxPrefs.getString("anilist_score_type", "POINT_10")
|
fun anilistScoreType() = rxPrefs.getString("anilist_score_type", "POINT_10")
|
||||||
|
|
||||||
|
@ -22,6 +22,9 @@ abstract class TrackService(val id: Int) {
|
|||||||
@StringRes
|
@StringRes
|
||||||
abstract fun nameRes(): Int
|
abstract fun nameRes(): Int
|
||||||
|
|
||||||
|
// Application and remote support for reading dates
|
||||||
|
open val supportsReadingDates: Boolean = false
|
||||||
|
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
abstract fun getLogo(): Int
|
abstract fun getLogo(): Int
|
||||||
|
|
||||||
@ -43,6 +46,8 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun displayScore(track: Track): String
|
abstract fun displayScore(track: Track): String
|
||||||
|
|
||||||
|
abstract suspend fun add(track: Track): Track
|
||||||
|
|
||||||
abstract suspend fun update(track: Track): Track
|
abstract suspend fun update(track: Track): Track
|
||||||
|
|
||||||
abstract suspend fun bind(track: Track): Track
|
abstract suspend fun bind(track: Track): Track
|
||||||
|
@ -122,6 +122,12 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun add(track: Track): Track {
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
return api.addLibManga(track)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun update(track: Track): Track {
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
@ -145,10 +151,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
add(track)
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
|
||||||
track.status = DEFAULT_STATUS
|
|
||||||
api.addLibManga(track)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +190,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
super.logout()
|
super.logout()
|
||||||
preferences.trackToken(this).set(null)
|
preferences.trackToken(this).delete()
|
||||||
interceptor.setAuth(null)
|
interceptor.setAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,22 +37,25 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun add(track: Track): Track {
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
api.addLibManga(track)
|
||||||
|
return update(track)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track): Track {
|
||||||
val statusTrack = api.statusLibManga(track)
|
val statusTrack = api.statusLibManga(track)
|
||||||
val remoteTrack = api.findLibManga(track)
|
val remoteTrack = api.findLibManga(track)
|
||||||
if (statusTrack != null && remoteTrack != null) {
|
return if (statusTrack != null && remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
track.status = remoteTrack.status
|
track.status = remoteTrack.status
|
||||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||||
refresh(track)
|
refresh(track)
|
||||||
} else {
|
} else {
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
add(track)
|
||||||
track.status = DEFAULT_STATUS
|
|
||||||
api.addLibManga(track)
|
|
||||||
update(track)
|
|
||||||
}
|
}
|
||||||
return track
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
@ -133,8 +136,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
super.logout()
|
super.logout()
|
||||||
preferences.trackToken(this).set(null)
|
preferences.trackToken(this).delete()
|
||||||
interceptor.clearOauth()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -92,16 +92,20 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun add(track: Track): Track {
|
||||||
|
track.score = DEFAULT_SCORE
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
return api.addLibManga(track, getUserId())
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUserId())
|
val remoteTrack = api.findLibManga(track, getUserId())
|
||||||
if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.media_id = remoteTrack.media_id
|
track.media_id = remoteTrack.media_id
|
||||||
return update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
track.score = DEFAULT_SCORE
|
add(track)
|
||||||
track.status = DEFAULT_STATUS
|
|
||||||
return api.addLibManga(track, getUserId())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,10 @@ class TrackSearch : Track {
|
|||||||
|
|
||||||
var start_date: String = ""
|
var start_date: String = ""
|
||||||
|
|
||||||
|
override var started_reading_date: Long = 0
|
||||||
|
|
||||||
|
override var finished_reading_date: Long = 0
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (other == null || javaClass != other.javaClass) return false
|
if (other == null || javaClass != other.javaClass) return false
|
||||||
|
@ -8,17 +8,24 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
private val interceptor by lazy { MyAnimeListInterceptor(this) }
|
private val json: Json by injectLazy()
|
||||||
|
private val interceptor by lazy { MyAnimeListInterceptor(this, getPassword()) }
|
||||||
private val api by lazy { MyAnimeListApi(client, interceptor) }
|
private val api by lazy { MyAnimeListApi(client, interceptor) }
|
||||||
|
|
||||||
@StringRes
|
@StringRes
|
||||||
override fun nameRes() = R.string.myanimelist
|
override fun nameRes() = R.string.myanimelist
|
||||||
|
|
||||||
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.ic_tracker_mal
|
override fun getLogo() = R.drawable.ic_tracker_mal
|
||||||
|
|
||||||
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
override fun getLogoColor() = Color.rgb(46, 81, 162)
|
||||||
@ -59,26 +66,26 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(track: Track): Track {
|
override suspend fun add(track: Track): Track {
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
track.status = READING
|
||||||
track.status = COMPLETED
|
track.score = 0F
|
||||||
}
|
return api.updateItem(track)
|
||||||
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
override suspend fun update(track: Track): Track {
|
||||||
|
return api.updateItem(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track): Track {
|
||||||
val remoteTrack = api.findLibManga(track)
|
val remoteTrack = api.findListItem(track)
|
||||||
if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
add(track)
|
||||||
track.status = DEFAULT_STATUS
|
|
||||||
return api.addLibManga(track)
|
|
||||||
}
|
}
|
||||||
return track
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun canRemoveFromService(): Boolean = true
|
override fun canRemoveFromService(): Boolean = true
|
||||||
@ -88,22 +95,33 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String): List<TrackSearch> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
if (query.startsWith(SEARCH_ID_PREFIX)) {
|
||||||
|
query.substringAfter(SEARCH_ID_PREFIX).toIntOrNull()?.let { id ->
|
||||||
|
return listOf(api.getMangaDetails(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.startsWith(SEARCH_LIST_PREFIX)) {
|
||||||
|
query.substringAfter(SEARCH_LIST_PREFIX).let { title ->
|
||||||
|
return api.findListItems(title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun refresh(track: Track): Track {
|
override suspend fun refresh(track: Track): Track {
|
||||||
val remoteTrack = api.getLibManga(track)
|
return api.findListItem(track) ?: add(track)
|
||||||
track.copyPersonalFrom(remoteTrack)
|
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
|
||||||
return track
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun login(csrfToken: String) = login("myanimelist", csrfToken)
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
override suspend fun login(username: String, password: String): Boolean {
|
suspend fun login(authCode: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
saveCSRF(password)
|
val oauth = api.getAccessToken(authCode)
|
||||||
saveCredentials(username, password)
|
interceptor.setAuth(oauth)
|
||||||
|
val username = api.getCurrentUser()
|
||||||
|
saveCredentials(username, oauth.access_token)
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
@ -112,45 +130,37 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to login again if cookies have been cleared but credentials are still filled
|
|
||||||
suspend fun ensureLoggedIn() {
|
|
||||||
if (isAuthorized) return
|
|
||||||
if (!isLogged) throw Exception("MAL Login Credentials not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
super.logout()
|
super.logout()
|
||||||
preferences.trackToken(this).delete()
|
preferences.trackToken(this).delete()
|
||||||
networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!)
|
interceptor.setAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val isAuthorized: Boolean
|
fun saveOAuth(oAuth: OAuth?) {
|
||||||
get() = super.isLogged && getCSRF().isNotEmpty() && checkCookies()
|
preferences.trackToken(this).set(json.encodeToString(oAuth))
|
||||||
|
}
|
||||||
|
|
||||||
fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
fun loadOAuth(): OAuth? {
|
||||||
|
return try {
|
||||||
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
|
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
|
||||||
|
} catch (e: Exception) {
|
||||||
private fun checkCookies(): Boolean {
|
null
|
||||||
var ckCount = 0
|
|
||||||
val url = BASE_URL.toHttpUrlOrNull()!!
|
|
||||||
for (ck in networkService.cookieManager.get(url)) {
|
|
||||||
if (ck.name == USER_SESSION_COOKIE || ck.name == LOGGED_IN_COOKIE) ckCount++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ckCount == 2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
const val ON_HOLD = 3
|
const val ON_HOLD = 3
|
||||||
const val DROPPED = 4
|
const val DROPPED = 4
|
||||||
const val PLAN_TO_READ = 6
|
const val PLAN_TO_READ = 6
|
||||||
|
const val REREADING = 7
|
||||||
|
|
||||||
const val DEFAULT_STATUS = READING
|
const val DEFAULT_STATUS = READING
|
||||||
const val DEFAULT_SCORE = 0
|
const val DEFAULT_SCORE = 0
|
||||||
|
|
||||||
|
private const val SEARCH_ID_PREFIX = "id:"
|
||||||
|
private const val SEARCH_LIST_PREFIX = "my:"
|
||||||
|
|
||||||
const val BASE_URL = "https://myanimelist.net"
|
const val BASE_URL = "https://myanimelist.net"
|
||||||
const val USER_SESSION_COOKIE = "MALSESSIONID"
|
const val USER_SESSION_COOKIE = "MALSESSIONID"
|
||||||
const val LOGGED_IN_COOKIE = "is_logged_in"
|
const val LOGGED_IN_COOKIE = "is_logged_in"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
@ -9,13 +10,26 @@ import eu.kanade.tachiyomi.network.POST
|
|||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.consumeBody
|
import eu.kanade.tachiyomi.network.consumeBody
|
||||||
import eu.kanade.tachiyomi.network.consumeXmlBody
|
import eu.kanade.tachiyomi.network.consumeXmlBody
|
||||||
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
|
import eu.kanade.tachiyomi.util.PkceUtil
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
|
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.boolean
|
||||||
|
import kotlinx.serialization.json.contentOrNull
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonObject
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
@ -24,52 +38,202 @@ import org.jsoup.nodes.Document
|
|||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||||
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
suspend fun search(query: String): List<TrackSearch> {
|
suspend fun getAccessToken(authCode: String): OAuth {
|
||||||
return withContext(Dispatchers.IO) {
|
return withIOContext {
|
||||||
if (query.startsWith(PREFIX_MY)) {
|
val formBody: RequestBody = FormBody.Builder()
|
||||||
queryUsersList(query)
|
.add("client_id", clientId)
|
||||||
} else {
|
.add("code", authCode)
|
||||||
val realQuery = query.take(100)
|
.add("code_verifier", codeVerifier)
|
||||||
val response = client.newCall(GET(searchUrl(realQuery))).await()
|
.add("grant_type", "authorization_code")
|
||||||
val matches = Jsoup.parse(response.consumeBody())
|
.build()
|
||||||
.select("div.js-categories-seasonal.js-block-list.list").select("table")
|
client.newCall(POST("$baseOAuthUrl/token", body = formBody))
|
||||||
.select("tbody").select("tr").drop(1)
|
.await()
|
||||||
|
.parseAs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
matches.filter { row -> row.select(TD)[2].text() != "Novel" }.map { row ->
|
suspend fun getCurrentUser(): String {
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
return withIOContext {
|
||||||
title = row.searchTitle()
|
val request = Request.Builder()
|
||||||
media_id = row.searchMediaId()
|
.url("$baseApiUrl/users/@me")
|
||||||
total_chapters = row.searchTotalChapters()
|
.get()
|
||||||
summary = row.searchSummary()
|
.build()
|
||||||
cover_url = row.searchCoverUrl()
|
authClient.newCall(request)
|
||||||
tracking_url = mangaUrl(media_id)
|
.await()
|
||||||
publishing_status = row.searchPublishingStatus()
|
.parseAs<JsonObject>()
|
||||||
publishing_type = row.searchPublishingType()
|
.let { it["name"]!!.jsonPrimitive.content }
|
||||||
start_date = row.searchStartDate()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun search(query: String): List<TrackSearch> {
|
||||||
|
return withIOContext {
|
||||||
|
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
||||||
|
.appendQueryParameter("q", query)
|
||||||
|
.appendQueryParameter("nsfw", "true")
|
||||||
|
.build()
|
||||||
|
authClient.newCall(GET(url.toString()))
|
||||||
|
.await()
|
||||||
|
.parseAs<JsonObject>()
|
||||||
|
.let {
|
||||||
|
it["data"]!!.jsonArray
|
||||||
|
.map { data -> data.jsonObject["node"]!!.jsonObject }
|
||||||
|
.map { node ->
|
||||||
|
val id = node["id"]!!.jsonPrimitive.int
|
||||||
|
async { getMangaDetails(id) }
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.filter { trackSearch -> trackSearch.publishing_type != "novel" }
|
||||||
}
|
}
|
||||||
}.toList()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun getMangaDetails(id: Int): TrackSearch {
|
||||||
|
return withIOContext {
|
||||||
|
val url = "$baseApiUrl/manga".toUri().buildUpon()
|
||||||
|
.appendPath(id.toString())
|
||||||
|
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
|
||||||
|
.build()
|
||||||
|
authClient.newCall(GET(url.toString()))
|
||||||
|
.await()
|
||||||
|
.parseAs<JsonObject>()
|
||||||
|
.let {
|
||||||
|
val obj = it.jsonObject
|
||||||
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||||
|
media_id = obj["id"]!!.jsonPrimitive.int
|
||||||
|
title = obj["title"]!!.jsonPrimitive.content
|
||||||
|
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
|
||||||
|
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
||||||
|
cover_url = obj["main_picture"]?.jsonObject?.get("large")?.jsonPrimitive?.content ?: ""
|
||||||
|
tracking_url = "https://myanimelist.net/manga/$media_id"
|
||||||
|
publishing_status = obj["status"]!!.jsonPrimitive.content.replace("_", " ")
|
||||||
|
publishing_type = obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
|
||||||
|
start_date = try {
|
||||||
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
|
outputDf.format(obj["start_date"]!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateItem(track: Track): Track {
|
||||||
|
return withIOContext {
|
||||||
|
val formBodyBuilder = FormBody.Builder()
|
||||||
|
.add("status", track.toMyAnimeListStatus() ?: "reading")
|
||||||
|
.add("is_rereading", (track.status == MyAnimeList.REREADING).toString())
|
||||||
|
.add("score", track.score.toString())
|
||||||
|
.add("num_chapters_read", track.last_chapter_read.toString())
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(mangaUrl(track.media_id).toString())
|
||||||
|
.put(formBodyBuilder.build())
|
||||||
|
.build()
|
||||||
|
authClient.newCall(request)
|
||||||
|
.await()
|
||||||
|
.parseAs<JsonObject>()
|
||||||
|
.let { parseMangaItem(it, track) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun findListItem(track: Track): Track? {
|
||||||
|
return withIOContext {
|
||||||
|
val uri = "$baseApiUrl/manga".toUri().buildUpon()
|
||||||
|
.appendPath(track.media_id.toString())
|
||||||
|
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
|
||||||
|
.build()
|
||||||
|
authClient.newCall(GET(uri.toString()))
|
||||||
|
.await()
|
||||||
|
.parseAs<JsonObject>()
|
||||||
|
.let { obj ->
|
||||||
|
track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
||||||
|
obj.jsonObject["my_list_status"]?.jsonObject?.let {
|
||||||
|
parseMangaItem(it, track)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findListItems(query: String, offset: Int = 0): List<TrackSearch> {
|
||||||
|
return withIOContext {
|
||||||
|
val json = getListPage(offset)
|
||||||
|
val obj = json.jsonObject
|
||||||
|
|
||||||
|
val matches = obj["data"]!!.jsonArray
|
||||||
|
.filter {
|
||||||
|
it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains(
|
||||||
|
query,
|
||||||
|
ignoreCase = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
val id = it.jsonObject["node"]!!.jsonObject["id"]!!.jsonPrimitive.int
|
||||||
|
async { getMangaDetails(id) }
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
|
||||||
|
// Check next page if there's more
|
||||||
|
if (!obj["paging"]!!.jsonObject["next"]?.jsonPrimitive?.contentOrNull.isNullOrBlank()) {
|
||||||
|
matches + findListItems(query, offset + listPaginationAmount)
|
||||||
|
} else {
|
||||||
|
matches
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun queryUsersList(query: String): List<TrackSearch> {
|
private suspend fun getListPage(offset: Int): JsonObject {
|
||||||
val realQuery = query.removePrefix(PREFIX_MY).take(100)
|
return withIOContext {
|
||||||
return getList().filter { it.title.contains(realQuery, true) }.toList()
|
val urlBuilder = "$baseApiUrl/users/@me/mangalist".toUri().buildUpon()
|
||||||
|
.appendQueryParameter("fields", "list_status{start_date,finish_date}")
|
||||||
|
.appendQueryParameter("limit", listPaginationAmount.toString())
|
||||||
|
if (offset > 0) {
|
||||||
|
urlBuilder.appendQueryParameter("offset", offset.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(urlBuilder.build().toString())
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
authClient.newCall(request)
|
||||||
|
.await()
|
||||||
|
.parseAs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun addLibManga(track: Track): Track {
|
private fun parseMangaItem(response: JsonObject, track: Track): Track {
|
||||||
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await()
|
val obj = response.jsonObject
|
||||||
return track
|
return track.apply {
|
||||||
|
val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean
|
||||||
|
status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]!!.jsonPrimitive.content)
|
||||||
|
last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.int
|
||||||
|
score = obj["score"]!!.jsonPrimitive.int.toFloat()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateLibManga(track: Track): Track {
|
private fun parseDate(isoDate: String): Long {
|
||||||
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track))).await()
|
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||||
return track
|
}
|
||||||
|
|
||||||
|
private fun convertToIsoDate(epochTime: Long): String? {
|
||||||
|
if (epochTime == 0L) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
|
outputDf.format(epochTime)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun remove(track: Track): Boolean {
|
suspend fun remove(track: Track): Boolean {
|
||||||
@ -82,152 +246,43 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun findLibManga(track: Track): Track? {
|
private fun removeUrl(mediaId: Int) = "$baseApiUrl/manga".toUri().buildUpon().appendPath(mediaId.toString())
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await()
|
|
||||||
var remoteTrack: Track? = null
|
|
||||||
response.use {
|
|
||||||
if (it.priorResponse?.isRedirect != true) {
|
|
||||||
val trackForm = Jsoup.parse(it.consumeBody())
|
|
||||||
|
|
||||||
remoteTrack = Track.create(TrackManager.MYANIMELIST).apply {
|
|
||||||
last_chapter_read =
|
|
||||||
trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
|
|
||||||
total_chapters = trackForm.select("#totalChap").text().toInt()
|
|
||||||
status =
|
|
||||||
trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
|
|
||||||
score = trackForm.select("#add_manga_score > option[selected]").`val`()
|
|
||||||
.toFloatOrNull() ?: 0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
remoteTrack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getLibManga(track: Track): Track {
|
|
||||||
val result = findLibManga(track)
|
|
||||||
if (result == null) {
|
|
||||||
throw Exception("Could not find manga")
|
|
||||||
} else {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getList(): List<TrackSearch> {
|
|
||||||
val results = getListXml(getListUrl()).select("manga")
|
|
||||||
|
|
||||||
return results.map {
|
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
|
||||||
title = it.selectText("manga_title")!!
|
|
||||||
media_id = it.selectInt("manga_mangadb_id")
|
|
||||||
last_chapter_read = it.selectInt("my_read_chapters")
|
|
||||||
status = getStatus(it.selectText("my_status")!!)
|
|
||||||
score = it.selectInt("my_score").toFloat()
|
|
||||||
total_chapters = it.selectInt("manga_chapters")
|
|
||||||
tracking_url = mangaUrl(media_id)
|
|
||||||
}
|
|
||||||
}.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getListUrl(): String {
|
|
||||||
return withContext(Dispatchers.IO) {
|
|
||||||
val response =
|
|
||||||
authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).execute()
|
|
||||||
|
|
||||||
baseUrl + Jsoup.parse(response.consumeBody()).select("div.goodresult").select("a")
|
|
||||||
.attr("href")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getListXml(url: String): Document {
|
|
||||||
val response = authClient.newCall(GET(url)).await()
|
|
||||||
return Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val CSRF = "csrf_token"
|
|
||||||
|
|
||||||
const val baseUrl = "https://myanimelist.net"
|
|
||||||
private const val baseMangaUrl = "$baseUrl/manga/"
|
|
||||||
private const val baseModifyListUrl = "$baseUrl/ownlist/manga"
|
|
||||||
private const val PREFIX_MY = "my:"
|
|
||||||
private const val TD = "td"
|
|
||||||
|
|
||||||
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
|
|
||||||
|
|
||||||
fun loginUrl() = baseUrl.toUri().buildUpon().appendPath("login.php").build()
|
|
||||||
|
|
||||||
private fun searchUrl(query: String): String {
|
|
||||||
val col = "c[]"
|
|
||||||
return baseUrl.toUri().buildUpon().appendPath("manga.php")
|
|
||||||
.appendQueryParameter("q", query).appendQueryParameter(col, "a")
|
|
||||||
.appendQueryParameter(col, "b").appendQueryParameter(col, "c")
|
|
||||||
.appendQueryParameter(col, "d").appendQueryParameter(col, "e")
|
|
||||||
.appendQueryParameter(col, "g").toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exportListUrl() = baseUrl.toUri().buildUpon().appendPath("panel.php")
|
|
||||||
.appendQueryParameter("go", "export").toString()
|
|
||||||
|
|
||||||
private fun updateUrl() =
|
|
||||||
baseModifyListUrl.toUri().buildUpon().appendPath("edit.json").toString()
|
|
||||||
|
|
||||||
private fun removeUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon().appendPath(mediaId.toString())
|
|
||||||
.appendPath("delete").toString()
|
.appendPath("delete").toString()
|
||||||
|
|
||||||
private fun addUrl() =
|
companion object {
|
||||||
baseModifyListUrl.toUri().buildUpon().appendPath("add.json").toString()
|
// Registered under jay's MAL account
|
||||||
|
private const val clientId = "8d3821c90edb495432a5ecb61de59200"
|
||||||
|
|
||||||
private fun listEntryUrl(mediaId: Int) =
|
private const val baseOAuthUrl = "https://myanimelist.net/v1/oauth2"
|
||||||
baseModifyListUrl.toUri().buildUpon().appendPath(mediaId.toString())
|
private const val baseApiUrl = "https://api.myanimelist.net/v2"
|
||||||
.appendPath("edit").toString()
|
|
||||||
|
|
||||||
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
|
private const val listPaginationAmount = 250
|
||||||
return FormBody.Builder().add("user_name", username).add("password", password)
|
|
||||||
.add("cookie", "1").add("sublogin", "Login").add("submit", "1").add(CSRF, csrf)
|
private var codeVerifier: String = ""
|
||||||
|
|
||||||
|
fun authUrl(): Uri = "$baseOAuthUrl/authorize".toUri().buildUpon()
|
||||||
|
.appendQueryParameter("client_id", clientId)
|
||||||
|
.appendQueryParameter("code_challenge", getPkceChallengeCode())
|
||||||
|
.appendQueryParameter("response_type", "code")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
fun mangaUrl(id: Int): Uri = "$baseApiUrl/manga".toUri().buildUpon()
|
||||||
|
.appendPath(id.toString())
|
||||||
|
.appendPath("my_list_status")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun refreshTokenRequest(refreshToken: String): Request {
|
||||||
|
val formBody: RequestBody = FormBody.Builder()
|
||||||
|
.add("client_id", clientId)
|
||||||
|
.add("refresh_token", refreshToken)
|
||||||
|
.add("grant_type", "refresh_token")
|
||||||
|
.build()
|
||||||
|
return POST("$baseOAuthUrl/token", body = formBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun exportPostBody(): RequestBody {
|
private fun getPkceChallengeCode(): String {
|
||||||
return FormBody.Builder().add("type", "2").add("subexport", "Export My List").build()
|
codeVerifier = PkceUtil.generateCodeVerifier()
|
||||||
}
|
return codeVerifier
|
||||||
|
|
||||||
private fun mangaPostPayload(track: Track): RequestBody {
|
|
||||||
val body = JSONObject().put("manga_id", track.media_id).put("status", track.status)
|
|
||||||
.put("score", track.score).put("num_read_chapters", track.last_chapter_read)
|
|
||||||
|
|
||||||
return body.toString()
|
|
||||||
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Element.searchTitle() = select("strong").text()!!
|
|
||||||
|
|
||||||
private fun Element.searchTotalChapters() =
|
|
||||||
if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
|
||||||
|
|
||||||
private fun Element.searchCoverUrl() =
|
|
||||||
select("img").attr("data-src").split("\\?")[0].replace("/r/50x70/", "/")
|
|
||||||
|
|
||||||
private fun Element.searchMediaId() =
|
|
||||||
select("div.picSurround").select("a").attr("id").replace("sarea", "").toInt()
|
|
||||||
|
|
||||||
private fun Element.searchSummary() = select("div.pt4").first().ownText()!!
|
|
||||||
|
|
||||||
private fun Element.searchPublishingStatus() =
|
|
||||||
if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
|
||||||
|
|
||||||
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
|
||||||
|
|
||||||
private fun Element.searchStartDate() = select(TD)[6].text()!!
|
|
||||||
|
|
||||||
private fun getStatus(status: String) = when (status) {
|
|
||||||
"Reading" -> 1
|
|
||||||
"Completed" -> 2
|
|
||||||
"On-Hold" -> 3
|
|
||||||
"Dropped" -> 4
|
|
||||||
"Plan to Read" -> 6
|
|
||||||
else -> 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
@ -11,51 +13,57 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okio.Buffer
|
import okio.Buffer
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor {
|
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor {
|
||||||
|
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private var oauth: OAuth? = null
|
||||||
|
set(value) {
|
||||||
|
field = value?.copy(expires_in = System.currentTimeMillis() + (value.expires_in * 1000))
|
||||||
|
}
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
runBlocking {
|
val originalRequest = chain.request()
|
||||||
myanimelist.ensureLoggedIn()
|
|
||||||
}
|
|
||||||
val request = chain.request()
|
|
||||||
return chain.proceed(updateRequest(request))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateRequest(request: Request): Request {
|
if (token.isNullOrEmpty()) {
|
||||||
return request.body?.let {
|
throw Exception("Not authenticated with MyAnimeList")
|
||||||
val contentType = it.contentType().toString()
|
}
|
||||||
val updatedBody = when {
|
if (oauth == null) {
|
||||||
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
|
oauth = myanimelist.loadOAuth()
|
||||||
contentType.contains("json") -> updateJsonBody(it)
|
}
|
||||||
else -> it
|
// Refresh access token if null or expired.
|
||||||
|
if (oauth!!.isExpired()) {
|
||||||
|
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use {
|
||||||
|
if (it.isSuccessful) {
|
||||||
|
setAuth(json.decodeFromString(it.body!!.string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
request.newBuilder().post(updatedBody).build()
|
|
||||||
} ?: request
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bodyToString(requestBody: RequestBody): String {
|
|
||||||
Buffer().use {
|
|
||||||
requestBody.writeTo(it)
|
|
||||||
return it.readUtf8()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Throw on null auth.
|
||||||
|
if (oauth == null) {
|
||||||
|
throw Exception("No authentication token")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the authorization header to the original request.
|
||||||
|
val authRequest = originalRequest.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFormBody(requestBody: RequestBody): RequestBody {
|
/**
|
||||||
val formString = bodyToString(requestBody)
|
* Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token
|
||||||
|
* and the oauth object.
|
||||||
return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(
|
*/
|
||||||
requestBody.contentType()
|
fun setAuth(oauth: OAuth?) {
|
||||||
)
|
token = oauth?.access_token
|
||||||
}
|
this.oauth = oauth
|
||||||
|
myanimelist.saveOAuth(oauth)
|
||||||
private fun updateJsonBody(requestBody: RequestBody): RequestBody {
|
|
||||||
val jsonString = bodyToString(requestBody)
|
|
||||||
val newBody = JSONObject(jsonString)
|
|
||||||
.put(MyAnimeListApi.CSRF, myanimelist.getCSRF())
|
|
||||||
|
|
||||||
return newBody.toString().toRequestBody(requestBody.contentType())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
|
||||||
|
fun Track.toMyAnimeListStatus() = when (status) {
|
||||||
|
MyAnimeList.READING -> "reading"
|
||||||
|
MyAnimeList.COMPLETED -> "completed"
|
||||||
|
MyAnimeList.ON_HOLD -> "on_hold"
|
||||||
|
MyAnimeList.DROPPED -> "dropped"
|
||||||
|
MyAnimeList.PLAN_TO_READ -> "plan_to_read"
|
||||||
|
MyAnimeList.REREADING -> "reading"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStatus(status: String) = when (status) {
|
||||||
|
"reading" -> MyAnimeList.READING
|
||||||
|
"completed" -> MyAnimeList.COMPLETED
|
||||||
|
"on_hold" -> MyAnimeList.ON_HOLD
|
||||||
|
"dropped" -> MyAnimeList.DROPPED
|
||||||
|
"plan_to_read" -> MyAnimeList.PLAN_TO_READ
|
||||||
|
else -> MyAnimeList.READING
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class OAuth(
|
||||||
|
val refresh_token: String,
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val expires_in: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun isExpired() = System.currentTimeMillis() > expires_in
|
||||||
|
}
|
@ -70,20 +70,21 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.updateLibManga(track, getUsername())
|
return api.updateLibManga(track, getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun add(track: Track): Track {
|
||||||
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
|
track.status = DEFAULT_STATUS
|
||||||
|
return api.addLibManga(track, getUsername())
|
||||||
|
}
|
||||||
override suspend fun bind(track: Track): Track {
|
override suspend fun bind(track: Track): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUsername())
|
val remoteTrack = api.findLibManga(track, getUsername())
|
||||||
|
|
||||||
if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
add(track)
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
|
||||||
track.status = DEFAULT_STATUS
|
|
||||||
return api.addLibManga(track, getUsername())
|
|
||||||
}
|
}
|
||||||
return track
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun search(query: String) = api.search(query)
|
override suspend fun search(query: String) = api.search(query)
|
||||||
@ -130,7 +131,7 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
super.logout()
|
super.logout()
|
||||||
preferences.trackToken(this).set(null)
|
preferences.trackToken(this).delete()
|
||||||
interceptor.newAuth(null)
|
interceptor.newAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package eu.kanade.tachiyomi.network
|
package eu.kanade.tachiyomi.network
|
||||||
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
@ -10,6 +12,8 @@ import okhttp3.Response
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.fullType
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
@ -105,6 +109,15 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
|
|||||||
return progressClient.newCall(request)
|
return progressClient.newCall(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> Response.parseAs(): T {
|
||||||
|
// Avoiding Injekt.get<Json>() due to compiler issues
|
||||||
|
val json = Injekt.getInstance<Json>(fullType<Json>().type)
|
||||||
|
this.use {
|
||||||
|
val responseBody = it.body?.string().orEmpty()
|
||||||
|
return json.decodeFromString(responseBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun MediaType.Companion.jsonType(): MediaType = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
fun MediaType.Companion.jsonType(): MediaType = "application/json; charset=utf-8".toMediaTypeOrNull()!!
|
||||||
|
|
||||||
fun Response.consumeBody(): String? {
|
fun Response.consumeBody(): String? {
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.base.activity
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.ThemeUtil
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class BaseThemedActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(ThemeUtil.nightMode(preferences.theme()))
|
||||||
|
setTheme(ThemeUtil.theme(preferences.theme()))
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
@ -46,5 +46,7 @@ class TrackAdapter(controller: OnClickListener) : RecyclerView.Adapter<TrackHold
|
|||||||
fun onChaptersClick(position: Int)
|
fun onChaptersClick(position: Int)
|
||||||
fun onScoreClick(position: Int)
|
fun onScoreClick(position: Int)
|
||||||
fun onRemoveClick(position: Int)
|
fun onRemoveClick(position: Int)
|
||||||
|
fun onStartDateClick(position: Int)
|
||||||
|
fun onFinishDateClick(position: Int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.ui.recently_read
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
@ -10,9 +10,10 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
|
||||||
|
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
|
||||||
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
||||||
import eu.kanade.tachiyomi.ui.setting.track.MyAnimeListLoginActivity
|
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||||
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
||||||
import eu.kanade.tachiyomi.widget.preference.TrackLogoutDialog
|
import eu.kanade.tachiyomi.widget.preference.TrackLogoutDialog
|
||||||
@ -38,49 +39,45 @@ class SettingsTrackingController :
|
|||||||
titleRes = R.string.services
|
titleRes = R.string.services
|
||||||
|
|
||||||
trackPreference(trackManager.myAnimeList) {
|
trackPreference(trackManager.myAnimeList) {
|
||||||
onClick {
|
activity?.openInBrowser(MyAnimeListApi.authUrl(), trackManager.myAnimeList.getLogoColor())
|
||||||
if (trackManager.myAnimeList.isLogged) {
|
|
||||||
val dialog = TrackLogoutDialog(trackManager.myAnimeList)
|
|
||||||
dialog.targetController = this@SettingsTrackingController
|
|
||||||
dialog.showDialog(router)
|
|
||||||
} else {
|
|
||||||
startActivity(MyAnimeListLoginActivity.newIntent(context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
trackPreference(trackManager.aniList) {
|
trackPreference(trackManager.aniList) {
|
||||||
onClick {
|
activity?.openInBrowser(AnilistApi.authUrl(), trackManager.aniList.getLogoColor())
|
||||||
showDialog(trackManager.aniList, AnilistApi.authUrl())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
trackPreference(trackManager.kitsu) {
|
trackPreference(trackManager.kitsu) {
|
||||||
onClick {
|
val dialog = TrackLoginDialog(trackManager.kitsu, R.string.email)
|
||||||
showDialog(trackManager.kitsu, userNameLabel = context.getString(R.string.email))
|
dialog.targetController = this@SettingsTrackingController
|
||||||
}
|
dialog.showDialog(router)
|
||||||
}
|
}
|
||||||
trackPreference(trackManager.shikimori) {
|
trackPreference(trackManager.shikimori) {
|
||||||
onClick {
|
activity?.openInBrowser(ShikimoriApi.authUrl(), trackManager.shikimori.getLogoColor())
|
||||||
showDialog(trackManager.shikimori, ShikimoriApi.authUrl())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
trackPreference(trackManager.bangumi) {
|
trackPreference(trackManager.bangumi) {
|
||||||
onClick {
|
activity?.openInBrowser(BangumiApi.authUrl(), trackManager.bangumi.getLogoColor())
|
||||||
showDialog(trackManager.bangumi, BangumiApi.authUrl())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun PreferenceScreen.trackPreference(
|
private inline fun PreferenceScreen.trackPreference(
|
||||||
service: TrackService,
|
service: TrackService,
|
||||||
block: (@DSL LoginPreference).() -> Unit
|
crossinline login: () -> Unit
|
||||||
): LoginPreference {
|
): LoginPreference {
|
||||||
return initThenAdd(
|
return initThenAdd(
|
||||||
LoginPreference(context).apply {
|
LoginPreference(context).apply {
|
||||||
key = Keys.trackUsername(service.id)
|
key = Keys.trackUsername(service.id)
|
||||||
title = context.getString(service.nameRes())
|
title = context.getString(service.nameRes())
|
||||||
},
|
},
|
||||||
block
|
{
|
||||||
|
onClick {
|
||||||
|
if (service.isLogged) {
|
||||||
|
val dialog = TrackLogoutDialog(service)
|
||||||
|
dialog.targetController = this@SettingsTrackingController
|
||||||
|
dialog.showDialog(router)
|
||||||
|
} else {
|
||||||
|
login()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,24 +89,6 @@ class SettingsTrackingController :
|
|||||||
updatePreference(trackManager.bangumi.id)
|
updatePreference(trackManager.bangumi.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showDialog(trackService: TrackService, url: Uri? = null, userNameLabel: String? = null) {
|
|
||||||
if (trackService.isLogged) {
|
|
||||||
val dialog = TrackLogoutDialog(trackService)
|
|
||||||
dialog.targetController = this@SettingsTrackingController
|
|
||||||
dialog.showDialog(router)
|
|
||||||
} else if (url == null) {
|
|
||||||
val dialog = TrackLoginDialog(trackService, userNameLabel)
|
|
||||||
dialog.targetController = this@SettingsTrackingController
|
|
||||||
dialog.showDialog(router)
|
|
||||||
} else {
|
|
||||||
val tabsIntent = CustomTabsIntent.Builder()
|
|
||||||
.setToolbarColor(activity!!.getResourceColor(R.attr.colorPrimaryVariant))
|
|
||||||
.build()
|
|
||||||
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
|
||||||
tabsIntent.launchUrl(activity!!, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePreference(id: Int) {
|
private fun updatePreference(id: Int) {
|
||||||
val pref = findPreference(Keys.trackUsername(id)) as? LoginPreference
|
val pref = findPreference(Keys.trackUsername(id)) as? LoginPreference
|
||||||
pref?.notifyChanged()
|
pref?.notifyChanged()
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting.track
|
package eu.kanade.tachiyomi.ui.setting.track
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Gravity.CENTER
|
import android.view.Gravity.CENTER
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.util.system.launchIO
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@ -16,22 +19,13 @@ import kotlinx.coroutines.cancel
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class AnilistLoginActivity : AppCompatActivity() {
|
class AnilistLoginActivity : BaseOAuthLoginActivity() {
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
val view = ProgressBar(this)
|
|
||||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
|
||||||
|
|
||||||
|
override fun handleResult(data: Uri?) {
|
||||||
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
||||||
val matchResult = regex.find(intent.data?.fragment.toString())
|
val matchResult = regex.find(data?.fragment.toString())
|
||||||
if (matchResult?.groups?.get(1) != null) {
|
if (matchResult?.groups?.get(1) != null) {
|
||||||
scope.launch {
|
lifecycleScope.launchIO {
|
||||||
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
}
|
}
|
||||||
@ -40,17 +34,4 @@ class AnilistLoginActivity : AppCompatActivity() {
|
|||||||
returnToSettings()
|
returnToSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun returnToSettings() {
|
|
||||||
finish()
|
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,29 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting.track
|
package eu.kanade.tachiyomi.ui.setting.track
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Gravity.CENTER
|
import android.view.Gravity.CENTER
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.util.system.launchIO
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class BangumiLoginActivity : AppCompatActivity() {
|
class BangumiLoginActivity : BaseOAuthLoginActivity() {
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
override fun handleResult(data: Uri?) {
|
||||||
|
val code = data?.getQueryParameter("code")
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
val view = ProgressBar(this)
|
|
||||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
|
||||||
|
|
||||||
val code = intent.data?.getQueryParameter("code")
|
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
scope.launch {
|
lifecycleScope.launchIO {
|
||||||
trackManager.bangumi.login(code)
|
trackManager.bangumi.login(code)
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
}
|
}
|
||||||
@ -38,12 +32,4 @@ class BangumiLoginActivity : AppCompatActivity() {
|
|||||||
returnToSettings()
|
returnToSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun returnToSettings() {
|
|
||||||
finish()
|
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.setting.track
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.ui.base.activity.BaseThemedActivity
|
||||||
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class BaseOAuthLoginActivity : BaseThemedActivity() {
|
||||||
|
|
||||||
|
internal val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
abstract fun handleResult(data: Uri?)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val view = ProgressBar(this)
|
||||||
|
setContentView(
|
||||||
|
view,
|
||||||
|
FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
Gravity.CENTER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
handleResult(intent.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun returnToSettings() {
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
|
startActivity(intent)
|
||||||
|
finishAfterTransition()
|
||||||
|
}
|
||||||
|
}
|
@ -1,77 +1,21 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting.track
|
package eu.kanade.tachiyomi.ui.setting.track
|
||||||
|
|
||||||
import android.content.Context
|
import android.net.Uri
|
||||||
import android.content.Intent
|
import androidx.lifecycle.lifecycleScope
|
||||||
import android.os.Bundle
|
import eu.kanade.tachiyomi.util.system.launchIO
|
||||||
import android.webkit.WebView
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.webview.BaseWebViewActivity
|
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
|
||||||
import kotlinx.android.synthetic.main.webview_activity.*
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
class MyAnimeListLoginActivity : BaseWebViewActivity() {
|
class MyAnimeListLoginActivity : BaseOAuthLoginActivity() {
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
override fun handleResult(data: Uri?) {
|
||||||
|
val code = data?.getQueryParameter("code")
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
if (code != null) {
|
||||||
|
lifecycleScope.launchIO {
|
||||||
override fun onCreate(savedState: Bundle?) {
|
trackManager.myAnimeList.login(code)
|
||||||
super.onCreate(savedState)
|
returnToSettings()
|
||||||
|
|
||||||
title = "MyAnimeList"
|
|
||||||
|
|
||||||
webview.webViewClient = object : WebViewClientCompat() {
|
|
||||||
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
|
||||||
view.loadUrl(url)
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
override fun onPageFinished(view: WebView?, url: String?) {
|
trackManager.myAnimeList.logout()
|
||||||
super.onPageFinished(view, url)
|
returnToSettings()
|
||||||
|
|
||||||
// Get CSRF token from HTML after post-login redirect
|
|
||||||
if (url == MyAnimeListApi.baseUrl + "/") {
|
|
||||||
view?.evaluateJavascript(
|
|
||||||
"(function(){return document.querySelector('meta[name=csrf_token]').getAttribute('content')})();"
|
|
||||||
) {
|
|
||||||
scope.launch {
|
|
||||||
withContext(Dispatchers.IO) { trackManager.myAnimeList.login(it.replace("\"", "")) }
|
|
||||||
returnToSettings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
webview.loadUrl(MyAnimeListApi.loginUrl().toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
scope.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun returnToSettings() {
|
|
||||||
finish()
|
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun newIntent(context: Context): Intent {
|
|
||||||
val intent = Intent(context, MyAnimeListLoginActivity::class.java)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
||||||
return intent
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,28 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting.track
|
package eu.kanade.tachiyomi.ui.setting.track
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Gravity.CENTER
|
import android.view.Gravity.CENTER
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.util.system.launchIO
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
class ShikimoriLoginActivity : BaseOAuthLoginActivity() {
|
||||||
|
|
||||||
class ShikimoriLoginActivity : AppCompatActivity() {
|
override fun handleResult(data: Uri?) {
|
||||||
|
val code = data?.getQueryParameter("code")
|
||||||
private val trackManager: TrackManager by injectLazy()
|
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
|
|
||||||
val view = ProgressBar(this)
|
|
||||||
setContentView(view, FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT, CENTER))
|
|
||||||
|
|
||||||
val code = intent.data?.getQueryParameter("code")
|
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
scope.launch {
|
lifecycleScope.launchIO {
|
||||||
trackManager.shikimori.login(code)
|
trackManager.shikimori.login(code)
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
}
|
}
|
||||||
@ -38,12 +31,4 @@ class ShikimoriLoginActivity : AppCompatActivity() {
|
|||||||
returnToSettings()
|
returnToSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun returnToSettings() {
|
|
||||||
finish()
|
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
15
app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt
Normal file
15
app/src/main/java/eu/kanade/tachiyomi/util/PkceUtil.kt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import java.security.SecureRandom
|
||||||
|
|
||||||
|
object PkceUtil {
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -14,14 +14,17 @@ import android.content.res.Resources
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION
|
import androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
@ -235,14 +238,37 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
|
|||||||
.any { className == it.service.className }
|
.any { className == it.service.className }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Context.openInBrowser(url: String, @ColorInt toolbarColor: Int? = null) {
|
||||||
|
this.openInBrowser(url.toUri(), toolbarColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.openInBrowser(uri: Uri, @ColorInt toolbarColor: Int? = null) {
|
||||||
|
try {
|
||||||
|
val intent = CustomTabsIntent.Builder()
|
||||||
|
.setDefaultColorSchemeParams(
|
||||||
|
CustomTabColorSchemeParams.Builder()
|
||||||
|
.setToolbarColor(toolbarColor ?: getResourceColor(R.attr.colorPrimaryVariant))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
intent.launchUrl(this, uri)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
toast(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a URL in a custom tab.
|
* Opens a URL in a custom tab.
|
||||||
*/
|
*/
|
||||||
fun Context.openInBrowser(url: String, forceBrowser: Boolean = false): Boolean {
|
fun Context.openInBrowser(url: String, forceBrowser: Boolean): Boolean {
|
||||||
try {
|
try {
|
||||||
val parsedUrl = url.toUri()
|
val parsedUrl = url.toUri()
|
||||||
val intent = CustomTabsIntent.Builder()
|
val intent = CustomTabsIntent.Builder()
|
||||||
.setToolbarColor(getResourceColor(R.attr.colorPrimaryVariant))
|
.setDefaultColorSchemeParams(
|
||||||
|
CustomTabColorSchemeParams.Builder()
|
||||||
|
.setToolbarColor(getResourceColor(R.attr.colorPrimaryVariant))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
.build()
|
.build()
|
||||||
if (forceBrowser) {
|
if (forceBrowser) {
|
||||||
val packages = getCustomTabsPackages().maxBy { it.preferredOrder }
|
val packages = getCustomTabsPackages().maxBy { it.preferredOrder }
|
||||||
|
@ -6,9 +6,17 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
fun launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
fun launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||||
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block)
|
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block)
|
||||||
|
|
||||||
fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
|
fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
|
||||||
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block)
|
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block)
|
||||||
|
|
||||||
|
fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
||||||
|
launch(Dispatchers.IO, block = block)
|
||||||
|
|
||||||
|
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.Main, block)
|
||||||
|
|
||||||
|
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(Dispatchers.IO, block)
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.widget.preference
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.customview.customView
|
import com.afollestad.materialdialogs.customview.customView
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||||
@ -19,8 +20,8 @@ import rx.Subscription
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class LoginDialogPreference(
|
abstract class LoginDialogPreference(
|
||||||
private val usernameLabel: String? = null,
|
@StringRes private val usernameLabelRes: Int? = null,
|
||||||
bundle: Bundle? = null
|
bundle: Bundle? = null
|
||||||
) :
|
) :
|
||||||
DialogController(bundle) {
|
DialogController(bundle) {
|
||||||
|
|
||||||
@ -48,8 +49,8 @@ abstract class LoginDialogPreference(
|
|||||||
fun onViewCreated(view: View) {
|
fun onViewCreated(view: View) {
|
||||||
v = view.apply {
|
v = view.apply {
|
||||||
|
|
||||||
if (!usernameLabel.isNullOrEmpty()) {
|
if (usernameLabelRes != null) {
|
||||||
username_input.hint = usernameLabel
|
username_input.hint = view.context.getString(usernameLabelRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
login.setOnClickListener {
|
login.setOnClickListener {
|
||||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.widget.preference
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import br.com.simplepass.loadingbutton.animatedDrawables.ProgressType
|
import br.com.simplepass.loadingbutton.animatedDrawables.ProgressType
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
@ -12,15 +13,15 @@ import kotlinx.coroutines.launch
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
class TrackLoginDialog(@StringRes usernameLabelRes: Int? = null, bundle: Bundle? = null) :
|
||||||
LoginDialogPreference(usernameLabel, bundle) {
|
LoginDialogPreference(usernameLabelRes, bundle) {
|
||||||
|
|
||||||
private val service = Injekt.get<TrackManager>().getService(args.getInt("key"))!!
|
private val service = Injekt.get<TrackManager>().getService(args.getInt("key"))!!
|
||||||
|
|
||||||
override var canLogout = true
|
override var canLogout = true
|
||||||
|
|
||||||
constructor(service: TrackService, usernameLabel: String?) :
|
constructor(service: TrackService, @StringRes usernameLabelRes: Int?) :
|
||||||
this(usernameLabel, Bundle().apply { putInt("key", service.id) })
|
this(usernameLabelRes, Bundle().apply { putInt("key", service.id) })
|
||||||
|
|
||||||
override fun setCredentialsOnView(view: View) = with(view) {
|
override fun setCredentialsOnView(view: View) = with(view) {
|
||||||
val serviceName = context.getString(service.nameRes())
|
val serviceName = context.getString(service.nameRes())
|
||||||
|
@ -433,6 +433,7 @@
|
|||||||
<string name="kitsu" translatable="false">Kitsu</string>
|
<string name="kitsu" translatable="false">Kitsu</string>
|
||||||
<string name="bangumi" translatable="false">Bangumi</string>
|
<string name="bangumi" translatable="false">Bangumi</string>
|
||||||
<string name="shikimori" translatable="false">Shikimori</string>
|
<string name="shikimori" translatable="false">Shikimori</string>
|
||||||
|
<string name="myanimelist_relogin">Please login to MAL again</string>
|
||||||
|
|
||||||
<!-- Migration -->
|
<!-- Migration -->
|
||||||
<string name="select_sources">Select sources</string>
|
<string name="select_sources">Select sources</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user