Reorganize navigation

This commit is contained in:
Koitharu
2023-07-03 17:19:42 +03:00
parent fb674b6028
commit 4739da2774
33 changed files with 418 additions and 136 deletions

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.core.ui
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import kotlin.coroutines.suspendCoroutine
abstract class BaseListAdapter : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback),
FlowCollector<List<ListModel>> {
override suspend fun emit(value: List<ListModel>) = suspendCoroutine { cont ->
setItems(value, ContinuationResumeRunnable(cont))
}
fun addListListener(listListener: ListListener<ListModel>) {
differ.addListListener(listListener)
}
fun removeListListener(listListener: ListListener<ListModel>) {
differ.removeListListener(listListener)
}
}

View File

@@ -4,7 +4,6 @@ import android.content.res.Resources
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.core.util.ext.format
import org.koitharu.kotatsu.list.ui.model.ListModel
import java.util.Date
sealed class DateTimeAgo {
@@ -77,6 +76,7 @@ sealed class DateTimeAgo {
}
class DaysAgo(val days: Int) : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.days_ago, days, days)
}
@@ -93,6 +93,30 @@ sealed class DateTimeAgo {
override fun toString() = "days_ago_$days"
}
class MonthsAgo(val months: Int) : DateTimeAgo() {
override fun format(resources: Resources): String {
return if (months == 0) {
resources.getString(R.string.this_month)
} else {
resources.getQuantityString(R.plurals.months_ago, months, months)
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MonthsAgo
return months == other.months
}
override fun hashCode(): Int {
return months
}
}
class Absolute(private val date: Date) : DateTimeAgo() {
private val day = date.daysDiff(0)

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.util
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class ContinuationResumeRunnable(
private val continuation: Continuation<Unit>,
) : Runnable {
override fun run() {
continuation.resume(Unit)
}
}

View File

@@ -30,7 +30,7 @@ fun DialogFragment.showAllowStateLoss(manager: FragmentManager, tag: String?) {
}
fun Fragment.addMenuProvider(provider: MenuProvider) {
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.STARTED)
requireActivity().addMenuProvider(provider, viewLifecycleOwner, Lifecycle.State.RESUMED)
}
@MainThread

View File

@@ -133,7 +133,7 @@ class ExploreFragment :
override fun onRetryClick(error: Throwable) = Unit
override fun onEmptyActionClick() {
override fun onEmptyActionClick() {
startActivity(SettingsActivity.newManageSourcesIntent(context ?: return))
}
@@ -185,9 +185,4 @@ class ExploreFragment :
return true
}
}
companion object {
fun newInstance() = ExploreFragment()
}
}

View File

@@ -0,0 +1,54 @@
package org.koitharu.kotatsu.favourites.ui.container
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy
import kotlinx.coroutines.flow.FlowCollector
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.util.ContinuationResumeRunnable
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
import kotlin.coroutines.suspendCoroutine
class FavouritesContainerAdapter(fragment: Fragment) : FragmentStateAdapter(
fragment.childFragmentManager,
fragment.viewLifecycleOwner.lifecycle,
),
TabConfigurationStrategy,
FlowCollector<List<FavouriteCategory>> {
private val differ = AsyncListDiffer(this, DiffCallback())
override fun getItemCount(): Int = differ.currentList.size
override fun getItemId(position: Int): Long {
return differ.currentList[position].id
}
override fun createFragment(position: Int): Fragment {
return FavouritesListFragment.newInstance(getItemId(position))
}
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = differ.currentList[position]
tab.text = item.title
tab.tag = item
}
override suspend fun emit(value: List<FavouriteCategory>) = suspendCoroutine { cont ->
differ.submitList(value, ContinuationResumeRunnable(cont))
}
private class DiffCallback : DiffUtil.ItemCallback<FavouriteCategory>() {
override fun areItemsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: FavouriteCategory, newItem: FavouriteCategory): Boolean {
return oldItem.title == newItem.title
}
}
}

