From 82fda9394d6ef65286d70bfaf3f033bd86b9b2d3 Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 2 Feb 2020 09:44:13 +0200 Subject: [PATCH] Reader --- app/build.gradle | 5 + app/src/main/AndroidManifest.xml | 5 +- .../domain/repository/ReadmangaRepository.kt | 172 ++++++++++-------- .../kotatsu/ui/common/AlertDialogFragment.kt | 36 ++++ .../kotatsu/ui/common/BasePresenter.kt | 7 +- .../kotatsu/ui/details/ChaptersFragment.kt | 10 +- .../ui/main/list/ListModeSelectDialog.kt | 25 +-- .../kotatsu/ui/reader/ChaptersDialog.kt | 47 +++++ .../koitharu/kotatsu/ui/reader/PageHolder.kt | 49 +++++ .../koitharu/kotatsu/ui/reader/PageLoader.kt | 60 ++++++ .../kotatsu/ui/reader/PagesAdapter.kt | 13 ++ .../kotatsu/ui/reader/ReaderActivity.kt | 99 ++++++++++ .../kotatsu/ui/reader/ReaderPresenter.kt | 34 ++++ .../koitharu/kotatsu/ui/reader/ReaderView.kt | 19 ++ .../koitharu/kotatsu/utils/ext/ContextExt.kt | 10 + .../koitharu/kotatsu/utils/ext/StringExt.kt | 12 ++ app/src/main/res/drawable/ic_drop_down.xml | 11 ++ app/src/main/res/drawable/ic_error_large.xml | 11 ++ app/src/main/res/drawable/ic_favourites.xml | 11 ++ app/src/main/res/drawable/ic_star_half.xml | 14 -- app/src/main/res/layout/activity_reader.xml | 54 ++++++ app/src/main/res/layout/dialog_chapters.xml | 12 ++ app/src/main/res/layout/item_page.xml | 49 +++++ app/src/main/res/menu/nav_drawer.xml | 2 +- app/src/main/res/menu/opt_reader_bottom.xml | 4 + app/src/main/res/menu/opt_reader_top.xml | 13 ++ app/src/main/res/values/colors.xml | 8 +- app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/themes.xml | 6 +- .../kotatsu/parsers/MangaParserTest.kt | 4 + .../parsers/repository/ReadmangaRuTest.kt | 51 ++++-- .../utils/{MyAsserts.kt => TestUtil.kt} | 2 +- 32 files changed, 719 insertions(+), 140 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/common/AlertDialogFragment.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/ChaptersDialog.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/PageHolder.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/PagesAdapter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderActivity.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderPresenter.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderView.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/ext/ContextExt.kt create mode 100644 app/src/main/res/drawable/ic_drop_down.xml create mode 100644 app/src/main/res/drawable/ic_error_large.xml create mode 100644 app/src/main/res/drawable/ic_favourites.xml delete mode 100644 app/src/main/res/drawable/ic_star_half.xml create mode 100644 app/src/main/res/layout/activity_reader.xml create mode 100644 app/src/main/res/layout/dialog_chapters.xml create mode 100644 app/src/main/res/layout/item_page.xml create mode 100644 app/src/main/res/menu/opt_reader_bottom.xml create mode 100644 app/src/main/res/menu/opt_reader_top.xml rename app/src/test/java/org/koitharu/kotatsu/utils/{MyAsserts.kt => TestUtil.kt} (96%) diff --git a/app/build.gradle b/app/build.gradle index 22276bfa5..c9344047b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -36,6 +36,10 @@ android { disable 'MissingTranslation' abortOnError false } + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + } } androidExtensions { experimental = true @@ -74,5 +78,6 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' testImplementation 'junit:junit:4.13' + testImplementation 'androidx.test:core:1.2.0' testImplementation 'org.mockito:mockito-core:2.23.0' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c04b7c14b..7a9a413d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,14 +13,15 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> - + - + + \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/domain/repository/ReadmangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/domain/repository/ReadmangaRepository.kt index b0b20d484..de2876aac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/domain/repository/ReadmangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/domain/repository/ReadmangaRepository.kt @@ -10,81 +10,103 @@ import org.koitharu.kotatsu.utils.ext.* class ReadmangaRepository(loaderContext: MangaLoaderContext) : MangaRepository(loaderContext) { - override suspend fun getList( - offset: Int, - query: String?, - sortOrder: SortOrder?, - tags: Set? - ): List { - val doc = loaderContext.get("https://readmanga.me/list?sortType=updated&offset=$offset") - .parseHtml() - val root = doc.body().getElementById("mangaBox") - ?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root") - return root.select("div.tile").mapNotNull { node -> - val imgDiv = node.selectFirst("div.img") ?: return@mapNotNull null - val descDiv = node.selectFirst("div.desc") ?: return@mapNotNull null - val href = imgDiv.selectFirst("a").attr("href")?.withDomain("readmanga.me") - ?: return@mapNotNull null - val title = descDiv.selectFirst("h3")?.selectFirst("a")?.text() - ?: return@mapNotNull null - Manga( - id = href.longHashCode(), - url = href, - localizedTitle = title, - title = descDiv.selectFirst("h4")?.text() ?: title, - coverUrl = imgDiv.selectFirst("img.lazy")?.attr("data-original").orEmpty(), - summary = "", - rating = safe { - node.selectFirst("div.rating") - ?.attr("title") - ?.substringBefore(' ') - ?.toFloatOrNull() - ?.div(10f) - } ?: Manga.NO_RATING, - tags = safe { - descDiv.selectFirst("div.tile-info") - ?.select("a.element-link") - ?.map { - MangaTag( - title = it.text(), - key = it.attr("href").substringAfterLast('/') - ) - }?.toSet() - }.orEmpty(), - state = when { - node.selectFirst("div.tags") - ?.selectFirst("span.mangaCompleted") != null -> MangaState.FINISHED - else -> null - }, - source = MangaSource.READMANGA_RU - ) - } - } + override suspend fun getList( + offset: Int, + query: String?, + sortOrder: SortOrder?, + tags: Set? + ): List { + val doc = loaderContext.get("https://readmanga.me/list?sortType=updated&offset=$offset") + .parseHtml() + val root = doc.body().getElementById("mangaBox") + ?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root") + return root.select("div.tile").mapNotNull { node -> + val imgDiv = node.selectFirst("div.img") ?: return@mapNotNull null + val descDiv = node.selectFirst("div.desc") ?: return@mapNotNull null + val href = imgDiv.selectFirst("a").attr("href")?.withDomain("readmanga.me") + ?: return@mapNotNull null + val title = descDiv.selectFirst("h3")?.selectFirst("a")?.text() + ?: return@mapNotNull null + Manga( + id = href.longHashCode(), + url = href, + localizedTitle = title, + title = descDiv.selectFirst("h4")?.text() ?: title, + coverUrl = imgDiv.selectFirst("img.lazy")?.attr("data-original").orEmpty(), + summary = "", + rating = safe { + node.selectFirst("div.rating") + ?.attr("title") + ?.substringBefore(' ') + ?.toFloatOrNull() + ?.div(10f) + } ?: Manga.NO_RATING, + tags = safe { + descDiv.selectFirst("div.tile-info") + ?.select("a.element-link") + ?.map { + MangaTag( + title = it.text(), + key = it.attr("href").substringAfterLast('/') + ) + }?.toSet() + }.orEmpty(), + state = when { + node.selectFirst("div.tags") + ?.selectFirst("span.mangaCompleted") != null -> MangaState.FINISHED + else -> null + }, + source = MangaSource.READMANGA_RU + ) + } + } - override suspend fun getDetails(manga: Manga): Manga { - val doc = loaderContext.get(manga.url).parseHtml() - val root = doc.body().getElementById("mangaBox") - return manga.copy( - description = root.selectFirst("div.manga-description").firstChild()?.html()?.parseAsHtml(), - largeCoverUrl = root.selectFirst("div.subject-cower")?.selectFirst("img")?.attr( - "data-full" - ), - chapters = root.selectFirst("div.chapters-link")?.selectFirst("table") - ?.select("a")?.asReversed()?.mapIndexedNotNull { i, a -> - val href = - a.attr("href")?.withDomain("readmanga.me") ?: return@mapIndexedNotNull null - MangaChapter( - id = href.longHashCode(), - name = a.ownText(), - number = i + 1, - url = href, - source = MangaSource.READMANGA_RU - ) - } - ) - } + override suspend fun getDetails(manga: Manga): Manga { + val doc = loaderContext.get(manga.url).parseHtml() + val root = doc.body().getElementById("mangaBox") + return manga.copy( + description = root.selectFirst("div.manga-description").firstChild()?.html()?.parseAsHtml(), + largeCoverUrl = root.selectFirst("div.subject-cower")?.selectFirst("img")?.attr( + "data-full" + ), + chapters = root.selectFirst("div.chapters-link")?.selectFirst("table") + ?.select("a")?.asReversed()?.mapIndexedNotNull { i, a -> + val href = + a.attr("href")?.withDomain("readmanga.me") ?: return@mapIndexedNotNull null + MangaChapter( + id = href.longHashCode(), + name = a.ownText(), + number = i + 1, + url = href, + source = MangaSource.READMANGA_RU + ) + } + ) + } - override suspend fun getPages(chapter: MangaChapter): List { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + override suspend fun getPages(chapter: MangaChapter): List { + 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}") + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/AlertDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/AlertDialogFragment.kt new file mode 100644 index 000000000..43e482aba --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/AlertDialogFragment.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/BasePresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/BasePresenter.kt index e3267da93..30a924aac 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/BasePresenter.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/common/BasePresenter.kt @@ -1,9 +1,6 @@ package org.koitharu.kotatsu.ui.common -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.* import moxy.MvpPresenter import moxy.MvpView import org.koin.core.KoinComponent @@ -17,7 +14,7 @@ abstract class BasePresenter : MvpPresenter(), KoinComponent, Co get() = Dispatchers.Main + job override fun onDestroy() { - coroutineContext.cancelChildren() + coroutineContext.cancel() super.onDestroy() } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/details/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/details/ChaptersFragment.kt index 32813dc02..55ab25a29 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/details/ChaptersFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/details/ChaptersFragment.kt @@ -12,6 +12,7 @@ import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.ui.common.BaseFragment import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener +import org.koitharu.kotatsu.ui.reader.ReaderActivity class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsView, OnRecyclerItemClickListener { @@ -19,6 +20,8 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV @Suppress("unused") private val presenter by moxyPresenter { (activity as MangaDetailsActivity).presenter } + private var manga: Manga? = null + private lateinit var adapter: ChaptersAdapter override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -29,6 +32,7 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV } override fun onMangaUpdated(manga: Manga) { + this.manga = manga 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) { - //TODO + startActivity(ReaderActivity.newIntent( + context ?: return, + manga ?: return, + item.id + )) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/ListModeSelectDialog.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/ListModeSelectDialog.kt index 53aca3f66..42a5b744a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/ListModeSelectDialog.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/ListModeSelectDialog.kt @@ -1,36 +1,23 @@ package org.koitharu.kotatsu.ui.main.list -import android.app.Dialog -import android.content.DialogInterface import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.appcompat.app.AlertDialog -import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import kotlinx.android.synthetic.main.dialog_list_mode.* import org.koin.android.ext.android.inject import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings 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() - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - 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 onBuildDialog(builder: AlertDialog.Builder) { + builder.setTitle(R.string.list_mode) + .setCancelable(true) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -57,7 +44,7 @@ class ListModeSelectDialog : DialogFragment(), View.OnClickListener { companion object { - private const val TAG = "LIST_MODE" + private const val TAG = "ListModeSelectDialog" fun show(fm: FragmentManager) = ListModeSelectDialog().show(fm, TAG) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/ChaptersDialog.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ChaptersDialog.kt new file mode 100644 index 000000000..d2026ca8a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ChaptersDialog.kt @@ -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 { + + 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(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) = ChaptersDialog() + .withArgs(1) { + putParcelableArrayList(ARG_CHAPTERS, ArrayList(chapters)) + }.show(fm, TAG) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageHolder.kt new file mode 100644 index 000000000..3a05d6ba5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageHolder.kt @@ -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(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 +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt new file mode 100644 index 000000000..ff3bf1591 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PageLoader.kt @@ -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() + private val okHttp by inject() + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/PagesAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PagesAdapter.kt new file mode 100644 index 000000000..5a7bbdb5a --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/PagesAdapter.kt @@ -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() { + + override fun onCreateViewHolder(parent: ViewGroup) = PageHolder(parent, loader) + + override fun onGetItemId(item: MangaPage) = item.id +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderActivity.kt new file mode 100644 index 000000000..65d207e7c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderActivity.kt @@ -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(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) { + 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) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderPresenter.kt new file mode 100644 index 000000000..632d20cff --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderPresenter.kt @@ -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() { + + 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) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderView.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderView.kt new file mode 100644 index 000000000..cdeebb373 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/ReaderView.kt @@ -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) + + @StateStrategyType(AddToEndSingleStrategy::class) + fun onLoadingStateChanged(isLoading: Boolean) + + @StateStrategyType(OneExecutionStateStrategy::class) + fun onError(e: Exception) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ContextExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ContextExt.kt new file mode 100644 index 000000000..4fa3a83b5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ContextExt.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt index ba7c82545..382f18f80 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/StringExt.kt @@ -20,4 +20,16 @@ fun String.withDomain(domain: String, ssl: Boolean = true) = when { append(this@withDomain) } 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 } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_drop_down.xml b/app/src/main/res/drawable/ic_drop_down.xml new file mode 100644 index 000000000..82b2a119e --- /dev/null +++ b/app/src/main/res/drawable/ic_drop_down.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_error_large.xml b/app/src/main/res/drawable/ic_error_large.xml new file mode 100644 index 000000000..2c828f604 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_large.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_favourites.xml b/app/src/main/res/drawable/ic_favourites.xml new file mode 100644 index 000000000..ceaa8b738 --- /dev/null +++ b/app/src/main/res/drawable/ic_favourites.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_half.xml b/app/src/main/res/drawable/ic_star_half.xml deleted file mode 100644 index 463042243..000000000 --- a/app/src/main/res/drawable/ic_star_half.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/activity_reader.xml b/app/src/main/res/layout/activity_reader.xml new file mode 100644 index 000000000..7246537dd --- /dev/null +++ b/app/src/main/res/layout/activity_reader.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_chapters.xml b/app/src/main/res/layout/dialog_chapters.xml new file mode 100644 index 000000000..e53b0af80 --- /dev/null +++ b/app/src/main/res/layout/dialog_chapters.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_page.xml b/app/src/main/res/layout/item_page.xml new file mode 100644 index 000000000..9d1ba65a4 --- /dev/null +++ b/app/src/main/res/layout/item_page.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/nav_drawer.xml b/app/src/main/res/menu/nav_drawer.xml index adc5231d0..6df31b518 100644 --- a/app/src/main/res/menu/nav_drawer.xml +++ b/app/src/main/res/menu/nav_drawer.xml @@ -8,7 +8,7 @@ android:title="@string/local_storage" /> + + + \ No newline at end of file diff --git a/app/src/main/res/menu/opt_reader_top.xml b/app/src/main/res/menu/opt_reader_top.xml new file mode 100644 index 000000000..9c841f880 --- /dev/null +++ b/app/src/main/res/menu/opt_reader_top.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 2774c78b7..b9272c9bb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,8 @@ - #0288D1 - #0D47A1 - #F4511E + #0288D1 + #0D47A1 + #F4511E + #99000000 + #D32F2F \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d54b0f46..65f62af29 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,4 +15,8 @@ List mode Settings Remote sources + Loading… + Chapter %d of %d + Close + Try again \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index b43e77b5f..df2381637 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,9 +3,9 @@ \ No newline at end of file diff --git a/app/src/test/java/org/koitharu/kotatsu/parsers/MangaParserTest.kt b/app/src/test/java/org/koitharu/kotatsu/parsers/MangaParserTest.kt index 337e9cb4c..4b0f23834 100644 --- a/app/src/test/java/org/koitharu/kotatsu/parsers/MangaParserTest.kt +++ b/app/src/test/java/org/koitharu/kotatsu/parsers/MangaParserTest.kt @@ -3,4 +3,8 @@ package org.koitharu.kotatsu.parsers interface MangaParserTest { fun testMangaList() + + fun testMangaDetails() + + fun testMangaPages() } \ No newline at end of file diff --git a/app/src/test/java/org/koitharu/kotatsu/parsers/repository/ReadmangaRuTest.kt b/app/src/test/java/org/koitharu/kotatsu/parsers/repository/ReadmangaRuTest.kt index 42ce06d64..2e3ae6c27 100644 --- a/app/src/test/java/org/koitharu/kotatsu/parsers/repository/ReadmangaRuTest.kt +++ b/app/src/test/java/org/koitharu/kotatsu/parsers/repository/ReadmangaRuTest.kt @@ -8,27 +8,46 @@ import org.junit.runner.RunWith import org.koitharu.kotatsu.domain.repository.ReadmangaRepository import org.koitharu.kotatsu.parsers.MangaParserTest import org.koitharu.kotatsu.parsers.RepositoryTestEnvironment -import org.koitharu.kotatsu.utils.MyAsserts +import org.koitharu.kotatsu.utils.TestUtil import org.mockito.junit.MockitoJUnitRunner @RunWith(MockitoJUnitRunner::class) class ReadmangaRuTest : MangaParserTest { - @Test - override fun testMangaList() { - val list = runBlocking { repository.getList(1) } - Assert.assertTrue(list.size == 70) - val item = list[40] - Assert.assertTrue(item.title.isNotEmpty()) - Assert.assertTrue(item.rating in 0f..1f) - MyAsserts.assertValidUrl(item.url) - MyAsserts.assertValidUrl(item.coverUrl) - } + @Test + override fun testMangaList() { + val list = runBlocking { repository.getList(1) } + Assert.assertTrue(list.size == 70) + val item = list[40] + Assert.assertTrue(item.title.isNotEmpty()) + Assert.assertTrue(item.rating in 0f..1f) + TestUtil.assertValidUrl(item.url) + 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 - @BeforeClass - fun setUp() = initialize(ReadmangaRepository::class.java) - } + @Test + override fun testMangaPages() { + 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) + } } \ No newline at end of file diff --git a/app/src/test/java/org/koitharu/kotatsu/utils/MyAsserts.kt b/app/src/test/java/org/koitharu/kotatsu/utils/TestUtil.kt similarity index 96% rename from app/src/test/java/org/koitharu/kotatsu/utils/MyAsserts.kt rename to app/src/test/java/org/koitharu/kotatsu/utils/TestUtil.kt index f398a30a1..d2a0ea453 100644 --- a/app/src/test/java/org/koitharu/kotatsu/utils/MyAsserts.kt +++ b/app/src/test/java/org/koitharu/kotatsu/utils/TestUtil.kt @@ -4,7 +4,7 @@ import org.junit.Assert import java.net.HttpURLConnection import java.net.URL -object MyAsserts { +object TestUtil { private val VALID_RESPONSE_CODES = arrayOf( HttpURLConnection.HTTP_OK,