This commit is contained in:
Admin
2020-02-02 12:42:22 +02:00
parent 82fda9394d
commit 6eea278b4d
37 changed files with 569 additions and 133 deletions

View File

@@ -4,6 +4,7 @@
<w>koin</w>
<w>kotatsu</w>
<w>manga</w>
<w>upsert</w>
</words>
</dictionary>
</component>

View File

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

View File

@@ -1,12 +1,41 @@
package org.koitharu.kotatsu.core.db
import androidx.room.Dao
import androidx.room.Query
import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
import org.koitharu.kotatsu.core.db.entity.HistoryWithManga
import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao
interface HistoryDao {
abstract class HistoryDao {
/**
* @hide
*/
@Transaction
@Query("SELECT * FROM history ORDER BY :orderBy LIMIT :limit OFFSET :offset")
abstract suspend fun getAll(offset: Int, limit: Int, orderBy: String): List<HistoryWithManga>
@Query("DELETE FROM history")
abstract suspend fun clear()
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insert(entity: HistoryEntity): Long
@Insert(onConflict = OnConflictStrategy.IGNORE)
abstract suspend fun insertManga(manga: MangaEntity): Long
@Query("UPDATE history SET page = :page, chapter_id = :chapterId, updated_at = :updatedAt WHERE manga_id = :mangaId")
abstract suspend fun update(mangaId: Long, page: Int, chapterId: Long, updatedAt: Long): Int
suspend fun update(entity: HistoryWithManga) = update(entity.manga.id, entity.history.page, entity.history.chapterId, entity.history.updatedAt)
@Transaction
suspend open fun upsert(entity: HistoryWithManga) {
if (update(entity) == 0) {
insertManga(entity.manga)
insert(entity.history)
}
}
@Query("SELECT * FROM history")
suspend fun getAll(): List<HistoryEntity>
}

View File

@@ -3,6 +3,13 @@ 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.TagEntity
@Database(entities = [HistoryEntity::class], version = 1)
abstract class MangaDatabase : RoomDatabase()
@Database(entities = [MangaEntity::class, TagEntity::class, HistoryEntity::class], version = 1)
abstract class MangaDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
abstract fun tagsDao(): TagsDao
}

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.core.db
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
interface TagsDao {
@Transaction
@Query("SELECT * FROM tags")
fun getAllTags(): List<TagEntity>
}

View File

@@ -1,9 +1,25 @@
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.MangaHistory
import java.util.*
@Entity(tableName = "history")
data class HistoryEntity(
@PrimaryKey val id: Long
)
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
@ColumnInfo(name = "updated_at") val updatedAt: Long,
@ColumnInfo(name = "chapter_id") val chapterId: Long,
@ColumnInfo(name = "page") val page: Int
) {
fun toMangaHistory() = MangaHistory(
createdAt = Date(createdAt),
updatedAt = Date(updatedAt),
chapterId = chapterId,
page = page
)
}

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.Embedded
import androidx.room.Relation
data class HistoryWithManga(
@Embedded val history: HistoryEntity,
@Relation(
parentColumn = "manga_id",
entityColumn = "manga_id"
)
val manga: MangaEntity
)

View File

@@ -0,0 +1,55 @@
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.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaState
@Entity(tableName = "manga")
data class MangaEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val id: Long,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "localized_title") val localizedTitle: String? = null,
@ColumnInfo(name = "url") val url: String,
@ColumnInfo(name = "rating") val rating: Float = Manga.NO_RATING, //normalized value [0..1] or -1
@ColumnInfo(name = "cover_url") val coverUrl: String,
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String? = null,
@ColumnInfo(name = "summary") val summary: String,
@ColumnInfo(name = "state") val state: String? = null,
@ColumnInfo(name = "source") val source: String
) {
fun toManga() = Manga(
id = this.id,
title = this.title,
localizedTitle = this.localizedTitle,
summary = this.summary,
state = this.state?.let { MangaState.valueOf(it) },
rating = this.rating,
url = this.url,
coverUrl = this.coverUrl,
largeCoverUrl = this.largeCoverUrl,
source = MangaSource.valueOf(this.source)
// tags = this.tags.map(TagEntity::toMangaTag).toSet()
)
companion object {
fun from(manga: Manga) = MangaEntity(
id = manga.id,
url = manga.url,
source = manga.source.name,
largeCoverUrl = manga.largeCoverUrl,
coverUrl = manga.coverUrl,
localizedTitle = manga.localizedTitle,
rating = manga.rating,
state = manga.state?.name,
summary = manga.summary,
// tags = manga.tags.map(TagEntity.Companion::fromMangaTag),
title = manga.title
)
}
}

