diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/CheckableImageButton.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/CheckableImageButton.kt new file mode 100644 index 000000000..eb646729a --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/CheckableImageButton.kt @@ -0,0 +1,97 @@ +package org.koitharu.kotatsu.core.ui.widgets + +import android.content.Context +import android.os.Parcel +import android.os.Parcelable +import android.os.Parcelable.Creator +import android.util.AttributeSet +import android.widget.Checkable +import androidx.annotation.AttrRes +import androidx.appcompat.widget.AppCompatImageButton +import androidx.core.os.ParcelCompat +import androidx.customview.view.AbsSavedState + +class CheckableImageButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + @AttrRes defStyleAttr: Int = 0, +) : AppCompatImageButton(context, attrs, defStyleAttr), Checkable { + + private var isCheckedInternal = false + private var isBroadcasting = false + + var onCheckedChangeListener: OnCheckedChangeListener? = null + + override fun isChecked() = isCheckedInternal + + override fun toggle() { + isChecked = !isCheckedInternal + } + + override fun setChecked(checked: Boolean) { + if (checked != isCheckedInternal) { + isCheckedInternal = checked + refreshDrawableState() + if (!isBroadcasting) { + isBroadcasting = true + onCheckedChangeListener?.onCheckedChanged(this, checked) + isBroadcasting = false + } + } + } + + override fun onCreateDrawableState(extraSpace: Int): IntArray { + val state = super.onCreateDrawableState(extraSpace + 1) + if (isCheckedInternal) { + mergeDrawableStates(state, intArrayOf(android.R.attr.state_checked)) + } + return state + } + + override fun onSaveInstanceState(): Parcelable? { + val superState = super.onSaveInstanceState() ?: return null + return SavedState(superState, isChecked) + } + + override fun onRestoreInstanceState(state: Parcelable?) { + if (state is SavedState) { + super.onRestoreInstanceState(state.superState) + isChecked = state.isChecked + } else { + super.onRestoreInstanceState(state) + } + } + + fun interface OnCheckedChangeListener { + + fun onCheckedChanged(view: CheckableImageButton, isChecked: Boolean) + } + + private class SavedState : AbsSavedState { + + val isChecked: Boolean + + constructor(superState: Parcelable, checked: Boolean) : super(superState) { + isChecked = checked + } + + constructor(source: Parcel, classLoader: ClassLoader?) : super(source, classLoader) { + isChecked = ParcelCompat.readBoolean(source) + } + + override fun writeToParcel(out: Parcel, flags: Int) { + super.writeToParcel(out, flags) + ParcelCompat.writeBoolean(out, isChecked) + } + + companion object { + @Suppress("unused") + @JvmField + val CREATOR: Creator = object : Creator { + override fun createFromParcel(`in`: Parcel) = SavedState(`in`, SavedState::class.java.classLoader) + + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt index 607f9f732..51b4d2cbb 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/View.kt @@ -6,8 +6,10 @@ import android.view.View import android.view.View.MeasureSpec import android.view.ViewGroup import android.widget.Checkable +import androidx.annotation.StringRes import androidx.appcompat.widget.ActionMenuView import androidx.appcompat.widget.Toolbar +import androidx.appcompat.widget.TooltipCompat import androidx.core.view.children import androidx.core.view.descendants import androidx.core.view.isVisible @@ -192,3 +194,9 @@ fun Chip.setProgressIcon() { chipIcon = progressDrawable progressDrawable.start() } + +fun View.setContentDescriptionAndTooltip(@StringRes resId: Int) { + val text = resources.getString(resId) + contentDescription = text + TooltipCompat.setTooltipText(this, text) +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt index f4ffdecd7..460b25666 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.launch import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.ui.BaseListAdapter import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe +import org.koitharu.kotatsu.core.util.ext.setContentDescriptionAndTooltip import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.databinding.ItemDownloadBinding import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter @@ -41,7 +42,7 @@ fun downloadItemAD( R.id.button_skip -> listener.onSkipClick(item) R.id.button_skip_all -> listener.onSkipAllClick(item) R.id.button_pause -> listener.onPauseClick(item) - R.id.imageView_expand -> listener.onExpandClick(item) + R.id.button_expand -> listener.onExpandClick(item) else -> listener.onItemClick(item, v) } } @@ -59,7 +60,7 @@ fun downloadItemAD( binding.buttonResume.setOnClickListener(clickListener) binding.buttonSkip.setOnClickListener(clickListener) binding.buttonSkipAll.setOnClickListener(clickListener) - binding.imageViewExpand.setOnClickListener(clickListener) + binding.buttonExpand.setOnClickListener(clickListener) itemView.setOnClickListener(clickListener) itemView.setOnLongClickListener(clickListener) @@ -83,7 +84,7 @@ fun downloadItemAD( chaptersJob?.cancel() chaptersJob = lifecycleOwner.lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) { item.chapters.collect { chapters -> - binding.imageViewExpand.isGone = chapters.isNullOrEmpty() + binding.buttonExpand.isGone = chapters.isNullOrEmpty() chaptersAdapter.emit(chapters) scrollToCurrentChapter() } @@ -93,7 +94,8 @@ fun downloadItemAD( scrollToCurrentChapter() } } - binding.imageViewExpand.isChecked = item.isExpanded + binding.buttonExpand.isChecked = item.isExpanded + binding.buttonExpand.setContentDescriptionAndTooltip(if (item.isExpanded) R.string.collapse else R.string.expand) binding.recyclerViewChapters.isVisible = item.isExpanded when (item.workState) { WorkInfo.State.ENQUEUED, 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 66a90f640..c9e02187a 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 @@ -120,6 +120,9 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav ) navigationDelegate.addOnFragmentChangedListener(this) navigationDelegate.onCreate(this, savedInstanceState) + viewBinding.textViewTitle?.let { tv -> + navigationDelegate.observeTitle().observe(this) { tv.text = it } + } addMenuProvider(MainMenuProvider(router, viewModel)) @@ -409,7 +412,7 @@ class MainActivity : BaseActivity(), AppBarOwner, BottomNav } } - private fun SearchView.observeState() = callbackFlow { + private fun SearchView.observeState() = callbackFlow { val listener = SearchView.TransitionListener { _, _, state -> trySendBlocking(state) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt index 021b3b14e..066e9bc88 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/main/ui/MainNavigationDelegate.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.main.ui import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.View import androidx.activity.OnBackPressedCallback import androidx.annotation.IdRes import androidx.core.view.isEmpty @@ -13,11 +14,16 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.google.android.material.navigation.NavigationBarView +import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.transition.MaterialFadeThrough import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.R @@ -26,7 +32,9 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.NavItem import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView +import org.koitharu.kotatsu.core.util.ext.setContentDescriptionAndTooltip import org.koitharu.kotatsu.core.util.ext.smoothScrollToTop +import org.koitharu.kotatsu.databinding.NavigationRailFabBinding import org.koitharu.kotatsu.explore.ui.ExploreFragment import org.koitharu.kotatsu.favourites.ui.container.FavouritesContainerFragment import org.koitharu.kotatsu.history.ui.HistoryListFragment @@ -45,9 +53,12 @@ class MainNavigationDelegate( private val settings: AppSettings, ) : OnBackPressedCallback(false), NavigationBarView.OnItemSelectedListener, - NavigationBarView.OnItemReselectedListener { + NavigationBarView.OnItemReselectedListener, View.OnClickListener { private val listeners = LinkedList() + private val navRailHeader = (navBar as? NavigationRailView)?.headerView?.let { + NavigationRailFabBinding.bind(it) + } val primaryFragment: Fragment? get() = fragmentManager.findFragmentByTag(TAG_PRIMARY) @@ -55,6 +66,14 @@ class MainNavigationDelegate( init { navBar.setOnItemSelectedListener(this) navBar.setOnItemReselectedListener(this) + navRailHeader?.run { + val horizontalPadding = (navBar as NavigationRailView).itemActiveIndicatorExpandedMarginHorizontal + root.setPadding(horizontalPadding, 0, horizontalPadding, 0) + buttonExpand.setOnClickListener(this@MainNavigationDelegate) + buttonExpand.setContentDescriptionAndTooltip(R.string.expand) + railFab.isExtended = false + railFab.isAnimationEnabled = false + } } override fun onNavigationItemSelected(item: MenuItem): Boolean { @@ -70,6 +89,30 @@ class MainNavigationDelegate( onNavigationItemReselected() } + override fun onClick(v: View) { + when (v.id) { + R.id.button_expand -> { + if (navBar is NavigationRailView) { + if (navBar.isExpanded) { + navBar.collapse() + navRailHeader?.run { + railFab.shrink() + buttonExpand.setImageResource(R.drawable.ic_drawer_menu) + buttonExpand.setContentDescriptionAndTooltip(R.string.expand) + } + } else { + navBar.expand() + navRailHeader?.run { + railFab.extend() + buttonExpand.setImageResource(R.drawable.ic_drawer_menu_open) + buttonExpand.setContentDescriptionAndTooltip(R.string.collapse) + } + } + } + } + } + } + override fun handleOnBackPressed() { navBar.selectedItemId = firstItem()?.itemId ?: return } @@ -96,6 +139,16 @@ class MainNavigationDelegate( } } + fun observeTitle() = callbackFlow { + val listener = OnFragmentChangedListener { f, _ -> + trySendBlocking(getItemId(f)) + } + addOnFragmentChangedListener(listener) + awaitClose { removeOnFragmentChangedListener(listener) } + }.map { + navBar.menu.findItem(it)?.title + } + fun setCounter(item: NavItem, counter: Int) { setCounter(item.id, counter) } @@ -248,7 +301,7 @@ class MainNavigationDelegate( } } - interface OnFragmentChangedListener { + fun interface OnFragmentChangedListener { fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActionsView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActionsView.kt index 77da043ca..f51b91c6b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActionsView.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/ui/ReaderActionsView.kt @@ -12,9 +12,7 @@ import android.widget.Button import android.widget.FrameLayout import android.widget.LinearLayout import androidx.annotation.AttrRes -import androidx.annotation.StringRes import androidx.appcompat.widget.TooltipCompat -import androidx.core.view.ViewCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.google.android.material.slider.Slider @@ -25,6 +23,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ReaderControl import org.koitharu.kotatsu.core.util.ext.hasVisibleChildren import org.koitharu.kotatsu.core.util.ext.isRtl +import org.koitharu.kotatsu.core.util.ext.setContentDescriptionAndTooltip import org.koitharu.kotatsu.core.util.ext.setValueRounded import org.koitharu.kotatsu.databinding.LayoutReaderActionsBinding import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesSheet @@ -134,6 +133,7 @@ class ReaderActionsView @JvmOverloads constructor( override fun onLongClick(v: View): Boolean = when (v.id) { R.id.button_bookmark -> AppRouter.from(this) ?.showChapterPagesSheet(ChaptersPagesSheet.TAB_BOOKMARKS) + R.id.button_timer -> listener?.onScrollTimerClick(isLongClick = true) R.id.button_options -> AppRouter.from(this)?.openReaderSettings() else -> null @@ -206,7 +206,7 @@ class ReaderActionsView @JvmOverloads constructor( button.setIconResource( if (isPagesMode) R.drawable.ic_grid else R.drawable.ic_list, ) - button.setTitle( + button.setContentDescriptionAndTooltip( if (isPagesMode) R.string.pages else R.string.chapters, ) } @@ -216,7 +216,7 @@ class ReaderActionsView @JvmOverloads constructor( button.setIconResource( if (isBookmarkAdded) R.drawable.ic_bookmark_added else R.drawable.ic_bookmark, ) - button.setTitle( + button.setContentDescriptionAndTooltip( if (isBookmarkAdded) R.string.bookmark_remove else R.string.bookmark_add, ) } @@ -240,12 +240,12 @@ class ReaderActionsView @JvmOverloads constructor( when { !button.isVisible -> return isAutoRotationEnabled() -> { - button.setTitle(R.string.lock_screen_rotation) + button.setContentDescriptionAndTooltip(R.string.lock_screen_rotation) button.setIconResource(R.drawable.ic_screen_rotation_lock) } else -> { - button.setTitle(R.string.rotate_screen) + button.setContentDescriptionAndTooltip(R.string.rotate_screen) button.setIconResource(R.drawable.ic_screen_rotation) } } @@ -257,12 +257,6 @@ class ReaderActionsView @JvmOverloads constructor( TooltipCompat.setTooltipText(this, contentDescription) } - private fun Button.setTitle(@StringRes titleResId: Int) { - val text = resources.getString(titleResId) - contentDescription = text - TooltipCompat.setTooltipText(this, text) - } - private fun isAutoRotationEnabled(): Boolean = Settings.System.getInt( context.contentResolver, Settings.System.ACCELEROMETER_ROTATION, diff --git a/app/src/main/res/drawable/ic_drawer_menu.xml b/app/src/main/res/drawable/ic_drawer_menu.xml new file mode 100644 index 000000000..66b87ab98 --- /dev/null +++ b/app/src/main/res/drawable/ic_drawer_menu.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_drawer_menu_open.xml b/app/src/main/res/drawable/ic_drawer_menu_open.xml new file mode 100644 index 000000000..603857b16 --- /dev/null +++ b/app/src/main/res/drawable/ic_drawer_menu_open.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout-w600dp-land/activity_main.xml b/app/src/main/res/layout-w600dp-land/activity_main.xml index 38666b154..2953d29a5 100644 --- a/app/src/main/res/layout-w600dp-land/activity_main.xml +++ b/app/src/main/res/layout-w600dp-land/activity_main.xml @@ -12,6 +12,7 @@ android:id="@+id/navRail" android:layout_width="wrap_content" android:layout_height="match_parent" + android:clipToPadding="false" android:fitsSystemWindows="false" app:elevation="1dp" app:headerLayout="@layout/navigation_rail_fab" @@ -60,6 +61,16 @@ android:layout_height="wrap_content" app:layout_scrollFlags="scroll|enterAlways|snap"> + + - + app:constraint_referenced_ids="imageView_cover,textView_status,button_expand,textView_details" /> diff --git a/app/src/main/res/layout/navigation_rail_fab.xml b/app/src/main/res/layout/navigation_rail_fab.xml index 74caa5107..83299a0eb 100644 --- a/app/src/main/res/layout/navigation_rail_fab.xml +++ b/app/src/main/res/layout/navigation_rail_fab.xml @@ -1,9 +1,29 @@ - + android:layout_gravity="top|start" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="vertical"> + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b16dbafca..c8a98087f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -840,4 +840,6 @@ Hide from main screen Changelog Changes history for recently released versions + Collapse + Expand