tachiyomi/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsTrackingScreen.kt
2023-11-15 22:30:10 -05:00

353 lines
15 KiB
Kotlin

package eu.kanade.presentation.more.settings.screen
import android.content.Context
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.EnhancedTracker
import eu.kanade.tachiyomi.data.track.Tracker
import eu.kanade.tachiyomi.data.track.TrackerManager
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.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.presentation.core.components.material.padding
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object SettingsTrackingScreen : SearchableSettings {
@ReadOnlyComposable
@Composable
@StringRes
override fun getTitleRes() = R.string.pref_category_tracking
@Composable
override fun RowScope.AppBarAction() {
val uriHandler = LocalUriHandler.current
IconButton(onClick = { uriHandler.openUri("https://tachiyomi.org/docs/guides/tracking") }) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
contentDescription = stringResource(R.string.tracking_guide),
)
}
}
@Composable
override fun getPreferences(): List<Preference> {
val context = LocalContext.current
val trackPreferences = remember { Injekt.get<TrackPreferences>() }
val trackerManager = remember { Injekt.get<TrackerManager>() }
val sourceManager = remember { Injekt.get<SourceManager>() }
var dialog by remember { mutableStateOf<Any?>(null) }
dialog?.run {
when (this) {
is LoginDialog -> {
TrackingLoginDialog(
tracker = tracker,
uNameStringRes = uNameStringRes,
onDismissRequest = { dialog = null },
)
}
is LogoutDialog -> {
TrackingLogoutDialog(
tracker = tracker,
onDismissRequest = { dialog = null },
)
}
}
}
val enhancedTrackers = trackerManager.trackers
.filter { it is EnhancedTracker }
.partition { service ->
val acceptedSources = (service as EnhancedTracker).getAcceptedSources()
sourceManager.getCatalogueSources().any { it::class.qualifiedName in acceptedSources }
}
var enhancedTrackerInfo = stringResource(R.string.enhanced_tracking_info)
if (enhancedTrackers.second.isNotEmpty()) {
val missingSourcesInfo = stringResource(
R.string.enhanced_services_not_installed,
enhancedTrackers.second.joinToString { it.name },
)
enhancedTrackerInfo += "\n\n$missingSourcesInfo"
}
return listOf(
Preference.PreferenceItem.SwitchPreference(
pref = trackPreferences.autoUpdateTrack(),
title = stringResource(R.string.pref_auto_update_manga_sync),
),
Preference.PreferenceGroup(
title = stringResource(R.string.services),
preferenceItems = listOf(
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.myAnimeList.name,
tracker = trackerManager.myAnimeList,
login = { context.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.myAnimeList) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.aniList.name,
tracker = trackerManager.aniList,
login = { context.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.aniList) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.kitsu.name,
tracker = trackerManager.kitsu,
login = { dialog = LoginDialog(trackerManager.kitsu, R.string.email) },
logout = { dialog = LogoutDialog(trackerManager.kitsu) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.mangaUpdates.name,
tracker = trackerManager.mangaUpdates,
login = { dialog = LoginDialog(trackerManager.mangaUpdates, R.string.username) },
logout = { dialog = LogoutDialog(trackerManager.mangaUpdates) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.shikimori.name,
tracker = trackerManager.shikimori,
login = { context.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.shikimori) },
),
Preference.PreferenceItem.TrackerPreference(
title = trackerManager.bangumi.name,
tracker = trackerManager.bangumi,
login = { context.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) },
logout = { dialog = LogoutDialog(trackerManager.bangumi) },
),
Preference.PreferenceItem.InfoPreference(stringResource(R.string.tracking_info)),
),
),
Preference.PreferenceGroup(
title = stringResource(R.string.enhanced_services),
preferenceItems = enhancedTrackers.first
.map { service ->
Preference.PreferenceItem.TrackerPreference(
title = service.name,
tracker = service,
login = { (service as EnhancedTracker).loginNoop() },
logout = service::logout,
)
} + listOf(Preference.PreferenceItem.InfoPreference(enhancedTrackerInfo)),
),
)
}
@Composable
private fun TrackingLoginDialog(
tracker: Tracker,
@StringRes uNameStringRes: Int,
onDismissRequest: () -> Unit,
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
var username by remember { mutableStateOf(TextFieldValue(tracker.getUsername())) }
var password by remember { mutableStateOf(TextFieldValue(tracker.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, tracker.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(uNameStringRes)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
singleLine = true,
isError = inputError && !processing,
)
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.Filled.Visibility
} else {
Icons.Filled.VisibilityOff
},
contentDescription = null,
)
}
},
visualTransformation = if (hidePassword) {
PasswordVisualTransformation()
} else {
VisualTransformation.None
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done,
),
singleLine = true,
isError = inputError && !processing,
)
}
},
confirmButton = {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = !processing && username.text.isNotBlank() && password.text.isNotBlank(),
onClick = {
scope.launchIO {
processing = true
val result = checkLogin(
context = context,
tracker = tracker,
username = username.text,
password = password.text,
)
inputError = !result
if (result) onDismissRequest()
processing = false
}
},
) {
val id = if (processing) R.string.loading else R.string.login
Text(text = stringResource(id))
}
},
)
}
private suspend fun checkLogin(
context: Context,
tracker: Tracker,
username: String,
password: String,
): Boolean {
return try {
tracker.login(username, password)
withUIContext { context.toast(R.string.login_success) }
true
} catch (e: Throwable) {
tracker.logout()
withUIContext { context.toast(e.message.toString()) }
false
}
}
@Composable
private fun TrackingLogoutDialog(
tracker: Tracker,
onDismissRequest: () -> Unit,
) {
val context = LocalContext.current
AlertDialog(
onDismissRequest = onDismissRequest,
title = {
Text(
text = stringResource(R.string.logout_title, tracker.name),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
},
confirmButton = {
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny)) {
OutlinedButton(
modifier = Modifier.weight(1f),
onClick = onDismissRequest,
) {
Text(text = stringResource(R.string.action_cancel))
}
Button(
modifier = Modifier.weight(1f),
onClick = {
tracker.logout()
onDismissRequest()
context.toast(R.string.logout_success)
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError,
),
) {
Text(text = stringResource(R.string.logout))
}
}
},
)
}
}
private data class LoginDialog(
val tracker: Tracker,
@StringRes val uNameStringRes: Int,
)
private data class LogoutDialog(
val tracker: Tracker,
)