View File

@@ -0,0 +1,10 @@
package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
@Entity(tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"])
data class MangaTagsEntity(
@ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "tag_id") val tagId: Long
)

View File

@@ -0,0 +1,34 @@
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.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.utils.ext.longHashCode
@Entity(tableName = "tags")
data class TagEntity(
@PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "tag_id") val id: Long,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "key") val key: String,
@ColumnInfo(name = "source") val source: String
) {
fun toMangaTag() = MangaTag(
key = this.key,
title = this.title,
source = MangaSource.valueOf(this.source)
)
companion object {
fun fromMangaTag(tag: MangaTag) = TagEntity(
title = tag.title,
key = tag.key,
source = tag.source.name,
id = "${tag.key}_${tag.source.name}".longHashCode()
)
}
}

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.core.model
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import java.util.*
@Parcelize
data class MangaHistory(
val createdAt: Date,
val updatedAt: Date,
val chapterId: Long,
val page: Int
) : Parcelable

View File

@@ -0,0 +1,6 @@
package org.koitharu.kotatsu.core.model
data class MangaInfo <E>(
val manga: Manga,
val extra: E
)

View File

@@ -4,7 +4,6 @@ import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import org.koitharu.kotatsu.domain.MangaRepository
import org.koitharu.kotatsu.domain.repository.ReadmangaRepository
import kotlin.reflect.KClass
@Parcelize
enum class MangaSource(val title: String, val cls: Class<out MangaRepository>): Parcelable {

View File

@@ -6,5 +6,6 @@ import kotlinx.android.parcel.Parcelize
@Parcelize
data class MangaTag(
val title: String,
val key: String
val key: String,
val source: MangaSource
) : Parcelable

View File

@@ -3,8 +3,6 @@ package org.koitharu.kotatsu.core.prefs
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import androidx.core.content.edit
import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.delegates.prefs.EnumPreferenceDelegate
@@ -13,7 +11,7 @@ class AppSettings private constructor(private val resources: Resources, private
constructor(context: Context) : this(context.resources, PreferenceManager.getDefaultSharedPreferences(context))
var listMode by EnumPreferenceDelegate(ListMode::class.java, resources.getString(R.string.key_list_mode), ListMode.LIST)
var listMode by EnumPreferenceDelegate(ListMode::class.java, resources.getString(R.string.key_list_mode), ListMode.DETAILED_LIST)
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)

View File

@@ -0,0 +1,13 @@
package org.koitharu.kotatsu.domain
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.SortOrder
abstract class BaseMangaRepository(protected val loaderContext: MangaLoaderContext) : MangaRepository {
override val sortOrders: Set<SortOrder> get() = emptySet()
override val isSearchAvailable get() = true
override suspend fun getPageFullUrl(page: MangaPage) : String = page.url
}

View File

@@ -0,0 +1,71 @@
package org.koitharu.kotatsu.domain
import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.HistoryEntity
import org.koitharu.kotatsu.core.db.entity.HistoryWithManga
import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.model.*
import java.io.Closeable
class HistoryRepository() : KoinComponent, MangaRepository, Closeable {
private val db: MangaDatabase by inject()
override val sortOrders: Set<SortOrder> = setOf(SortOrder.NEWEST, SortOrder.POPULARITY)
override val isSearchAvailable = false
override suspend fun getList(
offset: Int,
query: String?,
sortOrder: SortOrder?,
tags: Set<String>?
): List<Manga> = getHistory(offset, query, sortOrder, tags).map { x -> x.manga }
suspend fun getHistory(
offset: Int,
query: String? = null,
sortOrder: SortOrder? = null,
tags: Set<String>? = null
): List<MangaInfo<MangaHistory>> {
val entities = db.historyDao().getAll(offset, 20, "updated_by")
return entities.map { x -> MangaInfo(x.manga.toManga(), x.history.toMangaHistory()) }
}
override suspend fun getDetails(manga: Manga): Manga {
throw UnsupportedOperationException("History repository does not support getDetails() method")
}
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
throw UnsupportedOperationException("History repository does not support getPages() method")
}
override suspend fun getPageFullUrl(page: MangaPage) = page.url
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int) {
val dao = db.historyDao()
val entity = HistoryEntity(
mangaId = manga.id,
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis(),
chapterId = chapterId,
page = page
)
dao.upsert(
HistoryWithManga(
history = entity,
manga = MangaEntity.from(manga)
)
)
}
suspend fun clear() {
db.historyDao().clear()
}
override fun close() {
db.close()
}
}

