diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt index f547e527a..be9a10694 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt @@ -45,7 +45,7 @@ abstract class BaseActivity : protected val exceptionResolver = ExceptionResolver(this) @JvmField - protected val insetsDelegate = WindowInsetsDelegate(this) + protected val insetsDelegate = WindowInsetsDelegate() @JvmField val actionModeDelegate = ActionModeDelegate() @@ -62,6 +62,7 @@ abstract class BaseActivity : super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) insetsDelegate.handleImeInsets = true + insetsDelegate.addInsetsListener(this) putDataToExtras(intent) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt index 6dfdadf1d..0809799b9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt @@ -26,7 +26,7 @@ abstract class BaseFragment : protected val exceptionResolver = ExceptionResolver(this) @JvmField - protected val insetsDelegate = WindowInsetsDelegate(this) + protected val insetsDelegate = WindowInsetsDelegate() protected val actionModeDelegate: ActionModeDelegate get() = (requireActivity() as BaseActivity<*>).actionModeDelegate @@ -44,11 +44,13 @@ abstract class BaseFragment : final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) insetsDelegate.onViewCreated(view) + insetsDelegate.addInsetsListener(this) onViewBindingCreated(requireViewBinding(), savedInstanceState) } override fun onDestroyView() { viewBinding = null + insetsDelegate.removeInsetsListener(this) insetsDelegate.onDestroyView() super.onDestroyView() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt index 23fef7433..c450f364a 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt @@ -26,7 +26,7 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : lateinit var settings: AppSettings @JvmField - protected val insetsDelegate = WindowInsetsDelegate(this) + protected val insetsDelegate = WindowInsetsDelegate() override val recyclerView: RecyclerView get() = listView @@ -35,9 +35,11 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : super.onViewCreated(view, savedInstanceState) listView.clipToPadding = false insetsDelegate.onViewCreated(view) + insetsDelegate.addInsetsListener(this) } override fun onDestroyView() { + insetsDelegate.removeInsetsListener(this) insetsDelegate.onDestroyView() super.onDestroyView() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt index 585f39e69..2296aef53 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/ActionModeDelegate.kt @@ -14,7 +14,7 @@ class ActionModeDelegate : OnBackPressedCallback(false) { get() = activeActionMode != null override fun handleOnBackPressed() { - activeActionMode?.finish() + finishActionMode() } fun onSupportActionModeStarted(mode: ActionMode) { @@ -45,6 +45,10 @@ class ActionModeDelegate : OnBackPressedCallback(false) { owner.lifecycle.addObserver(ListenerLifecycleObserver(listener)) } + fun finishActionMode() { + activeActionMode?.finish() + } + private inner class ListenerLifecycleObserver( private val listener: ActionModeListener, ) : DefaultLifecycleObserver { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/WindowInsetsDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/WindowInsetsDelegate.kt index a85868857..a5e8c2d43 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/WindowInsetsDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/util/WindowInsetsDelegate.kt @@ -5,10 +5,9 @@ import androidx.core.graphics.Insets import androidx.core.view.OnApplyWindowInsetsListener import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import java.util.LinkedList -class WindowInsetsDelegate( - private val listener: WindowInsetsListener, -) : OnApplyWindowInsetsListener, View.OnLayoutChangeListener { +class WindowInsetsDelegate : OnApplyWindowInsetsListener, View.OnLayoutChangeListener { @JvmField var handleImeInsets: Boolean = false @@ -16,6 +15,7 @@ class WindowInsetsDelegate( @JvmField var interceptingWindowInsetsListener: OnApplyWindowInsetsListener? = null + private val listeners = LinkedList() private var lastInsets: Insets? = null override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat { @@ -29,7 +29,7 @@ class WindowInsetsDelegate( handledInsets.getInsets(WindowInsetsCompat.Type.systemBars()) } if (newInsets != lastInsets) { - listener.onWindowInsetsChanged(newInsets) + listeners.forEach { it.onWindowInsetsChanged(newInsets) } lastInsets = newInsets } return handledInsets @@ -52,6 +52,15 @@ class WindowInsetsDelegate( } } + fun addInsetsListener(listener: WindowInsetsListener) { + listeners.add(listener) + lastInsets?.let { listener.onWindowInsetsChanged(it) } + } + + fun removeInsetsListener(listener: WindowInsetsListener) { + listeners.remove(listener) + } + fun onViewCreated(view: View) { ViewCompat.setOnApplyWindowInsetsListener(view, this) view.addOnLayoutChangeListener(this) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SlidingBottomNavigationView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SlidingBottomNavigationView.kt index b34a958bf..3a95fa398 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SlidingBottomNavigationView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/SlidingBottomNavigationView.kt @@ -92,6 +92,14 @@ class SlidingBottomNavigationView @JvmOverloads constructor( ) } + fun showOrHide(show: Boolean) { + if (show) { + show() + } else { + hide() + } + } + private fun animateTranslation(targetY: Float, duration: Long, interpolator: TimeInterpolator) { currentAnimator = animate() .translationY(targetY) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt new file mode 100644 index 000000000..cc6a94502 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/ButtonTip.kt @@ -0,0 +1,92 @@ +package org.koitharu.kotatsu.details.ui + +import android.transition.TransitionManager +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.graphics.Insets +import androidx.core.view.setMargins +import androidx.core.view.updateLayoutParams +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.ui.util.WindowInsetsDelegate +import org.koitharu.kotatsu.core.util.ext.getThemeDimensionPixelSize +import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled +import org.koitharu.kotatsu.databinding.ItemTipBinding +import com.google.android.material.R as materialR + +class ButtonTip( + private val root: ViewGroup, + private val insetsDelegate: WindowInsetsDelegate, + private val viewModel: DetailsViewModel, +) : View.OnClickListener, WindowInsetsDelegate.WindowInsetsListener { + + private var selfBinding = ItemTipBinding.inflate(LayoutInflater.from(root.context), root, false) + private val actionBarSize = root.context.getThemeDimensionPixelSize(materialR.attr.actionBarSize) + + init { + selfBinding.textView.setText(R.string.details_button_tip) + selfBinding.imageViewIcon.setImageResource(R.drawable.ic_tap) + selfBinding.root.id = R.id.layout_tip + selfBinding.buttonClose.setOnClickListener(this) + } + + override fun onClick(v: View?) { + remove() + } + + override fun onWindowInsetsChanged(insets: Insets) { + if (root is CoordinatorLayout) { + selfBinding.root.updateLayoutParams { + bottomMargin = topMargin + insets.bottom + insets.top + actionBarSize + } + } + } + + fun addToRoot() { + val lp: ViewGroup.LayoutParams = when (root) { + is CoordinatorLayout -> CoordinatorLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + // anchorId = R.id.layout_bottom + // anchorGravity = Gravity.TOP + gravity = Gravity.BOTTOM + setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal)) + bottomMargin += actionBarSize + } + + is ConstraintLayout -> ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + width = root.resources.getDimensionPixelSize(R.dimen.m3_side_sheet_width) + setMargins(root.resources.getDimensionPixelOffset(R.dimen.margin_normal)) + } + + else -> ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + root.addView(selfBinding.root, lp) + if (root is ConstraintLayout) { + val cs = ConstraintSet() + cs.clone(root) + cs.connect(R.id.layout_tip, ConstraintSet.TOP, R.id.appbar, ConstraintSet.BOTTOM) + cs.connect(R.id.layout_tip, ConstraintSet.START, R.id.card_chapters, ConstraintSet.START) + cs.connect(R.id.layout_tip, ConstraintSet.END, R.id.card_chapters, ConstraintSet.END) + cs.applyTo(root) + } + insetsDelegate.addInsetsListener(this) + } + + fun remove() { + if (root.context.isAnimationsEnabled) { + TransitionManager.beginDelayedTransition(root) + } + insetsDelegate.removeInsetsListener(this) + root.removeView(selfBinding.root) + viewModel.onButtonTipClosed() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index f8d2b146e..487c843a9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -55,6 +55,7 @@ import org.koitharu.kotatsu.main.ui.owners.NoModalBottomSheetOwner import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.thumbnails.PagesThumbnailsSheet +import java.lang.ref.WeakReference import javax.inject.Inject import com.google.android.material.R as materialR @@ -70,6 +71,7 @@ class DetailsActivity : lateinit var shortcutsUpdater: ShortcutsUpdater private lateinit var viewBadge: ViewBadge + private var buttonTip: WeakReference? = null private val viewModel: DetailsViewModel by viewModels() private lateinit var chaptersMenuProvider: ChaptersMenuProvider @@ -122,6 +124,7 @@ class DetailsActivity : viewModel.onShowToast.observeEvent(this) { makeSnackbar(getString(it), Snackbar.LENGTH_SHORT).show() } + viewModel.onShowTip.observeEvent(this) { showTip() } viewModel.historyInfo.observe(this, ::onHistoryChanged) viewModel.selectedBranch.observe(this) { viewBinding.toolbarChapters?.subtitle = it @@ -162,6 +165,8 @@ class DetailsActivity : override fun onLongClick(v: View): Boolean = when (v.id) { R.id.button_read -> { + buttonTip?.get()?.remove() + buttonTip = null val menu = PopupMenu(v.context, v) menu.inflate(R.menu.popup_read) menu.setOnMenuItemClickListener(this) @@ -345,8 +350,16 @@ class DetailsActivity : } } + private fun showTip() { + val tip = ButtonTip(viewBinding.root as ViewGroup, insetsDelegate, viewModel) + tip.addToRoot() + buttonTip = WeakReference(tip) + } + companion object { + const val TIP_BUTTON = "btn_read" + fun newIntent(context: Context, manga: Manga): Intent { return Intent(context, DetailsActivity::class.java) .putExtra(MangaIntent.KEY_MANGA, ParcelableManga(manga, withChapters = true)) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index ab56e71f6..2a7f0ae5d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -78,6 +80,7 @@ class DetailsViewModel @Inject constructor( private var loadingJob: Job val onShowToast = MutableEventFlow() + val onShowTip = MutableEventFlow() val onDownloadStarted = MutableEventFlow() val manga = doubleManga.map { it?.any } @@ -191,6 +194,12 @@ class DetailsViewModel @Inject constructor( localStorageChanges .collect { onDownloadComplete(it) } } + launchJob(Dispatchers.Default) { + if (settings.isTipEnabled(DetailsActivity.TIP_BUTTON)) { + manga.filterNot { it?.chapters.isNullOrEmpty() }.first() + onShowTip.call(Unit) + } + } } fun reload() { @@ -277,6 +286,10 @@ class DetailsViewModel @Inject constructor( } } + fun onButtonTipClosed() { + settings.closeTip(DetailsActivity.TIP_BUTTON) + } + private fun doLoad() = launchLoadingJob(Dispatchers.Default) { val result = doubleMangaLoadUseCase(intent) val manga = result.requireAny() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt index 731aa12de..a46c13ca7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainActivity.kt @@ -126,8 +126,10 @@ class MainActivity : viewBinding.navRail?.headerView?.setOnClickListener(this) viewBinding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null - navigationDelegate = - MainNavigationDelegate(checkNotNull(bottomNav ?: viewBinding.navRail), supportFragmentManager) + navigationDelegate = MainNavigationDelegate( + navBar = checkNotNull(bottomNav ?: viewBinding.navRail), + fragmentManager = supportFragmentManager, + ) navigationDelegate.addOnFragmentChangedListener(this) navigationDelegate.onCreate() @@ -156,6 +158,8 @@ class MainActivity : override fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) { adjustFabVisibility(topFragment = fragment) if (fromUser) { + actionModeDelegate.finishActionMode() + closeSearchCallback.handleOnBackPressed() viewBinding.appbar.setExpanded(true) } } @@ -243,13 +247,13 @@ class MainActivity : override fun onSupportActionModeStarted(mode: ActionMode) { super.onSupportActionModeStarted(mode) adjustFabVisibility() - showNav(false) + bottomNav?.hide() } override fun onSupportActionModeFinished(mode: ActionMode) { super.onSupportActionModeFinished(mode) adjustFabVisibility() - showNav(true) + bottomNav?.show() } private fun onOpenReader(manga: Manga) { @@ -301,17 +305,6 @@ class MainActivity : closeSearchCallback.isEnabled = false } - private fun showNav(visible: Boolean) { - bottomNav?.run { - if (visible) { - show() - } else { - hide() - } - } - viewBinding.navRail?.isVisible = visible - } - private fun isSearchOpened(): Boolean { return supportFragmentManager.findFragmentByTag(TAG_SEARCH) != null } @@ -340,7 +333,7 @@ class MainActivity : } private fun adjustFabVisibility( - isResumeEnabled: Boolean = viewModel.isResumeEnabled.value == true, + isResumeEnabled: Boolean = viewModel.isResumeEnabled.value, topFragment: Fragment? = navigationDelegate.primaryFragment, isSearchOpened: Boolean = isSearchOpened(), ) { @@ -383,7 +376,7 @@ class MainActivity : supportActionBar?.setHomeAsUpIndicator( if (isOpened) materialR.drawable.abc_ic_ab_back_material else materialR.drawable.abc_ic_search_api_material, ) - showNav(!isOpened) + bottomNav?.showOrHide(!isOpened) } private fun requestNotificationsPermission() { diff --git a/app/src/main/res/drawable/ic_tap.xml b/app/src/main/res/drawable/ic_tap.xml new file mode 100644 index 000000000..d0bdf143c --- /dev/null +++ b/app/src/main/res/drawable/ic_tap.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index cc780e28f..236011efe 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -5,4 +5,5 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a110477a4..1dbc0180f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -436,4 +436,5 @@ Show the current time and reading progress at the top of the screen Show page numbers in bottom corner Animate page switching + Press and hold the Read button to see more options