Library menu

This commit is contained in:
2022-07-06 11:50:43 +03:00
parent 5d54298a22
commit c4b03d1316
22 changed files with 261 additions and 12 deletions

View File

@@ -60,6 +60,9 @@
<activity <activity
android:name=".history.ui.HistoryActivity" android:name=".history.ui.HistoryActivity"
android:label="@string/history" /> android:label="@string/history" />
<activity
android:name=".favourites.ui.FavouritesActivity"
android:label="@string/favourites" />
<activity <activity
android:name="org.koitharu.kotatsu.settings.SettingsActivity" android:name="org.koitharu.kotatsu.settings.SettingsActivity"
android:exported="true" android:exported="true"

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.base.ui.dialog
import android.content.DialogInterface
class RememberSelectionDialogListener(initialValue: Int) : DialogInterface.OnClickListener {
var selection: Int = initialValue
private set
override fun onClick(dialog: DialogInterface?, which: Int) {
selection = which
}
}

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu.base.ui.util
import androidx.annotation.StringRes
import org.koitharu.kotatsu.base.domain.ReversibleHandle
class ReversibleAction(
@StringRes val stringResId: Int,
val handle: ReversibleHandle?,
)

View File

@@ -0,0 +1,53 @@
package org.koitharu.kotatsu.favourites.ui
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.graphics.Insets
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.databinding.ActivityContainerBinding
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment
import org.koitharu.kotatsu.favourites.ui.list.FavouritesListFragment.Companion.NO_ID
class FavouritesActivity : BaseActivity<ActivityContainerBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(ActivityContainerBinding.inflate(layoutInflater))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
val categoryTitle = intent.getStringExtra(EXTRA_TITLE)
if (categoryTitle != null) {
title = categoryTitle
}
val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) {
fm.commit {
val fragment = FavouritesListFragment.newInstance(intent.getLongExtra(EXTRA_CATEGORY_ID, NO_ID))
replace(R.id.container, fragment)
}
}
}
override fun onWindowInsetsChanged(insets: Insets) {
binding.toolbar.updatePadding(
left = insets.left,
right = insets.right,
)
}
companion object {
private const val EXTRA_CATEGORY_ID = "cat_id"
private const val EXTRA_TITLE = "title"
fun newIntent(context: Context) = Intent(context, FavouritesActivity::class.java)
fun newIntent(context: Context, category: FavouriteCategory) = Intent(context, FavouritesActivity::class.java)
.putExtra(EXTRA_CATEGORY_ID, category.id)
.putExtra(EXTRA_TITLE, category.title)
}
}

View File

