mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2025-01-23 04:21:11 +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" />
|
||||
<activity
|
||||
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
|
||||
android:name=".ui.setting.track.AnilistLoginActivity"
|
||||
android:label="Anilist">
|
||||
|
@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.chapter.ChapterFilter
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
import uy.kohesive.injekt.api.addSingleton
|
||||
@ -49,6 +50,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
addSingletonFactory { Gson() }
|
||||
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
addSingletonFactory { ChapterFilter() }
|
||||
|
||||
// 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.extension.ExtensionUpdateJob
|
||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.File
|
||||
@ -84,10 +85,15 @@ object Migrations {
|
||||
if (oldVersion < 66) {
|
||||
LibraryPresenter.updateCustoms()
|
||||
}
|
||||
if (oldVersion < 67) {
|
||||
if (oldVersion < 68) {
|
||||
// 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>()
|
||||
trackManager.myAnimeList.logout()
|
||||
if (trackManager.myAnimeList.isLogged) {
|
||||
trackManager.myAnimeList.logout()
|
||||
context.toast(R.string.myanimelist_relogin)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -150,7 +150,7 @@ class PreferencesHelper(val context: Context) {
|
||||
.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")
|
||||
|
||||
|
@ -22,6 +22,9 @@ abstract class TrackService(val id: Int) {
|
||||
@StringRes
|
||||
abstract fun nameRes(): Int
|
||||
|
||||
// Application and remote support for reading dates
|
||||
open val supportsReadingDates: Boolean = false
|
||||
|
||||
@DrawableRes
|
||||
abstract fun getLogo(): Int
|
||||
|
||||
@ -43,6 +46,8 @@ abstract class TrackService(val id: Int) {
|
||||
|
||||
abstract fun displayScore(track: Track): String
|
||||
|
||||
abstract suspend fun add(track: Track): Track
|
||||
|
||||
abstract suspend fun update(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 {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
@ -145,10 +151,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
track.library_id = remoteTrack.library_id
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
api.addLibManga(track)
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,7 +190,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun logout() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).set(null)
|
||||
preferences.trackToken(this).delete()
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
|
@ -37,22 +37,25 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||
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 {
|
||||
val statusTrack = api.statusLibManga(track)
|
||||
val remoteTrack = api.findLibManga(track)
|
||||
if (statusTrack != null && remoteTrack != null) {
|
||||
return if (statusTrack != null && remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
track.status = remoteTrack.status
|
||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||
refresh(track)
|
||||
} else {
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
api.addLibManga(track)
|
||||
update(track)
|
||||
add(track)
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
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() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).set(null)
|
||||
interceptor.clearOauth()
|
||||
preferences.trackToken(this).delete()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -92,16 +92,20 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
||||
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 {
|
||||
val remoteTrack = api.findLibManga(track, getUserId())
|
||||
if (remoteTrack != null) {
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.media_id = remoteTrack.media_id
|
||||
return update(track)
|
||||
update(track)
|
||||
} else {
|
||||
track.score = DEFAULT_SCORE
|
||||
track.status = DEFAULT_STATUS
|
||||
return api.addLibManga(track, getUserId())
|
||||
add(track)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,10 @@ class TrackSearch : Track {
|
||||
|
||||
var start_date: String = ""
|
||||
|
||||
override var started_reading_date: Long = 0
|
||||
|
||||
override var finished_reading_date: Long = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
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.track.TrackService
|
||||
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 timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
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) }
|
||||
|
||||
@StringRes
|
||||
override fun nameRes() = R.string.myanimelist
|
||||
|
||||
override val supportsReadingDates: Boolean = true
|
||||
|
||||
override fun getLogo() = R.drawable.ic_tracker_mal
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
override suspend fun update(track: Track): Track {
|
||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||
track.status = COMPLETED
|
||||
}
|
||||
override suspend fun add(track: Track): Track {
|
||||
track.status = READING
|
||||
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 {
|
||||
val remoteTrack = api.findLibManga(track)
|
||||
if (remoteTrack != null) {
|
||||
val remoteTrack = api.findListItem(track)
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
return api.addLibManga(track)
|
||||
add(track)
|
||||
}
|
||||
return track
|
||||
|
||||
}
|
||||
|
||||
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> {
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun refresh(track: Track): Track {
|
||||
val remoteTrack = api.getLibManga(track)
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.total_chapters = remoteTrack.total_chapters
|
||||
return track
|
||||
return api.findListItem(track) ?: add(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 {
|
||||
saveCSRF(password)
|
||||
saveCredentials(username, password)
|
||||
val oauth = api.getAccessToken(authCode)
|
||||
interceptor.setAuth(oauth)
|
||||
val username = api.getCurrentUser()
|
||||
saveCredentials(username, oauth.access_token)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
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() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).delete()
|
||||
networkService.cookieManager.remove(BASE_URL.toHttpUrlOrNull()!!)
|
||||
interceptor.setAuth(null)
|
||||
}
|
||||
|
||||
private val isAuthorized: Boolean
|
||||
get() = super.isLogged && getCSRF().isNotEmpty() && checkCookies()
|
||||
fun saveOAuth(oAuth: OAuth?) {
|
||||
preferences.trackToken(this).set(json.encodeToString(oAuth))
|
||||
}
|
||||
|
||||
fun getCSRF(): String = preferences.trackToken(this).getOrDefault()
|
||||
|
||||
private fun saveCSRF(csrf: String) = preferences.trackToken(this).set(csrf)
|
||||
|
||||
private fun checkCookies(): Boolean {
|
||||
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++
|
||||
fun loadOAuth(): OAuth? {
|
||||
return try {
|
||||
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
return ckCount == 2
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val READING = 1
|
||||
const val COMPLETED = 2
|
||||
const val ON_HOLD = 3
|
||||
const val DROPPED = 4
|
||||
const val PLAN_TO_READ = 6
|
||||
const val REREADING = 7
|
||||
|
||||
const val DEFAULT_STATUS = READING
|
||||
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 USER_SESSION_COOKIE = "MALSESSIONID"
|
||||
const val LOGGED_IN_COOKIE = "is_logged_in"
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.data.track.myanimelist
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
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.consumeBody
|
||||
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.selectText
|
||||
import eu.kanade.tachiyomi.util.system.withIOContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
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.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
@ -24,52 +38,202 @@ import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.parser.Parser
|
||||
import timber.log.Timber
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||
|
||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||
|
||||
suspend fun search(query: String): List<TrackSearch> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
if (query.startsWith(PREFIX_MY)) {
|
||||
queryUsersList(query)
|
||||
} else {
|
||||
val realQuery = query.take(100)
|
||||
val response = client.newCall(GET(searchUrl(realQuery))).await()
|
||||
val matches = Jsoup.parse(response.consumeBody())
|
||||
.select("div.js-categories-seasonal.js-block-list.list").select("table")
|
||||
.select("tbody").select("tr").drop(1)
|
||||
suspend fun getAccessToken(authCode: String): OAuth {
|
||||
return withIOContext {
|
||||
val formBody: RequestBody = FormBody.Builder()
|
||||
.add("client_id", clientId)
|
||||
.add("code", authCode)
|
||||
.add("code_verifier", codeVerifier)
|
||||
.add("grant_type", "authorization_code")
|
||||
.build()
|
||||
client.newCall(POST("$baseOAuthUrl/token", body = formBody))
|
||||
.await()
|
||||
.parseAs()
|
||||
}
|
||||
}
|
||||
|
||||
matches.filter { row -> row.select(TD)[2].text() != "Novel" }.map { row ->
|
||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||
title = row.searchTitle()
|
||||
media_id = row.searchMediaId()
|
||||
total_chapters = row.searchTotalChapters()
|
||||
summary = row.searchSummary()
|
||||
cover_url = row.searchCoverUrl()
|
||||
tracking_url = mangaUrl(media_id)
|
||||
publishing_status = row.searchPublishingStatus()
|
||||
publishing_type = row.searchPublishingType()
|
||||
start_date = row.searchStartDate()
|
||||
suspend fun getCurrentUser(): String {
|
||||
return withIOContext {
|
||||
val request = Request.Builder()
|
||||
.url("$baseApiUrl/users/@me")
|
||||
.get()
|
||||
.build()
|
||||
authClient.newCall(request)
|
||||
.await()
|
||||
.parseAs<JsonObject>()
|
||||
.let { it["name"]!!.jsonPrimitive.content }
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
val realQuery = query.removePrefix(PREFIX_MY).take(100)
|
||||
return getList().filter { it.title.contains(realQuery, true) }.toList()
|
||||
private suspend fun getListPage(offset: Int): JsonObject {
|
||||
return withIOContext {
|
||||
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 {
|
||||
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await()
|
||||
return track
|
||||
private fun parseMangaItem(response: JsonObject, track: Track): Track {
|
||||
val obj = response.jsonObject
|
||||
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 {
|
||||
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track))).await()
|
||||
return track
|
||||
private fun parseDate(isoDate: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -82,152 +246,43 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
||||
return false
|
||||
}
|
||||
|
||||
suspend fun findLibManga(track: Track): Track? {
|
||||
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())
|
||||
private fun removeUrl(mediaId: Int) = "$baseApiUrl/manga".toUri().buildUpon().appendPath(mediaId.toString())
|
||||
.appendPath("delete").toString()
|
||||
|
||||
private fun addUrl() =
|
||||
baseModifyListUrl.toUri().buildUpon().appendPath("add.json").toString()
|
||||
companion object {
|
||||
// Registered under jay's MAL account
|
||||
private const val clientId = "8d3821c90edb495432a5ecb61de59200"
|
||||
|
||||
private fun listEntryUrl(mediaId: Int) =
|
||||
baseModifyListUrl.toUri().buildUpon().appendPath(mediaId.toString())
|
||||
.appendPath("edit").toString()
|
||||
private const val baseOAuthUrl = "https://myanimelist.net/v1/oauth2"
|
||||
private const val baseApiUrl = "https://api.myanimelist.net/v2"
|
||||
|
||||
private fun loginPostBody(username: String, password: String, csrf: String): RequestBody {
|
||||
return FormBody.Builder().add("user_name", username).add("password", password)
|
||||
.add("cookie", "1").add("sublogin", "Login").add("submit", "1").add(CSRF, csrf)
|
||||
private const val listPaginationAmount = 250
|
||||
|
||||
private var codeVerifier: String = ""
|
||||
|
||||
fun authUrl(): Uri = "$baseOAuthUrl/authorize".toUri().buildUpon()
|
||||
.appendQueryParameter("client_id", clientId)
|
||||
.appendQueryParameter("code_challenge", getPkceChallengeCode())
|
||||
.appendQueryParameter("response_type", "code")
|
||||
.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 {
|
||||
return FormBody.Builder().add("type", "2").add("subexport", "Export My List").build()
|
||||
}
|
||||
|
||||
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
|
||||
private fun getPkceChallengeCode(): String {
|
||||
codeVerifier = PkceUtil.generateCodeVerifier()
|
||||
return codeVerifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
@ -11,51 +13,57 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import okio.Buffer
|
||||
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)
|
||||
|
||||
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 {
|
||||
runBlocking {
|
||||
myanimelist.ensureLoggedIn()
|
||||
}
|
||||
val request = chain.request()
|
||||
return chain.proceed(updateRequest(request))
|
||||
}
|
||||
val originalRequest = chain.request()
|
||||
|
||||
private fun updateRequest(request: Request): Request {
|
||||
return request.body?.let {
|
||||
val contentType = it.contentType().toString()
|
||||
val updatedBody = when {
|
||||
contentType.contains("x-www-form-urlencoded") -> updateFormBody(it)
|
||||
contentType.contains("json") -> updateJsonBody(it)
|
||||
else -> it
|
||||
if (token.isNullOrEmpty()) {
|
||||
throw Exception("Not authenticated with MyAnimeList")
|
||||
}
|
||||
if (oauth == null) {
|
||||
oauth = myanimelist.loadOAuth()
|
||||
}
|
||||
// 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)
|
||||
|
||||
return "$formString${if (formString.isNotEmpty()) "&" else ""}${MyAnimeListApi.CSRF}=${myanimelist.getCSRF()}".toRequestBody(
|
||||
requestBody.contentType()
|
||||
)
|
||||
}
|
||||
|
||||
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())
|
||||
/**
|
||||
* Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token
|
||||
* and the oauth object.
|
||||
*/
|
||||
fun setAuth(oauth: OAuth?) {
|
||||
token = oauth?.access_token
|
||||
this.oauth = oauth
|
||||
myanimelist.saveOAuth(oauth)
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
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 {
|
||||
val remoteTrack = api.findLibManga(track, getUsername())
|
||||
|
||||
if (remoteTrack != null) {
|
||||
return if (remoteTrack != null) {
|
||||
track.copyPersonalFrom(remoteTrack)
|
||||
track.library_id = remoteTrack.library_id
|
||||
update(track)
|
||||
} else {
|
||||
// Set default fields if it's not found in the list
|
||||
track.score = DEFAULT_SCORE.toFloat()
|
||||
track.status = DEFAULT_STATUS
|
||||
return api.addLibManga(track, getUsername())
|
||||
add(track)
|
||||
}
|
||||
return track
|
||||
}
|
||||
|
||||
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() {
|
||||
super.logout()
|
||||
preferences.trackToken(this).set(null)
|
||||
preferences.trackToken(this).delete()
|
||||
interceptor.newAuth(null)
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.MediaType
|
||||
@ -10,6 +12,8 @@ import okhttp3.Response
|
||||
import rx.Observable
|
||||
import rx.Producer
|
||||
import rx.Subscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.fullType
|
||||
import java.io.BufferedReader
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
@ -105,6 +109,15 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
|
||||
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 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 onScoreClick(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.items.IFlexible
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
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.anilist.AnilistApi
|
||||
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.ui.setting.track.MyAnimeListLoginActivity
|
||||
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.TrackLoginDialog
|
||||
import eu.kanade.tachiyomi.widget.preference.TrackLogoutDialog
|
||||
@ -38,49 +39,45 @@ class SettingsTrackingController :
|
||||
titleRes = R.string.services
|
||||
|
||||
trackPreference(trackManager.myAnimeList) {
|
||||
onClick {
|
||||
if (trackManager.myAnimeList.isLogged) {
|
||||
val dialog = TrackLogoutDialog(trackManager.myAnimeList)
|
||||
dialog.targetController = this@SettingsTrackingController
|
||||
dialog.showDialog(router)
|
||||
} else {
|
||||
startActivity(MyAnimeListLoginActivity.newIntent(context))
|
||||
}
|
||||
}
|
||||
activity?.openInBrowser(MyAnimeListApi.authUrl(), trackManager.myAnimeList.getLogoColor())
|
||||
}
|
||||
trackPreference(trackManager.aniList) {
|
||||
onClick {
|
||||
showDialog(trackManager.aniList, AnilistApi.authUrl())
|
||||
}
|
||||
activity?.openInBrowser(AnilistApi.authUrl(), trackManager.aniList.getLogoColor())
|
||||
}
|
||||
trackPreference(trackManager.kitsu) {
|
||||
onClick {
|
||||
showDialog(trackManager.kitsu, userNameLabel = context.getString(R.string.email))
|
||||
}
|
||||
val dialog = TrackLoginDialog(trackManager.kitsu, R.string.email)
|
||||
dialog.targetController = this@SettingsTrackingController
|
||||
dialog.showDialog(router)
|
||||
}
|
||||
trackPreference(trackManager.shikimori) {
|
||||
onClick {
|
||||
showDialog(trackManager.shikimori, ShikimoriApi.authUrl())
|
||||
}
|
||||
activity?.openInBrowser(ShikimoriApi.authUrl(), trackManager.shikimori.getLogoColor())
|
||||
}
|
||||
trackPreference(trackManager.bangumi) {
|
||||
onClick {
|
||||
showDialog(trackManager.bangumi, BangumiApi.authUrl())
|
||||
}
|
||||
activity?.openInBrowser(BangumiApi.authUrl(), trackManager.bangumi.getLogoColor())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun PreferenceScreen.trackPreference(
|
||||
private inline fun PreferenceScreen.trackPreference(
|
||||
service: TrackService,
|
||||
block: (@DSL LoginPreference).() -> Unit
|
||||
crossinline login: () -> Unit
|
||||
): LoginPreference {
|
||||
return initThenAdd(
|
||||
LoginPreference(context).apply {
|
||||
key = Keys.trackUsername(service.id)
|
||||
title = context.getString(service.nameRes())
|
||||
},
|
||||
block
|
||||
LoginPreference(context).apply {
|
||||
key = Keys.trackUsername(service.id)
|
||||
title = context.getString(service.nameRes())
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
val pref = findPreference(Keys.trackUsername(id)) as? LoginPreference
|
||||
pref?.notifyChanged()
|
||||
|
@ -1,14 +1,17 @@
|
||||
package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@ -16,22 +19,13 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class AnilistLoginActivity : AppCompatActivity() {
|
||||
|
||||
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))
|
||||
class AnilistLoginActivity : BaseOAuthLoginActivity() {
|
||||
|
||||
override fun handleResult(data: Uri?) {
|
||||
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) {
|
||||
scope.launch {
|
||||
lifecycleScope.launchIO {
|
||||
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
||||
returnToSettings()
|
||||
}
|
||||
@ -40,17 +34,4 @@ class AnilistLoginActivity : AppCompatActivity() {
|
||||
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
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class BangumiLoginActivity : AppCompatActivity() {
|
||||
class BangumiLoginActivity : 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))
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
override fun handleResult(data: Uri?) {
|
||||
val code = data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
scope.launch {
|
||||
lifecycleScope.launchIO {
|
||||
trackManager.bangumi.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
@ -38,12 +32,4 @@ class BangumiLoginActivity : AppCompatActivity() {
|
||||
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
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
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
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
|
||||
class MyAnimeListLoginActivity : BaseWebViewActivity() {
|
||||
class MyAnimeListLoginActivity : BaseOAuthLoginActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
title = "MyAnimeList"
|
||||
|
||||
webview.webViewClient = object : WebViewClientCompat() {
|
||||
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||
view.loadUrl(url)
|
||||
return true
|
||||
override fun handleResult(data: Uri?) {
|
||||
val code = data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
lifecycleScope.launchIO {
|
||||
trackManager.myAnimeList.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
|
||||
// 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
|
||||
} else {
|
||||
trackManager.myAnimeList.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,35 +1,28 @@
|
||||
package eu.kanade.tachiyomi.ui.setting.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity.CENTER
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.system.launchIO
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
class ShikimoriLoginActivity : BaseOAuthLoginActivity() {
|
||||
|
||||
class ShikimoriLoginActivity : AppCompatActivity() {
|
||||
|
||||
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")
|
||||
override fun handleResult(data: Uri?) {
|
||||
val code = data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
scope.launch {
|
||||
lifecycleScope.launchIO {
|
||||
trackManager.shikimori.login(code)
|
||||
returnToSettings()
|
||||
}
|
||||
@ -38,12 +31,4 @@ class ShikimoriLoginActivity : AppCompatActivity() {
|
||||
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.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION
|
||||
import androidx.core.app.NotificationCompat
|
||||
@ -235,14 +238,37 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
|
||||
.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.
|
||||
*/
|
||||
fun Context.openInBrowser(url: String, forceBrowser: Boolean = false): Boolean {
|
||||
fun Context.openInBrowser(url: String, forceBrowser: Boolean): Boolean {
|
||||
try {
|
||||
val parsedUrl = url.toUri()
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(getResourceColor(R.attr.colorPrimaryVariant))
|
||||
.setDefaultColorSchemeParams(
|
||||
CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(getResourceColor(R.attr.colorPrimaryVariant))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
if (forceBrowser) {
|
||||
val packages = getCustomTabsPackages().maxBy { it.preferredOrder }
|
||||
|
@ -6,9 +6,17 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
fun launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block)
|
||||
|
||||
fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
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.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
@ -19,8 +20,8 @@ import rx.Subscription
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
abstract class LoginDialogPreference(
|
||||
private val usernameLabel: String? = null,
|
||||
bundle: Bundle? = null
|
||||
@StringRes private val usernameLabelRes: Int? = null,
|
||||
bundle: Bundle? = null
|
||||
) :
|
||||
DialogController(bundle) {
|
||||
|
||||
@ -48,8 +49,8 @@ abstract class LoginDialogPreference(
|
||||
fun onViewCreated(view: View) {
|
||||
v = view.apply {
|
||||
|
||||
if (!usernameLabel.isNullOrEmpty()) {
|
||||
username_input.hint = usernameLabel
|
||||
if (usernameLabelRes != null) {
|
||||
username_input.hint = view.context.getString(usernameLabelRes)
|
||||
}
|
||||
|
||||
login.setOnClickListener {
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.widget.preference
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import br.com.simplepass.loadingbutton.animatedDrawables.ProgressType
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
@ -12,15 +13,15 @@ import kotlinx.coroutines.launch
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
||||
LoginDialogPreference(usernameLabel, bundle) {
|
||||
class TrackLoginDialog(@StringRes usernameLabelRes: Int? = null, bundle: Bundle? = null) :
|
||||
LoginDialogPreference(usernameLabelRes, bundle) {
|
||||
|
||||
private val service = Injekt.get<TrackManager>().getService(args.getInt("key"))!!
|
||||
|
||||
override var canLogout = true
|
||||
|
||||
constructor(service: TrackService, usernameLabel: String?) :
|
||||
this(usernameLabel, Bundle().apply { putInt("key", service.id) })
|
||||
constructor(service: TrackService, @StringRes usernameLabelRes: Int?) :
|
||||
this(usernameLabelRes, Bundle().apply { putInt("key", service.id) })
|
||||
|
||||
override fun setCredentialsOnView(view: View) = with(view) {
|
||||
val serviceName = context.getString(service.nameRes())
|
||||
|
@ -433,6 +433,7 @@
|
||||
<string name="kitsu" translatable="false">Kitsu</string>
|
||||
<string name="bangumi" translatable="false">Bangumi</string>
|
||||
<string name="shikimori" translatable="false">Shikimori</string>
|
||||
<string name="myanimelist_relogin">Please login to MAL again</string>
|
||||
|
||||
<!-- Migration -->
|
||||
<string name="select_sources">Select sources</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user