Webtoon reader

This commit is contained in:
Koitharu
2020-02-25 20:29:26 +02:00
parent 646d5df476
commit 6cf9e69f99
17 changed files with 357 additions and 38 deletions

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.core.db.entity.*
@Database(
entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class
], version = 1
)
abstract class MangaDatabase : RoomDatabase() {
@@ -20,5 +20,7 @@ abstract class MangaDatabase : RoomDatabase() {
abstract fun favouritesDao(): FavouritesDao
abstract fun preferencesDao(): PreferencesDao
abstract fun favouriteCategoriesDao(): FavouriteCategoriesDao
}

View File

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

View File

@@ -46,7 +46,6 @@ data class MangaEntity(
altTitle = manga.altTitle,
rating = manga.rating,
state = manga.state?.name,
// tags = manga.tags.map(TagEntity.Companion::fromMangaTag),
title = manga.title
)
}

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,14 @@ import org.koitharu.kotatsu.ui.common.AlertDialogFragment
class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), View.OnClickListener {
private val setting by inject<AppSettings>()
private val settings by inject<AppSettings>()
private lateinit var mode: ListMode
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mode = settings.listMode
}
override fun onBuildDialog(builder: AlertDialog.Builder) {
builder.setTitle(R.string.list_mode)
@@ -22,7 +29,6 @@ class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), Vie
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val mode = setting.listMode
button_list.isChecked = mode == ListMode.LIST
button_list_detailed.isChecked = mode == ListMode.DETAILED_LIST
button_grid.isChecked = mode == ListMode.GRID
@@ -35,10 +41,13 @@ class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), Vie
override fun onClick(v: View) {
when (v.id) {
R.id.button_ok -> dismiss()
R.id.button_list -> setting.listMode = ListMode.LIST
R.id.button_list_detailed -> setting.listMode = ListMode.DETAILED_LIST
R.id.button_grid -> setting.listMode = ListMode.GRID
R.id.button_ok -> {
settings.listMode = mode
dismiss()
}
R.id.button_list -> mode = ListMode.LIST
R.id.button_list_detailed -> mode = ListMode.DETAILED_LIST
R.id.button_grid -> mode = ListMode.GRID
}
}

View File

