diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a9c48cdcd1..f09339783c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -235,7 +235,6 @@ dependencies { implementation(libs.aboutlibraries) // UI - implementation(libs.loading.button) implementation(libs.fastadapter) implementation(libs.fastadapter.extensions.binding) implementation(libs.flexible.adapter) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt index ec94a99b03..b4cc177c1e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt @@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.widget.preference import android.app.Dialog import android.os.Bundle import android.view.View -import androidx.annotation.StringRes import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType import dev.icerock.moko.resources.StringResource @@ -53,7 +52,7 @@ abstract class LoginDialogPreference( binding.usernameInput.hint = view.context.getString(usernameLabelRes) } - binding.login.setOnClickListener { + binding.login.setOnClick { checkLogin() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt index 3ce1c6a5df..6d57a8bbd4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/TrackLoginDialog.kt @@ -2,13 +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 dev.icerock.moko.resources.StringResource -import eu.kanade.tachiyomi.R -import yokai.i18n.MR -import yokai.util.lang.getString -import dev.icerock.moko.resources.compose.stringResource import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.util.system.toast @@ -16,6 +10,8 @@ import eu.kanade.tachiyomi.util.system.withIOContext import kotlinx.coroutines.launch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import yokai.i18n.MR +import yokai.util.lang.getString class TrackLoginDialog(usernameLabelRes: StringResource? = null, bundle: Bundle? = null) : LoginDialogPreference(usernameLabelRes, bundle) { @@ -36,10 +32,7 @@ class TrackLoginDialog(usernameLabelRes: StringResource? = null, bundle: Bundle? override fun checkLogin() { v?.apply { - binding.login.apply { - progressType = ProgressType.INDETERMINATE - startAnimation() - } + binding.login.startAnimation() if (binding.username.text.isNullOrBlank() || binding.password.text.isNullOrBlank()) { errorResult() context.toast(MR.strings.username_must_not_be_blank) diff --git a/app/src/main/java/yokai/presentation/component/LoadingButton.kt b/app/src/main/java/yokai/presentation/component/LoadingButton.kt new file mode 100644 index 0000000000..7868757108 --- /dev/null +++ b/app/src/main/java/yokai/presentation/component/LoadingButton.kt @@ -0,0 +1,154 @@ +package yokai.presentation.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +// REF: https://gist.github.com/mmolosay/584ce5c47567cb66228b76ef98c3c4e4 + +private val SpringStiffness = Spring.StiffnessMediumLow + +@Composable +fun LoadingButton( + text: () -> String, + loading: () -> Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val transition = updateTransition( + targetState = loading(), + label = "master transition", + ) + val horizontalContentPadding by transition.animateDp( + transitionSpec = { + spring( + stiffness = SpringStiffness, + ) + }, + targetValueByState = { toLoading -> if (toLoading) 12.dp else 24.dp }, + label = "button's content padding", + ) + Button( + onClick = onClick, + modifier = modifier.defaultMinSize(minWidth = 1.dp), + contentPadding = PaddingValues( + horizontal = horizontalContentPadding, + vertical = 8.dp, + ), + ) { + Box(contentAlignment = Alignment.Center) { + LoadingContent( + loadingStateTransition = transition, + ) + PrimaryContent( + text = text(), + loadingStateTransition = transition, + ) + } + } +} + +@Composable +private fun LoadingContent( + loadingStateTransition: Transition, +) { + loadingStateTransition.AnimatedVisibility( + visible = { loading -> loading }, + enter = fadeIn(), + exit = fadeOut( + animationSpec = spring( + stiffness = SpringStiffness, + visibilityThreshold = 0.10f, + ), + ), + ) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + color = LocalContentColor.current, + strokeWidth = 1.5f.dp, + strokeCap = StrokeCap.Round, + ) + } +} + +@Composable +private fun PrimaryContent( + text: String, + loadingStateTransition: Transition, +) { + loadingStateTransition.AnimatedVisibility( + visible = { loading -> !loading }, + enter = fadeIn() + expandHorizontally( + animationSpec = spring( + stiffness = SpringStiffness, + dampingRatio = Spring.DampingRatioMediumBouncy, + visibilityThreshold = IntSize.VisibilityThreshold, + ), + expandFrom = Alignment.CenterHorizontally, + ), + exit = fadeOut( + animationSpec = spring( + stiffness = SpringStiffness, + visibilityThreshold = 0.10f, + ), + ) + shrinkHorizontally( + animationSpec = spring( + stiffness = SpringStiffness, + // dampingRatio is not applicable here, size cannot become negative + visibilityThreshold = IntSize.VisibilityThreshold, + ), + shrinkTowards = Alignment.CenterHorizontally, + ), + ) { + Text( + text = text, + modifier = Modifier + // so that bouncing button's width doesn't cut first and last letters + .padding(horizontal = 4.dp), + fontSize = 16.sp, + ) + } +} + +@Preview +@Composable +private fun LoadingButtonPreview() { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + LoadingButton( + modifier = Modifier.fillMaxWidth(), + text = { "Test" }, + loading = { false }, + onClick = {}, + ) + } +} diff --git a/app/src/main/java/yokai/presentation/component/LoadingButtonComposeView.kt b/app/src/main/java/yokai/presentation/component/LoadingButtonComposeView.kt new file mode 100644 index 0000000000..02313df144 --- /dev/null +++ b/app/src/main/java/yokai/presentation/component/LoadingButtonComposeView.kt @@ -0,0 +1,61 @@ +package yokai.presentation.component + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import eu.kanade.tachiyomi.R +import yokai.presentation.theme.YokaiTheme + +class LoadingButtonComposeView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : AbstractComposeView(context, attrs, defStyleAttr) { + + var text by mutableStateOf("placeholder") + private var onClick: () -> Unit = {} + private var isLoading by mutableStateOf(false) + + init { + layoutParams = FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER) + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindowOrReleasedFromPool) + attrs?.let { + val arr = context.obtainStyledAttributes(it, R.styleable.LoadingButtonComposeView) + text = context.getString(arr.getResourceId(R.styleable.LoadingButtonComposeView_android_text, R.string.log_in)) + } + } + + fun setOnClick(onClick: () -> Unit) { + this.onClick = onClick + } + + fun startAnimation() { + isLoading = true + } + + fun revertAnimation(after: () -> Unit = {}) { + isLoading = false + after() + } + + @Composable + override fun Content() { + YokaiTheme { + LoadingButton( + modifier = Modifier.fillMaxWidth(), + text = { text }, + loading = { isLoading }, + onClick = onClick, + ) + } + } +} diff --git a/app/src/main/res/layout/pref_account_login.xml b/app/src/main/res/layout/pref_account_login.xml index 9f7b757415..91f9c7ef35 100644 --- a/app/src/main/res/layout/pref_account_login.xml +++ b/app/src/main/res/layout/pref_account_login.xml @@ -49,21 +49,12 @@ android:inputType="textPassword" /> - - + android:text="@string/log_in" /> - \ No newline at end of file + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 7a70c56694..ca68c7d574 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -49,6 +49,10 @@ + + + + true 1 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3db2731347..2a020aa7ca 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -100,6 +100,8 @@ #99CC99 #106010 + #000 + #CC95818D #CCA6CFD5 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ab413631c1..fe94e11494 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,6 @@ jsoup = { module = "org.jsoup:jsoup", version = "1.17.1" } junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-android = { module = "androidx.test.ext:junit", version = "1.1.5" } -loading-button = { module = "com.github.leandroBorgesFerreira:LoadingButtonAndroid", version = "2.2.0" } # FIXME: Don't depends on this mockk = { module = "io.mockk:mockk", version = "1.13.11" } moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko" }