Favourites

This commit is contained in:
Koitharu
2020-02-04 21:53:41 +02:00
parent 738d1cb50e
commit bbe28f769d
21 changed files with 405 additions and 32 deletions

View File

@@ -18,6 +18,6 @@ abstract class FavouritesDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun add(favourite: FavouriteEntity)
@Delete
abstract suspend fun delete(favourite: FavouriteEntity)
@Query("DELETE FROM favourites WHERE manga_id = :mangaId AND category_id = :categoryId")
abstract suspend fun delete(categoryId: Long, mangaId: Long)
}

View File

@@ -4,6 +4,8 @@ import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
@@ -40,4 +42,16 @@ class FavouritesRepository : KoinComponent {
suspend fun removeCategory(id: Long) {
db.favouriteCategoriesDao().delete(id)
}
suspend fun addToCategory(manga: Manga, categoryId: Long) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.tagsDao().upsert(tags)
db.mangaDao().upsert(MangaEntity.from(manga), tags)
val entity = FavouriteEntity(manga.id, categoryId, System.currentTimeMillis())
db.favouritesDao().add(entity)
}
suspend fun removeFromCategory(manga: Manga, categoryId: Long) {
db.favouritesDao().delete(categoryId, manga.id)
}
}

View File

@@ -6,8 +6,9 @@ import android.view.View
import androidx.annotation.LayoutRes
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import moxy.MvpAppCompatDialogFragment
abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : DialogFragment() {
abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : MvpAppCompatDialogFragment() {
private var rootView: View? = null

View File

@@ -0,0 +1,69 @@
package org.koitharu.kotatsu.ui.common
import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.view.LayoutInflater
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import kotlinx.android.synthetic.main.dialog_input.view.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.ext.hideKeyboard
import org.koitharu.kotatsu.utils.ext.showKeyboard
class TextInputDialog private constructor(private val delegate: AlertDialog) :
DialogInterface by delegate {
init {
delegate.setOnShowListener {
delegate.currentFocus?.showKeyboard()
}
}
fun show() = delegate.show()
class Builder(context: Context) {
@SuppressLint("InflateParams")
private val view = LayoutInflater.from(context).inflate(R.layout.dialog_input, null, false)
private val delegate = AlertDialog.Builder(context)
.setView(view)
fun setTitle(@StringRes titleResId: Int): Builder {
delegate.setTitle(titleResId)
return this
}
fun setTitle(title: CharSequence): Builder {
delegate.setTitle(title)
return this
}
fun setHint(@StringRes hintResId: Int): Builder {
view.inputLayout.hint = view.context.getString(hintResId)
return this
}
fun setInputType(inputType: Int): Builder {
view.inputEdit.inputType = inputType
return this
}
fun setPositiveButton(@StringRes textId: Int, listener: (DialogInterface, String) -> Unit): Builder {
delegate.setPositiveButton(textId) { dialog, _ ->
view.hideKeyboard()
listener(dialog, view.inputEdit.text?.toString().orEmpty())
}
return this
}
fun setNegativeButton(@StringRes textId: Int, listener: DialogInterface.OnClickListener? = null): Builder {
delegate.setNegativeButton(textId, listener)
return this
}
fun create() = TextInputDialog(delegate.create())
}
}

View File

@@ -9,6 +9,7 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesDialog
import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.utils.ext.setChips
import kotlin.math.roundToInt
@@ -40,6 +41,9 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
tag = it
)
}
imageView_favourite.setOnClickListener {
FavouriteCategoriesDialog.show(childFragmentManager, manga)
}
updateReadButton()
}

View File

@@ -1,7 +0,0 @@
package org.koitharu.kotatsu.ui.main.list.favourites
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.common.AlertDialogFragment
class FavouriteCategoriesDialog() : AlertDialogFragment(R.layout.dialog_favorite_categories) {
}

View File