@@ -67,6 +67,9 @@ abstract class HistoryDao {
@Query("DELETE FROM history WHERE manga_id = :mangaId") @Query("DELETE FROM history WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long) abstract suspend fun delete(mangaId: Long)
@Query("DELETE FROM history WHERE created_at >= :minDate")
abstract suspend fun deleteAfter(minDate: Long)
suspend fun update(entity: HistoryEntity) = update( suspend fun update(entity: HistoryEntity) = update(
mangaId = entity.mangaId, mangaId = entity.mangaId,
page = entity.page, page = entity.page,

View File

@@ -115,6 +115,10 @@ class HistoryRepository(
} }
} }
suspend fun deleteAfter(minDate: Long) {
db.historyDao.delete(minDate)
}
suspend fun deleteReversible(ids: Collection<Long>): ReversibleHandle { suspend fun deleteReversible(ids: Collection<Long>): ReversibleHandle {
val entities = db.withTransaction { val entities = db.withTransaction {
val entities = db.historyDao.findAll(ids.toList()).filterNotNull() val entities = db.historyDao.findAll(ids.toList()).filterNotNull()

View File

@@ -3,9 +3,7 @@ package org.koitharu.kotatsu.history.ui
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup
import androidx.core.graphics.Insets import androidx.core.graphics.Insets
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -32,9 +30,6 @@ class HistoryActivity : BaseActivity<ActivityContainerBinding>() {
left = insets.left, left = insets.left,
right = insets.right, right = insets.right,
) )
binding.container.updatePadding(
bottom = insets.bottom
)
} }
companion object { companion object {

View File

@@ -10,12 +10,15 @@ import com.google.android.material.snackbar.Snackbar
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.reverseAsync
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController import org.koitharu.kotatsu.base.ui.list.SectionedSelectionController
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.databinding.FragmentLibraryBinding import org.koitharu.kotatsu.databinding.FragmentLibraryBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.favourites.ui.FavouritesActivity
import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet import org.koitharu.kotatsu.favourites.ui.categories.select.FavouriteCategoriesBottomSheet
import org.koitharu.kotatsu.history.ui.HistoryActivity import org.koitharu.kotatsu.history.ui.HistoryActivity
import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter import org.koitharu.kotatsu.library.ui.adapter.LibraryAdapter
@@ -27,6 +30,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.flattenTo import org.koitharu.kotatsu.parsers.util.flattenTo
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.ext.addMenuProvider
import org.koitharu.kotatsu.utils.ext.findViewsByType import org.koitharu.kotatsu.utils.ext.findViewsByType
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@@ -58,9 +62,11 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEvent
) )
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
addMenuProvider(LibraryMenuProvider(view.context, viewModel))
viewModel.content.observe(viewLifecycleOwner, ::onListChanged) viewModel.content.observe(viewLifecycleOwner, ::onListChanged)
viewModel.onError.observe(viewLifecycleOwner, ::onError) viewModel.onError.observe(viewLifecycleOwner, ::onError)
viewModel.onActionDone.observe(viewLifecycleOwner, ::onActionDone)
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -83,7 +89,7 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEvent
override fun onSectionClick(section: LibrarySectionModel, view: View) { override fun onSectionClick(section: LibrarySectionModel, view: View) {
val intent = when (section) { val intent = when (section) {
is LibrarySectionModel.History -> HistoryActivity.newIntent(view.context) is LibrarySectionModel.History -> HistoryActivity.newIntent(view.context)
is LibrarySectionModel.Favourites -> TODO() is LibrarySectionModel.Favourites -> FavouritesActivity.newIntent(view.context, section.category)
} }
startActivity(intent) startActivity(intent)
} }
@@ -174,6 +180,16 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(), LibraryListEvent
).show() ).show()
} }
private fun onActionDone(action: ReversibleAction) {
val handle = action.handle
val length = if (handle == null) Snackbar.LENGTH_SHORT else Snackbar.LENGTH_LONG
val snackbar = Snackbar.make(binding.recyclerView, action.stringResId, length)
if (handle != null) {
snackbar.setAction(R.string.undo) { handle.reverseAsync() }
}
snackbar.show()
}
companion object { companion object {
fun newInstance() = LibraryFragment() fun newInstance() = LibraryFragment()

View File

@@ -0,0 +1,65 @@
package org.koitharu.kotatsu.library.ui
import android.content.Context
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.dialog.RememberSelectionDialogListener
import org.koitharu.kotatsu.favourites.ui.categories.CategoriesActivity
import org.koitharu.kotatsu.utils.ext.startOfDay
import java.util.*
import java.util.concurrent.TimeUnit
import com.google.android.material.R as materialR
class LibraryMenuProvider(
private val context: Context,
private val viewModel: LibraryViewModel,
) : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.opt_library, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.action_clear_history -> {
showClearHistoryDialog()
true
}
R.id.action_categories -> {
context.startActivity(CategoriesActivity.newIntent(context))
true
}
else -> false
}
}
private fun showClearHistoryDialog() {
val selectionListener = RememberSelectionDialogListener(-1)
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

@@ -7,7 +7,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle
import org.koitharu.kotatsu.base.domain.plus
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -23,6 +26,7 @@ import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.daysDiff import org.koitharu.kotatsu.utils.ext.daysDiff
import java.util.* import java.util.*
@@ -37,6 +41,8 @@ class LibraryViewModel(
private val settings: AppSettings, private val settings: AppSettings,
) : BaseViewModel(), ListExtraProvider { ) : BaseViewModel(), ListExtraProvider {
val onActionDone = SingleLiveEvent<ReversibleAction>()
val content: LiveData<List<ListModel>> = combine( val content: LiveData<List<ListModel>> = combine(
historyRepository.observeAllWithHistory(), historyRepository.observeAllWithHistory(),
favouritesRepository.observeAllGrouped(SortOrder.NEWEST), favouritesRepository.observeAllGrouped(SortOrder.NEWEST),
@@ -77,6 +83,33 @@ class LibraryViewModel(
return result return result
} }
fun removeFromHistory(ids: Set<Long>) {
if (ids.isEmpty()) {
return
}
launchJob(Dispatchers.Default) {
val handle = historyRepository.deleteReversible(ids) + ReversibleHandle {
shortcutsRepository.updateShortcuts()
}
shortcutsRepository.updateShortcuts()
onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle))
}
}
fun clearHistory(minDate: Long) {
launchJob(Dispatchers.Default) {
val stringRes = if (minDate <= 0) {
historyRepository.clear()
R.string.history_cleared
} else {
historyRepository.deleteAfter(minDate)
R.string.removed_from_history
}
shortcutsRepository.updateShortcuts()
onActionDone.postCall(ReversibleAction(stringRes, null))
}
}
private suspend fun mapList( private suspend fun mapList(
history: List<MangaWithHistory>, history: List<MangaWithHistory>,
favourites: Map<FavouriteCategory, List<Manga>>, favourites: Map<FavouriteCategory, List<Manga>>,

View File

@@ -301,6 +301,7 @@ class MainActivity :
} }
private fun onSearchClosed() { private fun onSearchClosed() {
binding.searchView.hideKeyboard()
TransitionManager.beginDelayedTransition(binding.appbar) TransitionManager.beginDelayedTransition(binding.appbar)
binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> { binding.toolbarCard.updateLayoutParams<AppBarLayout.LayoutParams> {
scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP scrollFlags = SCROLL_FLAG_SCROLL or SCROLL_FLAG_ENTER_ALWAYS or SCROLL_FLAG_SNAP

View File

@@ -17,4 +17,14 @@ fun Date.daysDiff(other: Long): Int {
val thisDay = time / TimeUnit.DAYS.toMillis(1L) val thisDay = time / TimeUnit.DAYS.toMillis(1L)
val otherDay = other / TimeUnit.DAYS.toMillis(1L) val otherDay = other / TimeUnit.DAYS.toMillis(1L)
return (thisDay - otherDay).toInt() return (thisDay - otherDay).toInt()
}
fun Date.startOfDay(): Long {
val calendar = Calendar.getInstance()
calendar.time = this
calendar[Calendar.HOUR_OF_DAY] = 0
calendar[Calendar.MINUTE] = 0
calendar[Calendar.SECOND] = 0
calendar[Calendar.MILLISECOND] = 0
return calendar.timeInMillis
} }

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.7" android:color="?attr/colorSecondaryContainer" />
</selector>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.7" android:color="?attr/colorSecondaryContainer" /> <item android:alpha="0.7" android:color="@color/kotatsu_secondaryContainer" />
</selector> </selector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid
android:color="?attr/colorAccent" />
<size android:height="32dp" />
</shape>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:color="?attr/colorAccent"
android:drawable="@drawable/tab_rounded_rectangle"
android:padding="1dp" />
</selector>

View File

@@ -4,7 +4,7 @@
<corners android:radius="6dp" /> <corners android:radius="6dp" />
<solid <solid
android:color="?attr/colorAccent" /> android:color="@color/kotatsu_secondary" />
<size android:height="32dp" /> <size android:height="32dp" />
</shape> </shape>

View File

@@ -2,7 +2,7 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:color="?attr/colorAccent" android:color="@color/kotatsu_secondary"
android:drawable="@drawable/tab_rounded_rectangle" android:drawable="@drawable/tab_rounded_rectangle"
android:padding="1dp" /> android:padding="1dp" />

View File

@@ -29,9 +29,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:background="@null" android:background="@null" />
app:tabGravity="center"
app:tabMode="scrollable" />
</com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.MaterialToolbar>

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_categories"
android:orderInCategory="50"
android:title="@string/categories_"
app:showAsAction="never" />
<item
android:id="@+id/action_clear_history"
android:orderInCategory="50"
android:title="@string/clear_history"
app:showAsAction="never" />
</menu>

View File

@@ -320,4 +320,7 @@
<string name="exclude_nsfw_from_history_summary">Manga marked as NSFW will never added to the history and your progress will not be saved</string> <string name="exclude_nsfw_from_history_summary">Manga marked as NSFW will never added to the history and your progress will not be saved</string>
<string name="clear_cookies_summary">Can help in case of some issues. All authorizations will be invalidated</string> <string name="clear_cookies_summary">Can help in case of some issues. All authorizations will be invalidated</string>
<string name="show_all">Show all</string> <string name="show_all">Show all</string>
<string name="clear_all_history">Clear all history</string>
<string name="last_2_hours">Last 2 hours</string>
<string name="history_cleared">History cleared</string>
</resources> </resources>

View File

@@ -72,6 +72,7 @@
<item name="tabPaddingStart">8dp</item> <item name="tabPaddingStart">8dp</item>
<item name="tabPaddingEnd">8dp</item> <item name="tabPaddingEnd">8dp</item>
<item name="dividerThickness">0dp</item> <item name="dividerThickness">0dp</item>
<item name="android:clipChildren">false</item>
</style> </style>
<style name="Widget.Kotatsu.SearchView" parent="@style/Widget.AppCompat.SearchView"> <style name="Widget.Kotatsu.SearchView" parent="@style/Widget.AppCompat.SearchView">