MangaDex OAuth
Co-authored-by: Carlos <2092019+CarlosEsco@users.noreply.github.com>
This commit is contained in:
+13
-149
@@ -2,25 +2,15 @@ package eu.kanade.presentation.more.settings.screen
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -35,15 +25,11 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import eu.kanade.domain.UnsortedPreferences
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.util.collectAsState
|
||||
import eu.kanade.presentation.util.padding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||
@@ -51,8 +37,9 @@ import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withUIContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import exh.log.xLogW
|
||||
import exh.md.utils.MdConstants
|
||||
import exh.md.utils.MdUtil
|
||||
import logcat.LogPriority
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -71,134 +58,17 @@ object SettingsMangadexScreen : SearchableSettings {
|
||||
override fun getPreferences(): List<Preference> {
|
||||
val sourcePreferences: SourcePreferences = remember { Injekt.get() }
|
||||
val unsortedPreferences: UnsortedPreferences = remember { Injekt.get() }
|
||||
val trackPreferences: TrackPreferences = remember { Injekt.get() }
|
||||
val mdex = remember { MdUtil.getEnabledMangaDex(unsortedPreferences, sourcePreferences) } ?: return emptyList()
|
||||
|
||||
return listOf(
|
||||
loginPreference(mdex),
|
||||
loginPreference(mdex, trackPreferences),
|
||||
preferredMangaDexId(unsortedPreferences, sourcePreferences),
|
||||
syncMangaDexIntoThis(unsortedPreferences),
|
||||
syncLibraryToMangaDex(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginDialog(
|
||||
mdex: MangaDex,
|
||||
onDismissRequest: () -> Unit,
|
||||
onLoginSuccess: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var username by remember { mutableStateOf(TextFieldValue(mdex.getUsername())) }
|
||||
var password by remember { mutableStateOf(TextFieldValue(mdex.getPassword())) }
|
||||
var processing by remember { mutableStateOf(false) }
|
||||
var inputError by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResource(R.string.login_title, mdex.name),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(onClick = onDismissRequest) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.action_close),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text(text = stringResource(R.string.username)) },
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
singleLine = true,
|
||||
isError = inputError && username.text.isEmpty(),
|
||||
)
|
||||
|
||||
var hidePassword by remember { mutableStateOf(true) }
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(text = stringResource(R.string.password)) },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { hidePassword = !hidePassword }) {
|
||||
Icon(
|
||||
imageVector = if (hidePassword) {
|
||||
Icons.Outlined.Visibility
|
||||
} else {
|
||||
Icons.Outlined.VisibilityOff
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation = if (hidePassword) {
|
||||
PasswordVisualTransformation()
|
||||
} else {
|
||||
VisualTransformation.None
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||
singleLine = true,
|
||||
isError = inputError && password.text.isEmpty(),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !processing,
|
||||
onClick = {
|
||||
if (username.text.isEmpty() || password.text.isEmpty()) {
|
||||
inputError = true
|
||||
return@Button
|
||||
}
|
||||
scope.launchIO {
|
||||
try {
|
||||
inputError = false
|
||||
processing = true
|
||||
val result = mdex.login(
|
||||
username = username.text,
|
||||
password = password.text,
|
||||
twoFactorCode = null,
|
||||
)
|
||||
if (result) {
|
||||
onDismissRequest()
|
||||
onLoginSuccess()
|
||||
withUIContext {
|
||||
context.toast(R.string.login_success)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
xLogW("Login to Mangadex error", e)
|
||||
withUIContext {
|
||||
e.message?.let { context.toast(it) }
|
||||
}
|
||||
} finally {
|
||||
processing = false
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
val id = if (processing) R.string.loading else R.string.login
|
||||
Text(text = stringResource(id))
|
||||
}
|
||||
},
|
||||
properties = DialogProperties(
|
||||
dismissOnBackPress = !processing,
|
||||
dismissOnClickOutside = !processing,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogoutDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
@@ -223,18 +93,10 @@ object SettingsMangadexScreen : SearchableSettings {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun loginPreference(mdex: MangaDex): Preference.PreferenceItem.MangaDexPreference {
|
||||
fun loginPreference(mdex: MangaDex, trackPreferences: TrackPreferences): Preference.PreferenceItem.MangaDexPreference {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var loggedIn by remember { mutableStateOf(mdex.isLogged()) }
|
||||
var loginDialogOpen by remember { mutableStateOf(false) }
|
||||
if (loginDialogOpen) {
|
||||
LoginDialog(
|
||||
mdex = mdex,
|
||||
onDismissRequest = { loginDialogOpen = false },
|
||||
onLoginSuccess = { loggedIn = true },
|
||||
)
|
||||
}
|
||||
val loggedIn by remember { trackPreferences.trackToken(mdex.mdList) }.collectAsState()
|
||||
var logoutDialogOpen by remember { mutableStateOf(false) }
|
||||
if (logoutDialogOpen) {
|
||||
LogoutDialog(
|
||||
@@ -244,7 +106,6 @@ object SettingsMangadexScreen : SearchableSettings {
|
||||
scope.launchIO {
|
||||
try {
|
||||
if (mdex.logout()) {
|
||||
loggedIn = false
|
||||
withUIContext {
|
||||
context.toast(R.string.logout_success)
|
||||
}
|
||||
@@ -265,9 +126,12 @@ object SettingsMangadexScreen : SearchableSettings {
|
||||
}
|
||||
return Preference.PreferenceItem.MangaDexPreference(
|
||||
title = mdex.name + " Login",
|
||||
loggedIn = loggedIn,
|
||||
loggedIn = loggedIn.isNotEmpty(),
|
||||
login = {
|
||||
loginDialogOpen = true
|
||||
context.openInBrowser(
|
||||
MdConstants.Login.authUrl(MdUtil.getPkceChallengeCode()),
|
||||
forceDefaultBrowser = true,
|
||||
)
|
||||
},
|
||||
logout = {
|
||||
logoutDialogOpen = true
|
||||
|
||||
@@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.lang.awaitSingle
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.md.network.MangaDexAuthInterceptor
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -23,6 +24,8 @@ class MdList(private val context: Context, id: Long) : TrackService(id) {
|
||||
|
||||
private val mdex by lazy { MdUtil.getEnabledMangaDex(Injekt.get()) }
|
||||
|
||||
val interceptor = MangaDexAuthInterceptor(trackPreferences, this)
|
||||
|
||||
@StringRes
|
||||
override fun nameRes(): Int = R.string.mdlist
|
||||
|
||||
@@ -157,5 +160,8 @@ class MdList(private val context: Context, id: Long) : TrackService(id) {
|
||||
trackPreferences.trackToken(this).delete()
|
||||
}
|
||||
|
||||
override val isLogged: Boolean
|
||||
get() = trackPreferences.trackToken(this).get().isNotEmpty()
|
||||
|
||||
class MangaDexNotFoundException : Exception("Mangadex not enabled")
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import exh.md.handlers.MangaPlusHandler
|
||||
import exh.md.handlers.PageHandler
|
||||
import exh.md.handlers.SimilarHandler
|
||||
import exh.md.network.MangaDexLoginHelper
|
||||
import exh.md.network.TokenAuthenticator
|
||||
import exh.md.service.MangaDexAuthService
|
||||
import exh.md.service.MangaDexService
|
||||
import exh.md.service.SimilarService
|
||||
@@ -76,14 +75,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
context.getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val mangadexAuthServiceLazy = lazy { MangaDexAuthService(baseHttpClient, headers, trackPreferences, mdList) }
|
||||
|
||||
private val loginHelper = MangaDexLoginHelper(mangadexAuthServiceLazy, trackPreferences, mdList)
|
||||
private val loginHelper = MangaDexLoginHelper(network.client, trackPreferences, mdList, mdList.interceptor)
|
||||
|
||||
override val baseHttpClient: OkHttpClient = delegate.client.newBuilder()
|
||||
.authenticator(
|
||||
TokenAuthenticator(loginHelper),
|
||||
)
|
||||
.addInterceptor(mdList.interceptor)
|
||||
.build()
|
||||
|
||||
private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false)
|
||||
@@ -94,7 +89,9 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
private val mangadexService by lazy {
|
||||
MangaDexService(client)
|
||||
}
|
||||
private val mangadexAuthService by mangadexAuthServiceLazy
|
||||
private val mangadexAuthService by lazy {
|
||||
MangaDexAuthService(baseHttpClient, headers)
|
||||
}
|
||||
private val similarService by lazy {
|
||||
SimilarService(client)
|
||||
}
|
||||
@@ -232,24 +229,12 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
return mdList.getPassword()
|
||||
}
|
||||
|
||||
override suspend fun login(
|
||||
username: String,
|
||||
password: String,
|
||||
twoFactorCode: String?,
|
||||
): Boolean {
|
||||
val result = loginHelper.login(username, password)
|
||||
return if (result) {
|
||||
mdList.saveCredentials(username, password)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
override suspend fun login(authCode: String): Boolean {
|
||||
return loginHelper.login(authCode)
|
||||
}
|
||||
|
||||
override suspend fun logout(): Boolean {
|
||||
loginHelper.logout()
|
||||
mdList.logout()
|
||||
return true
|
||||
return loginHelper.logout()
|
||||
}
|
||||
|
||||
// FollowsSource methods
|
||||
|
||||
Reference in New Issue
Block a user