pull/1/head
Admin 6 years ago
parent 82fda9394d
commit 6eea278b4d

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

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

@ -1,12 +1,41 @@
package org.koitharu.kotatsu.core.db package org.koitharu.kotatsu.core.db
import androidx.room.Dao import androidx.room.*
import androidx.room.Query
import org.koitharu.kotatsu.core.db.entity.HistoryEntity 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 @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>
} }

@ -3,6 +3,13 @@ package org.koitharu.kotatsu.core.db
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import org.koitharu.kotatsu.core.db.entity.HistoryEntity 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) @Database(entities = [MangaEntity::class, TagEntity::class, HistoryEntity::class], version = 1)
abstract class MangaDatabase : RoomDatabase() abstract class MangaDatabase : RoomDatabase() {
abstract fun historyDao(): HistoryDao
abstract fun tagsDao(): TagsDao
}

@ -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>
}

@ -1,9 +1,25 @@
package org.koitharu.kotatsu.core.db.entity package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.model.MangaHistory
import java.util.*
@Entity(tableName = "history") @Entity(tableName = "history")
data class HistoryEntity( 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
)
}

@ -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
)

@ -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
)
}
}

@ -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
)

@ -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()
)
}
}

@ -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

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

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

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

@ -3,8 +3,6 @@ package org.koitharu.kotatsu.core.prefs
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import androidx.core.content.edit
import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.utils.delegates.prefs.EnumPreferenceDelegate 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)) 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) { fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener) prefs.registerOnSharedPreferenceChangeListener(listener)

@ -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
}

@ -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()
}
}

@ -1,18 +1,21 @@
package org.koitharu.kotatsu.domain 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
} }

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

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

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

@ -5,17 +5,17 @@ import coil.api.load
import coil.request.RequestDisposable import coil.request.RequestDisposable
import kotlinx.android.synthetic.main.item_manga_grid.* import kotlinx.android.synthetic.main.item_manga_grid.*
import org.koitharu.kotatsu.R 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.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 private var coverRequest: RequestDisposable? = null
override fun onBind(data: Manga) { override fun onBind(data: MangaInfo<E>) {
coverRequest?.dispose() coverRequest?.dispose()
textView_title.text = data.title textView_title.text = data.manga.title
coverRequest = imageView_cover.load(data.coverUrl) { coverRequest = imageView_cover.load(data.manga.coverUrl) {
crossfade(true) crossfade(true)
} }
} }

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

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

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

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

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

@ -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()
}
}

@ -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()
}
}

@ -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)
}
}
}

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

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

@ -5,12 +5,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import moxy.InjectViewState import moxy.InjectViewState
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.domain.HistoryRepository
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
@InjectViewState @InjectViewState
class ReaderPresenter() : BasePresenter<ReaderView>() { class ReaderPresenter : BasePresenter<ReaderView>() {
fun loadChapter(chapter: MangaChapter) { fun loadChapter(chapter: MangaChapter) {
launch { 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)
}
}
}
} }

@ -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>

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -26,6 +27,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
@ -34,7 +37,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android: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> </LinearLayout>

@ -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>

@ -19,4 +19,7 @@
<string name="chapter_d_of_d">Chapter %d of %d</string> <string name="chapter_d_of_d">Chapter %d of %d</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="try_again">Try again</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> </resources>
Loading…
Cancel
Save