Webtoon reader
parent
646d5df476
commit
6cf9e69f99
@ -0,0 +1,24 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
import androidx.room.*
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaPrefsEntity
|
||||
|
||||
@Dao
|
||||
abstract class PreferencesDao {
|
||||
|
||||
@Query("SELECT * FROM preferences WHERE manga_id = :mangaId")
|
||||
abstract suspend fun find(mangaId: Long): MangaPrefsEntity?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(pref: MangaPrefsEntity): Long
|
||||
|
||||
@Update
|
||||
abstract suspend fun update(pref: MangaPrefsEntity): Int
|
||||
|
||||
@Transaction
|
||||
open suspend fun upsert(pref: MangaPrefsEntity) {
|
||||
if (update(pref) == 0) {
|
||||
insert(pref)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "preferences")
|
||||
data class MangaPrefsEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "mode") val mode: Int
|
||||
)
|
||||
@ -0,0 +1,13 @@
|
||||
package org.koitharu.kotatsu.core.prefs
|
||||
|
||||
enum class ReaderMode(val id: Int) {
|
||||
|
||||
UNKNOWN(0),
|
||||
STANDARD(1),
|
||||
WEBTOON(2);
|
||||
|
||||
companion object {
|
||||
|
||||
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
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.MangaPrefsEntity
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
|
||||
class MangaPreferencesRepository : KoinComponent {
|
||||
|
||||
private val db: MangaDatabase by inject()
|
||||
|
||||
suspend fun saveData(mangaId: Long, mode: ReaderMode) {
|
||||
db.preferencesDao().upsert(
|
||||
MangaPrefsEntity(
|
||||
mangaId = mangaId,
|
||||
mode = mode.id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getReaderMode(mangaId: Long): ReaderMode {
|
||||
return db.preferencesDao().find(mangaId)?.let { ReaderMode.valueOf(it.mode) }
|
||||
?: ReaderMode.UNKNOWN
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package org.koitharu.kotatsu.ui.reader.wetoon
|
||||
|
||||
import android.view.ViewGroup
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
|
||||
class WebtoonAdapter(private val loader: PageLoader) : BaseRecyclerAdapter<MangaPage, Unit>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup) =
|
||||
WebtoonHolder(parent, loader)
|
||||
|
||||
override fun onGetItemId(item: MangaPage) = item.id
|
||||
|
||||
override fun getExtra(item: MangaPage, position: Int) = Unit
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package org.koitharu.kotatsu.ui.reader.wetoon
|
||||
|
||||
import android.graphics.PointF
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.isVisible
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||
import kotlinx.android.synthetic.main.item_page.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
|
||||
|
||||
class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
|
||||
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page),
|
||||
SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader {
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
init {
|
||||
ssiv.setOnImageEventListener(this)
|
||||
button_retry.setOnClickListener {
|
||||
doLoad(boundData ?: return@setOnClickListener, force = true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(data: MangaPage, extra: Unit) {
|
||||
doLoad(data, force = false)
|
||||
}
|
||||
|
||||
private fun doLoad(data: MangaPage, force: Boolean) {
|
||||
job?.cancel()
|
||||
job = launch {
|
||||
layout_error.isVisible = false
|
||||
progressBar.isVisible = true
|
||||
ssiv.recycle()
|
||||
try {
|
||||
val uri = withContext(Dispatchers.IO) {
|
||||
loader.loadFile(data.url, force)
|
||||
}.toUri()
|
||||
ssiv.setImage(ImageSource.uri(uri))
|
||||
} catch (e: CancellationException) {
|
||||
//do nothing
|
||||
} catch (e: Exception) {
|
||||
onError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReady() {
|
||||
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
|
||||
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
|
||||
ssiv.setScaleAndCenter(
|
||||
ssiv.minScale,
|
||||
PointF(ssiv.sWidth / 2f, 0f)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onImageLoadError(e: Exception) = onError(e)
|
||||
|
||||
override fun onImageLoaded() {
|
||||
progressBar.isVisible = false
|
||||
}
|
||||
|
||||
override fun onTileLoadError(e: Exception?) = Unit
|
||||
|
||||
override fun onPreviewReleased() = Unit
|
||||
|
||||
override fun onPreviewLoadError(e: Exception?) = Unit
|
||||
|
||||
private fun onError(e: Throwable) {
|
||||
textView_error.text = e.getDisplayMessage(context.resources)
|
||||
layout_error.isVisible = true
|
||||
progressBar.isVisible = false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
package org.koitharu.kotatsu.ui.reader.wetoon
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import kotlinx.android.synthetic.main.fragment_reader_webtoon.*
|
||||
import moxy.ktx.moxyPresenter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.MangaPage
|
||||
import org.koitharu.kotatsu.core.prefs.ReaderMode
|
||||
import org.koitharu.kotatsu.ui.reader.BaseReaderFragment
|
||||
import org.koitharu.kotatsu.ui.reader.PageLoader
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderPresenter
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.utils.ext.firstItem
|
||||
|
||||
class WebtoonReaderFragment : BaseReaderFragment(R.layout.fragment_reader_webtoon) {
|
||||
|
||||
private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance)
|
||||
|
||||
private var adapter: WebtoonAdapter? = null
|
||||
private lateinit var loader: PageLoader
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
loader = PageLoader()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
adapter = WebtoonAdapter(loader)
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
override fun onInitReader(pages: List<MangaPage>, mode: ReaderMode, state: ReaderState) {
|
||||
adapter?.let {
|
||||
it.replaceData(pages)
|
||||
recyclerView.firstItem = state.page
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
loader.dispose()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override val hasItems: Boolean
|
||||
get() = adapter?.hasItems == true
|
||||
|
||||
override val currentPageIndex: Int
|
||||
get() = recyclerView.firstItem
|
||||
|
||||
override val pages: List<MangaPage>
|
||||
get() = adapter?.items.orEmpty()
|
||||
|
||||
override fun setCurrentPage(index: Int, smooth: Boolean) {
|
||||
if (smooth) {
|
||||
recyclerView.smoothScrollToPosition(index)
|
||||
} else {
|
||||
recyclerView.firstItem = index
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
Loading…
Reference in New Issue