@@ -1,21 +1,34 @@
package org.koitharu.kotatsu.ui.main.list.favourites
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.util.SparseBooleanArray
import android.view.ViewGroup
import android.widget.Checkable
import androidx.core.util.set
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.disableFor
class CategoriesAdapter(private val listener: OnCategoryCheckListener) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>() {
private val checkedIds = SparseBooleanArray()
fun setCheckedIds(ids: Iterable<Int>) {
checkedIds.clear()
ids.forEach {
checkedIds[it] = true
}
notifyDataSetChanged()
}
override fun getExtra(item: FavouriteCategory, position: Int) =
checkedIds.get(item.id.toInt(), false)
override fun onCreateViewHolder(parent: ViewGroup) = CategoryHolder(parent)
override fun onCreateViewHolder(parent: ViewGroup) =
CategoryHolder(
parent
)
override fun onGetItemId(item: FavouriteCategory) = item.id
@@ -24,6 +37,7 @@ class CategoriesAdapter(private val listener: OnCategoryCheckListener) :
holder.itemView.setOnClickListener {
if (it !is Checkable) return@setOnClickListener
it.toggle()
it.disableFor(200)
if (it.isChecked) {
listener.onCategoryChecked(holder.requireData())
} else {

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_caegory_checkable.*

View File

@@ -0,0 +1,91 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.dialog_favorite_categories.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.common.AlertDialogFragment
import org.koitharu.kotatsu.ui.common.TextInputDialog
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.withArgs
class FavouriteCategoriesDialog() : AlertDialogFragment(R.layout.dialog_favorite_categories),
FavouriteCategoriesView,
OnCategoryCheckListener {
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
private val manga get() = arguments?.getParcelable<Manga>(ARG_MANGA)
private var adapter: CategoriesAdapter? = null
override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.add_to_favourites)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = CategoriesAdapter(this)
recyclerView_categories.adapter = adapter
textView_add.setOnClickListener {
createCategory()
}
manga?.let {
presenter.loadMangaCategories(it)
}
}
override fun onDestroyView() {
adapter = null
super.onDestroyView()
}
override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
adapter?.replaceData(categories)
}
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) {
adapter?.setCheckedIds(checkedIds)
}
override fun onCategoryChecked(category: FavouriteCategory) {
presenter.addToCategory(manga ?: return, category.id)
}
override fun onCategoryUnchecked(category: FavouriteCategory) {
presenter.removeFromCategory(manga ?: return, category.id)
}
override fun onError(e: Exception) {
Toast.makeText(context ?: return, e.getDisplayMessage(resources), Toast.LENGTH_SHORT).show()
}
private fun createCategory() {
TextInputDialog.Builder(context ?: return)
.setTitle(R.string.add_new_category)
.setHint(R.string.enter_category_name)
.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
.setNegativeButton(android.R.string.cancel)
.setPositiveButton(R.string.add) { _, name ->
presenter.createCategory(name)
}.create()
.show()
}
companion object {
private const val ARG_MANGA = "manga"
private const val TAG = "FavouriteCategoriesDialog"
fun show(fm: FragmentManager, manga: Manga) = FavouriteCategoriesDialog().withArgs(1) {
putParcelable(ARG_MANGA, manga)
}.show(fm, TAG)
}
}

View File