View File

@@ -0,0 +1,66 @@
package org.koitharu.kotatsu.favourites.ui.container
import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.ui.util.ActionModeListener
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.databinding.FragmentFavouritesContainerBinding
@AndroidEntryPoint
class FavouritesContainerFragment : BaseFragment<FragmentFavouritesContainerBinding>(), ActionModeListener {
private val viewModel: FavouritesContainerViewModel by viewModels()
override fun onCreateViewBinding(
inflater: LayoutInflater,
container: ViewGroup?,
) = FragmentFavouritesContainerBinding.inflate(inflater, container, false)
override fun onViewBindingCreated(binding: FragmentFavouritesContainerBinding, savedInstanceState: Bundle?) {
super.onViewBindingCreated(binding, savedInstanceState)
val adapter = FavouritesContainerAdapter(this)
binding.pager.adapter = adapter
TabLayoutMediator(
binding.tabs,
binding.pager,
adapter,
).attach()
binding.pager.offscreenPageLimit = 1
actionModeDelegate.addListener(this)
viewModel.categories.observe(viewLifecycleOwner, adapter)
}
override fun onDestroyView() {
actionModeDelegate.removeListener(this)
super.onDestroyView()
}
override fun onWindowInsetsChanged(insets: Insets) {
viewBinding?.tabs?.updatePadding(
left = insets.left,
right = insets.right,
)
}
override fun onActionModeStarted(mode: ActionMode) {
viewBinding?.run {
pager.isUserInputEnabled = false
tabs.isEnabled = false
}
}
override fun onActionModeFinished(mode: ActionMode) {
viewBinding?.run {
pager.isUserInputEnabled = true
tabs.isEnabled = true
}
}
}

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.favourites.ui.container
import dagger.hilt.android.lifecycle.HiltViewModel
import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import javax.inject.Inject
@HiltViewModel
class FavouritesContainerViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
) : BaseViewModel() {
val categories = favouritesRepository.observeCategories()
}

View File

@@ -29,7 +29,7 @@ class HistoryActivity :
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
setReorderingAllowed(true)
val fragment = HistoryListFragment.newInstance()
val fragment = HistoryListFragment()
replace(R.id.container, fragment)
}
}

View File

@@ -4,9 +4,9 @@ import android.content.Context
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.list.ui.adapter.MangaListAdapter
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.model.ListHeader
class HistoryListAdapter(
coil: ImageLoader,
@@ -18,8 +18,8 @@ class HistoryListAdapter(
val list = items
for (i in (0..position).reversed()) {
val item = list.getOrNull(i) ?: continue
if (item is DateTimeAgo) {
return item.format(context.resources)
if (item is ListHeader) {
return item.getText(context)
}
}
return null

View File

@@ -55,9 +55,4 @@ class HistoryListFragment : MangaListFragment() {
}
override fun onCreateAdapter() = HistoryListAdapter(coil, viewLifecycleOwner, this)
companion object {
fun newInstance() = HistoryListFragment()
}
}

View File

@@ -5,9 +5,13 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.google.android.material.R as materialR
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.core.util.ext.startOfDay
import java.util.Date
import java.util.concurrent.TimeUnit
import com.google.android.material.R as materialR
class HistoryListMenuProvider(
private val context: Context,
@@ -20,24 +24,45 @@ class HistoryListMenuProvider(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.action_clear_history -> {
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
.setTitle(R.string.clear_history)
.setMessage(R.string.text_clear_history_prompt)
.setIcon(R.drawable.ic_delete)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.clear) { _, _ ->
viewModel.clearHistory()
}.show()
showClearHistoryDialog()
true
}
R.id.action_history_grouping -> {
viewModel.setGrouping(!menuItem.isChecked)
true
}
else -> false
}
override fun onPrepareMenu(menu: Menu) {
menu.findItem(R.id.action_history_grouping)?.isChecked = viewModel.isGroupingEnabled.value == true
}
}
private fun showClearHistoryDialog() {
val selectionListener = RememberSelectionDialogListener(2)
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
.setTitle(R.string.clear_history)
.setSingleChoiceItems(
arrayOf(
context.getString(R.string.last_2_hours),
context.getString(R.string.today),
context.getString(R.string.clear_all_history),
),
selectionListener.selection,
selectionListener,
)
.setIcon(R.drawable.ic_delete)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.clear) { _, _ ->
val minDate = when (selectionListener.selection) {
0 -> System.currentTimeMillis() - TimeUnit.HOURS.toMillis(2)
1 -> Date().startOfDay()
2 -> 0L
else -> return@setPositiveButton
}
viewModel.clearHistory(minDate)
}.show()
}
}

