This commit is contained in:
Koitharu
2020-02-04 17:14:19 +02:00
23 changed files with 403 additions and 13 deletions

View File

@@ -61,7 +61,6 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha01'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0-alpha01'
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.2.0-alpha04'
@@ -80,7 +79,7 @@ dependencies {
implementation 'org.jsoup:jsoup:1.12.1'
implementation 'org.koin:koin-android:2.0.1'
implementation 'io.coil-kt:coil:0.9.2'
implementation 'io.coil-kt:coil:0.9.4'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
testImplementation 'junit:junit:4.13'

View File

@@ -54,5 +54,5 @@ class KotatsuApp : Application() {
applicationContext,
MangaDatabase::class.java,
"kotatsu-db"
).fallbackToDestructiveMigration() //TODO remove
)
}

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.core.db
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
@Dao
abstract class FavouriteCategoriesDao {
@Query("SELECT category_id,title,created_at FROM favourite_categories ORDER BY :orderBy")
abstract suspend fun findAll(orderBy: String): List<FavouriteCategoryEntity>
@Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
@Query("DELETE FROM favourite_categories WHERE category_id = :id")
abstract suspend fun delete(id: Long)
}

View File

@@ -0,0 +1,23 @@
package org.koitharu.kotatsu.core.db
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.FavouriteEntity
import org.koitharu.kotatsu.core.db.entity.FavouriteManga
@Dao
abstract class FavouritesDao {
@Transaction
@Query("SELECT * FROM favourites ORDER BY :orderBy LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int, orderBy: String): List<FavouriteManga>
@Transaction
@Query("SELECT * FROM favourites WHERE manga_id = :id")
abstract suspend fun find(id: Long): FavouriteManga?
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun add(favourite: FavouriteEntity)
@Delete
abstract suspend fun delete(favourite: FavouriteEntity)
}

View File

@@ -2,12 +2,14 @@ package org.koitharu.kotatsu.core.db
import androidx.room.Database
import androidx.room.RoomDatabase
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.MangaTagsEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.db.entity.*
@Database(entities = [MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class], version = 1)
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class
], version = 1
)
abstract class MangaDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
@@ -15,4 +17,8 @@ abstract class MangaDatabase : RoomDatabase() {
abstract fun tagsDao(): TagsDao
abstract fun mangaDao(): MangaDao
abstract fun favouritesDao(): FavouritesDao
abstract fun favouriteCategoriesDao(): FavouriteCategoriesDao
}

View File

@@ -0,0 +1,22 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.model.FavouriteCategory
import java.util.*
@Entity(tableName = "favourite_categories")
data class FavouriteCategoryEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "category_id") val categoryId: Int,
@ColumnInfo(name = "created_at") val createdAt: Long,
@ColumnInfo(name = "title") val title: String
) {
fun toFavouriteCategory(id: Long? = null) = FavouriteCategory(
id = id ?: categoryId.toLong(),
title = title,
createdAt = Date(createdAt)
)
}

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
@Entity(tableName = "favourites", primaryKeys = ["manga_id", "category_id"])
data class FavouriteEntity(
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "category_id") val categoryId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long
)

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Junction
import androidx.room.Relation
data class FavouriteManga(
@Embedded val favourite: FavouriteEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "manga_id"
)
val manga: MangaEntity,
@Relation(
parentColumn = "category_id",
entityColumn = "category_id"
)
val categories: List<FavouriteCategoryEntity>,
@Relation(
parentColumn = "manga_id",
entityColumn = "tag_id",
associateBy = Junction(MangaTagsEntity::class)
)
val tags: List<TagEntity>
)

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import java.util.*
@Parcelize
data class FavouriteCategory(
val id: Long,
val title: String,
val createdAt: Date
) : Parcelable

View File

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.domain.favourites
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.TagEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
class FavouritesRepository : KoinComponent {
private val db: MangaDatabase by inject()
suspend fun getAllManga(offset: Int): List<Manga> {
val entities = db.favouritesDao().findAll(offset, 20, "created_at")
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
}
suspend fun getAllCategories(): List<FavouriteCategory> {
val entities = db.favouriteCategoriesDao().findAll("created_at")
return entities.map { it.toFavouriteCategory() }
}
suspend fun getCategories(mangaId: Long): List<FavouriteCategory> {
val entities = db.favouritesDao().find(mangaId)?.categories
return entities?.map { it.toFavouriteCategory() }.orEmpty()
}
suspend fun addCategory(title: String): FavouriteCategory {
val entity = FavouriteCategoryEntity(
title = title,
createdAt = System.currentTimeMillis(),
categoryId = 0
)
val id = db.favouriteCategoriesDao().insert(entity)
return entity.toFavouriteCategory(id)
}
suspend fun removeCategory(id: Long) {
db.favouriteCategoriesDao().delete(id)
}
}

View File