@@ -0,0 +1,101 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.InjectViewState
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.ui.common.BasePresenter
@InjectViewState
class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
private lateinit var repository: FavouritesRepository
override fun onFirstViewAttach() {
repository = FavouritesRepository()
super.onFirstViewAttach()
loadAllCategories()
}
fun loadAllCategories() {
launch {
try {
val categories = withContext(Dispatchers.IO) {
repository.getAllCategories()
}
viewState.onCategoriesChanged(categories)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
fun loadMangaCategories(manga: Manga) {
launch {
try {
val categories = withContext(Dispatchers.IO) {
repository.getCategories(manga.id)
}
viewState.onCheckedCategoriesChanged(categories.map { it.id.toInt() }.toSet())
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
fun createCategory(name: String) {
launch {
try {
val categories = withContext(Dispatchers.IO) {
repository.addCategory(name)
repository.getAllCategories()
}
viewState.onCategoriesChanged(categories)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
fun addToCategory(manga: Manga, categoryId: Long) {
launch {
try {
val categories = withContext(Dispatchers.IO) {
repository.addToCategory(manga,categoryId)
}
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
fun removeFromCategory(manga: Manga, categoryId: Long) {
launch {
try {
val categories = withContext(Dispatchers.IO) {
repository.removeFromCategory(manga, categoryId)
}
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
}

View File

@@ -0,0 +1,19 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import moxy.MvpView
import moxy.viewstate.strategy.AddToEndSingleStrategy
import moxy.viewstate.strategy.OneExecutionStateStrategy
import moxy.viewstate.strategy.StateStrategyType
import org.koitharu.kotatsu.core.model.FavouriteCategory
interface FavouriteCategoriesView : MvpView {
@StateStrategyType(AddToEndSingleStrategy::class)
fun onCategoriesChanged(categories: List<FavouriteCategory>)
@StateStrategyType(AddToEndSingleStrategy::class)
fun onCheckedCategoriesChanged(checkedIds: Set<Int>)
@StateStrategyType(OneExecutionStateStrategy::class)
fun onError(e: Exception)
}

View File

@@ -1,4 +1,4 @@
package org.koitharu.kotatsu.ui.main.list.favourites
package org.koitharu.kotatsu.ui.main.list.favourites.categories
import org.koitharu.kotatsu.core.model.FavouriteCategory

View File

@@ -10,6 +10,7 @@ import android.widget.EditText
import android.widget.TextView
import androidx.annotation.LayoutRes
import androidx.core.view.isGone
import androidx.core.view.postDelayed
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -88,4 +89,11 @@ var RecyclerView.firstItem: Int
if (value != RecyclerView.NO_POSITION) {
(layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(value, 0)
}
}
}
fun View.disableFor(timeInMillis: Long) {
isEnabled = false
postDelayed(timeInMillis) {
isEnabled = true
}
}

View File

@@ -28,7 +28,7 @@
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
<androidx.fragment.app.FragmentContainerView
android:id="@id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@@ -4,6 +4,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:paddingTop="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@@ -16,16 +17,21 @@
android:scrollbars="vertical"
tools:listitem="@layout/item_caegory_checkable" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:listDivider" />
<TextView
android:id="@+id/textView_add"
android:layout_width="match_parent"
android:layout_height="?listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:selectableItemBackground"
android:gravity="start|center_vertical"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorPrimary"
android:text="" />
android:text="@string/add_new_category" />
</LinearLayout>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="14dp"
android:paddingTop="8dp"
android:paddingEnd="14dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/inputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:boxBackgroundMode="filled">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/inputEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:maxLines="1"
tools:text="@tools:sample/lorem[2]" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>

View File

@@ -27,8 +27,8 @@
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:maxLines="3"
android:layout_marginEnd="8dp"
android:maxLines="3"
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
android:textColor="?android:textColorPrimary"
app:layout_constraintEnd_toEndOf="parent"
@@ -41,9 +41,9 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:maxLines="2"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="?android:textColorSecondary"
android:maxLines="2"
app:layout_constraintEnd_toEndOf="@id/textView_title"
app:layout_constraintStart_toStartOf="@id/textView_title"
app:layout_constraintTop_toBottomOf="@id/textView_title"
@@ -63,19 +63,34 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/button_read"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:enabled="false"
android:text="@string/read"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
app:icon="@drawable/ic_read"
app:iconPadding="12dp"
android:enabled="false"
android:layout_marginEnd="4dp"
app:layout_constraintBottom_toBottomOf="@id/imageView_cover"
app:layout_constraintEnd_toEndOf="@id/textView_title"
app:layout_constraintTop_toBottomOf="@id/ratingBar"
app:layout_constraintVertical_bias="1" />
<ImageView
android:id="@+id/imageView_favourite"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="4dp"
android:background="?selectableItemBackgroundBorderless"
android:contentDescription="@string/add_to_favourites"
android:scaleType="center"
android:src="@drawable/ic_favourites"
android:tint="?colorAccent"
app:layout_constraintBottom_toBottomOf="@id/button_read"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toStartOf="@id/button_read"
app:layout_constraintTop_toTopOf="@id/button_read" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_title"
android:layout_width="wrap_content"

View File

@@ -4,12 +4,12 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/checkedTextView"
android:layout_width="match_parent"
android:layout_height="?listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:selectableItemBackground"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:gravity="start|center_vertical"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:paddingStart="?android:listPreferredItemPaddingStart"
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorPrimary"
tools:checked="true"

View File

@@ -3,6 +3,11 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardElevation="0dp"
app:strokeColor="?android:textColorPrimary"
app:strokeWidth="1px"
app:cardBackgroundColor="?android:windowBackground"
android:layout_height="@dimen/manga_list_details_item_height">
<RelativeLayout

View File

@@ -26,4 +26,8 @@
<string name="continue_">Continue</string>
<string name="add_bookmark">Add bookmark</string>
<string name="you_have_not_favourites_yet">You have not favourites yet</string>
<string name="add_to_favourites">Add to favourites</string>
<string name="add_new_category">Add new category</string>
<string name="add">Add</string>
<string name="enter_category_name">Enter category name</string>
</resources>