View File

@@ -78,9 +78,16 @@ class HistoryListViewModel @Inject constructor(
override fun onRetry() = Unit
fun clearHistory() {
launchLoadingJob(Dispatchers.Default) {
repository.clear()
fun clearHistory(minDate: Long) {
launchJob(Dispatchers.Default) {
val stringRes = if (minDate <= 0) {
repository.clear()
R.string.history_cleared
} else {
repository.deleteAfter(minDate)
R.string.removed_from_history
}
onActionDone.call(ReversibleAction(stringRes, null))
}
}
@@ -131,6 +138,7 @@ class HistoryListViewModel @Inject constructor(
diffDays < 1 -> DateTimeAgo.Today
diffDays == 1 -> DateTimeAgo.Yesterday
diffDays < 6 -> DateTimeAgo.DaysAgo(diffDays)
diffDays < 200 -> DateTimeAgo.MonthsAgo(diffDays / 30)
else -> DateTimeAgo.LongAgo
}
}

View File

@@ -2,15 +2,13 @@ package org.koitharu.kotatsu.list.ui.adapter
import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.core.ui.BaseListAdapter
open class MangaListAdapter(
coil: ImageLoader,
lifecycleOwner: LifecycleOwner,
listener: MangaListListener,
) : AsyncListDifferDelegationAdapter<ListModel>(ListModelDiffCallback) {
) : BaseListAdapter() {
init {
delegatesManager

View File

@@ -5,14 +5,13 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.os.Bundle
import android.util.SparseIntArray
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultCallback
import androidx.activity.viewModels
import androidx.appcompat.view.ActionMode
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.util.size
@@ -39,21 +38,18 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
import org.koitharu.kotatsu.core.os.VoiceInputContract
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.core.ui.BaseActivity
import org.koitharu.kotatsu.core.ui.widgets.SlidingBottomNavigationView
import org.koitharu.kotatsu.core.util.ext.drawableEnd
import org.koitharu.kotatsu.core.util.ext.hideKeyboard
import org.koitharu.kotatsu.core.util.ext.observe
import org.koitharu.kotatsu.core.util.ext.observeEvent
import org.koitharu.kotatsu.core.util.ext.resolve
import org.koitharu.kotatsu.core.util.ext.scaleUpActivityOptionsOf
import org.koitharu.kotatsu.core.util.ext.setNavigationBarTransparentCompat
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.databinding.ActivityMainBinding
import org.koitharu.kotatsu.details.service.MangaPrefetchService
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.local.ui.LocalStorageCleanupWorker
import org.koitharu.kotatsu.main.ui.owners.AppBarOwner
import org.koitharu.kotatsu.main.ui.owners.BottomNavOwner
@@ -66,30 +62,23 @@ import org.koitharu.kotatsu.search.ui.multi.MultiSearchActivity
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionFragment
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
import org.koitharu.kotatsu.settings.SettingsActivity
import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.shelf.ui.ShelfFragment
import javax.inject.Inject
import com.google.android.material.R as materialR
private const val TAG_SEARCH = "search"
@AndroidEntryPoint
class MainActivity :
BaseActivity<ActivityMainBinding>(),
AppBarOwner,
BottomNavOwner,
View.OnClickListener,
View.OnFocusChangeListener,
SearchSuggestionListener,
MainNavigationDelegate.OnFragmentChangedListener {
class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNavOwner, View.OnClickListener,
View.OnFocusChangeListener, SearchSuggestionListener, MainNavigationDelegate.OnFragmentChangedListener {
@Inject
lateinit var settings: AppSettings
private val viewModel by viewModels<MainViewModel>()
private val searchSuggestionViewModel by viewModels<SearchSuggestionViewModel>()
private val voiceInputLauncher = registerForActivityResult(VoiceInputContract(), VoiceInputCallback())
private val closeSearchCallback = CloseSearchCallback()
private lateinit var navigationDelegate: MainNavigationDelegate
@@ -122,7 +111,6 @@ class MainActivity :
viewBinding.fab?.setOnClickListener(this)
viewBinding.navRail?.headerView?.setOnClickListener(this)
viewBinding.searchView.isVoiceSearchEnabled = voiceInputLauncher.resolve(this, null) != null
navigationDelegate = MainNavigationDelegate(
navBar = checkNotNull(bottomNav ?: viewBinding.navRail),
@@ -162,18 +150,41 @@ class MainActivity :
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home && !isSearchOpened()) {
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.opt_main, menu)
return true
}
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
menu?.findItem(R.id.action_incognito)?.isChecked = searchSuggestionViewModel.isIncognitoModeEnabled.value
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> if (isSearchOpened()) {
super.onOptionsItemSelected(item)
} else {
viewBinding.searchView.requestFocus()
return true
true
}
return super.onOptionsItemSelected(item)
R.id.action_settings -> {
startActivity(SettingsActivity.newIntent(this))
true
}
R.id.action_incognito -> {
viewModel.setIncognitoMode(!item.isChecked)
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onClick(v: View) {
when (v.id) {
R.id.fab -> viewModel.openLastReader()
R.id.railFab -> viewModel.openLastReader()
R.id.fab, R.id.railFab -> viewModel.openLastReader()
}
}
@@ -220,19 +231,6 @@ class MainActivity :
searchSuggestionViewModel.onQueryChanged(query)
}
override fun onVoiceSearchClick() {
val options = viewBinding.searchView.drawableEnd?.bounds?.let { bounds ->
ActivityOptionsCompat.makeScaleUpAnimation(
viewBinding.searchView,
bounds.centerX(),
bounds.centerY(),
bounds.width(),
bounds.height(),
)
}
voiceInputLauncher.tryLaunch(viewBinding.searchView.hint?.toString(), options)
}
override fun onSourceToggle(source: MangaSource, isEnabled: Boolean) {
searchSuggestionViewModel.onSourceToggle(source, isEnabled)
}
@@ -282,6 +280,7 @@ class MainActivity :
options and EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING.inv()
}
viewBinding.searchView.imeOptions = options
invalidateMenu()
}
private fun onLoadingStateChanged(isLoading: Boolean) {
@@ -334,12 +333,7 @@ class MainActivity :
isSearchOpened: Boolean = isSearchOpened(),
) {
val fab = viewBinding.fab ?: return
if (
isResumeEnabled &&
!actionModeDelegate.isActionModeStarted &&
!isSearchOpened &&
topFragment is ShelfFragment
) {
if (isResumeEnabled && !actionModeDelegate.isActionModeStarted && !isSearchOpened && topFragment is HistoryListFragment) {
if (!fab.isVisible) {
fab.show()
}
@@ -376,23 +370,15 @@ class MainActivity :
}
private fun requestNotificationsPermission() {
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PERMISSION_GRANTED
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS,
) != PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
}
}
private inner class VoiceInputCallback : ActivityResultCallback<String?> {
override fun onActivityResult(result: String?) {
if (result != null) {
viewBinding.searchView.query = result
}
}
}
private inner class CloseSearchCallback : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {

View File

@@ -13,8 +13,8 @@ import org.koitharu.kotatsu.core.ui.util.RecyclerViewOwner
import org.koitharu.kotatsu.core.util.ext.firstVisibleItemPosition
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.explore.ui.ExploreFragment
import org.koitharu.kotatsu.settings.tools.ToolsFragment
import org.koitharu.kotatsu.shelf.ui.ShelfFragment
import org.koitharu.kotatsu.favourites.ui.container.FavouritesContainerFragment
import org.koitharu.kotatsu.history.ui.HistoryListFragment
import org.koitharu.kotatsu.tracker.ui.feed.FeedFragment
import java.util.LinkedList
@@ -53,7 +53,7 @@ class MainNavigationDelegate(
}
override fun handleOnBackPressed() {
navBar.selectedItemId = R.id.nav_shelf
navBar.selectedItemId = R.id.nav_history
}
fun onCreate() {
@@ -99,20 +99,20 @@ class MainNavigationDelegate(
private fun onNavigationItemSelected(@IdRes itemId: Int): Boolean {
return setPrimaryFragment(
when (itemId) {
R.id.nav_shelf -> ShelfFragment.newInstance()
R.id.nav_explore -> ExploreFragment.newInstance()
R.id.nav_feed -> FeedFragment.newInstance()
R.id.nav_tools -> ToolsFragment.newInstance()
R.id.nav_history -> HistoryListFragment()
R.id.nav_favourites -> FavouritesContainerFragment()
R.id.nav_explore -> ExploreFragment()
R.id.nav_feed -> FeedFragment()
else -> return false
},
)
}
private fun getItemId(fragment: Fragment) = when (fragment) {
is ShelfFragment -> R.id.nav_shelf
is HistoryListFragment -> R.id.nav_history
is FavouritesContainerFragment -> R.id.nav_favourites
is ExploreFragment -> R.id.nav_explore
is FeedFragment -> R.id.nav_feed
is ToolsFragment -> R.id.nav_tools
else -> 0
}
@@ -130,7 +130,7 @@ class MainNavigationDelegate(
}
private fun onFragmentChanged(fragment: Fragment, fromUser: Boolean) {
isEnabled = fragment !is ShelfFragment
isEnabled = fragment !is HistoryListFragment
listeners.forEach { it.onFragmentChanged(fragment, fromUser) }
}

View File

@@ -28,7 +28,7 @@ class MainViewModel @Inject constructor(
private val historyRepository: HistoryRepository,
private val appUpdateRepository: AppUpdateRepository,
trackingRepository: TrackingRepository,
settings: AppSettings,
private val settings: AppSettings,
readingResumeEnabledUseCase: ReadingResumeEnabledUseCase,
) : BaseViewModel() {
@@ -51,7 +51,7 @@ class MainViewModel @Inject constructor(
trackingRepository.observeUpdatedMangaCount(),
) { appUpdate, tracks ->
val a = SparseIntArray(2)
a[R.id.nav_tools] = if (appUpdate != null) 1 else 0
// a[R.id.nav_tools] = if (appUpdate != null) 1 else 0
a[R.id.nav_feed] = tracks
a
}.stateIn(
@@ -72,4 +72,8 @@ class MainViewModel @Inject constructor(
onOpenReader.call(manga)
}
}
fun setIncognitoMode(isEnabled: Boolean) {
settings.isIncognitoModeEnabled = isEnabled
}
}

View File

@@ -10,6 +10,7 @@ import androidx.recyclerview.widget.ItemTouchHelper
import coil.ImageLoader
import dagger.hilt.android.AndroidEntryPoint
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.VoiceInputContract
import org.koitharu.kotatsu.core.ui.BaseFragment
import org.koitharu.kotatsu.core.util.ext.addMenuProvider
import org.koitharu.kotatsu.core.util.ext.observe
@@ -26,6 +27,11 @@ class SearchSuggestionFragment :
lateinit var coil: ImageLoader
private val viewModel by activityViewModels<SearchSuggestionViewModel>()
private val voiceInputLauncher = registerForActivityResult(VoiceInputContract()) { result ->
if (result != null) {
viewModel.onQueryChanged(result)
}
}
override fun onCreateViewBinding(
inflater: LayoutInflater,
@@ -39,7 +45,7 @@ class SearchSuggestionFragment :
lifecycleOwner = viewLifecycleOwner,
listener = requireActivity() as SearchSuggestionListener,
)
addMenuProvider(SearchSuggestionMenuProvider(binding.root.context, viewModel))
addMenuProvider(SearchSuggestionMenuProvider(binding.root.context, voiceInputLauncher, viewModel))
binding.root.adapter = adapter
binding.root.setHasFixedSize(true)
viewModel.suggestion.observe(viewLifecycleOwner) {

View File

@@ -17,6 +17,4 @@ interface SearchSuggestionListener {
fun onSourceClick(source: MangaSource)
fun onTagClick(tag: MangaTag)
fun onVoiceSearchClick()
}
}

View File

@@ -4,13 +4,17 @@ import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.activity.result.ActivityResultLauncher
import androidx.core.view.MenuProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.resolve
import org.koitharu.kotatsu.core.util.ext.tryLaunch
import com.google.android.material.R as materialR
class SearchSuggestionMenuProvider(
private val context: Context,
private val voiceInputLauncher: ActivityResultLauncher<String?>,
private val viewModel: SearchSuggestionViewModel,
) : MenuProvider {
@@ -24,10 +28,20 @@ class SearchSuggestionMenuProvider(
clearSearchHistory()
true
}
R.id.action_voice_search -> {
voiceInputLauncher.tryLaunch(context.getString(R.string.search_manga), null)
}
else -> false
}
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
menu.findItem(R.id.action_voice_search)?.isVisible = voiceInputLauncher.resolve(context, null) != null
}
private fun clearSearchHistory() {
MaterialAlertDialogBuilder(context, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered)
.setTitle(R.string.clear_search_history)
@@ -38,4 +52,4 @@ class SearchSuggestionMenuProvider(
viewModel.clearSearchHistory()
}.show()
}
}
}

View File

@@ -15,7 +15,6 @@ import android.view.inputmethod.EditorInfo
import androidx.annotation.AttrRes
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.content.ContextCompat
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.drawableEnd
import org.koitharu.kotatsu.core.util.ext.drawableStart
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
@@ -31,19 +30,12 @@ class SearchEditText @JvmOverloads constructor(
var searchSuggestionListener: SearchSuggestionListener? = null
private val clearIcon = ContextCompat.getDrawable(context, materialR.drawable.abc_ic_clear_material)
private val voiceIcon = ContextCompat.getDrawable(context, R.drawable.ic_voice_input)
private var isEmpty = text.isNullOrEmpty()
init {
wrapHint()
}
var isVoiceSearchEnabled: Boolean = false
set(value) {
field = value
updateActionIcon()
}
var query: String
get() = text?.trim()?.toString().orEmpty()
set(value) {
@@ -117,14 +109,12 @@ class SearchEditText @JvmOverloads constructor(
private fun onActionIconClick() {
when {
!text.isNullOrEmpty() -> text?.clear()
isVoiceSearchEnabled -> searchSuggestionListener?.onVoiceSearchClick()
}
}
private fun updateActionIcon() {
val icon = when {
!text.isNullOrEmpty() -> clearIcon
isVoiceSearchEnabled -> voiceIcon
else -> null
}
if (icon !== drawableEnd) {

View File

@@ -19,6 +19,10 @@ abstract class SuggestionDao {
@Query("SELECT * FROM suggestions ORDER BY relevance DESC LIMIT :limit")
abstract fun observeAll(limit: Int): Flow<List<SuggestionWithManga>>
@Transaction
@Query("SELECT * FROM suggestions ORDER BY RANDOM() LIMIT 1")
abstract suspend fun getRandom(): SuggestionWithManga?
@Query("SELECT COUNT(*) FROM suggestions")
abstract suspend fun count(): Int

View File

@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.core.util.ext.mapItems
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.suggestions.data.SuggestionEntity
import org.koitharu.kotatsu.suggestions.data.SuggestionWithManga
import javax.inject.Inject
class SuggestionRepository @Inject constructor(
@@ -28,6 +29,10 @@ class SuggestionRepository @Inject constructor(
}
}
suspend fun getRandom(): SuggestionWithManga? {
return db.suggestionDao.getRandom()
}
suspend fun clear() {
db.suggestionDao.deleteAll()
}

View File

@@ -140,9 +140,4 @@ class FeedFragment :
override fun onReadClick(manga: Manga, view: View) = Unit
override fun onTagClick(manga: Manga, tag: MangaTag, view: View) = Unit
companion object {
fun newInstance() = FeedFragment()
}
}

View File

@@ -5,7 +5,6 @@ import androidx.lifecycle.LifecycleOwner
import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.MangaListListener
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
@@ -14,6 +13,7 @@ import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel
class FeedAdapter(
@@ -36,8 +36,8 @@ class FeedAdapter(
val list = items
for (i in (0..position).reversed()) {
val item = list.getOrNull(i) ?: continue
if (item is DateTimeAgo) {
return item.format(context.resources)
if (item is ListHeader) {
return item.getText(context)
}
}
return null

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="0dp"
app:elevation="0dp"
app:liftOnScroll="false">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="start"
app:tabMode="scrollable" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -3,9 +3,14 @@
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_shelf"
android:icon="@drawable/ic_bookshelf_selector"
android:title="@string/manga_shelf" />
android:id="@+id/nav_history"
android:icon="@drawable/ic_history"
android:title="@string/history" />
<item
android:id="@+id/nav_favourites"
android:icon="@drawable/ic_heart_outline"
android:title="@string/favourites" />
<item
android:id="@+id/nav_explore"
@@ -17,9 +22,4 @@
android:icon="@drawable/ic_feed_selector"
android:title="@string/feed" />
<item
android:id="@+id/nav_tools"
android:icon="@drawable/ic_tools_selector"
android:title="@string/options" />
</menu>

View File

@@ -7,14 +7,14 @@
android:id="@+id/action_history_grouping"
android:checkable="true"
android:checked="true"
android:orderInCategory="15"
android:orderInCategory="25"
android:title="@string/group"
app:showAsAction="never" />
<item
android:id="@+id/action_clear_history"
android:orderInCategory="50"
android:orderInCategory="10"
android:title="@string/clear_history"
app:showAsAction="never" />
</menu>
</menu>

View File

@@ -9,4 +9,4 @@
android:title="@string/list_mode"
app:showAsAction="never" />
</menu>
</menu>

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_incognito"
android:checkable="true"
android:orderInCategory="90"
android:title="@string/incognito_mode"
app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:orderInCategory="90"
android:title="@string/settings"
app:showAsAction="never" />
</menu>

View File

@@ -3,9 +3,16 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_voice_search"
android:icon="@drawable/ic_voice_input"
android:orderInCategory="0"
android:title="@string/voice_search"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_clear"
android:title="@string/clear_search_history"
app:showAsAction="never" />
</menu>
</menu>

View File

@@ -24,4 +24,8 @@
<item quantity="one">%1$d day ago</item>
<item quantity="other">%1$d days ago</item>
</plurals>
<plurals name="months_ago">
<item quantity="one">%1$d month ago</item>
<item quantity="other">%1$d months ago</item>
</plurals>
</resources>

View File

@@ -450,4 +450,6 @@
<string name="no_access_to_file">You have no access to this file or directory</string>
<string name="local_manga_directories">Local manga directories</string>
<string name="description">Description</string>
<string name="this_month">This month</string>
<string name="voice_search">Voice search</string>
</resources>