View File

@@ -1,18 +1,21 @@
package org.koitharu.kotatsu.domain
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.SortOrder
abstract class MangaRepository(protected val loaderContext: MangaLoaderContext) {
interface MangaRepository {
open val sortOrders: Set<SortOrder> get() = emptySet()
val sortOrders: Set<SortOrder>
open val isSearchAvailable get() = true
val isSearchAvailable: Boolean
abstract suspend fun getList(offset: Int, query: String? = null, sortOrder: SortOrder? = null, tags: Set<String>? = null): List<Manga>
suspend fun getList(offset: Int, query: String? = null, sortOrder: SortOrder? = null, tags: Set<String>? = null): List<Manga>
abstract suspend fun getDetails(manga: Manga) : Manga
suspend fun getDetails(manga: Manga) : Manga
abstract suspend fun getPages(chapter: MangaChapter) : List<MangaPage>
suspend fun getPages(chapter: MangaChapter) : List<MangaPage>
open suspend fun getPageFullUrl(page: MangaPage) : String = page.url
suspend fun getPageFullUrl(page: MangaPage) : String
}

View File

@@ -1,14 +1,13 @@
package org.koitharu.kotatsu.domain.repository
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.domain.BaseMangaRepository
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.domain.MangaRepository
import org.koitharu.kotatsu.domain.exceptions.ParseException
import org.koitharu.kotatsu.utils.ext.*
class ReadmangaRepository(loaderContext: MangaLoaderContext) : MangaRepository(loaderContext) {
class ReadmangaRepository(loaderContext: MangaLoaderContext) : BaseMangaRepository(loaderContext) {
override suspend fun getList(
offset: Int,
@@ -47,7 +46,8 @@ class ReadmangaRepository(loaderContext: MangaLoaderContext) : MangaRepository(l
?.map {
MangaTag(
title = it.text(),
key = it.attr("href").substringAfterLast('/')
key = it.attr("href").substringAfterLast('/'),
source = MangaSource.READMANGA_RU
)
}?.toSet()
}.orEmpty(),

View File

@@ -10,7 +10,8 @@ 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.MangaListFragment
import org.koitharu.kotatsu.ui.main.list.history.HistoryListFragment
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener {
@@ -29,7 +30,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
navigationView.setNavigationItemSelectedListener(this)
if (!supportFragmentManager.isStateSaved) {
setPrimaryFragment(MangaListFragment.newInstance(MangaSource.READMANGA_RU))
setPrimaryFragment(RemoteListFragment.newInstance(MangaSource.READMANGA_RU))
}
}
@@ -51,9 +52,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
override fun onNavigationItemSelected(item: MenuItem): Boolean {
if (item.groupId == R.id.group_remote_sources) {
val source = MangaSource.values().getOrNull(item.itemId) ?: return false
setPrimaryFragment(MangaListFragment.newInstance(source))
setPrimaryFragment(RemoteListFragment.newInstance(source))
} else when (item.itemId) {
R.id.nav_history -> Unit
R.id.nav_history -> setPrimaryFragment(HistoryListFragment.newInstance())
R.id.nav_favourites -> Unit
R.id.nav_local_storage -> Unit
else -> return false

View File

@@ -46,6 +46,8 @@ class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), Vie
private const val TAG = "ListModeSelectDialog"
fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG)
fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm,
TAG
)
}
}

View File

@@ -5,17 +5,17 @@ import coil.api.load
import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_grid.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
class MangaGridHolder(parent: ViewGroup) : BaseViewHolder<Manga>(parent, R.layout.item_manga_grid) {
class MangaGridHolder<E>(parent: ViewGroup) : BaseViewHolder<MangaInfo<E>>(parent, R.layout.item_manga_grid) {
private var coverRequest: RequestDisposable? = null
override fun onBind(data: Manga) {
override fun onBind(data: MangaInfo<E>) {
coverRequest?.dispose()
textView_title.text = data.title
coverRequest = imageView_cover.load(data.coverUrl) {
textView_title.text = data.manga.title
coverRequest = imageView_cover.load(data.manga.coverUrl) {
crossfade(true)
}
}

View File

@@ -1,21 +1,21 @@
package org.koitharu.kotatsu.ui.main.list
import android.view.ViewGroup
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
class MangaListAdapter(onItemClickListener: OnRecyclerItemClickListener<Manga>) :
BaseRecyclerAdapter<Manga>(onItemClickListener) {
class MangaListAdapter<E>(onItemClickListener: OnRecyclerItemClickListener<MangaInfo<E>>) :
BaseRecyclerAdapter<MangaInfo<E>>(onItemClickListener) {
var listMode: ListMode = ListMode.LIST
override fun onCreateViewHolder(parent: ViewGroup) = when(listMode) {
ListMode.LIST -> MangaListHolder(parent)
ListMode.DETAILED_LIST -> MangaListDetailsHolder(parent)
ListMode.LIST -> MangaListHolder<E>(parent)
ListMode.DETAILED_LIST -> MangaListDetailsHolder<E>(parent)
ListMode.GRID -> MangaGridHolder(parent)
}
override fun onGetItemId(item: Manga) = item.id
override fun onGetItemId(item: MangaInfo<E>) = item.manga.id
}

View File

@@ -8,29 +8,30 @@ import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_list_details.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.textAndVisible
import kotlin.math.roundToInt
class MangaListDetailsHolder(parent: ViewGroup) : BaseViewHolder<Manga>(parent, R.layout.item_manga_list_details) {
class MangaListDetailsHolder<E>(parent: ViewGroup) : BaseViewHolder<MangaInfo<E>>(parent, R.layout.item_manga_list_details) {
private var coverRequest: RequestDisposable? = null
@SuppressLint("SetTextI18n")
override fun onBind(data: Manga) {
override fun onBind(data: MangaInfo<E>) {
coverRequest?.dispose()
textView_title.text = data.title
textView_subtitle.textAndVisible = data.localizedTitle
coverRequest = imageView_cover.load(data.coverUrl) {
textView_title.text = data.manga.title
textView_subtitle.textAndVisible = data.manga.localizedTitle
coverRequest = imageView_cover.load(data.manga.coverUrl) {
crossfade(true)
}
if(data.rating == Manga.NO_RATING) {
if(data.manga.rating == Manga.NO_RATING) {
textView_rating.isVisible = false
} else {
textView_rating.text = "${(data.rating * 10).roundToInt()}/10"
textView_rating.text = "${(data.manga.rating * 10).roundToInt()}/10"
textView_rating.isVisible = true
}
textView_tags.text = data.tags.joinToString(", ") {
textView_tags.text = data.manga.tags.joinToString(", ") {
it.title
}
}

View File

@@ -13,26 +13,23 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener
import org.koitharu.kotatsu.ui.common.list.SpacingItemDecoration
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.clearItemDecorations
import org.koitharu.kotatsu.utils.ext.firstItem
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasItems
class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<Manga> {
abstract class MangaListFragment <E> : BaseFragment(R.layout.fragment_list), MangaListView<E>,
PaginationScrollListener.Callback, OnRecyclerItemClickListener<MangaInfo<E>> {
private val presenter by moxyPresenter(factory = ::MangaListPresenter)
private val source by arg<MangaSource>(ARG_SOURCE)
private lateinit var adapter: MangaListAdapter
private lateinit var adapter: MangaListAdapter<E>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -46,7 +43,7 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
recyclerView.adapter = adapter
recyclerView.addOnScrollListener(PaginationScrollListener(4, this))
swipeRefreshLayout.setOnRefreshListener {
presenter.loadList(source, 0)
onRequestMoreItems(0)
}
settings.subscribe(this)
}
@@ -58,7 +55,9 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.loadList(source, 0)
if (!recyclerView.hasItems) {
onRequestMoreItems(0)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -74,19 +73,16 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
else -> super.onOptionsItemSelected(item)
}
override fun onItemClick(item: Manga, position: Int, view: View) {
startActivity(MangaDetailsActivity.newIntent(context ?: return, item))
override fun onItemClick(item: MangaInfo<E>, position: Int, view: View) {
startActivity(MangaDetailsActivity.newIntent(context ?: return, item.manga))
}
override fun onRequestMoreItems(offset: Int) {
presenter.loadList(source, offset)
}
override fun onListChanged(list: List<Manga>) {
override fun onListChanged(list: List<MangaInfo<E>>) {
adapter.replaceData(list)
layout_holder.isVisible = list.isEmpty()
}
override fun onListAppended(list: List<Manga>) {
override fun onListAppended(list: List<MangaInfo<E>>) {
adapter.appendData(list)
}
@@ -101,10 +97,9 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
progressBar.isVisible = isLoading && !hasItems
swipeRefreshLayout.isRefreshing = isLoading && hasItems
swipeRefreshLayout.isEnabled = !progressBar.isVisible
}
override fun getTitle(): CharSequence? {
return source.title
if (isLoading) {
layout_holder.isVisible = false
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
@@ -133,13 +128,4 @@ class MangaListFragment : BaseFragment(R.layout.fragment_list), MangaListView,
adapter.notifyDataSetChanged()
recyclerView.firstItem = position
}
companion object {
private const val ARG_SOURCE = "provider"
fun newInstance(provider: MangaSource) = MangaListFragment().withArgs(1) {
putParcelable(ARG_SOURCE, provider)
}
}
}

View File

@@ -5,19 +5,19 @@ import coil.api.load
import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_list.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaInfo
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.utils.ext.textAndVisible
class MangaListHolder(parent: ViewGroup) : BaseViewHolder<Manga>(parent, R.layout.item_manga_list) {
class MangaListHolder<E>(parent: ViewGroup) : BaseViewHolder<MangaInfo<E>>(parent, R.layout.item_manga_list) {
private var coverRequest: RequestDisposable? = null
override fun onBind(data: Manga) {
override fun onBind(data: MangaInfo<E>) {
coverRequest?.dispose()
textView_title.text = data.title
textView_subtitle.textAndVisible = data.localizedTitle
coverRequest = imageView_cover.load(data.coverUrl) {
textView_title.text = data.manga.title
textView_subtitle.textAndVisible = data.manga.localizedTitle
coverRequest = imageView_cover.load(data.manga.coverUrl) {
crossfade(true)
}
}

View File

@@ -2,15 +2,15 @@ package org.koitharu.kotatsu.ui.main.list
import moxy.MvpView
import moxy.viewstate.strategy.*
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaInfo
interface MangaListView : MvpView {
interface MangaListView<E> : MvpView {
@StateStrategyType(AddToEndSingleTagStrategy::class, tag = "content")
fun onListChanged(list: List<Manga>)
fun onListChanged(list: List<MangaInfo<E>>)
@StateStrategyType(AddToEndStrategy::class, tag = "content")
fun onListAppended(list: List<Manga>)
fun onListAppended(list: List<MangaInfo<E>>)
@StateStrategyType(AddToEndSingleStrategy::class)
fun onLoadingChanged(isLoading: Boolean)

View File

@@ -0,0 +1,49 @@
package org.koitharu.kotatsu.ui.main.list.history
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.ui.main.list.MangaListView
class HistoryListFragment : MangaListFragment<MangaHistory>(), MangaListView<MangaHistory>{
private val presenter by moxyPresenter(factory = ::HistoryListPresenter)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textView_holder.setText(R.string.history_is_empty)
}
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) {
R.id.action_clear_history -> {
presenter.clearHistory()
true
}
else -> super.onOptionsItemSelected(item)
}
override fun getTitle(): CharSequence? {
return getString(R.string.history)
}
companion object {
fun newInstance() = HistoryListFragment()
}
}

View File

@@ -0,0 +1,70 @@
package org.koitharu.kotatsu.ui.main.list.history
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.InjectViewState
import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaHistory
import org.koitharu.kotatsu.domain.HistoryRepository
import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView
@InjectViewState
class HistoryListPresenter : BasePresenter<MangaListView<MangaHistory>>() {
private lateinit var repository: HistoryRepository
override fun onFirstViewAttach() {
repository = HistoryRepository()
super.onFirstViewAttach()
}
fun loadList(offset: Int) {
launch {
viewState.onLoadingChanged(true)
try {
val list = withContext(Dispatchers.IO) {
repository.getHistory(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)
}
}
}
fun clearHistory() {
launch {
viewState.onLoadingChanged(true)
try {
withContext(Dispatchers.IO) {
repository.clear()
}
viewState.onListChanged(emptyList())
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
} finally {
viewState.onLoadingChanged(false)
}
}
}
override fun onDestroy() {
repository.closeQuietly()
super.onDestroy()
}
}

View File

@@ -0,0 +1,39 @@
package org.koitharu.kotatsu.ui.main.list.remote
import android.os.Bundle
import android.view.View
import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.utils.ext.withArgs
class RemoteListFragment : MangaListFragment<Unit>() {
private val presenter by moxyPresenter(factory = ::RemoteListPresenter)
private val source by arg<MangaSource>(ARG_SOURCE)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textView_holder.setText(R.string.nothing_found)
}
override fun onRequestMoreItems(offset: Int) {
presenter.loadList(source, offset)
}
override fun getTitle(): CharSequence? {
return source.title
}
companion object {
private const val ARG_SOURCE = "provider"
fun newInstance(provider: MangaSource) = RemoteListFragment().withArgs(1) {
putParcelable(ARG_SOURCE, provider)
}
}
}

View File

@@ -1,16 +1,18 @@
package org.koitharu.kotatsu.ui.main.list
package org.koitharu.kotatsu.ui.main.list.remote
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.MangaInfo
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.main.list.MangaListView
@InjectViewState
class MangaListPresenter : BasePresenter<MangaListView>() {
class RemoteListPresenter : BasePresenter<MangaListView<Unit>>() {
fun loadList(source: MangaSource, offset: Int) {
launch {
@@ -19,6 +21,7 @@ class MangaListPresenter : BasePresenter<MangaListView>() {
val list = withContext(Dispatchers.IO) {
MangaProviderFactory.create(source)
.getList(offset)
.map { MangaInfo(it, Unit) }
}
if (offset == 0) {
viewState.onListChanged(list)

View File

@@ -6,12 +6,10 @@ import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.utils.ext.showDialog
@@ -58,6 +56,11 @@ class ReaderActivity : BaseActivity(), ReaderView {
super.onDestroy()
}
override fun onPause() {
presenter.addToHistory(manga, chapterId, pager.currentItem)
super.onPause()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.opt_reader_top, menu)
return super.onCreateOptionsMenu(menu)

View File

@@ -5,12 +5,14 @@ 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.core.model.MangaChapter
import org.koitharu.kotatsu.domain.HistoryRepository
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BasePresenter
@InjectViewState
class ReaderPresenter() : BasePresenter<ReaderView>() {
class ReaderPresenter : BasePresenter<ReaderView>() {
fun loadChapter(chapter: MangaChapter) {
launch {
@@ -31,4 +33,12 @@ class ReaderPresenter() : BasePresenter<ReaderView>() {
}
}
fun addToHistory(manga: Manga, chapterId: Long, page: Int) {
launch(Dispatchers.IO) {
HistoryRepository().use {
it.addOrUpdate(manga, chapterId, page)
}
}
}
}

View File

@@ -1,31 +0,0 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<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"
@@ -26,6 +27,8 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_horizontal"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:orientation="vertical">
<TextView
@@ -34,7 +37,9 @@
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
tools:text="@tools:sample/lorem" />
android:text="?android:textColorSecondary"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
tools:text="@tools:sample/lorem[3]" />
</LinearLayout>

View File

@@ -0,0 +1,12 @@
<?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_clear_history"
android:orderInCategory="50"
android:title="@string/clear_history"
app:showAsAction="never" />
</menu>

View File

@@ -19,4 +19,7 @@
<string name="chapter_d_of_d">Chapter %d of %d</string>
<string name="close">Close</string>
<string name="try_again">Try again</string>
<string name="clear_history">Clear history</string>
<string name="nothing_found">Nothing found</string>
<string name="history_is_empty">History is empty</string>
</resources>