@@ -3,6 +3,8 @@ package org.koitharu.kotatsu.ui.details
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_details.*
import moxy.ktx.moxyPresenter
@@ -39,11 +41,24 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
Snackbar.make(pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_details, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_favourite -> {
true
}
else -> super.onOptionsItemSelected(item)
}
companion object {
private const val EXTRA_MANGA = "manga"
fun newIntent(context: Context, manga: Manga) = Intent(context, MangaDetailsActivity::class.java)
.putExtra(EXTRA_MANGA, manga)
fun newIntent(context: Context, manga: Manga) =
Intent(context, MangaDetailsActivity::class.java)
.putExtra(EXTRA_MANGA, manga)
}
}

View File

@@ -10,6 +10,7 @@ import kotlinx.android.synthetic.main.activity_main.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.main.list.favourites.FavouritesListFragment
import org.koitharu.kotatsu.ui.main.list.history.HistoryListFragment
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
@@ -56,7 +57,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
setPrimaryFragment(RemoteListFragment.newInstance(source))
} else when (item.itemId) {
R.id.nav_history -> setPrimaryFragment(HistoryListFragment.newInstance())
R.id.nav_favourites -> Unit
R.id.nav_favourites -> setPrimaryFragment(FavouritesListFragment.newInstance())
R.id.nav_local_storage -> Unit
else -> return false
}

View File

@@ -0,0 +1,34 @@
package org.koitharu.kotatsu.ui.main.list.favourites
import android.util.SparseBooleanArray
import android.view.ViewGroup
import android.widget.Checkable
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class CategoriesAdapter(private val listener: OnCategoryCheckListener) :
BaseRecyclerAdapter<FavouriteCategory, Boolean>() {
private val checkedIds = SparseBooleanArray()
override fun getExtra(item: FavouriteCategory, position: Int) =
checkedIds.get(item.id.toInt(), false)
override fun onCreateViewHolder(parent: ViewGroup) = CategoryHolder(parent)
override fun onGetItemId(item: FavouriteCategory) = item.id
override fun onViewHolderCreated(holder: BaseViewHolder<FavouriteCategory, Boolean>) {
super.onViewHolderCreated(holder)
holder.itemView.setOnClickListener {
if (it !is Checkable) return@setOnClickListener
it.toggle()
if (it.isChecked) {
listener.onCategoryChecked(holder.requireData())
} else {
listener.onCategoryUnchecked(holder.requireData())
}
}
}
}

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.ui.main.list.favourites
import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_caegory_checkable.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class CategoryHolder(parent: ViewGroup) :
BaseViewHolder<FavouriteCategory, Boolean>(parent, R.layout.item_caegory_checkable) {
override fun onBind(data: FavouriteCategory, extra: Boolean) {
checkedTextView.text = data.title
checkedTextView.isChecked = extra
}
}

View File

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,43 @@
package org.koitharu.kotatsu.ui.main.list.favourites
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.ui.main.list.MangaListView
class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit>{
private val presenter by moxyPresenter(factory = ::FavouritesListPresenter)
override fun onRequestMoreItems(offset: Int) {
presenter.loadList(offset)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// inflater.inflate(R.menu.opt_history, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) {
else -> super.onOptionsItemSelected(item)
}
override fun getTitle(): CharSequence? {
return getString(R.string.favourites)
}
override fun setUpEmptyListHolder() {
textView_holder.setText(R.string.you_have_not_favourites_yet)
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
}
companion object {
fun newInstance() = FavouritesListFragment()
}
}

View File

@@ -0,0 +1,44 @@
package org.koitharu.kotatsu.ui.main.list.favourites
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.InjectViewState
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView
@InjectViewState
class FavouritesListPresenter : BasePresenter<MangaListView<Unit>>() {
private lateinit var repository: FavouritesRepository
override fun onFirstViewAttach() {
repository = FavouritesRepository()
super.onFirstViewAttach()
}
fun loadList(offset: Int) {
launch {
viewState.onLoadingChanged(true)
try {
val list = withContext(Dispatchers.IO) {
repository.getAllManga(offset = offset)
}
if (offset == 0) {
viewState.onListChanged(list)
} else {
viewState.onListAppended(list)
}
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
} finally {
viewState.onLoadingChanged(false)
}
}
}
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.ui.main.list.favourites
import org.koitharu.kotatsu.core.model.FavouriteCategory
interface OnCategoryCheckListener {
fun onCategoryChecked(category: FavouriteCategory)
fun onCategoryUnchecked(category: FavouriteCategory)
}

View File

@@ -8,13 +8,13 @@
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/ThemeOverlay.MaterialComponents.Light" />

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_categories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:scrollbars="vertical"
tools:listitem="@layout/item_caegory_checkable" />
<TextView
android:id="@+id/textView_add"
android:layout_width="match_parent"
android:layout_height="?listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:gravity="start|center_vertical"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorPrimary"
android:text="" />
</LinearLayout>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/checkedTextView"
android:layout_width="match_parent"
android:layout_height="?listPreferredItemHeightSmall"
android:background="?selectableItemBackground"
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
android:gravity="start|center_vertical"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorPrimary"
tools:checked="true"
tools:text="@tools:sample/lorem[4]" />

View File

@@ -0,0 +1,11 @@
<?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_favourite"
android:icon="@drawable/ic_favourites"
android:title="@string/favourites"
app:showAsAction="ifRoom" />
</menu>

View File

@@ -25,4 +25,5 @@
<string name="read">Read</string>
<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>
</resources>