diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c507e5e6db..60cd6b7425 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -273,9 +273,6 @@ dependencies { implementation(libs.bundles.voyager) implementation(libs.wheelpicker) - // FlowBinding - implementation(libs.flowbinding.android) - // Logging implementation(libs.logcat) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index b6edb4a0fe..77f351aeaa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.util.system import android.app.ActivityManager -import android.app.KeyguardManager -import android.app.Notification import android.app.NotificationManager import android.content.ClipData import android.content.ClipboardManager @@ -24,11 +22,9 @@ import android.util.TypedValue import android.view.Display import android.view.View import android.view.WindowManager -import android.view.inputmethod.InputMethodManager import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.appcompat.view.ContextThemeWrapper -import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.graphics.alpha @@ -75,34 +71,6 @@ fun Context.copyToClipboard(label: String, content: String) { } } -/** - * Helper method to create a notification builder. - * - * @param id the channel id. - * @param block the function that will execute inside the builder. - * @return a notification to be displayed or updated. - */ -fun Context.notificationBuilder(channelId: String, block: (NotificationCompat.Builder.() -> Unit)? = null): NotificationCompat.Builder { - val builder = NotificationCompat.Builder(this, channelId) - .setColor(getColor(R.color.accent_blue)) - if (block != null) { - builder.block() - } - return builder -} - -/** - * Helper method to create a notification. - * - * @param id the channel id. - * @param block the function that will execute inside the builder. - * @return a notification to be displayed or updated. - */ -fun Context.notification(channelId: String, block: (NotificationCompat.Builder.() -> Unit)?): Notification { - val builder = notificationBuilder(channelId, block) - return builder.build() -} - /** * Checks if the give permission is granted. * @@ -146,12 +114,6 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio val getDisplayMaxHeightInPx: Int get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) } -/** - * Converts to dp. - */ -val Int.pxToDp: Int - get() = (this / Resources.getSystem().displayMetrics.density).toInt() - /** * Converts to px. */ @@ -182,12 +144,6 @@ val Context.wifiManager: WifiManager val Context.powerManager: PowerManager get() = getSystemService()!! -val Context.keyguardManager: KeyguardManager - get() = getSystemService()!! - -val Context.inputMethodManager: InputMethodManager - get() = getSystemService()!! - val Context.displayCompat: Display? get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { display diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt index 70e44d645d..19ef75a8b2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/NotificationExtensions.kt @@ -1,7 +1,11 @@ package eu.kanade.tachiyomi.util.system +import android.app.Notification +import android.content.Context import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationChannelGroupCompat +import androidx.core.app.NotificationCompat +import eu.kanade.tachiyomi.R /** * Helper method to build a notification channel group. @@ -36,3 +40,31 @@ fun buildNotificationChannel( builder.block() return builder.build() } + +/** + * Helper method to create a notification builder. + * + * @param id the channel id. + * @param block the function that will execute inside the builder. + * @return a notification to be displayed or updated. + */ +fun Context.notificationBuilder(channelId: String, block: (NotificationCompat.Builder.() -> Unit)? = null): NotificationCompat.Builder { + val builder = NotificationCompat.Builder(this, channelId) + .setColor(getColor(R.color.accent_blue)) + if (block != null) { + builder.block() + } + return builder +} + +/** + * Helper method to create a notification. + * + * @param id the channel id. + * @param block the function that will execute inside the builder. + * @return a notification to be displayed or updated. + */ +fun Context.notification(channelId: String, block: (NotificationCompat.Builder.() -> Unit)?): Notification { + val builder = notificationBuilder(channelId, block) + return builder.build() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt index 3f6623cea8..dfeb7281e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ImageViewExtensions.kt @@ -1,19 +1,9 @@ package eu.kanade.tachiyomi.util.view -import android.content.Context -import android.graphics.drawable.Animatable -import android.graphics.drawable.ColorDrawable import android.widget.ImageView import androidx.annotation.AttrRes import androidx.annotation.DrawableRes import androidx.appcompat.content.res.AppCompatResources -import coil.ImageLoader -import coil.imageLoader -import coil.load -import coil.request.ImageRequest -import coil.target.ImageViewTarget -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.system.animatorDurationScale import eu.kanade.tachiyomi.util.system.getResourceColor /** @@ -29,33 +19,3 @@ fun ImageView.setVectorCompat(@DrawableRes drawable: Int, @AttrRes tint: Int? = } setImageDrawable(vector) } - -/** - * Load the image referenced by [data] and set it on this [ImageView], - * and if the image is animated, this will also disable that animation - * if [Context.animatorDurationScale] is 0 - */ -fun ImageView.loadAutoPause( - data: Any?, - loader: ImageLoader = context.imageLoader, - builder: ImageRequest.Builder.() -> Unit = {}, -) { - load(data, loader) { - placeholder(ColorDrawable(context.getColor(R.color.cover_placeholder))) - error(R.drawable.cover_error) - - // Build the original request so we can add on our success listener - val originalListener = apply(builder).build().listener - listener( - onSuccess = { request, metadata -> - (request.target as? ImageViewTarget)?.drawable.let { - if (it is Animatable && context.animatorDurationScale == 0f) it.stop() - } - originalListener?.onSuccess(request, metadata) - }, - onStart = { request -> originalListener?.onStart(request) }, - onCancel = { request -> originalListener?.onCancel(request) }, - onError = { request, throwable -> originalListener?.onError(request, throwable) }, - ) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 66cc885036..14b0f76d39 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -11,7 +11,6 @@ import android.view.Gravity import android.view.Menu import android.view.MenuItem import android.view.View -import android.view.ViewGroup import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.MenuRes @@ -29,15 +28,11 @@ import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.core.view.children -import androidx.core.view.descendants import androidx.core.view.forEach import com.google.android.material.shape.MaterialShapeDrawable -import com.google.android.material.snackbar.Snackbar import eu.kanade.presentation.theme.TachiyomiTheme import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.getResourceColor -import eu.kanade.tachiyomi.util.system.inputMethodManager inline fun ComposeView.setComposeContent(crossinline content: @Composable () -> Unit) { consumeWindowInsets = false @@ -70,24 +65,6 @@ inline fun ComponentActivity.setComposeContent( } } -/** - * Shows a snackbar in this view. - * - * @param message the message to show. - * @param length the duration of the snack. - * @param f a function to execute in the snack, allowing for example to define a custom action. - */ -inline fun View.snack( - message: String, - length: Int = 10_000, - f: Snackbar.() -> Unit = {}, -): Snackbar { - val snack = Snackbar.make(this, message, length) - snack.f() - snack.show() - return snack -} - /** * Adds a tooltip shown on long press. * @@ -173,20 +150,6 @@ inline fun View.popupMenu( return popup } -/** - * Returns this ViewGroup's first child of specified class - */ -inline fun ViewGroup.findChild(): T? { - return children.find { it is T } as? T -} - -/** - * Returns this ViewGroup's first descendant of specified class - */ -inline fun ViewGroup.findDescendant(): T? { - return descendants.find { it is T } as? T -} - /** * Returns a deep copy of the provided [Drawable] */ @@ -210,7 +173,3 @@ fun View?.isVisibleOnScreen(): Boolean { val screen = Rect(0, 0, Resources.getSystem().displayMetrics.widthPixels, Resources.getSystem().displayMetrics.heightPixels) return actualPosition.intersect(screen) } - -fun View.hideKeyboard() { - context.inputMethodManager.hideSoftInputFromWindow(windowToken, 0) -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiAppBarLayout.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiAppBarLayout.kt deleted file mode 100644 index eb89e027fc..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiAppBarLayout.kt +++ /dev/null @@ -1,214 +0,0 @@ -@file:Suppress("PackageDirectoryMismatch") - -package com.google.android.material.appbar - -import android.animation.AnimatorSet -import android.animation.ValueAnimator -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Canvas -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.animation.LinearInterpolator -import android.widget.TextView -import androidx.annotation.FloatRange -import androidx.core.graphics.withTranslation -import androidx.lifecycle.coroutineScope -import androidx.lifecycle.findViewTreeLifecycleOwner -import com.google.android.material.shape.MaterialShapeDrawable -import dev.chrisbanes.insetter.applyInsetter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.view.findChild -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import reactivecircus.flowbinding.android.view.HierarchyChangeEvent -import reactivecircus.flowbinding.android.view.hierarchyChangeEvents - -/** - * [AppBarLayout] with our own lift state handler and custom title alpha. - * - * Inside this package to access some package-private methods. - */ -class TachiyomiAppBarLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, -) : AppBarLayout(context, attrs) { - - private var lifted = true - - private val toolbar by lazy { findViewById(R.id.toolbar) } - - @FloatRange(from = 0.0, to = 1.0) - var titleTextAlpha = 1F - set(value) { - field = value - titleTextView?.alpha = field - } - - private var titleTextView: TextView? = null - set(value) { - field = value - field?.alpha = titleTextAlpha - } - - private var animatorSet: AnimatorSet? = null - - private var statusBarForegroundAnimator: ValueAnimator? = null - private var currentOffset = 0 - - var isTransparentWhenNotLifted = false - set(value) { - if (field != value) { - field = value - updateStates() - } - } - - /** - * Disabled. Lift on scroll is handled manually with [eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout] - */ - override fun isLiftOnScroll(): Boolean = false - - override fun isLifted(): Boolean = lifted - - override fun setLifted(lifted: Boolean): Boolean { - return if (this.lifted != lifted) { - this.lifted = lifted - updateStates() - true - } else { - false - } - } - - override fun setLiftedState(lifted: Boolean, force: Boolean): Boolean = false - - override fun draw(canvas: Canvas) { - super.draw(canvas) - canvas.withTranslation(y = -currentOffset.toFloat()) { - statusBarForeground?.draw(this) - } - } - - override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { - super.onLayout(changed, l, t, r, b) - statusBarForeground?.setBounds(0, 0, width, paddingTop) - } - - override fun onOffsetChanged(offset: Int) { - currentOffset = offset - super.onOffsetChanged(offset) - - // Show status bar foreground when offset - val foreground = (statusBarForeground as? MaterialShapeDrawable) ?: return - val start = foreground.alpha - val end = if (offset != 0) 255 else 0 - - statusBarForegroundAnimator?.cancel() - if (animatorSet?.isRunning == true) { - foreground.alpha = end - return - } - if (start != end) { - statusBarForegroundAnimator = ValueAnimator.ofInt(start, end).apply { - duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong() - interpolator = LINEAR_INTERPOLATOR - addUpdateListener { - foreground.alpha = it.animatedValue as Int - } - start() - } - } - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - toolbar.background.alpha = 0 // Use app bar background - - titleTextView = toolbar.findChild() - findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.let { scope -> - toolbar.hierarchyChangeEvents() - .onEach { - when (it) { - is HierarchyChangeEvent.ChildAdded -> { - if (it.child is TextView) { - titleTextView = it.child as TextView - } - } - is HierarchyChangeEvent.ChildRemoved -> { - if (it.child == titleTextView) { - titleTextView = null - } - } - } - } - .launchIn(scope) - } - } - - override fun setStatusBarForeground(drawable: Drawable?) { - super.setStatusBarForeground(drawable) - setWillNotDraw(statusBarForeground == null) - } - - @SuppressLint("Recycle") - private fun updateStates() { - val animators = mutableListOf() - - val fromElevation = elevation - val toElevation = if (lifted) { - resources.getDimension(R.dimen.design_appbar_elevation) - } else { - 0F - } - if (fromElevation != toElevation) { - ValueAnimator.ofFloat(fromElevation, toElevation).apply { - addUpdateListener { - elevation = it.animatedValue as Float - (statusBarForeground as? MaterialShapeDrawable)?.elevation = it.animatedValue as Float - } - animators.add(this) - } - } - - val transparent = if (lifted) false else isTransparentWhenNotLifted - val fromAlpha = (background as? MaterialShapeDrawable)?.alpha ?: background.alpha - val toAlpha = if (transparent) 0 else 255 - if (fromAlpha != toAlpha) { - ValueAnimator.ofInt(fromAlpha, toAlpha).apply { - addUpdateListener { - val value = it.animatedValue as Int - background.alpha = value - } - animators.add(this) - } - } - - if (animators.isNotEmpty()) { - animatorSet?.cancel() - animatorSet = AnimatorSet().apply { - duration = resources.getInteger(R.integer.app_bar_elevation_anim_duration).toLong() - interpolator = LINEAR_INTERPOLATOR - playTogether(*animators.toTypedArray()) - start() - } - } - } - - init { - statusBarForeground = MaterialShapeDrawable.createWithElevationOverlay(context) - applyInsetter { - type(navigationBars = true) { - margin(horizontal = true) - } - type(statusBars = true) { - padding(top = true) - } - ignoreVisibility(true) - } - } - - companion object { - private val LINEAR_INTERPOLATOR = LinearInterpolator() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiCoordinatorLayout.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiCoordinatorLayout.kt deleted file mode 100644 index 2ad8326f31..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiCoordinatorLayout.kt +++ /dev/null @@ -1,130 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.content.Context -import android.os.Parcel -import android.os.Parcelable -import android.util.AttributeSet -import android.view.View -import androidx.compose.ui.platform.ComposeView -import androidx.coordinatorlayout.R -import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.view.ViewCompat -import androidx.core.view.doOnLayout -import androidx.core.view.isVisible -import androidx.customview.view.AbsSavedState -import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.tabs.TabLayout -import eu.kanade.tachiyomi.util.system.isTabletUi -import eu.kanade.tachiyomi.util.view.findChild - -/** - * [CoordinatorLayout] with its own app bar lift state handler. - * This parent view checks for the app bar lift state from the following: - * - * 1. When nested scroll detected, lift state will be decided from the nested - * scroll target. (See [onNestedScroll]) - * - * With those conditions, this view expects the following direct child: - * - * 1. An [AppBarLayout]. - */ -class TachiyomiCoordinatorLayout @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.coordinatorLayoutStyle, -) : CoordinatorLayout(context, attrs, defStyleAttr) { - - private var appBarLayout: AppBarLayout? = null - private var tabLayout: TabLayout? = null - - override fun onNestedScroll( - target: View, - dxConsumed: Int, - dyConsumed: Int, - dxUnconsumed: Int, - dyUnconsumed: Int, - type: Int, - consumed: IntArray, - ) { - super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed) - // Disable elevation overlay when tabs are visible - if (context.isTabletUi().not()) { - if (target is ComposeView) { - val scrollCondition = if (type == ViewCompat.TYPE_NON_TOUCH) { - dyUnconsumed >= 0 - } else { - dyConsumed != 0 || dyUnconsumed >= 0 - } - appBarLayout?.isLifted = scrollCondition && tabLayout?.isVisible == false - } else { - appBarLayout?.isLifted = (dyConsumed != 0 || dyUnconsumed >= 0) && tabLayout?.isVisible == false - } - } - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - appBarLayout = findChild() - tabLayout = appBarLayout?.findChild() - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - appBarLayout = null - tabLayout = null - } - - override fun onSaveInstanceState(): Parcelable? { - val superState = super.onSaveInstanceState() - return if (superState != null) { - SavedState(superState).also { - it.appBarLifted = appBarLayout?.isLifted ?: false - } - } else { - superState - } - } - - override fun onRestoreInstanceState(state: Parcelable?) { - if (state is SavedState) { - super.onRestoreInstanceState(state.superState) - doOnLayout { - appBarLayout?.isLifted = state.appBarLifted - } - } else { - super.onRestoreInstanceState(state) - } - } - - internal class SavedState : AbsSavedState { - var appBarLifted = false - - constructor(superState: Parcelable) : super(superState) - - constructor(source: Parcel, loader: ClassLoader?) : super(source, loader) { - appBarLifted = source.readByte().toInt() == 1 - } - - override fun writeToParcel(out: Parcel, flags: Int) { - super.writeToParcel(out, flags) - out.writeByte((if (appBarLifted) 1 else 0).toByte()) - } - - companion object { - @JvmField - val CREATOR: Parcelable.ClassLoaderCreator = object : Parcelable.ClassLoaderCreator { - override fun createFromParcel(source: Parcel, loader: ClassLoader): SavedState { - return SavedState(source, loader) - } - - override fun createFromParcel(source: Parcel): SavedState { - return SavedState(source, null) - } - - override fun newArray(size: Int): Array { - return newArray(size) - } - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiScrollingViewBehavior.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiScrollingViewBehavior.kt deleted file mode 100644 index a9ec76cef5..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiScrollingViewBehavior.kt +++ /dev/null @@ -1,15 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import com.google.android.material.appbar.AppBarLayout - -/** - * [AppBarLayout.ScrollingViewBehavior] that lets the app bar overlaps the scrolling child. - */ -class TachiyomiScrollingViewBehavior : AppBarLayout.ScrollingViewBehavior() { - - var shouldHeaderOverlap = false - - override fun shouldHeaderOverlapScrollingChild(): Boolean { - return shouldHeaderOverlap - } -} diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 2b415d0481..6381ba4ec1 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -39,4 +39,4 @@ workmanager = ["work-runtime", "guava"] [plugins] application = { id = "com.android.application", version.ref = "agp_version" } library = { id = "com.android.library", version.ref = "agp_version" } -test = { id = "com.android.test", version.ref = "agp_version"} +test = { id = "com.android.test", version.ref = "agp_version" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 17327c299e..56e3526140 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,8 +65,6 @@ insetter = "dev.chrisbanes.insetter:insetter:0.6.1" cascade = "me.saket.cascade:cascade-compose:2.0.0-beta1" wheelpicker = "com.github.commandiron:WheelPickerCompose:1.0.11" -flowbinding-android = "io.github.reactivecircus.flowbinding:flowbinding-android:1.2.0" - logcat = "com.squareup.logcat:logcat:0.1" acra-http = "ch.acra:acra-http:5.9.7"