@@ -15,24 +15,25 @@ import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.*
import moxy.MvpDelegate
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.MangaHistory
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.ui.common.BaseFullscreenActivity
import org.koitharu.kotatsu.ui.reader.standard.StandardReaderFragment
import org.koitharu.kotatsu.ui.reader.thumbnails.OnPageSelectListener
import org.koitharu.kotatsu.ui.reader.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.ui.reader.wetoon.WebtoonReaderFragment
import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.anim.Motion
import org.koitharu.kotatsu.utils.ext.*
class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnChapterChangeListener,
GridTouchHelper.OnGridTouchListener, OnPageSelectListener {
GridTouchHelper.OnGridTouchListener, OnPageSelectListener, ReaderConfigDialog.Callback {
private val presenter by moxyPresenter(factory = ReaderPresenter.Companion::getInstance)
@@ -70,14 +71,22 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
insets
}
if (reader == null) {
supportFragmentManager.commit {
replace(R.id.container, StandardReaderFragment())
}
}
presenter.loadChapter(state)
}
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
presenter.loadChapter(state)
override fun onInitReader(pages: List<MangaPage>, mode: ReaderMode, state: ReaderState) {
val currentReader = reader
when (mode) {
ReaderMode.WEBTOON -> if (currentReader !is WebtoonReaderFragment) {
supportFragmentManager.commit {
replace(R.id.container, WebtoonReaderFragment())
}
}
else -> if (currentReader !is StandardReaderFragment) {
supportFragmentManager.commit {
replace(R.id.container, StandardReaderFragment())
}
}
}
}
@@ -94,9 +103,20 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
return super.onCreateOptionsMenu(menu)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putParcelable(EXTRA_STATE, state)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_settings -> {
ReaderConfigDialog.show(supportFragmentManager)
ReaderConfigDialog.show(
supportFragmentManager, when (reader) {
is StandardReaderFragment -> ReaderMode.STANDARD
is WebtoonReaderFragment -> ReaderMode.WEBTOON
else -> ReaderMode.UNKNOWN
}
)
true
}
R.id.action_chapters -> {
@@ -136,10 +156,6 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
else -> super.onOptionsItemSelected(item)
}
override fun onPagesReady(pages: List<MangaPage>, index: Int) {
}
override fun onLoadingStateChanged(isLoading: Boolean) {
layout_loading.isVisible = isLoading
}
@@ -202,12 +218,11 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
}
override fun onChapterChanged(chapter: MangaChapter) {
presenter.loadChapter(
state.copy(
chapterId = chapter.id,
page = 0
)
state = state.copy(
chapterId = chapter.id,
page = 0
)
presenter.loadChapter(state)
}
override fun onPageSelected(page: MangaPage) {
@@ -219,6 +234,14 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
}
}
override fun onReaderModeChanged(mode: ReaderMode) {
reader?.let {
state = state.copy(page = it.currentPageIndex)
}
presenter.saveState(state, mode)
recreate()
}
override fun onPageSaved(uri: Uri?) {
if (uri != null) {
Snackbar.make(container, R.string.page_saved, Snackbar.LENGTH_LONG)

View File

@@ -4,33 +4,65 @@ import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager
import kotlinx.android.synthetic.main.dialog_list_mode.button_ok
import kotlinx.android.synthetic.main.dialog_reader_config.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.ui.common.AlertDialogFragment
import org.koitharu.kotatsu.utils.ext.withArgs
class ReaderConfigDialog : AlertDialogFragment(R.layout.dialog_reader_config),
View.OnClickListener {
private lateinit var mode: ReaderMode
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mode = arguments?.getInt(ARG_MODE, ReaderMode.UNKNOWN.id)
?.let { ReaderMode.valueOf(it) }
?.takeUnless { it == ReaderMode.UNKNOWN }
?: ReaderMode.STANDARD
}
override fun onBuildDialog(builder: AlertDialog.Builder) {
builder//.setTitle(R.string.list_mode)
builder.setTitle(R.string.read_mode)
.setCancelable(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
button_standard.isChecked = mode == ReaderMode.STANDARD
button_webtoon.isChecked = mode == ReaderMode.WEBTOON
button_ok.setOnClickListener(this)
button_standard.setOnClickListener(this)
button_webtoon.setOnClickListener(this)
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_ok -> dismiss()
R.id.button_ok -> {
((parentFragment as? Callback)
?: (activity as? Callback))?.onReaderModeChanged(mode)
dismiss()
}
R.id.button_standard -> mode = ReaderMode.STANDARD
R.id.button_webtoon -> mode = ReaderMode.WEBTOON
}
}
interface Callback {
fun onReaderModeChanged(mode: ReaderMode)
}
companion object {
private const val TAG = "ReaderConfigDialog"
private const val ARG_MODE = "mode"
fun show(fm: FragmentManager) = ReaderConfigDialog().show(fm, TAG)
fun show(fm: FragmentManager, mode: ReaderMode) = ReaderConfigDialog().withArgs(1) {
putInt(ARG_MODE, mode.id)
}.show(fm, TAG)
}
}

View File

@@ -12,6 +12,8 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.domain.MangaPreferencesRepository
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.ui.common.BasePresenter
@@ -27,14 +29,14 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
presenterScope.launch {
viewState.onLoadingStateChanged(isLoading = true)
try {
val pages = withContext(Dispatchers.IO) {
val (pages, mode) = withContext(Dispatchers.IO) {
val repo = MangaProviderFactory.create(state.manga.source)
val chapter = state.chapter ?: repo.getDetails(state.manga).chapters
?.first { it.id == state.chapterId }
?: throw RuntimeException("Chapter ${state.chapterId} not found")
repo.getPages(chapter)
repo.getPages(chapter) to MangaPreferencesRepository().getReaderMode(state.manga.id)
}
viewState.onPagesReady(pages, state.page)
viewState.onInitReader(pages, mode, state)
} catch (e: Exception) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
@@ -46,13 +48,19 @@ class ReaderPresenter : BasePresenter<ReaderView>() {
}
}
fun saveState(state: ReaderState) {
fun saveState(state: ReaderState, mode: ReaderMode? = null) {
presenterScope.launch(Dispatchers.IO) {
HistoryRepository().addOrUpdate(
manga = state.manga,
chapterId = state.chapterId,
page = state.page
)
if (mode != null) {
MangaPreferencesRepository().saveData(
mangaId = state.manga.id,
mode = mode
)
}
}
}

View File

@@ -5,11 +5,12 @@ import moxy.MvpView
import moxy.viewstate.strategy.alias.AddToEndSingle
import moxy.viewstate.strategy.alias.OneExecution
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.prefs.ReaderMode
interface ReaderView : MvpView {
@AddToEndSingle
fun onPagesReady(pages: List<MangaPage>, index: Int)
fun onInitReader(pages: List<MangaPage>, mode: ReaderMode, state: ReaderState)
@AddToEndSingle
fun onLoadingStateChanged(isLoading: Boolean)

View File

@@ -6,9 +6,11 @@ import kotlinx.android.synthetic.main.fragment_reader_standard.*
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
class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_standard) {
@@ -29,10 +31,10 @@ class StandardReaderFragment : BaseReaderFragment(R.layout.fragment_reader_stand
pager.offscreenPageLimit = 2
}
override fun onPagesReady(pages: List<MangaPage>, index: Int) {
override fun onInitReader(pages: List<MangaPage>, mode: ReaderMode, state: ReaderState) {
adapter?.let {
it.replaceData(pages)
pager.setCurrentItem(index, false)
pager.setCurrentItem(state.page, false)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -82,4 +82,5 @@
<string name="downloading_d_percent">Downloading: %d%%</string>
<string name="standard">Standard</string>
<string name="webtoon">Webtoon</string>
<string name="read_mode">Read mode</string>
</resources>