mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
feat(TV UI): Accounts PIN login support (#1123)
This commit is contained in:
parent
b702b7b1ec
commit
afa178a63a
13 changed files with 286 additions and 17 deletions
|
@ -217,6 +217,7 @@ dependencies {
|
||||||
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
|
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
|
||||||
implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
|
implementation ("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
|
||||||
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
|
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
|
||||||
|
implementation("io.github.g0dkar:qrcode-kotlin:4.1.1") // QR code for PIN Auth on TV
|
||||||
|
|
||||||
// Extensions & Other Libs
|
// Extensions & Other Libs
|
||||||
implementation("org.mozilla:rhino:1.7.15") // run JavaScript
|
implementation("org.mozilla:rhino:1.7.15") // run JavaScript
|
||||||
|
|
|
@ -5,7 +5,23 @@ import androidx.fragment.app.FragmentActivity
|
||||||
interface OAuth2API : AuthAPI {
|
interface OAuth2API : AuthAPI {
|
||||||
val key: String
|
val key: String
|
||||||
val redirectUrl: String
|
val redirectUrl: String
|
||||||
|
val supportDeviceAuth: Boolean
|
||||||
|
|
||||||
suspend fun handleRedirect(url: String) : Boolean
|
suspend fun handleRedirect(url: String) : Boolean
|
||||||
fun authenticate(activity: FragmentActivity?)
|
fun authenticate(activity: FragmentActivity?)
|
||||||
|
suspend fun getDevicePin() : PinAuthData? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun handleDeviceAuth(pinAuthData: PinAuthData) : Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PinAuthData(
|
||||||
|
val deviceCode: String,
|
||||||
|
val userCode: String,
|
||||||
|
val verificationUrl: String,
|
||||||
|
val expiresIn: Int,
|
||||||
|
val interval: Int,
|
||||||
|
)
|
||||||
}
|
}
|
|
@ -32,6 +32,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override val redirectUrl = "anilistlogin"
|
override val redirectUrl = "anilistlogin"
|
||||||
override val idPrefix = "anilist"
|
override val idPrefix = "anilist"
|
||||||
override var requireLibraryRefresh = true
|
override var requireLibraryRefresh = true
|
||||||
|
override val supportDeviceAuth = false
|
||||||
override var mainUrl = "https://anilist.co"
|
override var mainUrl = "https://anilist.co"
|
||||||
override val icon = R.drawable.ic_anilist_icon
|
override val icon = R.drawable.ic_anilist_icon
|
||||||
override val requiresLogin = false
|
override val requiresLogin = false
|
||||||
|
|
|
@ -11,6 +11,7 @@ class Dropbox : OAuth2API {
|
||||||
override val key = "zlqsamadlwydvb2"
|
override val key = "zlqsamadlwydvb2"
|
||||||
override val redirectUrl = "dropboxlogin"
|
override val redirectUrl = "dropboxlogin"
|
||||||
override val requiresLogin = true
|
override val requiresLogin = true
|
||||||
|
override val supportDeviceAuth = false
|
||||||
override val createAccountUrl: String? = null
|
override val createAccountUrl: String? = null
|
||||||
|
|
||||||
override val icon: Int
|
override val icon: Int
|
||||||
|
|
|
@ -21,6 +21,7 @@ class LocalList : SyncAPI {
|
||||||
override val name = "Local"
|
override val name = "Local"
|
||||||
override val icon: Int = R.drawable.ic_baseline_storage_24
|
override val icon: Int = R.drawable.ic_baseline_storage_24
|
||||||
override val requiresLogin = false
|
override val requiresLogin = false
|
||||||
|
override val supportDeviceAuth = false
|
||||||
override val createAccountUrl: Nothing? = null
|
override val createAccountUrl: Nothing? = null
|
||||||
override val idPrefix = "local"
|
override val idPrefix = "local"
|
||||||
override var requireLibraryRefresh = true
|
override var requireLibraryRefresh = true
|
||||||
|
|
|
@ -40,6 +40,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
private val apiUrl = "https://api.myanimelist.net"
|
private val apiUrl = "https://api.myanimelist.net"
|
||||||
override val icon = R.drawable.mal_logo
|
override val icon = R.drawable.mal_logo
|
||||||
override val requiresLogin = false
|
override val requiresLogin = false
|
||||||
|
override val supportDeviceAuth = false
|
||||||
override val syncIdName = SyncIdName.MyAnimeList
|
override val syncIdName = SyncIdName.MyAnimeList
|
||||||
override var requireLibraryRefresh = true
|
override var requireLibraryRefresh = true
|
||||||
override val createAccountUrl = "$mainUrl/register.php"
|
override val createAccountUrl = "$mainUrl/register.php"
|
||||||
|
|
|
@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
import com.lagradost.cloudstream3.ui.SyncWatchType
|
import com.lagradost.cloudstream3.ui.SyncWatchType
|
||||||
|
@ -45,6 +46,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
override var name = "Simkl"
|
override var name = "Simkl"
|
||||||
override val key = "simkl-key"
|
override val key = "simkl-key"
|
||||||
override val redirectUrl = "simkl"
|
override val redirectUrl = "simkl"
|
||||||
|
override val supportDeviceAuth = true
|
||||||
override val idPrefix = "simkl"
|
override val idPrefix = "simkl"
|
||||||
override var requireLibraryRefresh = true
|
override var requireLibraryRefresh = true
|
||||||
override var mainUrl = "https://api.simkl.com"
|
override var mainUrl = "https://api.simkl.com"
|
||||||
|
@ -267,6 +269,21 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class PinAuthResponse(
|
||||||
|
@JsonProperty("result") val result: String,
|
||||||
|
@JsonProperty("device_code") val deviceCode: String,
|
||||||
|
@JsonProperty("user_code") val userCode: String,
|
||||||
|
@JsonProperty("verification_url") val verificationUrl: String,
|
||||||
|
@JsonProperty("expires_in") val expiresIn: Int,
|
||||||
|
@JsonProperty("interval") val interval: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PinExchangeResponse(
|
||||||
|
@JsonProperty("result") val result: String,
|
||||||
|
@JsonProperty("message") val message: String? = null,
|
||||||
|
@JsonProperty("access_token") val accessToken: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
// -------------------
|
// -------------------
|
||||||
data class ActivitiesResponse(
|
data class ActivitiesResponse(
|
||||||
val all: String?,
|
val all: String?,
|
||||||
|
@ -1045,6 +1062,44 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
return simklUrlRegex.find(url)?.groupValues?.get(1) ?: ""
|
return simklUrlRegex.find(url)?.groupValues?.get(1) ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getDevicePin(): OAuth2API.PinAuthData? {
|
||||||
|
val pinAuthResp = app.get(
|
||||||
|
"$mainUrl/oauth/pin?client_id=$clientId&redirect_uri=$appString://${redirectUrl}"
|
||||||
|
).parsedSafe<PinAuthResponse>() ?: return null
|
||||||
|
|
||||||
|
return OAuth2API.PinAuthData(
|
||||||
|
deviceCode = pinAuthResp.deviceCode,
|
||||||
|
userCode = pinAuthResp.userCode,
|
||||||
|
verificationUrl = pinAuthResp.verificationUrl,
|
||||||
|
expiresIn = pinAuthResp.expiresIn,
|
||||||
|
interval = pinAuthResp.interval
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun handleDeviceAuth(pinAuthData: OAuth2API.PinAuthData): Boolean {
|
||||||
|
val pinAuthResp = app.get(
|
||||||
|
"$mainUrl/oauth/pin/${pinAuthData.userCode}?client_id=$clientId"
|
||||||
|
).parsedSafe<PinExchangeResponse>() ?: return false
|
||||||
|
|
||||||
|
if (pinAuthResp.accessToken != null) {
|
||||||
|
switchToNewAccount()
|
||||||
|
setKey(accountId, SIMKL_TOKEN_KEY, pinAuthResp.accessToken)
|
||||||
|
|
||||||
|
val user = getUser()
|
||||||
|
if (user == null) {
|
||||||
|
removeKey(accountId, SIMKL_TOKEN_KEY)
|
||||||
|
switchToOldAccount()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
setKey(accountId, SIMKL_USER_KEY, user)
|
||||||
|
registerAccount()
|
||||||
|
requireLibraryRefresh = true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun handleRedirect(url: String): Boolean {
|
override suspend fun handleRedirect(url: String): Boolean {
|
||||||
val uri = url.toUri()
|
val uri = url.toUri()
|
||||||
val state = uri.getQueryParameter("state")
|
val state = uri.getQueryParameter("state")
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.ui.result
|
package com.lagradost.cloudstream3.ui.result
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -84,12 +85,14 @@ sealed class UiImage {
|
||||||
) : UiImage()
|
) : UiImage()
|
||||||
|
|
||||||
data class Drawable(@DrawableRes val resId: Int) : UiImage()
|
data class Drawable(@DrawableRes val resId: Int) : UiImage()
|
||||||
|
data class Bitmap(val bitmap: android.graphics.Bitmap) : UiImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) {
|
fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) {
|
||||||
when (value) {
|
when (value) {
|
||||||
is UiImage.Image -> setImageImage(value, fadeIn)
|
is UiImage.Image -> setImageImage(value, fadeIn)
|
||||||
is UiImage.Drawable -> setImageDrawable(value)
|
is UiImage.Drawable -> setImageDrawable(value)
|
||||||
|
is UiImage.Bitmap -> setImageBitmap(value)
|
||||||
null -> {
|
null -> {
|
||||||
this?.isVisible = false
|
this?.isVisible = false
|
||||||
}
|
}
|
||||||
|
@ -107,6 +110,12 @@ fun ImageView?.setImageDrawable(value: UiImage.Drawable) {
|
||||||
this.setImage(UiImage.Drawable(value.resId))
|
this.setImage(UiImage.Drawable(value.resId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ImageView?.setImageBitmap(value: UiImage.Bitmap) {
|
||||||
|
if (this == null) return
|
||||||
|
this.isVisible = true
|
||||||
|
this.setImageBitmap(value.bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmName("imgNull")
|
@JvmName("imgNull")
|
||||||
fun img(
|
fun img(
|
||||||
url: String?,
|
url: String?,
|
||||||
|
@ -129,6 +138,10 @@ fun img(@DrawableRes drawable: Int): UiImage {
|
||||||
return UiImage.Drawable(drawable)
|
return UiImage.Drawable(drawable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun img(bitmap: Bitmap): UiImage {
|
||||||
|
return UiImage.Bitmap(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
fun txt(value: String): UiText {
|
fun txt(value: String): UiText {
|
||||||
return UiText.DynamicString(value)
|
return UiText.DynamicString(value)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package com.lagradost.cloudstream3.ui.settings
|
package com.lagradost.cloudstream3.ui.settings
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.CountDownTimer
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.*
|
import android.view.View.FOCUS_DOWN
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.UiThread
|
import androidx.annotation.UiThread
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.toBitmapOrNull
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
@ -21,6 +25,7 @@ import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.AccountManagmentBinding
|
import com.lagradost.cloudstream3.databinding.AccountManagmentBinding
|
||||||
import com.lagradost.cloudstream3.databinding.AccountSwitchBinding
|
import com.lagradost.cloudstream3.databinding.AccountSwitchBinding
|
||||||
import com.lagradost.cloudstream3.databinding.AddAccountInputBinding
|
import com.lagradost.cloudstream3.databinding.AddAccountInputBinding
|
||||||
|
import com.lagradost.cloudstream3.databinding.DeviceAuthBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||||
|
@ -31,6 +36,10 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subDlAp
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
import com.lagradost.cloudstream3.syncproviders.OAuth2API
|
||||||
|
import com.lagradost.cloudstream3.ui.result.img
|
||||||
|
import com.lagradost.cloudstream3.ui.result.setImage
|
||||||
|
import com.lagradost.cloudstream3.ui.result.setText
|
||||||
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
@ -51,9 +60,13 @@ import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
|
||||||
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
|
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
|
import qrcode.QRCode
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback {
|
class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.BiometricAuthCallback {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -134,7 +147,109 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome
|
||||||
try {
|
try {
|
||||||
when (api) {
|
when (api) {
|
||||||
is OAuth2API -> {
|
is OAuth2API -> {
|
||||||
|
if (isLayout(PHONE) || !api.supportDeviceAuth) {
|
||||||
api.authenticate(activity)
|
api.authenticate(activity)
|
||||||
|
} else if (api.supportDeviceAuth && activity != null) {
|
||||||
|
|
||||||
|
val binding: DeviceAuthBinding =
|
||||||
|
DeviceAuthBinding.inflate(activity.layoutInflater, null, false)
|
||||||
|
|
||||||
|
val builder =
|
||||||
|
AlertDialog.Builder(activity)
|
||||||
|
.setView(binding.root)
|
||||||
|
|
||||||
|
builder.apply {
|
||||||
|
setNegativeButton(R.string.cancel) { _, _ -> }
|
||||||
|
setPositiveButton(R.string.auth_locally) { _, _ ->
|
||||||
|
api.authenticate(activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
|
||||||
|
ioSafe {
|
||||||
|
try {
|
||||||
|
val pinCodeData = api.getDevicePin()
|
||||||
|
if (pinCodeData == null) {
|
||||||
|
showToast(R.string.device_pin_error_message)
|
||||||
|
api.authenticate(activity)
|
||||||
|
return@ioSafe
|
||||||
|
}
|
||||||
|
|
||||||
|
/*val logoBytes = ContextCompat.getDrawable(
|
||||||
|
activity,
|
||||||
|
R.drawable.cloud_2_solid
|
||||||
|
)?.toBitmapOrNull()?.let { bitmap ->
|
||||||
|
val csLogo = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, csLogo)
|
||||||
|
csLogo.toByteArray()
|
||||||
|
}*/
|
||||||
|
|
||||||
|
val qrCodeImage = QRCode.ofRoundedSquares()
|
||||||
|
.withColor(activity.colorFromAttribute(R.attr.textColor))
|
||||||
|
.withBackgroundColor(activity.colorFromAttribute(R.attr.primaryBlackBackground))
|
||||||
|
//.withLogo(logoBytes, 200.toPx, 200.toPx) //For later if logo needed anytime
|
||||||
|
.build(pinCodeData.verificationUrl)
|
||||||
|
.render().nativeImage() as Bitmap
|
||||||
|
|
||||||
|
activity.runOnUiThread {
|
||||||
|
dialog.show()
|
||||||
|
binding.apply {
|
||||||
|
devicePinCode.setText(txt(pinCodeData.userCode))
|
||||||
|
deviceAuthMessage.setText(
|
||||||
|
txt(
|
||||||
|
R.string.device_pin_url_message,
|
||||||
|
pinCodeData.verificationUrl
|
||||||
|
)
|
||||||
|
)
|
||||||
|
deviceAuthQrcode.setImage(
|
||||||
|
img(qrCodeImage)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val expirationMillis =
|
||||||
|
pinCodeData.expiresIn.times(1000).toLong()
|
||||||
|
|
||||||
|
object : CountDownTimer(expirationMillis, 1000) {
|
||||||
|
|
||||||
|
override fun onTick(millisUntilFinished: Long) {
|
||||||
|
val secondsUntilFinished =
|
||||||
|
millisUntilFinished.div(1000).toInt()
|
||||||
|
|
||||||
|
binding.deviceAuthValidationCounter.setText(
|
||||||
|
txt(
|
||||||
|
R.string.device_pin_counter_text,
|
||||||
|
secondsUntilFinished.div(60),
|
||||||
|
secondsUntilFinished.rem(60)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ioSafe {
|
||||||
|
if (secondsUntilFinished.rem(pinCodeData.interval) == 0 && api.handleDeviceAuth(pinCodeData)) {
|
||||||
|
showToast(
|
||||||
|
txt(
|
||||||
|
R.string.authenticated_user,
|
||||||
|
api.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dialog.dismissSafe(activity)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFinish() {
|
||||||
|
showToast(R.string.device_pin_expired_message)
|
||||||
|
dialog.dismissSafe(activity)
|
||||||
|
}
|
||||||
|
|
||||||
|
}.start()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is InAppAuthAPI -> {
|
is InAppAuthAPI -> {
|
||||||
|
@ -227,23 +342,15 @@ class SettingsAccount : PreferenceFragmentCompat(), BiometricAuthenticator.Biome
|
||||||
server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null,
|
server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null,
|
||||||
)
|
)
|
||||||
ioSafe {
|
ioSafe {
|
||||||
val isSuccessful = try {
|
|
||||||
api.login(loginData)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
activity.runOnUiThread {
|
|
||||||
try {
|
try {
|
||||||
showToast(
|
showToast(
|
||||||
activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail)
|
txt(
|
||||||
.format(
|
if (api.login(loginData)) R.string.authenticated_user else R.string.authenticated_user_fail,
|
||||||
api.name
|
api.name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e) // format might fail
|
logError(e)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dialog.dismissSafe(activity)
|
dialog.dismissSafe(activity)
|
||||||
|
|
8
app/src/main/res/drawable/cloud_2_solid.xml
Normal file
8
app/src/main/res/drawable/cloud_2_solid.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<vector xmlns:tools="http://schemas.android.com/tools" android:name="vector"
|
||||||
|
android:width="200dp" android:height="200dp" android:viewportWidth="283"
|
||||||
|
android:viewportHeight="283" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:name="path"
|
||||||
|
android:pathData="M 245.05 148.63 C 242.249 148.627 239.463 149.052 236.79 149.89 C 235.151 141.364 230.698 133.63 224.147 127.931 C 217.597 122.233 209.321 118.893 200.65 118.45 C 195.913 105.431 186.788 94.458 174.851 87.427 C 162.914 80.396 148.893 77.735 135.21 79.905 C 121.527 82.074 109.017 88.941 99.84 99.32 C 89.871 95.945 79.051 96.024 69.133 99.545 C 59.215 103.065 50.765 109.826 45.155 118.73 C 39.545 127.634 37.094 138.174 38.2 148.64 L 37.94 148.64 C 30.615 148.64 23.582 151.553 18.403 156.733 C 13.223 161.912 10.31 168.945 10.31 176.27 C 10.31 183.595 13.223 190.628 18.403 195.807 C 23.582 200.987 30.615 203.9 37.94 203.9 L 245.05 203.9 C 252.375 203.9 259.408 200.987 264.587 195.807 C 269.767 190.628 272.68 183.595 272.68 176.27 C 272.68 168.945 269.767 161.912 264.587 156.733 C 259.408 151.553 252.375 148.64 245.05 148.64 Z"
|
||||||
|
android:fillColor="?attr/textColor" android:strokeWidth="1"
|
||||||
|
tools:ignore="VectorPath"/>
|
||||||
|
</vector>
|
BIN
app/src/main/res/drawable/example_qr.png
Normal file
BIN
app/src/main/res/drawable/example_qr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
59
app/src/main/res/layout/device_auth.xml
Normal file
59
app/src/main/res/layout/device_auth.xml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/device_pin_code"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:textColor="?attr/colorPrimary"
|
||||||
|
android:textSize="30sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="YJTSKL" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/device_auth_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:textColor="?attr/textColor"
|
||||||
|
android:textSize="17sp"
|
||||||
|
tools:text="Visit simkl.com/pin on your smartphone or computer and enter the above code" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/device_auth_qrcode"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_width="200dp"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:visibility="visible"
|
||||||
|
android:background="?attr/primaryGrayBackground"
|
||||||
|
android:contentDescription="@string/qr_image"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:src="@drawable/example_qr" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/device_auth_validation_counter"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="20dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
|
||||||
|
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
|
||||||
|
android:textColor="?attr/textColor"
|
||||||
|
android:textSize="17sp"
|
||||||
|
tools:text="Code expires in 13m 2s" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -497,6 +497,7 @@
|
||||||
<string name="account">account</string>
|
<string name="account">account</string>
|
||||||
<string name="logout">Log out</string>
|
<string name="logout">Log out</string>
|
||||||
<string name="login">Log in</string>
|
<string name="login">Log in</string>
|
||||||
|
<string name="auth_locally">Auth Locally</string>
|
||||||
<string name="switch_account">Switch account</string>
|
<string name="switch_account">Switch account</string>
|
||||||
<string name="add_account">Add account</string>
|
<string name="add_account">Add account</string>
|
||||||
<string name="create_account">Create account</string>
|
<string name="create_account">Create account</string>
|
||||||
|
@ -562,6 +563,7 @@
|
||||||
<string name="quality_sdr">SDR</string>
|
<string name="quality_sdr">SDR</string>
|
||||||
<string name="quality_webrip">Web</string>
|
<string name="quality_webrip">Web</string>
|
||||||
<string name="poster_image">Poster Image</string>
|
<string name="poster_image">Poster Image</string>
|
||||||
|
<string name="qr_image">QR Code Image</string>
|
||||||
<string name="category_player">Player</string>
|
<string name="category_player">Player</string>
|
||||||
<string name="resolution_and_title">Resolution and title</string>
|
<string name="resolution_and_title">Resolution and title</string>
|
||||||
<string name="title">Title</string>
|
<string name="title">Title</string>
|
||||||
|
@ -780,4 +782,8 @@
|
||||||
<string name="custom_media_singluar">Media</string>
|
<string name="custom_media_singluar">Media</string>
|
||||||
<string name="reset_btn">Reset</string>
|
<string name="reset_btn">Reset</string>
|
||||||
<string name="cs3wiki">CloudStream Wiki</string>
|
<string name="cs3wiki">CloudStream Wiki</string>
|
||||||
|
<string name="device_pin_url_message">Visit <b>%s</b> on your smartphone or computer and enter the above code</string>
|
||||||
|
<string name="device_pin_error_message">Can\'t get the device PIN code, try local authentication</string>
|
||||||
|
<string name="device_pin_expired_message">PIN code is now expired !</string>
|
||||||
|
<string name="device_pin_counter_text">Code expires in %1$dm %2$ds</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue