Reader
This commit is contained in:
@@ -36,6 +36,10 @@ android {
|
|||||||
disable 'MissingTranslation'
|
disable 'MissingTranslation'
|
||||||
abortOnError false
|
abortOnError false
|
||||||
}
|
}
|
||||||
|
testOptions {
|
||||||
|
unitTests.includeAndroidResources = true
|
||||||
|
unitTests.returnDefaultValues = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
androidExtensions {
|
androidExtensions {
|
||||||
experimental = true
|
experimental = true
|
||||||
@@ -74,5 +78,6 @@ dependencies {
|
|||||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||||
|
|
||||||
testImplementation 'junit:junit:4.13'
|
testImplementation 'junit:junit:4.13'
|
||||||
|
testImplementation 'androidx.test:core:1.2.0'
|
||||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||||
}
|
}
|
||||||
@@ -13,14 +13,15 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
<activity android:name="org.koitharu.kotatsu.ui.main.MainActivity">
|
<activity android:name=".ui.main.MainActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="org.koitharu.kotatsu.ui.details.MangaDetailsActivity" />
|
<activity android:name=".ui.details.MangaDetailsActivity" />
|
||||||
|
<activity android:name=".ui.reader.ReaderActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -10,81 +10,103 @@ import org.koitharu.kotatsu.utils.ext.*
|
|||||||
|
|
||||||
class ReadmangaRepository(loaderContext: MangaLoaderContext) : MangaRepository(loaderContext) {
|
class ReadmangaRepository(loaderContext: MangaLoaderContext) : MangaRepository(loaderContext) {
|
||||||
|
|
||||||
override suspend fun getList(
|
override suspend fun getList(
|
||||||
offset: Int,
|
offset: Int,
|
||||||
query: String?,
|
query: String?,
|
||||||
sortOrder: SortOrder?,
|
sortOrder: SortOrder?,
|
||||||
tags: Set<String>?
|
tags: Set<String>?
|
||||||
): List<Manga> {
|
): List<Manga> {
|
||||||
val doc = loaderContext.get("https://readmanga.me/list?sortType=updated&offset=$offset")
|
val doc = loaderContext.get("https://readmanga.me/list?sortType=updated&offset=$offset")
|
||||||
.parseHtml()
|
.parseHtml()
|
||||||
val root = doc.body().getElementById("mangaBox")
|
val root = doc.body().getElementById("mangaBox")
|
||||||
?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root")
|
?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root")
|
||||||
return root.select("div.tile").mapNotNull { node ->
|
return root.select("div.tile").mapNotNull { node ->
|
||||||
val imgDiv = node.selectFirst("div.img") ?: return@mapNotNull null
|
val imgDiv = node.selectFirst("div.img") ?: return@mapNotNull null
|
||||||
val descDiv = node.selectFirst("div.desc") ?: return@mapNotNull null
|
val descDiv = node.selectFirst("div.desc") ?: return@mapNotNull null
|
||||||
val href = imgDiv.selectFirst("a").attr("href")?.withDomain("readmanga.me")
|
val href = imgDiv.selectFirst("a").attr("href")?.withDomain("readmanga.me")
|
||||||
?: return@mapNotNull null
|
?: return@mapNotNull null
|
||||||
val title = descDiv.selectFirst("h3")?.selectFirst("a")?.text()
|
val title = descDiv.selectFirst("h3")?.selectFirst("a")?.text()
|
||||||
?: return@mapNotNull null
|
?: return@mapNotNull null
|
||||||
Manga(
|
Manga(
|
||||||
id = href.longHashCode(),
|
id = href.longHashCode(),
|
||||||
url = href,
|
url = href,
|
||||||
localizedTitle = title,
|
localizedTitle = title,
|
||||||
title = descDiv.selectFirst("h4")?.text() ?: title,
|
title = descDiv.selectFirst("h4")?.text() ?: title,
|
||||||
coverUrl = imgDiv.selectFirst("img.lazy")?.attr("data-original").orEmpty(),
|
coverUrl = imgDiv.selectFirst("img.lazy")?.attr("data-original").orEmpty(),
|
||||||
summary = "",
|
summary = "",
|
||||||
rating = safe {
|
rating = safe {
|
||||||
node.selectFirst("div.rating")
|
node.selectFirst("div.rating")
|
||||||
?.attr("title")
|
?.attr("title")
|
||||||
?.substringBefore(' ')
|
?.substringBefore(' ')
|
||||||
?.toFloatOrNull()
|
?.toFloatOrNull()
|
||||||
?.div(10f)
|
?.div(10f)
|
||||||
} ?: Manga.NO_RATING,
|
} ?: Manga.NO_RATING,
|
||||||
tags = safe {
|
tags = safe {
|
||||||
descDiv.selectFirst("div.tile-info")
|
descDiv.selectFirst("div.tile-info")
|
||||||
?.select("a.element-link")
|
?.select("a.element-link")
|
||||||
?.map {
|
?.map {
|
||||||
MangaTag(
|
MangaTag(
|
||||||
title = it.text(),
|
title = it.text(),
|
||||||
key = it.attr("href").substringAfterLast('/')
|
key = it.attr("href").substringAfterLast('/')
|
||||||
)
|
)
|
||||||
}?.toSet()
|
}?.toSet()
|
||||||
}.orEmpty(),
|
}.orEmpty(),
|
||||||
state = when {
|
state = when {
|
||||||
node.selectFirst("div.tags")
|
node.selectFirst("div.tags")
|
||||||
?.selectFirst("span.mangaCompleted") != null -> MangaState.FINISHED
|
?.selectFirst("span.mangaCompleted") != null -> MangaState.FINISHED
|
||||||
else -> null
|
else -> null
|
||||||
},
|
},
|
||||||
source = MangaSource.READMANGA_RU
|
source = MangaSource.READMANGA_RU
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga {
|
override suspend fun getDetails(manga: Manga): Manga {
|
||||||
val doc = loaderContext.get(manga.url).parseHtml()
|
val doc = loaderContext.get(manga.url).parseHtml()
|
||||||
val root = doc.body().getElementById("mangaBox")
|
val root = doc.body().getElementById("mangaBox")
|
||||||
return manga.copy(
|
return manga.copy(
|
||||||
description = root.selectFirst("div.manga-description").firstChild()?.html()?.parseAsHtml(),
|
description = root.selectFirst("div.manga-description").firstChild()?.html()?.parseAsHtml(),
|
||||||
largeCoverUrl = root.selectFirst("div.subject-cower")?.selectFirst("img")?.attr(
|
largeCoverUrl = root.selectFirst("div.subject-cower")?.selectFirst("img")?.attr(
|
||||||
"data-full"
|
"data-full"
|
||||||
),
|
),
|
||||||
chapters = root.selectFirst("div.chapters-link")?.selectFirst("table")
|
chapters = root.selectFirst("div.chapters-link")?.selectFirst("table")
|
||||||
?.select("a")?.asReversed()?.mapIndexedNotNull { i, a ->
|
?.select("a")?.asReversed()?.mapIndexedNotNull { i, a ->
|
||||||
val href =
|
val href =
|
||||||
a.attr("href")?.withDomain("readmanga.me") ?: return@mapIndexedNotNull null
|
a.attr("href")?.withDomain("readmanga.me") ?: return@mapIndexedNotNull null
|
||||||
MangaChapter(
|
MangaChapter(
|
||||||
id = href.longHashCode(),
|
id = href.longHashCode(),
|
||||||
name = a.ownText(),
|
name = a.ownText(),
|
||||||
number = i + 1,
|
number = i + 1,
|
||||||
url = href,
|
url = href,
|
||||||
source = MangaSource.READMANGA_RU
|
source = MangaSource.READMANGA_RU
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
val doc = loaderContext.get(chapter.url).parseHtml()
|
||||||
}
|
val scripts = doc.select("script")
|
||||||
|
for (script in scripts) {
|
||||||
|
val data = script.html()
|
||||||
|
val pos = data.indexOf("rm_h.init")
|
||||||
|
if (pos == -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val json = data.substring(pos).substringAfter('[').substringBeforeLast(']')
|
||||||
|
val matches = Regex("\\[.*?]").findAll(json).toList()
|
||||||
|
val regex = Regex("['\"].*?['\"]")
|
||||||
|
return matches.map { x ->
|
||||||
|
val parts = regex.findAll(x.value).toList()
|
||||||
|
val url = parts[1].value.removeSurrounding('"', '\'') +
|
||||||
|
parts[2].value.removeSurrounding('"', '\'')
|
||||||
|
MangaPage(
|
||||||
|
id = url.longHashCode(),
|
||||||
|
url = url,
|
||||||
|
source = MangaSource.READMANGA_RU
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw ParseException("Pages list not found at ${chapter.url}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.common
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
|
||||||
|
abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : DialogFragment() {
|
||||||
|
|
||||||
|
private var rootView: View? = null
|
||||||
|
|
||||||
|
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val view = activity?.layoutInflater?.inflate(layoutResId, null)
|
||||||
|
rootView = view
|
||||||
|
if (view != null) {
|
||||||
|
onViewCreated(view, savedInstanceState)
|
||||||
|
}
|
||||||
|
return AlertDialog.Builder(requireContext(), theme)
|
||||||
|
.setView(view)
|
||||||
|
.also(::onBuildDialog)
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
rootView = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getView(): View? {
|
||||||
|
return rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onBuildDialog(builder: AlertDialog.Builder) = Unit
|
||||||
|
}
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.ui.common
|
package org.koitharu.kotatsu.ui.common
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.cancelChildren
|
|
||||||
import moxy.MvpPresenter
|
import moxy.MvpPresenter
|
||||||
import moxy.MvpView
|
import moxy.MvpView
|
||||||
import org.koin.core.KoinComponent
|
import org.koin.core.KoinComponent
|
||||||
@@ -17,7 +14,7 @@ abstract class BasePresenter<V : MvpView> : MvpPresenter<V>(), KoinComponent, Co
|
|||||||
get() = Dispatchers.Main + job
|
get() = Dispatchers.Main + job
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
coroutineContext.cancelChildren()
|
coroutineContext.cancel()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ 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.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.reader.ReaderActivity
|
||||||
|
|
||||||
class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsView,
|
class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsView,
|
||||||
OnRecyclerItemClickListener<MangaChapter> {
|
OnRecyclerItemClickListener<MangaChapter> {
|
||||||
@@ -19,6 +20,8 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
private val presenter by moxyPresenter { (activity as MangaDetailsActivity).presenter }
|
private val presenter by moxyPresenter { (activity as MangaDetailsActivity).presenter }
|
||||||
|
|
||||||
|
private var manga: Manga? = null
|
||||||
|
|
||||||
private lateinit var adapter: ChaptersAdapter
|
private lateinit var adapter: ChaptersAdapter
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -29,6 +32,7 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMangaUpdated(manga: Manga) {
|
override fun onMangaUpdated(manga: Manga) {
|
||||||
|
this.manga = manga
|
||||||
adapter.replaceData(manga.chapters.orEmpty())
|
adapter.replaceData(manga.chapters.orEmpty())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +45,10 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onItemClick(item: MangaChapter, position: Int, view: View) {
|
override fun onItemClick(item: MangaChapter, position: Int, view: View) {
|
||||||
//TODO
|
startActivity(ReaderActivity.newIntent(
|
||||||
|
context ?: return,
|
||||||
|
manga ?: return,
|
||||||
|
item.id
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,36 +1,23 @@
|
|||||||
package org.koitharu.kotatsu.ui.main.list
|
package org.koitharu.kotatsu.ui.main.list
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.DialogFragment
|
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import kotlinx.android.synthetic.main.dialog_list_mode.*
|
import kotlinx.android.synthetic.main.dialog_list_mode.*
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||||
|
import org.koitharu.kotatsu.ui.common.AlertDialogFragment
|
||||||
|
|
||||||
class ListModeSelectDialog : DialogFragment(), View.OnClickListener {
|
class ListModeSelectDialog : AlertDialogFragment(R.layout.dialog_list_mode), View.OnClickListener {
|
||||||
|
|
||||||
private val setting by inject<AppSettings>()
|
private val setting by inject<AppSettings>()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||||
inflater: LayoutInflater,
|
builder.setTitle(R.string.list_mode)
|
||||||
container: ViewGroup?,
|
.setCancelable(true)
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View? {
|
|
||||||
return inflater.inflate(R.layout.dialog_list_mode, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
return super.onCreateDialog(savedInstanceState).apply {
|
|
||||||
setTitle(R.string.list_mode)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
@@ -57,7 +44,7 @@ class ListModeSelectDialog : DialogFragment(), View.OnClickListener {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val TAG = "LIST_MODE"
|
private const val TAG = "ListModeSelectDialog"
|
||||||
|
|
||||||
fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG)
|
fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.synthetic.main.dialog_chapters.*
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.ui.common.AlertDialogFragment
|
||||||
|
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
|
||||||
|
import org.koitharu.kotatsu.ui.details.ChaptersAdapter
|
||||||
|
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||||
|
|
||||||
|
class ChaptersDialog : AlertDialogFragment(R.layout.dialog_chapters), OnRecyclerItemClickListener<MangaChapter> {
|
||||||
|
|
||||||
|
override fun onBuildDialog(builder: AlertDialog.Builder) {
|
||||||
|
builder.setTitle(R.string.chapters)
|
||||||
|
.setNegativeButton(R.string.close, null)
|
||||||
|
.setCancelable(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
recyclerView_chapters.addItemDecoration(DividerItemDecoration(requireContext(), RecyclerView.VERTICAL))
|
||||||
|
recyclerView_chapters.adapter = ChaptersAdapter(this).apply {
|
||||||
|
arguments?.getParcelableArrayList<MangaChapter>(ARG_CHAPTERS)?.let(this::replaceData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(item: MangaChapter, position: Int, view: View) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val TAG = "ChaptersDialog"
|
||||||
|
|
||||||
|
private const val ARG_CHAPTERS = "chapters"
|
||||||
|
|
||||||
|
fun show(fm: FragmentManager, chapters: List<MangaChapter>) = ChaptersDialog()
|
||||||
|
.withArgs(1) {
|
||||||
|
putParcelableArrayList(ARG_CHAPTERS, ArrayList(chapters))
|
||||||
|
}.show(fm, TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
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 org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
|
||||||
|
class PageHolder(parent: ViewGroup, private val loader: PageLoader) : BaseViewHolder<MangaPage>(parent, R.layout.item_page),
|
||||||
|
SubsamplingScaleImageView.OnImageEventListener {
|
||||||
|
|
||||||
|
init {
|
||||||
|
ssiv.setOnImageEventListener(this)
|
||||||
|
button_retry.setOnClickListener {
|
||||||
|
onBind(boundData ?: return@setOnClickListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(data: MangaPage) {
|
||||||
|
layout_error.isVisible = false
|
||||||
|
progressBar.show()
|
||||||
|
ssiv.recycle()
|
||||||
|
loader.load(data.url) {
|
||||||
|
ssiv.setImage(ImageSource.uri(it.toUri()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReady() {
|
||||||
|
progressBar.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImageLoadError(e: Exception) {
|
||||||
|
textView_error.text = e.getDisplayMessage(context.resources)
|
||||||
|
layout_error.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onImageLoaded() = Unit
|
||||||
|
|
||||||
|
override fun onTileLoadError(e: Exception?) = Unit
|
||||||
|
|
||||||
|
override fun onPreviewReleased() = Unit
|
||||||
|
|
||||||
|
override fun onPreviewLoadError(e: Exception?) = Unit
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.LongSparseArray
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
|
import org.koin.core.inject
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.await
|
||||||
|
import org.koitharu.kotatsu.utils.ext.longHashCode
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class PageLoader(context: Context) : KoinComponent, CoroutineScope, DisposableHandle {
|
||||||
|
|
||||||
|
private val job = SupervisorJob()
|
||||||
|
private val tasks = HashMap<String, Job>()
|
||||||
|
private val okHttp by inject<OkHttpClient>()
|
||||||
|
private val cacheDir = File(context.externalCacheDir ?: context.cacheDir, "pages")
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (!cacheDir.exists()) {
|
||||||
|
cacheDir.mkdir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext
|
||||||
|
get() = Dispatchers.Main + job
|
||||||
|
|
||||||
|
fun load(url: String, callback: (File) -> Unit) = launch {
|
||||||
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
loadFile(url, false)
|
||||||
|
}
|
||||||
|
callback(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadFile(url: String, force: Boolean): File {
|
||||||
|
val file = File(cacheDir, url.longHashCode().toString())
|
||||||
|
if (!force && file.exists()) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.get()
|
||||||
|
.build()
|
||||||
|
okHttp.newCall(request).await().use { response ->
|
||||||
|
file.outputStream().use { out ->
|
||||||
|
response.body!!.byteStream().copyTo(out)
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
coroutineContext.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
|
||||||
|
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
|
||||||
|
|
||||||
|
class PagesAdapter(private val loader: PageLoader) : BaseRecyclerAdapter<MangaPage>() {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader)
|
||||||
|
|
||||||
|
override fun onGetItemId(item: MangaPage) = item.id
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
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
|
||||||
|
|
||||||
|
class ReaderActivity : BaseActivity(), ReaderView {
|
||||||
|
|
||||||
|
private val presenter by moxyPresenter { ReaderPresenter() }
|
||||||
|
|
||||||
|
private val manga by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
intent.getParcelableExtra<Manga>(EXTRA_MANGA)!!
|
||||||
|
}
|
||||||
|
private val chapterId by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
intent.getLongExtra(EXTRA_CHAPTER_ID, 0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var loader: PageLoader
|
||||||
|
private lateinit var adapter: PagesAdapter
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
setContentView(R.layout.activity_reader)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
bottomBar.inflateMenu(R.menu.opt_reader_bottom)
|
||||||
|
|
||||||
|
val chapter = manga.chapters?.find { x -> x.id == chapterId }
|
||||||
|
if (chapter == null) {
|
||||||
|
// TODO
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
title = chapter.name
|
||||||
|
manga.chapters?.run {
|
||||||
|
supportActionBar?.subtitle = getString(R.string.chapter_d_of_d, chapter.number, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
loader = PageLoader(this)
|
||||||
|
adapter = PagesAdapter(loader)
|
||||||
|
pager.adapter = adapter
|
||||||
|
presenter.loadChapter(chapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
loader.dispose()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.opt_reader_top, menu)
|
||||||
|
return super.onCreateOptionsMenu(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) {
|
||||||
|
R.id.action_chapters -> {
|
||||||
|
ChaptersDialog.show(supportFragmentManager, manga.chapters.orEmpty())
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPagesReady(pages: List<MangaPage>) {
|
||||||
|
adapter.replaceData(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadingStateChanged(isLoading: Boolean) {
|
||||||
|
layout_loading.isVisible = isLoading
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onError(e: Exception) {
|
||||||
|
showDialog {
|
||||||
|
setTitle(R.string.error_occurred)
|
||||||
|
setMessage(e.message)
|
||||||
|
setPositiveButton(R.string.close, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val EXTRA_MANGA = "manga"
|
||||||
|
private const val EXTRA_CHAPTER_ID = "chapter_id"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, manga: Manga, chapterId: Long) = Intent(context, ReaderActivity::class.java)
|
||||||
|
.putExtra(EXTRA_MANGA, manga)
|
||||||
|
.putExtra(EXTRA_CHAPTER_ID, chapterId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
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.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
||||||
|
import org.koitharu.kotatsu.ui.common.BasePresenter
|
||||||
|
|
||||||
|
@InjectViewState
|
||||||
|
class ReaderPresenter() : BasePresenter<ReaderView>() {
|
||||||
|
|
||||||
|
fun loadChapter(chapter: MangaChapter) {
|
||||||
|
launch {
|
||||||
|
viewState.onLoadingStateChanged(isLoading = true)
|
||||||
|
try {
|
||||||
|
val pages = withContext(Dispatchers.IO) {
|
||||||
|
MangaProviderFactory.create(chapter.source).getPages(chapter)
|
||||||
|
}
|
||||||
|
viewState.onPagesReady(pages)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
viewState.onError(e)
|
||||||
|
} finally {
|
||||||
|
viewState.onLoadingStateChanged(isLoading = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.reader
|
||||||
|
|
||||||
|
import moxy.MvpView
|
||||||
|
import moxy.viewstate.strategy.AddToEndSingleStrategy
|
||||||
|
import moxy.viewstate.strategy.OneExecutionStateStrategy
|
||||||
|
import moxy.viewstate.strategy.StateStrategyType
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaPage
|
||||||
|
|
||||||
|
interface ReaderView : MvpView {
|
||||||
|
|
||||||
|
@StateStrategyType(AddToEndSingleStrategy::class)
|
||||||
|
fun onPagesReady(pages: List<MangaPage>)
|
||||||
|
|
||||||
|
@StateStrategyType(AddToEndSingleStrategy::class)
|
||||||
|
fun onLoadingStateChanged(isLoading: Boolean)
|
||||||
|
|
||||||
|
@StateStrategyType(OneExecutionStateStrategy::class)
|
||||||
|
fun onError(e: Exception)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package org.koitharu.kotatsu.utils.ext
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
|
||||||
|
fun Context.showDialog(block: AlertDialog.Builder.() -> Unit): AlertDialog {
|
||||||
|
return AlertDialog.Builder(this)
|
||||||
|
.apply(block)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
@@ -20,4 +20,16 @@ fun String.withDomain(domain: String, ssl: Boolean = true) = when {
|
|||||||
append(this@withDomain)
|
append(this@withDomain)
|
||||||
}
|
}
|
||||||
else -> this
|
else -> this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.removeSurrounding(vararg chars: Char): String {
|
||||||
|
if (length == 0) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
for (c in chars) {
|
||||||
|
if (first() == c && last() == c) {
|
||||||
|
return substring(1, length - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
11
app/src/main/res/drawable/ic_drop_down.xml
Normal file
11
app/src/main/res/drawable/ic_drop_down.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?android:textColorPrimary"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M7,10l5,5 5,-5z" />
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/ic_error_large.xml
Normal file
11
app/src/main/res/drawable/ic_error_large.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:tint="@color/error"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M11,15h2v2h-2zM11,7h2v6h-2zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z" />
|
||||||
|
</vector>
|
||||||
11
app/src/main/res/drawable/ic_favourites.xml
Normal file
11
app/src/main/res/drawable/ic_favourites.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z" />
|
||||||
|
</vector>
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<group>
|
|
||||||
<clip-path android:pathData="M0,0h24v24H0V0z M 0,0" />
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4V6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" />
|
|
||||||
</group>
|
|
||||||
</vector>
|
|
||||||
|
|
||||||
54
app/src/main/res/layout/activity_reader.xml
Normal file
54
app/src/main/res/layout/activity_reader.xml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.circularreveal.CircularRevealFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
|
android:id="@+id/pager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/dim"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.MaterialComponents.Light" />
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/bottomBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="@color/dim"
|
||||||
|
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
|
||||||
|
app:popupTheme="@style/ThemeOverlay.MaterialComponents.Light" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_loading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView_loading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/loading_"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.circularreveal.CircularRevealFrameLayout>
|
||||||
12
app/src/main/res/layout/dialog_chapters.xml
Normal file
12
app/src/main/res/layout/dialog_chapters.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?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"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/recyclerView_chapters"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_chapter" />
|
||||||
49
app/src/main/res/layout/item_page.xml
Normal file
49
app/src/main/res/layout/item_page.xml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
|
||||||
|
android:id="@+id/ssiv"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<androidx.core.widget.ContentLoadingProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginStart="60dp"
|
||||||
|
android:layout_marginEnd="60dp"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:id="@+id/layout_error" >
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:drawableTop="@drawable/ic_error_large"
|
||||||
|
android:id="@+id/textView_error"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
tools:text="@tools:sample/lorem[6]"
|
||||||
|
android:drawablePadding="12dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||||
|
android:gravity="center_horizontal"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/button_retry"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||||
|
android:text="@string/try_again" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
android:title="@string/local_storage" />
|
android:title="@string/local_storage" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_favourites"
|
android:id="@+id/nav_favourites"
|
||||||
android:icon="@drawable/ic_star_half"
|
android:icon="@drawable/ic_favourites"
|
||||||
android:title="@string/favourites" />
|
android:title="@string/favourites" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/nav_history"
|
android:id="@+id/nav_history"
|
||||||
|
|||||||
4
app/src/main/res/menu/opt_reader_bottom.xml
Normal file
4
app/src/main/res/menu/opt_reader_bottom.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</menu>
|
||||||
13
app/src/main/res/menu/opt_reader_top.xml
Normal file
13
app/src/main/res/menu/opt_reader_top.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?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_chapters"
|
||||||
|
android:icon="@drawable/ic_drop_down"
|
||||||
|
android:title="@string/chapters"
|
||||||
|
android:orderInCategory="0"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="colorPrimary">#0288D1</color>
|
<color name="primary">#0288D1</color>
|
||||||
<color name="colorPrimaryDark">#0D47A1</color>
|
<color name="primary_dark">#0D47A1</color>
|
||||||
<color name="colorAccent">#F4511E</color>
|
<color name="accent">#F4511E</color>
|
||||||
|
<color name="dim">#99000000</color>
|
||||||
|
<color name="error">#D32F2F</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -15,4 +15,8 @@
|
|||||||
<string name="list_mode">List mode</string>
|
<string name="list_mode">List mode</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="remote_sources">Remote sources</string>
|
<string name="remote_sources">Remote sources</string>
|
||||||
|
<string name="loading_">Loading…</string>
|
||||||
|
<string name="chapter_d_of_d">Chapter %d of %d</string>
|
||||||
|
<string name="close">Close</string>
|
||||||
|
<string name="try_again">Try again</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/primary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/primary_dark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/accent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
@@ -3,4 +3,8 @@ package org.koitharu.kotatsu.parsers
|
|||||||
interface MangaParserTest {
|
interface MangaParserTest {
|
||||||
|
|
||||||
fun testMangaList()
|
fun testMangaList()
|
||||||
|
|
||||||
|
fun testMangaDetails()
|
||||||
|
|
||||||
|
fun testMangaPages()
|
||||||
}
|
}
|
||||||
@@ -8,27 +8,46 @@ import org.junit.runner.RunWith
|
|||||||
import org.koitharu.kotatsu.domain.repository.ReadmangaRepository
|
import org.koitharu.kotatsu.domain.repository.ReadmangaRepository
|
||||||
import org.koitharu.kotatsu.parsers.MangaParserTest
|
import org.koitharu.kotatsu.parsers.MangaParserTest
|
||||||
import org.koitharu.kotatsu.parsers.RepositoryTestEnvironment
|
import org.koitharu.kotatsu.parsers.RepositoryTestEnvironment
|
||||||
import org.koitharu.kotatsu.utils.MyAsserts
|
import org.koitharu.kotatsu.utils.TestUtil
|
||||||
import org.mockito.junit.MockitoJUnitRunner
|
import org.mockito.junit.MockitoJUnitRunner
|
||||||
|
|
||||||
@RunWith(MockitoJUnitRunner::class)
|
@RunWith(MockitoJUnitRunner::class)
|
||||||
class ReadmangaRuTest : MangaParserTest {
|
class ReadmangaRuTest : MangaParserTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
override fun testMangaList() {
|
override fun testMangaList() {
|
||||||
val list = runBlocking { repository.getList(1) }
|
val list = runBlocking { repository.getList(1) }
|
||||||
Assert.assertTrue(list.size == 70)
|
Assert.assertTrue(list.size == 70)
|
||||||
val item = list[40]
|
val item = list[40]
|
||||||
Assert.assertTrue(item.title.isNotEmpty())
|
Assert.assertTrue(item.title.isNotEmpty())
|
||||||
Assert.assertTrue(item.rating in 0f..1f)
|
Assert.assertTrue(item.rating in 0f..1f)
|
||||||
MyAsserts.assertValidUrl(item.url)
|
TestUtil.assertValidUrl(item.url)
|
||||||
MyAsserts.assertValidUrl(item.coverUrl)
|
TestUtil.assertValidUrl(item.coverUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : RepositoryTestEnvironment() {
|
@Test
|
||||||
|
override fun testMangaDetails() {
|
||||||
|
val manga = runBlocking { repository.getDetails(repository.getList(1).last()) }
|
||||||
|
Assert.assertNotNull(manga.largeCoverUrl)
|
||||||
|
TestUtil.assertValidUrl(manga.largeCoverUrl!!)
|
||||||
|
Assert.assertNotNull(manga.chapters)
|
||||||
|
val chapter = manga.chapters!!.last()
|
||||||
|
TestUtil.assertValidUrl(chapter.url)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@Test
|
||||||
@BeforeClass
|
override fun testMangaPages() {
|
||||||
fun setUp() = initialize(ReadmangaRepository::class.java)
|
val chapter = runBlocking { repository.getDetails(repository.getList(1).last()).chapters!!.first() }
|
||||||
}
|
val pages = runBlocking { repository.getPages(chapter) }
|
||||||
|
Assert.assertFalse(pages.isEmpty())
|
||||||
|
TestUtil.assertValidUrl(runBlocking { repository.getPageFullUrl(pages.first()) })
|
||||||
|
TestUtil.assertValidUrl(runBlocking { repository.getPageFullUrl(pages.last()) })
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : RepositoryTestEnvironment() {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@BeforeClass
|
||||||
|
fun setUp() = initialize(ReadmangaRepository::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import org.junit.Assert
|
|||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
object MyAsserts {
|
object TestUtil {
|
||||||
|
|
||||||
private val VALID_RESPONSE_CODES = arrayOf(
|
private val VALID_RESPONSE_CODES = arrayOf(
|
||||||
HttpURLConnection.HTTP_OK,
|
HttpURLConnection.HTTP_OK,
|
||||||
Reference in New Issue
Block a user