diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index e520b8638..0947cf71a 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -1,6 +1,7 @@
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index ad07dec78..e8a72de1d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,7 +15,7 @@ android {
minSdkVersion 21
targetSdkVersion 29
versionCode gitCommits
- versionName '0.3'
+ versionName '0.3.1'
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt
index 78dd571ce..daca081c5 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt
@@ -20,5 +20,6 @@ enum class MangaSource(
MANGACHAN("Манга-тян", "ru", MangaChanRepository::class.java),
DESUME("Desu.me", "ru", DesuMeRepository::class.java),
HENCHAN("Хентай-тян", "ru", HenChanRepository::class.java),
- YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java)
+ YAOICHAN("Яой-тян", "ru", YaoiChanRepository::class.java),
+ MANGATOWN("MangaTown", "en", MangaTownRepository::class.java)
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt
new file mode 100644
index 000000000..cee43c3d6
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/site/MangaTownRepository.kt
@@ -0,0 +1,172 @@
+package org.koitharu.kotatsu.core.parser.site
+
+import org.intellij.lang.annotations.Language
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.core.exceptions.ParseException
+import org.koitharu.kotatsu.core.model.*
+import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
+import org.koitharu.kotatsu.utils.ext.*
+import java.util.*
+
+class MangaTownRepository : RemoteMangaRepository() {
+
+ override val source = MangaSource.MANGATOWN
+
+ override val sortOrders = setOf(
+ SortOrder.ALPHABETICAL,
+ SortOrder.RATING,
+ SortOrder.POPULARITY,
+ SortOrder.UPDATED
+ )
+
+ override suspend fun getList(
+ offset: Int,
+ query: String?,
+ sortOrder: SortOrder?,
+ tag: MangaTag?
+ ): List {
+ val domain = conf.getDomain(DOMAIN)
+ val ssl = conf.isUseSsl(false)
+ val scheme = if (ssl) "https" else "http"
+ val sortKey = when (sortOrder) {
+ SortOrder.ALPHABETICAL -> "?name.az"
+ SortOrder.RATING -> "?rating.za"
+ SortOrder.UPDATED -> "?last_chapter_time.za"
+ else -> ""
+ }
+ val page = (offset / 30) + 1
+ val url = when {
+ !query.isNullOrEmpty() -> "$scheme://$domain/search?name=${query.urlEncoded()}"
+ tag != null -> "$scheme://$domain/directory/${tag.key}/$page.htm$sortKey"
+ else -> "$scheme://$domain/directory/$page.htm$sortKey"
+ }
+ val doc = loaderContext.httpGet(url).parseHtml()
+ val root = doc.body().selectFirst("ul.manga_pic_list")
+ ?: throw ParseException("Root not found")
+ return root.select("li").mapNotNull { li ->
+ val a = li.selectFirst("a.manga_cover")
+ val href = a.attr("href").withDomain(domain, ssl)
+ val views = li.select("p.view")
+ val status = views.findOwnText { x -> x.startsWith("Status:") }
+ ?.substringAfter(':')?.trim()?.toLowerCase(Locale.ROOT)
+ Manga(
+ id = href.longHashCode(),
+ title = a.attr("title"),
+ coverUrl = a.selectFirst("img").attr("src"),
+ source = MangaSource.MANGATOWN,
+ altTitle = null,
+ rating = li.selectFirst("p.score")?.selectFirst("b")
+ ?.ownText()?.toFloatOrNull()?.div(5f) ?: Manga.NO_RATING,
+ largeCoverUrl = null,
+ author = views.findText { x -> x.startsWith("Author:") }?.substringAfter(':')
+ ?.trim(),
+ state = when (status) {
+ "ongoing" -> MangaState.ONGOING
+ "completed" -> MangaState.FINISHED
+ else -> null
+ },
+ tags = li.selectFirst("p.keyWord")?.select("a")?.mapNotNull tags@{ x ->
+ MangaTag(
+ title = x.attr("title"),
+ key = x.attr("href").parseTagKey() ?: return@tags null,
+ source = MangaSource.MANGATOWN
+ )
+ }?.toSet().orEmpty(),
+ url = href
+ )
+ }
+ }
+
+ override suspend fun getDetails(manga: Manga): Manga {
+ val domain = conf.getDomain(DOMAIN)
+ val ssl = conf.isUseSsl(false)
+ val doc = loaderContext.httpGet(manga.url).parseHtml()
+ val root = doc.body().selectFirst("section.main")
+ ?.selectFirst("div.article_content") ?: throw ParseException("Cannot find root")
+ val info = root.selectFirst("div.detail_info").selectFirst("ul")
+ val chaptersList = root.selectFirst("div.chapter_content")
+ ?.selectFirst("ul.chapter_list")?.select("li")?.asReversed()
+ return manga.copy(
+ tags = manga.tags + info.select("li").find { x ->
+ x.selectFirst("b")?.ownText() == "Genre(s):"
+ }?.select("a")?.mapNotNull { a ->
+ MangaTag(
+ title = a.attr("title"),
+ key = a.attr("href").parseTagKey() ?: return@mapNotNull null,
+ source = MangaSource.MANGATOWN
+ )
+ }.orEmpty(),
+ description = info.getElementById("show")?.ownText(),
+ chapters = chaptersList?.mapIndexedNotNull { i, li ->
+ val href = li.selectFirst("a").attr("href").withDomain(domain, ssl)
+ val name = li.select("span").filter { it.className().isEmpty() }.joinToString(" - ") { it.text() }.trim()
+ MangaChapter(
+ id = href.longHashCode(),
+ url = href,
+ source = MangaSource.MANGATOWN,
+ number = i + 1,
+ name = if (name.isEmpty()) "${manga.title} - ${i + 1}" else name
+ )
+ }
+ )
+ }
+
+ override suspend fun getPages(chapter: MangaChapter): List {
+ val domain = conf.getDomain(DOMAIN)
+ val ssl = conf.isUseSsl(false)
+ val doc = loaderContext.httpGet(chapter.url).parseHtml()
+ val root = doc.body().selectFirst("div.page_select")
+ ?: throw ParseException("Cannot find root")
+ return root.selectFirst("select").select("option").mapNotNull {
+ val href = it.attr("value").withDomain(domain, ssl)
+ if (href.endsWith("featured.html")) {
+ return@mapNotNull null
+ }
+ MangaPage(
+ id = href.longHashCode(),
+ url = href,
+ source = MangaSource.MANGATOWN
+ )
+ }
+ }
+
+ override suspend fun getPageFullUrl(page: MangaPage): String {
+ val domain = conf.getDomain(DOMAIN)
+ val ssl = conf.isUseSsl(false)
+ val doc = loaderContext.httpGet(page.url).parseHtml()
+ return doc.getElementById("image").attr("src").withDomain(domain, ssl)
+ }
+
+ override suspend fun getTags(): Set {
+ val domain = conf.getDomain(DOMAIN)
+ val doc = loaderContext.httpGet("http://$domain/directory/").parseHtml()
+ val root = doc.body().selectFirst("aside.right")
+ .getElementsContainingOwnText("Genres")
+ .first()
+ .nextElementSibling()
+ return root.select("li").mapNotNull { li ->
+ val a = li.selectFirst("a") ?: return@mapNotNull null
+ val key = a.attr("href").parseTagKey()
+ if (key.isNullOrEmpty()) {
+ return@mapNotNull null
+ }
+ MangaTag(
+ source = MangaSource.MANGATOWN,
+ key = key,
+ title = a.text()
+ )
+ }.toSet()
+ }
+
+
+ override fun onCreatePreferences() = setOf(R.string.key_parser_domain, R.string.key_parser_ssl)
+
+ private fun String.parseTagKey() = split('/').findLast { TAG_REGEX matches it }
+
+ private companion object {
+
+ @Language("RegExp")
+ val TAG_REGEX = Regex("[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+-[^\\-]+")
+ const val DOMAIN = "www.mangatown.com"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceConfig.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceConfig.kt
index 49995eae4..30194765c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceConfig.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/SourceConfig.kt
@@ -8,16 +8,20 @@ interface SourceConfig {
fun getDomain(defaultValue: String): String
+ fun isUseSsl(defaultValue: Boolean): Boolean
+
private class PrefSourceConfig(context: Context, source: MangaSource) : SourceConfig {
private val prefs = context.getSharedPreferences(source.name, Context.MODE_PRIVATE)
private val keyDomain = context.getString(R.string.key_parser_domain)
+ private val keySsl = context.getString(R.string.key_parser_ssl)
override fun getDomain(defaultValue: String) = prefs.getString(keyDomain, defaultValue)
?.takeUnless(String::isBlank)
?: defaultValue
+ override fun isUseSsl(defaultValue: Boolean) = prefs.getBoolean(keySsl, defaultValue)
}
companion object {
diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt
index 2de37a7b3..124bfe607 100644
--- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/standard/PageHolder.kt
@@ -9,6 +9,7 @@ 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.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@@ -43,7 +44,8 @@ class PageHolder(parent: ViewGroup, private val loader: PageLoader) :
ssiv.recycle()
try {
val uri = withContext(Dispatchers.IO) {
- loader.loadFile(data.url, force)
+ val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
+ loader.loadFile(pageUrl, force)
}.toUri()
ssiv.setImage(ImageSource.uri(uri))
} catch (e: CancellationException) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt
index 4181a9797..481940d17 100644
--- a/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/ui/reader/wetoon/WebtoonHolder.kt
@@ -10,6 +10,7 @@ import kotlinx.android.synthetic.main.item_page_webtoon.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
+import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
@@ -42,7 +43,8 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
ssiv.recycle()
try {
val uri = withContext(Dispatchers.IO) {
- loader.loadFile(data.url, force)
+ val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
+ loader.loadFile(pageUrl, force)
}.toUri()
ssiv.setImage(ImageSource.uri(uri))
} catch (e: CancellationException) {
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt
index 8c30fcca1..6b2e38276 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt
@@ -33,16 +33,16 @@ inline fun File.findParent(predicate: (File) -> Boolean): File? {
return current
}
-fun File.getStorageName(context: Context): String {
+fun File.getStorageName(context: Context): String = safe {
val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
manager.getStorageVolume(this)?.getDescription(context)?.let {
- return it
+ return@safe it
}
}
- return when {
+ when {
Environment.isExternalStorageEmulated(this) -> context.getString(R.string.internal_storage)
Environment.isExternalStorageRemovable(this) -> context.getString(R.string.external_storage)
- else -> context.getString(R.string.other_storage)
+ else -> null
}
-}
\ No newline at end of file
+} ?: context.getString(R.string.other_storage)
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt
index ea3cfd91e..048525a0e 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ParseExt.kt
@@ -5,6 +5,7 @@ import okhttp3.internal.closeQuietly
import org.json.JSONObject
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
+import org.jsoup.select.Elements
fun Response.parseHtml(): Document {
try {
@@ -27,4 +28,24 @@ fun Response.parseJson(): JSONObject {
} finally {
closeQuietly()
}
+}
+
+inline fun Elements.findOwnText(predicate: (String) -> Boolean): String? {
+ for (x in this) {
+ val ownText = x.ownText()
+ if (predicate(ownText)) {
+ return ownText
+ }
+ }
+ return null
+}
+
+inline fun Elements.findText(predicate: (String) -> Boolean): String? {
+ for (x in this) {
+ val text = x.text()
+ if (predicate(text)) {
+ return text
+ }
+ }
+ return null
}
\ 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 13fe030a9..871f3ca38 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
@@ -14,6 +14,14 @@ fun String.longHashCode(): Long {
}
fun String.withDomain(domain: String, ssl: Boolean = true) = when {
+ this.startsWith("//") -> buildString {
+ append("http")
+ if (ssl) {
+ append('s')
+ }
+ append(":")
+ append(this@withDomain)
+ }
this.startsWith("/") -> buildString {
append("http")
if (ssl) {
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 1bcc315a3..51f62d15e 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -130,4 +130,5 @@
Недоступно
Не удалось найти ни одного доступного хранилища
Другое хранилище
+ Защищённое соединение (HTTPS)
\ No newline at end of file
diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml
index cb332612c..bc69568ca 100644
--- a/app/src/main/res/values/constants.xml
+++ b/app/src/main/res/values/constants.xml
@@ -22,6 +22,7 @@
reader_animation
domain
+ ssl
- -1
- 1
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4913a198c..0dfb3988b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -131,4 +131,5 @@
Not available
Cannot find any available storage
Other storage
+ Use secure connection (HTTPS)
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_source.xml b/app/src/main/res/xml/pref_source.xml
index aebbbf090..195ad5a21 100644
--- a/app/src/main/res/xml/pref_source.xml
+++ b/app/src/main/res/xml/pref_source.xml
@@ -7,4 +7,9 @@
android:title="@string/domain"
app:iconSpaceReserved="false" />
+
+
\ No newline at end of file