diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6f2b355c61..fbceb0b03d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -277,7 +277,7 @@ dependencies { implementation(libs.bundles.conductor) // FlowBinding - implementation(libs.bundles.flowbinding) + implementation(libs.flowbinding.android) // Logging implementation(libs.logcat) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt deleted file mode 100644 index 48d6fd4c48..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt +++ /dev/null @@ -1,216 +0,0 @@ -package eu.kanade.tachiyomi.ui.base.controller - -import android.app.Activity -import android.os.Bundle -import android.text.style.CharacterStyle -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import androidx.appcompat.widget.SearchView -import androidx.core.text.getSpans -import androidx.core.widget.doAfterTextChanged -import androidx.viewbinding.ViewBinding -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import reactivecircus.flowbinding.appcompat.QueryTextEvent -import reactivecircus.flowbinding.appcompat.queryTextEvents - -/** - * Implementation of the NucleusController that has a built-in ViewSearch - */ -abstract class SearchableNucleusController>(bundle: Bundle? = null) : NucleusController(bundle) { - - enum class SearchViewState { LOADING, LOADED, COLLAPSING, FOCUSED } - - /** - * Used to bypass the initial searchView being set to empty string after an onResume - */ - private var currentSearchViewState: SearchViewState = SearchViewState.LOADING - - /** - * Store the query text that has not been submitted to reassign it after an onResume, UI-only - */ - protected var nonSubmittedQuery: String = "" - - /** - * To be called by classes that extend this subclass in onCreateOptionsMenu - */ - protected fun createOptionsMenu( - menu: Menu, - inflater: MenuInflater, - menuId: Int, - searchItemId: Int, - ) { - inflater.inflate(menuId, menu) - - // Initialize search option. - val searchItem = menu.findItem(searchItemId) - val searchView = searchItem.actionView as SearchView - searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() }) - searchView.maxWidth = Int.MAX_VALUE - - // Remove formatting from pasted text - val searchAutoComplete: SearchView.SearchAutoComplete = searchView.findViewById( - R.id.search_src_text, - ) - searchAutoComplete.doAfterTextChanged { editable -> - editable?.getSpans()?.forEach { editable.removeSpan(it) } - } - - searchView.queryTextEvents() - .onEach { - val newText = it.queryText.toString() - - if (newText.isNotBlank() or acceptEmptyQuery()) { - if (it is QueryTextEvent.QuerySubmitted) { - // Abstract function for implementation - // Run it first in case the old query data is needed (like BrowseSourceController) - onSearchViewQueryTextSubmit(newText) - presenter.query = newText - nonSubmittedQuery = "" - } else if ((it is QueryTextEvent.QueryChanged) && (presenter.query != newText)) { - nonSubmittedQuery = newText - - // Abstract function for implementation - onSearchViewQueryTextChange(newText) - } - } - // clear the collapsing flag - setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.COLLAPSING) - } - .launchIn(viewScope) - - val query = presenter.query - - // Restoring a query the user had not submitted - if (nonSubmittedQuery.isNotBlank() and (nonSubmittedQuery != query)) { - searchItem.expandActionView() - searchView.setQuery(nonSubmittedQuery, false) - onSearchViewQueryTextChange(nonSubmittedQuery) - } - - // Workaround for weird behavior where searchView gets empty text change despite - // query being set already, prevents the query from being cleared - binding.root.post { - setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.LOADING) - } - - searchView.setOnQueryTextFocusChangeListener { _, hasFocus -> - if (hasFocus) { - setCurrentSearchViewState(SearchViewState.FOCUSED) - } else { - setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.FOCUSED) - } - } - - searchItem.setOnActionExpandListener( - object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - onSearchMenuItemActionExpand(item) - return true - } - - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - val localSearchView = searchItem.actionView as SearchView - - // if it is blank the flow event won't trigger so we would stay in a COLLAPSING state - if (localSearchView.toString().isNotBlank()) { - setCurrentSearchViewState(SearchViewState.COLLAPSING) - } - - onSearchMenuItemActionCollapse(item) - return true - } - }, - ) - } - - override fun onActivityResumed(activity: Activity) { - super.onActivityResumed(activity) - // Until everything is up and running don't accept empty queries - setCurrentSearchViewState(SearchViewState.LOADING) - } - - private fun acceptEmptyQuery(): Boolean { - return when (currentSearchViewState) { - SearchViewState.COLLAPSING, SearchViewState.FOCUSED -> true - else -> false - } - } - - private fun setCurrentSearchViewState(to: SearchViewState, from: SearchViewState? = null) { - // When loading ignore all requests other than loaded - if ((currentSearchViewState == SearchViewState.LOADING) && (to != SearchViewState.LOADED)) { - return - } - - // Prevent changing back to an unwanted state when using async flows (ie onFocus event doing - // COLLAPSING -> LOADED) - if ((from != null) && (currentSearchViewState != from)) { - return - } - - currentSearchViewState = to - } - - /** - * Called by the SearchView since since the implementation of these can vary in subclasses - * Not abstract as they are optional - */ - protected open fun onSearchViewQueryTextChange(newText: String?) { - } - - protected open fun onSearchViewQueryTextSubmit(query: String?) { - } - - protected open fun onSearchMenuItemActionExpand(item: MenuItem?) { - } - - protected open fun onSearchMenuItemActionCollapse(item: MenuItem?) { - } - - /** - * Workaround for buggy menu item layout after expanding/collapsing an expandable item like a SearchView. - * This method should be removed when fixed upstream. - * Issue link: https://issuetracker.google.com/issues/37657375 - */ - private var expandActionViewFromInteraction = false - - private fun MenuItem.fixExpand(onExpand: ((MenuItem) -> Boolean)? = null, onCollapse: ((MenuItem) -> Boolean)? = null) { - setOnActionExpandListener( - object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem): Boolean { - return onExpand?.invoke(item) ?: true - } - - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - activity?.invalidateOptionsMenu() - - return onCollapse?.invoke(item) ?: true - } - }, - ) - - if (expandActionViewFromInteraction) { - expandActionViewFromInteraction = false - expandActionView() - } - } - - /** - * During the conversion to SearchableNucleusController (after which I plan to merge its code - * into BaseController) this addresses an issue where the searchView.onTextFocus event is not - * triggered - */ - private fun invalidateMenuOnExpand(): Boolean { - return if (expandActionViewFromInteraction) { - activity?.invalidateOptionsMenu() - setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here - false - } else { - true - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt index 2a373aad4e..b8796d38fa 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/presenter/BasePresenter.kt @@ -7,17 +7,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import nucleus.presenter.RxPresenter -import rx.Observable open class BasePresenter : RxPresenter() { var presenterScope: CoroutineScope = MainScope() - /** - * Query from the view where applicable - */ - var query: String = "" - override fun onCreate(savedState: Bundle?) { try { super.onCreate(savedState) @@ -39,13 +33,4 @@ open class BasePresenter : RxPresenter() { } fun Preference.asState() = PreferenceMutableState(this, presenterScope) - - /** - * Subscribes an observable with [deliverLatestCache] and adds it to the presenter's lifecycle - * subscription list. - * - * @param onNext function to execute when the observable emits an item. - * @param onError function to execute when the observable throws an error. - */ - fun Observable.subscribeLatestCache(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit) = { _, _ -> }) = compose(deliverLatestCache()).subscribe(split(onNext, onError)).apply { add(this) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 32c45b3399..113d68f4b8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -898,6 +898,15 @@ class ReaderPresenter( */ private fun Observable.subscribeFirst(onNext: (ReaderActivity, T) -> Unit, onError: ((ReaderActivity, Throwable) -> Unit) = { _, _ -> }) = compose(deliverFirst()).subscribe(split(onNext, onError)).apply { add(this) } + /** + * Subscribes an observable with [deliverLatestCache] and adds it to the presenter's lifecycle + * subscription list. + * + * @param onNext function to execute when the observable emits an item. + * @param onError function to execute when the observable throws an error. + */ + private fun Observable.subscribeLatestCache(onNext: (ReaderActivity, T) -> Unit, onError: ((ReaderActivity, Throwable) -> Unit) = { _, _ -> }) = compose(deliverLatestCache()).subscribe(split(onNext, onError)).apply { add(this) } + companion object { // Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8) private const val MAX_FILE_NAME_BYTES = 250 diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt deleted file mode 100644 index 70076d6696..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/AutofitRecyclerView.kt +++ /dev/null @@ -1,46 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.content.Context -import android.util.AttributeSet -import androidx.core.content.withStyledAttributes -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlin.math.max - -class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - RecyclerView(context, attrs) { - - private val manager = GridLayoutManager(context, 1) - - private var columnWidth = -1 - - var spanCount = 0 - set(value) { - field = value - if (value > 0) { - manager.spanCount = value - } - } - - val itemWidth: Int - get() = measuredWidth / manager.spanCount - - init { - if (attrs != null) { - val attrsArray = intArrayOf(android.R.attr.columnWidth) - context.withStyledAttributes(attrs, attrsArray) { - columnWidth = getDimensionPixelSize(0, -1) - } - } - - layoutManager = manager - } - - override fun onMeasure(widthSpec: Int, heightSpec: Int) { - super.onMeasure(widthSpec, heightSpec) - if (spanCount == 0 && columnWidth > 0) { - val count = max(1, measuredWidth / columnWidth) - spanCount = count - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt deleted file mode 100644 index ae8d2b2032..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/DialogCheckboxView.kt +++ /dev/null @@ -1,31 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.LinearLayout -import androidx.annotation.StringRes -import eu.kanade.tachiyomi.databinding.CommonDialogWithCheckboxBinding - -class DialogCheckboxView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - LinearLayout(context, attrs) { - - private val binding: CommonDialogWithCheckboxBinding - - init { - binding = CommonDialogWithCheckboxBinding.inflate(LayoutInflater.from(context), this, false) - addView(binding.root) - } - - fun setDescription(@StringRes id: Int) { - binding.description.text = context.getString(id) - } - - fun setOptionDescription(@StringRes id: Int) { - binding.checkboxOption.text = context.getString(id) - } - - fun isChecked(): Boolean { - return binding.checkboxOption.isChecked - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt deleted file mode 100644 index 2e4d63b53a..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/EmptyView.kt +++ /dev/null @@ -1,47 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.content.Context -import android.util.AttributeSet -import androidx.annotation.StringRes -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.AbstractComposeView -import androidx.core.view.isVisible -import eu.kanade.presentation.components.EmptyScreen -import eu.kanade.presentation.theme.TachiyomiTheme - -class EmptyView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : - AbstractComposeView(context, attrs) { - - var message by mutableStateOf("") - - /** - * Hide the information view - */ - fun hide() { - this.isVisible = false - } - - /** - * Show the information view - * @param textResource text of information view - */ - fun show(@StringRes textResource: Int) { - message = context.getString(textResource) - this.isVisible = true - } - - @Composable - override fun Content() { - TachiyomiTheme { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) { - EmptyScreen(message = message) - } - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiSearchView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiSearchView.kt deleted file mode 100644 index 5cf3d2c4c4..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiSearchView.kt +++ /dev/null @@ -1,64 +0,0 @@ -package eu.kanade.tachiyomi.widget - -import android.content.Context -import android.util.AttributeSet -import android.view.inputmethod.EditorInfo -import androidx.appcompat.widget.SearchView -import androidx.core.view.inputmethod.EditorInfoCompat -import eu.kanade.domain.base.BasePreferences -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.util.preference.asHotFlow -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.launchIn -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -/** - * A custom [SearchView] that sets [EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING] to imeOptions - * if [BasePreferences.incognitoMode] is true. Some IMEs may not respect this flag. - */ -class TachiyomiSearchView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = R.attr.searchViewStyle, -) : SearchView(context, attrs, defStyleAttr) { - - private var scope: CoroutineScope? = null - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) - Injekt.get().incognitoMode() - .asHotFlow { - imeOptions = if (it) { - imeOptions or EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING - } else { - imeOptions and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv() - } - } - .launchIn(scope!!) - } - - override fun setOnQueryTextListener(listener: OnQueryTextListener?) { - super.setOnQueryTextListener(listener) - val searchAutoComplete: SearchAutoComplete = findViewById(R.id.search_src_text) - searchAutoComplete.setOnEditorActionListener { _, actionID, _ -> - if (actionID == EditorInfo.IME_ACTION_SEARCH || actionID == EditorInfo.IME_NULL) { - clearFocus() - listener?.onQueryTextSubmit(query.toString()) - true - } else { - false - } - } - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - scope?.cancel() - scope = null - } -} diff --git a/app/src/main/res/layout/common_dialog_with_checkbox.xml b/app/src/main/res/layout/common_dialog_with_checkbox.xml deleted file mode 100644 index 5a4f3fe9d0..0000000000 --- a/app/src/main/res/layout/common_dialog_with_checkbox.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 82a5010d9b..0053985145 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,6 @@ okhttp_version = "5.0.0-alpha.10" nucleus_version = "3.0.0" coil_version = "2.2.2" conductor_version = "3.1.8" -flowbinding_version = "1.2.0" shizuku_version = "12.2.0" sqldelight = "1.5.4" leakcanary = "2.10" @@ -68,8 +67,7 @@ wheelpicker = "com.github.commandiron:WheelPickerCompose:1.0.11" conductor-core = { module = "com.bluelinelabs:conductor", version.ref = "conductor_version" } conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version.ref = "conductor_version" } -flowbinding-android = { module = "io.github.reactivecircus.flowbinding:flowbinding-android", version.ref = "flowbinding_version" } -flowbinding-appcompat = { module = "io.github.reactivecircus.flowbinding:flowbinding-appcompat", version.ref = "flowbinding_version" } +flowbinding-android = "io.github.reactivecircus.flowbinding:flowbinding-android:1.2.0" logcat = "com.squareup.logcat:logcat:0.1" @@ -102,7 +100,6 @@ js-engine = ["quickjs-android"] sqlite = ["sqlitektx", "sqlite-android"] nucleus = ["nucleus-core", "nucleus-supportv7"] coil = ["coil-core", "coil-gif", "coil-compose"] -flowbinding = ["flowbinding-android", "flowbinding-appcompat"] conductor = ["conductor-core", "conductor-support-preference"] shizuku = ["shizuku-api", "shizuku-provider"] voyager = ["voyager-navigator", "voyager-transitions"]