diff --git a/.gitignore b/.gitignore
index af291a578..9edead0f8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,5 @@
.idea/
*iml
*.iml
-*/build
\ No newline at end of file
+*/build
+/mainframer.sh
diff --git a/app/.gitignore b/app/.gitignore
index 90de2b9c8..012bccc6a 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1,4 +1,5 @@
/build
*iml
*.iml
-custom.gradle
\ No newline at end of file
+custom.gradle
+google-services.json
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index dd93f3e49..b30fd1cdb 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -28,18 +28,19 @@ ext {
}
}
+
android {
compileSdkVersion 25
buildToolsVersion "25.0.1"
publishNonDefault true
defaultConfig {
- applicationId "eu.kanade.tachiyomi"
+ applicationId "eu.kanade.tachiyomi.eh"
minSdkVersion 16
targetSdkVersion 25
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
- versionCode 17
- versionName "0.4.1"
+ versionCode 4000
+ versionName "v4.0.0-EH"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@@ -190,15 +191,25 @@ dependencies {
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
compile 'de.hdodenhof:circleimageview:2.1.0'
+ //Firebase (EH)
+ final firebase_version = '10.0.1'
+ releaseCompile "com.google.firebase:firebase-core:$firebase_version"
+ releaseCompile "com.google.firebase:firebase-messaging:$firebase_version"
+ releaseCompile "com.google.firebase:firebase-crash:$firebase_version"
+
+ //SnappyDB (EH)
+ compile 'io.paperdb:paperdb:2.0'
+
// Tests
- testCompile 'junit:junit:4.12'
+ //Paper DB screws up tests
+ /*testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1'
testCompile 'org.mockito:mockito-core:1.10.19'
final robolectric_version = '3.1.4'
testCompile "org.robolectric:robolectric:$robolectric_version"
testCompile "org.robolectric:shadows-multidex:$robolectric_version"
- testCompile "org.robolectric:shadows-play-services:$robolectric_version"
+ testCompile "org.robolectric:shadows-play-services:$robolectric_version"*/
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
@@ -216,3 +227,6 @@ buildscript {
repositories {
mavenCentral()
}
+
+//Firebase (EH)
+//apply plugin: 'com.google.gms.google-services'
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index b738c0a7a..2e2ef994e 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -95,4 +95,7 @@
-dontwarn org.yaml.snakeyaml.**
# Duktape
--keep class com.squareup.duktape.** { *; }
\ No newline at end of file
+-keep class com.squareup.duktape.** { *; }
+
+# [EH]
+-keep class exh.** { *; }
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2d21a2262..bc1e14c52 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -98,6 +98,13 @@
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" />
+
+
+
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt
index 6bc7e91ab..2a5232dcc 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/App.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt
@@ -8,6 +8,7 @@ import com.evernote.android.job.JobManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.util.LocaleHelper
+import io.paperdb.Paper
import org.acra.ACRA
import org.acra.annotation.ReportsCrashes
import timber.log.Timber
@@ -33,6 +34,7 @@ open class App : Application() {
setupAcra()
setupJobManager()
+ Paper.init(this) //Setup metadata DB (EH)
LocaleHelper.updateConfiguration(this, resources.configuration)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
index 613fa0504..833454562 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
@@ -144,4 +144,13 @@ class PreferencesHelper(val context: Context) {
fun lang() = prefs.getString(keys.lang, "")
+ //EH
+ fun enableExhentai() = rxPrefs.getBoolean("enable_exhentai", false)
+
+ fun secureEXH() = rxPrefs.getBoolean("secure_exh", true)
+
+ //EH Cookies
+ fun memberIdVal() = rxPrefs.getString("eh_ipb_member_id", null)
+ fun passHashVal() = rxPrefs.getString("eh_ipb_pass_hash", null)
+ fun igneousVal() = rxPrefs.getString("eh_igneous", null)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt
index 88751ac2a..7408072fb 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/SourceManager.kt
@@ -4,8 +4,12 @@ import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.content.Context
import android.os.Environment
import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.source.online.YamlOnlineSource
+import eu.kanade.tachiyomi.data.source.online.all.EHentai
+import eu.kanade.tachiyomi.data.source.online.all.EHentaiMetadata
import eu.kanade.tachiyomi.data.source.online.english.*
import eu.kanade.tachiyomi.data.source.online.german.WieManga
import eu.kanade.tachiyomi.data.source.online.russian.Mangachan
@@ -14,11 +18,14 @@ import eu.kanade.tachiyomi.data.source.online.russian.Readmanga
import eu.kanade.tachiyomi.util.hasPermission
import org.yaml.snakeyaml.Yaml
import timber.log.Timber
+import uy.kohesive.injekt.injectLazy
import java.io.File
open class SourceManager(private val context: Context) {
- private val sourcesMap = createSources()
+ private val prefs: PreferencesHelper by injectLazy()
+
+ private var sourcesMap = createSources()
open fun get(sourceKey: Int): Source? {
return sourcesMap[sourceKey]
@@ -27,19 +34,39 @@ open class SourceManager(private val context: Context) {
fun getOnlineSources() = sourcesMap.values.filterIsInstance(OnlineSource::class.java)
private fun createOnlineSourceList(): List = listOf(
- Batoto(1),
- Mangahere(2),
- Mangafox(3),
- Kissmanga(4),
- Readmanga(5),
- Mintmanga(6),
- Mangachan(7),
- Readmangatoday(8),
- Mangasee(9),
- WieManga(10)
+ Batoto(101),
+ Mangahere(102),
+ Mangafox(103),
+ Kissmanga(104),
+ Readmanga(105),
+ Mintmanga(106),
+ Mangachan(107),
+ Readmangatoday(108),
+ Mangasee(109),
+ WieManga(110)
)
+ private fun createEHSources(): List {
+ val exSrcs = mutableListOf(
+ EHentai(1, false, context),
+ EHentaiMetadata(3, false, context)
+ )
+ if(prefs.enableExhentai().getOrDefault()) {
+ exSrcs += EHentai(2, true, context)
+ exSrcs += EHentaiMetadata(4, true, context)
+ }
+ return exSrcs
+ }
+
+ init {
+ prefs.enableExhentai().asObservable().subscribe {
+ //Refresh sources when ExHentai enabled/disabled change
+ sourcesMap = createSources()
+ }
+ }
+
private fun createSources(): Map = hashMapOf().apply {
+ createEHSources().forEach { put(it.id, it) }
createOnlineSourceList().forEach { put(it.id, it) }
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/Page.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/Page.kt
index 2aa3c04b4..f8fef147d 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/source/model/Page.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/model/Page.kt
@@ -7,7 +7,7 @@ import rx.subjects.Subject
class Page(
val index: Int,
- val url: String = "",
+ var url: String = "",
var imageUrl: String? = null,
@Transient var uri: Uri? = null
) : ProgressListener {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/all/EHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/all/EHentai.kt
new file mode 100644
index 000000000..bfea5ac71
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/all/EHentai.kt
@@ -0,0 +1,343 @@
+package eu.kanade.tachiyomi.data.source.online.all
+
+import android.content.Context
+import android.net.Uri
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.network.GET
+import eu.kanade.tachiyomi.data.network.asObservableSuccess
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.data.preference.getOrDefault
+import eu.kanade.tachiyomi.data.source.model.MangasPage
+import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import eu.kanade.tachiyomi.util.asJsoup
+import exh.metadata.*
+import exh.metadata.models.ExGalleryMetadata
+import exh.metadata.models.Tag
+import exh.plusAssign
+import okhttp3.Response
+import org.jsoup.nodes.Element
+import rx.Observable
+import uy.kohesive.injekt.injectLazy
+import java.net.URLEncoder
+import java.util.*
+import exh.ui.login.LoginActivity
+
+class EHentai(override val id: Int,
+ val exh: Boolean,
+ val context: Context) : OnlineSource() {
+
+
+ val schema: String
+ get() = if(prefs.secureEXH().getOrDefault())
+ "https"
+ else
+ "http"
+
+ override val baseUrl: String
+ get() = if(exh)
+ "$schema://exhentai.org"
+ else
+ "http://g.e-hentai.org"
+
+ override val lang = "all"
+ override val supportsLatest = true
+
+ val prefs: PreferencesHelper by injectLazy()
+
+ val metadataHelper = MetadataHelper()
+
+ /**
+ * Gallery list entry
+ */
+ data class ParsedManga(val fav: String?, val manga: Manga)
+
+ /**
+ * Parse a list of galleries
+ */
+ fun genericMangaParse(response: Response, page: MangasPage? = null)
+ = with(response.asJsoup()) {
+ //Parse mangas
+ val parsedMangas = select(".gtr0,.gtr1").map {
+ ParsedManga(
+ fav = it.select(".itd .it3 > .i[id]").first()?.attr("title"),
+ manga = Manga.create(id).apply {
+ //Get title
+ it.select(".itd .it5 a").first()?.apply {
+ title = text()
+ setUrlWithoutDomain(addParam(attr("href"), "nw", "always"))
+ }
+ //Get image
+ it.select(".itd .it2").first()?.apply {
+ children().first()?.let {
+ thumbnail_url = it.attr("src")
+ } ?: let {
+ text().split("~").apply {
+ thumbnail_url = "http://${this[1]}/${this[2]}"
+ }
+ }
+ }
+ })
+
+ }
+ //Add to page if required
+ page?.let { page ->
+ page.mangas += parsedMangas.map { it.manga }
+ select("a[onclick=return false]").last()?.let {
+ if(it.text() == ">") page.nextPageUrl = it.attr("href")
+ }
+ }
+ //Return parsed mangas anyways
+ parsedMangas
+ }
+
+ override fun fetchChapterList(manga: Manga): Observable>
+ = Observable.just(listOf(Chapter.create().apply {
+ manga_id = manga.id
+ url = manga.url
+ name = "Chapter"
+ chapter_number = 1f
+ }))
+
+ override fun fetchPageListFromNetwork(chapter: Chapter)
+ = fetchChapterPage(chapter, 0).map {
+ it.mapIndexed { i, s ->
+ Page(i, s)
+ }
+ }!!
+
+ private fun fetchChapterPage(chapter: Chapter, id: Int): Observable> {
+ val urls = mutableListOf()
+ return chapterPageCall(chapter, id).flatMap {
+ val jsoup = it.asJsoup()
+ urls += parseChapterPage(jsoup)
+ if(nextPageUrl(jsoup) != null) {
+ fetchChapterPage(chapter, id + 1)
+ } else {
+ Observable.just(urls)
+ }
+ }
+ }
+ private fun parseChapterPage(response: Element)
+ = with(response) {
+ select(".gdtm a").map {
+ Pair(it.child(0).attr("alt").toInt(), it.attr("href"))
+ }.sortedBy(Pair::first).map { it.second }
+ }
+ private fun chapterPageCall(chapter: Chapter, pn: Int) = client.newCall(chapterPageRequest(chapter, pn)).asObservableSuccess()
+ private fun chapterPageRequest(chapter: Chapter, pn: Int) = GET("$baseUrl${chapter.url}?p=$pn", headers)
+
+ private fun nextPageUrl(element: Element): String?
+ = element.select("a[onclick=return false]").last()?.let {
+ return if (it.text() == ">") it.attr("href") else null
+ }
+
+ private fun buildGenreString(filters: List): String {
+ val genreString = StringBuilder()
+ for (genre in GENRE_LIST) {
+ genreString += "&f_"
+ genreString += genre
+ genreString += "="
+ genreString += if (filters.isEmpty()
+ || !filters
+ .map { it.id }
+ .find { it == genre }
+ .isNullOrEmpty())
+ "1"
+ else
+ "0"
+ }
+ return genreString.toString()
+ }
+
+ override fun popularMangaInitialUrl() = if(exh)
+ latestUpdatesInitialUrl()
+ else
+ "$baseUrl/toplist.php?tl=15"
+
+ override fun popularMangaParse(response: Response, page: MangasPage) {
+ genericMangaParse(response, page)
+ }
+
+ override fun searchMangaInitialUrl(query: String, filters: List)
+ = "$baseUrl$QUERY_PREFIX${buildGenreString(filters)}&f_search=${URLEncoder.encode(query, "UTF-8")}"
+
+ override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List) {
+ genericMangaParse(response, page)
+ }
+
+ override fun latestUpdatesInitialUrl() = baseUrl
+
+ override fun latestUpdatesParse(response: Response, page: MangasPage) {
+ genericMangaParse(response, page)
+ }
+
+ /**
+ * Parse gallery page to metadata model
+ */
+ override fun mangaDetailsParse(response: Response, manga: Manga) = with(response.asJsoup()) {
+ val metdata = ExGalleryMetadata()
+ with(metdata) {
+ url = manga.url
+ exh = this@EHentai.exh
+ title = select("#gn").text().nullIfBlank()
+ altTitle = select("#gj").text().nullIfBlank()
+
+ thumbnailUrl = select("#gd1 img").attr("src").nullIfBlank()
+
+ genre = select(".ic").attr("alt").nullIfBlank()
+
+ uploader = select("#gdn").text().nullIfBlank()
+
+ //Parse the table
+ select("#gdd tr").forEach {
+ it.select(".gdt1")
+ .text()
+ .nullIfBlank()
+ ?.trim()
+ ?.let { left ->
+ it.select(".gdt2")
+ .text()
+ .nullIfBlank()
+ ?.trim()
+ ?.let { right ->
+ ignore {
+ when (left.removeSuffix(":")
+ .toLowerCase()) {
+ "posted" -> datePosted = EX_DATE_FORMAT.parse(right).time
+ "visible" -> visible = right.nullIfBlank()
+ "language" -> {
+ language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank()
+ translated = right.endsWith(TR_SUFFIX, true)
+ }
+ "file size" -> size = parseHumanReadableByteCount(right)?.toLong()
+ "length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
+ "favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
+ }
+ }
+ }
+ }
+ }
+
+ //Parse ratings
+ ignore {
+ averageRating = select("#rating_label")
+ .text()
+ .removePrefix("Average:")
+ .trim()
+ .nullIfBlank()
+ ?.toDouble()
+ ratingCount = select("#rating_count")
+ .text()
+ .trim()
+ .nullIfBlank()
+ ?.toInt()
+ }
+
+ //Parse tags
+ tags.clear()
+ select("#taglist tr").forEach {
+ val namespace = it.select(".tc").text().removeSuffix(":")
+ val currentTags = it.select("div").map {
+ Tag(it.text().trim(),
+ it.hasClass("gtl"))
+ }
+ tags.put(namespace, ArrayList(currentTags))
+ }
+
+ //Save metadata
+ metadataHelper.writeGallery(this)
+
+ //Copy metadata to manga
+ copyTo(manga)
+ }
+ }
+
+ override fun chapterListParse(response: Response, chapters: MutableList) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun pageListParse(response: Response, pages: MutableList) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun imageUrlParse(response: Response): String {
+ throw UnsupportedOperationException()
+ }
+
+ //Copy and paste from OnlineSource as we need the page argument
+ override public fun fetchImageUrl(page: Page): Observable {
+ page.status = Page.LOAD_PAGE
+ return client
+ .newCall(imageUrlRequest(page))
+ .asObservableSuccess()
+ .map { imageUrlParse(it, page) }
+ .doOnError { page.status = Page.ERROR }
+ .onErrorReturn { null }
+ .doOnNext { page.imageUrl = it }
+ .map { page }
+ }
+
+ fun imageUrlParse(response: Response, page: Page): String {
+ with(response.asJsoup()) {
+ val currentImage = select("img[onerror]").attr("src")
+ //Each press of the retry button will choose another server
+ select("#loadfail").attr("onclick").nullIfBlank()?.let {
+ page.url = addParam(page.url, "nl", it.substring(it.indexOf('\'') + 1 .. it.lastIndexOf('\'') - 1))
+ }
+ return currentImage
+ }
+ }
+
+ val cookiesHeader by lazy {
+ val cookies: MutableMap = HashMap()
+ if(prefs.enableExhentai().getOrDefault()) {
+ cookies.put(LoginActivity.MEMBER_ID_COOKIE, prefs.memberIdVal().getOrDefault())
+ cookies.put(LoginActivity.PASS_HASH_COOKIE, prefs.passHashVal().getOrDefault())
+ cookies.put(LoginActivity.IGNEOUS_COOKIE, prefs.igneousVal().getOrDefault())
+ }
+ buildCookies(cookies)
+ }
+
+ //Headers
+ override fun headersBuilder()
+ = super.headersBuilder().add("Cookie", cookiesHeader)!!
+
+ fun buildCookies(cookies: Map)
+ = cookies.entries.map {
+ "${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
+ }.joinToString(separator = "; ", postfix = ";")
+
+ fun addParam(url: String, param: String, value: String)
+ = Uri.parse(url)
+ .buildUpon()
+ .appendQueryParameter(param, value)
+ .toString()
+
+ override val client = super.client.newBuilder()
+ .addInterceptor { chain ->
+ val newReq = chain
+ .request()
+ .newBuilder()
+ .addHeader("Cookie", cookiesHeader)
+ .build()
+
+ chain.proceed(newReq)
+ }.build()!!
+
+ //Filters
+ val generatedFilters = GENRE_LIST.map { Filter(it, it) }
+ override fun getFilterList() = generatedFilters
+
+ override val name = if(exh)
+ "ExHentai"
+ else
+ "E-Hentai"
+
+ companion object {
+ val QUERY_PREFIX = "?f_apply=Apply+Filter"
+ val GENRE_LIST = arrayOf("doujinshi", "manga", "artistcg", "gamecg", "western", "non-h", "imageset", "cosplay", "asianporn", "misc")
+ val TR_SUFFIX = "TR"
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/source/online/all/EHentaiMetadata.kt b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/all/EHentaiMetadata.kt
new file mode 100644
index 000000000..0564882a3
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/source/online/all/EHentaiMetadata.kt
@@ -0,0 +1,135 @@
+package eu.kanade.tachiyomi.data.source.online.all
+
+import android.content.Context
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.source.model.MangasPage
+import eu.kanade.tachiyomi.data.source.model.Page
+import eu.kanade.tachiyomi.data.source.online.OnlineSource
+import exh.metadata.MetadataHelper
+import exh.metadata.copyTo
+import exh.metadata.models.ExGalleryMetadata
+import okhttp3.Response
+import rx.Observable
+
+/**
+ * Offline metadata store source
+ */
+
+class EHentaiMetadata(override val id: Int,
+ val exh: Boolean,
+ val context: Context) : OnlineSource() {
+
+ val metadataHelper = MetadataHelper()
+
+ val internalEx = EHentai(id - 2, exh, context)
+
+ override val baseUrl: String
+ get() = throw UnsupportedOperationException()
+ override val lang: String
+ get() = "advanced"
+ override val supportsLatest: Boolean
+ get() = true
+
+ override fun popularMangaInitialUrl(): String {
+ throw UnsupportedOperationException()
+ }
+
+ override fun popularMangaParse(response: Response, page: MangasPage) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun searchMangaInitialUrl(query: String, filters: List): String {
+ throw UnsupportedOperationException()
+ }
+
+ override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun latestUpdatesInitialUrl(): String {
+ throw UnsupportedOperationException()
+ }
+
+ override fun latestUpdatesParse(response: Response, page: MangasPage) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun mangaDetailsParse(response: Response, manga: Manga) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun chapterListParse(response: Response, chapters: MutableList) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun pageListParse(response: Response, pages: MutableList) {
+ throw UnsupportedOperationException()
+ }
+
+ override fun imageUrlParse(response: Response): String {
+ throw UnsupportedOperationException()
+ }
+
+ override fun fetchChapterList(manga: Manga): Observable>
+ = Observable.just(listOf(Chapter.create().apply {
+ manga_id = manga.id
+ url = manga.url
+ name = "ONLINE - Chapter"
+ chapter_number = 1f
+ }))
+
+ override fun fetchPageListFromNetwork(chapter: Chapter) = internalEx.fetchPageListFromNetwork(chapter)
+
+ override fun fetchImageUrl(page: Page) = internalEx.fetchImageUrl(page)
+
+ fun List.mapToManga() = filter { it.exh == exh }
+ .map {
+ Manga.create(id).apply {
+ it.copyTo(this)
+ source = this@EHentaiMetadata.id
+ }
+ }
+
+ fun sortedByTimeGalleries() = metadataHelper.getAllGalleries().sortedByDescending {
+ it.datePosted ?: 0
+ }
+
+ override fun fetchPopularManga(page: MangasPage)
+ = Observable.fromCallable {
+ page.mangas.addAll(metadataHelper.getAllGalleries().sortedByDescending {
+ it.ratingCount ?: 0
+ }.mapToManga())
+ page
+ }!!
+
+ override fun fetchSearchManga(page: MangasPage, query: String, filters: List)
+ = Observable.fromCallable {
+ page.mangas.addAll(sortedByTimeGalleries().filter { manga ->
+ filters.isEmpty() || filters.filter { it.id == manga.genre }.isNotEmpty()
+ }.mapToManga())
+ page
+ }!!
+
+ override fun fetchLatestUpdates(page: MangasPage)
+ = Observable.fromCallable {
+ page.mangas.addAll(sortedByTimeGalleries().mapToManga())
+ page
+ }!!
+
+ override fun fetchMangaDetails(manga: Manga) = Observable.fromCallable {
+ //Hack to convert the gallery into an online gallery when favoriting it or reading it
+ metadataHelper.fetchMetadata(manga.url, exh).copyTo(manga)
+ manga
+ }!!
+
+ override fun getFilterList() = internalEx.getFilterList()
+
+ override val name: String
+ get() = if(exh) {
+ "ExHentai"
+ } else {
+ "E-Hentai"
+ } + " - METADATA"
+
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt
index 42ff97324..5e0aa932e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/GithubService.kt
@@ -23,7 +23,7 @@ interface GithubService {
}
}
- @GET("/repos/inorichi/tachiyomi/releases/latest")
+ @GET("/repos/NerdNumber9/tachiyomi/releases/latest")
fun getLatestVersion(): Observable
}
\ No newline at end of file
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt
index 449cb9ff0..4b4b3bd60 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsActivity.kt
@@ -66,6 +66,7 @@ class SettingsActivity : BaseActivity(),
"tracking_screen" -> SettingsTrackingFragment.newInstance(key)
"advanced_screen" -> SettingsAdvancedFragment.newInstance(key)
"about_screen" -> SettingsAboutFragment.newInstance(key)
+ "eh_screen" -> SettingsEhFragment.newInstance(key) //EH
else -> SettingsFragment.newInstance(key)
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhFragment.kt
new file mode 100644
index 000000000..769ab6b66
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhFragment.kt
@@ -0,0 +1,52 @@
+package eu.kanade.tachiyomi.ui.setting
+
+import android.content.Intent
+import android.os.Bundle
+import android.support.v7.preference.XpPreferenceFragment
+import android.view.View
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.util.plusAssign
+import exh.ui.login.LoginActivity
+import net.xpece.android.support.preference.SwitchPreference
+import uy.kohesive.injekt.injectLazy
+
+/**
+ * EH Settings fragment
+ */
+
+class SettingsEhFragment : SettingsFragment() {
+ companion object {
+ fun newInstance(rootKey: String): SettingsEhFragment {
+ val args = Bundle()
+ args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
+ return SettingsEhFragment().apply { arguments = args }
+ }
+ }
+
+ private val preferences: PreferencesHelper by injectLazy()
+
+ val enableExhentaiPref by lazy {
+ findPreference("enable_exhentai") as SwitchPreference
+ }
+
+ override fun onViewCreated(view: View, savedState: Bundle?) {
+ super.onViewCreated(view, savedState)
+
+ subscriptions += preferences
+ .enableExhentai()
+ .asObservable().subscribe {
+ enableExhentaiPref.isChecked = it
+ }
+
+ enableExhentaiPref.setOnPreferenceChangeListener { preference, newVal ->
+ newVal as Boolean
+ if(!newVal) {
+ preferences.enableExhentai().set(false)
+ true
+ } else {
+ startActivity(Intent(context, LoginActivity::class.java))
+ false
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt
index 9db02ad00..9b05e972e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsFragment.kt
@@ -29,6 +29,7 @@ open class SettingsFragment : XpPreferenceFragment() {
addPreferencesFromResource(R.xml.pref_downloads)
addPreferencesFromResource(R.xml.pref_sources)
addPreferencesFromResource(R.xml.pref_tracking)
+ addPreferencesFromResource(R.xml.eh_pref_eh) //EH
addPreferencesFromResource(R.xml.pref_advanced)
addPreferencesFromResource(R.xml.pref_about)
diff --git a/app/src/main/java/exh/FavoritesSyncManager.java b/app/src/main/java/exh/FavoritesSyncManager.java
new file mode 100644
index 000000000..9766da5a6
--- /dev/null
+++ b/app/src/main/java/exh/FavoritesSyncManager.java
@@ -0,0 +1,191 @@
+package exh;
+
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.v7.app.AlertDialog;
+
+import com.pushtorefresh.storio.sqlite.operations.put.PutResult;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import eu.kanade.tachiyomi.data.database.DatabaseHelper;
+import eu.kanade.tachiyomi.data.database.models.Category;
+import eu.kanade.tachiyomi.data.database.models.Manga;
+import eu.kanade.tachiyomi.data.database.models.MangaCategory;
+//import eu.kanade.tachiyomi.data.source.online.english.EHentai;
+
+public class FavoritesSyncManager {
+ /*
+ Context context;
+ DatabaseHelper db;
+
+ public FavoritesSyncManager(Context context, DatabaseHelper db) {
+ this.context = context;
+ this.db = db;
+ }
+
+ public void guiSyncFavorites(final Runnable onComplete) {
+ if(!DialogLogin.isLoggedIn(context, false)) {
+ new AlertDialog.Builder(context).setTitle("Error")
+ .setMessage("You are not logged in! Please log in and try again!")
+ .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ }).show();
+ return;
+ }
+ final ProgressDialog dialog = ProgressDialog.show(context, "Downloading Favorites", "Please wait...", true, false);
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ Handler mainLooper = new Handler(Looper.getMainLooper());
+ try {
+ syncFavorites();
+ } catch (Exception e) {
+ mainLooper.post(new Runnable() {
+ @Override
+ public void run() {
+ new AlertDialog.Builder(context)
+ .setTitle("Error")
+ .setMessage("There was an error downloading your favorites, please try again later!")
+ .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ }).show();
+ }
+ });
+ e.printStackTrace();
+ }
+ dialog.dismiss();
+ mainLooper.post(onComplete);
+ }
+ }).start();
+ }
+
+ public void syncFavorites() throws IOException {
+ EHentai.FavoritesResponse favResponse = EHentai.fetchFavorites(context);
+ Map> favorites = favResponse.favs;
+ List ourCategories = new ArrayList<>(db.getCategories().executeAsBlocking());
+ List ourMangas = new ArrayList<>(db.getMangas().executeAsBlocking());
+ //Add required categories (categories do not sync upwards)
+ List categoriesToInsert = new ArrayList<>();
+ for (String theirCategory : favorites.keySet()) {
+ boolean haveCategory = false;
+ for (Category category : ourCategories) {
+ if (category.getName().endsWith(theirCategory)) {
+ haveCategory = true;
+ }
+ }
+ if (!haveCategory) {
+ Category category = Category.Companion.create(theirCategory);
+ ourCategories.add(category);
+ categoriesToInsert.add(category);
+ }
+ }
+ if (!categoriesToInsert.isEmpty()) {
+ for(Map.Entry result : db.insertCategories(categoriesToInsert).executeAsBlocking().results().entrySet()) {
+ if(result.getValue().wasInserted()) {
+ result.getKey().setId(result.getValue().insertedId().intValue());
+ }
+ }
+ }
+ //Build category map
+ Map categoryMap = new HashMap<>();
+ for (Category category : ourCategories) {
+ categoryMap.put(category.getName(), category);
+ }
+ //Insert new mangas
+ List mangaToInsert = new ArrayList<>();
+ Map mangaToSetCategories = new HashMap<>();
+ for (Map.Entry> entry : favorites.entrySet()) {
+ Category category = categoryMap.get(entry.getKey());
+ for (Manga manga : entry.getValue()) {
+ boolean alreadyHaveManga = false;
+ for (Manga ourManga : ourMangas) {
+ if (ourManga.getUrl().equals(manga.getUrl())) {
+ alreadyHaveManga = true;
+ manga = ourManga;
+ break;
+ }
+ }
+ if (!alreadyHaveManga) {
+ ourMangas.add(manga);
+ mangaToInsert.add(manga);
+ }
+ mangaToSetCategories.put(manga, category);
+ manga.setFavorite(true);
+ }
+ }
+ for (Map.Entry results : db.insertMangas(mangaToInsert).executeAsBlocking().results().entrySet()) {
+ if(results.getValue().wasInserted()) {
+ results.getKey().setId(results.getValue().insertedId());
+ }
+ }
+ for(Map.Entry entry : mangaToSetCategories.entrySet()) {
+ db.setMangaCategories(Collections.singletonList(MangaCategory.Companion.create(entry.getKey(), entry.getValue())),
+ Collections.singletonList(entry.getKey()));
+ }
+ //Determines what
+ /*Map> toUpload = new HashMap<>();
+ for (Manga manga : ourMangas) {
+ if(manga.getFavorite()) {
+ boolean remoteHasManga = false;
+ for (List remoteMangas : favorites.values()) {
+ for (Manga remoteManga : remoteMangas) {
+ if (remoteManga.getUrl().equals(manga.getUrl())) {
+ remoteHasManga = true;
+ break;
+ }
+ }
+ }
+ if (!remoteHasManga) {
+ List mangaCategories = db.getCategoriesForManga(manga).executeAsBlocking();
+ for (Category category : mangaCategories) {
+ int categoryIndex = favResponse.favCategories.indexOf(category.getName());
+ if (categoryIndex >= 0) {
+ List uploadMangas = toUpload.get(categoryIndex);
+ if (uploadMangas == null) {
+ uploadMangas = new ArrayList<>();
+ toUpload.put(categoryIndex, uploadMangas);
+ }
+ uploadMangas.add(manga);
+ }
+ }
+ }
+ }
+ }*/
+ /********** NON-FUNCTIONAL, modifygids[] CANNOT ADD NEW FAVORITES! (or as of my testing it can't, maybe I'll do more testing)**/
+ /*PreferencesHelper helper = new PreferencesHelper(context);
+ for(Map.Entry> entry : toUpload.entrySet()) {
+ FormBody.Builder formBody = new FormBody.Builder()
+ .add("ddact", "fav" + entry.getKey());
+ for(Manga manga : entry.getValue()) {
+ List splitUrl = new ArrayList<>(Arrays.asList(manga.getUrl().split("/")));
+ splitUrl.removeAll(Collections.singleton(""));
+ if(splitUrl.size() < 2) {
+ continue;
+ }
+ formBody.add("modifygids[]", splitUrl.get(1).trim());
+ }
+ formBody.add("apply", "Apply");
+ Request request = RequestsKt.POST(EHentai.buildFavoritesBase(context, helper.getPrefs()).favoritesBase,
+ EHentai.getHeadersBuilder(helper).build(),
+ formBody.build(),
+ RequestsKt.getDEFAULT_CACHE_CONTROL());
+ Response response = NetworkManager.getInstance().getClient().newCall(request).execute();
+ Util.d("EHentai", response.body().string());
+ }*/
+// }
+}
diff --git a/app/src/main/java/exh/StringBuilderExtensions.kt b/app/src/main/java/exh/StringBuilderExtensions.kt
new file mode 100644
index 000000000..1dffedbb0
--- /dev/null
+++ b/app/src/main/java/exh/StringBuilderExtensions.kt
@@ -0,0 +1,3 @@
+package exh
+
+operator fun StringBuilder.plusAssign(other: String) { append(other) }
diff --git a/app/src/main/java/exh/metadata/MetadataHelper.kt b/app/src/main/java/exh/metadata/MetadataHelper.kt
new file mode 100644
index 000000000..77b633620
--- /dev/null
+++ b/app/src/main/java/exh/metadata/MetadataHelper.kt
@@ -0,0 +1,22 @@
+package exh.metadata
+
+import exh.metadata.models.ExGalleryMetadata
+import io.paperdb.Paper
+
+class MetadataHelper {
+
+ fun writeGallery(galleryMetadata: ExGalleryMetadata)
+ = exGalleryBook().write(galleryMetadata.galleryUniqueIdentifier(), galleryMetadata)
+
+ fun fetchMetadata(url: String, exh: Boolean) = ExGalleryMetadata().apply {
+ this.url = url
+ this.exh = exh
+ return exGalleryBook().read(galleryUniqueIdentifier())
+ }
+
+ fun getAllGalleries() = exGalleryBook().allKeys.map {
+ exGalleryBook().read(it)
+ }
+
+ fun exGalleryBook() = Paper.book("gallery-ex")!!
+}
\ No newline at end of file
diff --git a/app/src/main/java/exh/metadata/MetadataUtil.kt b/app/src/main/java/exh/metadata/MetadataUtil.kt
new file mode 100644
index 000000000..73a205ea1
--- /dev/null
+++ b/app/src/main/java/exh/metadata/MetadataUtil.kt
@@ -0,0 +1,47 @@
+package exh.metadata
+
+/**
+ * Metadata utils
+ */
+fun humanReadableByteCount(bytes: Long, si: Boolean): String {
+ val unit = if (si) 1000 else 1024
+ if (bytes < unit) return bytes.toString() + " B"
+ val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt()
+ val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
+ return String.format("%.1f %sB", bytes / Math.pow(unit.toDouble(), exp.toDouble()), pre)
+}
+
+private val KB_FACTOR: Long = 1000
+private val KIB_FACTOR: Long = 1024
+private val MB_FACTOR = 1000 * KB_FACTOR
+private val MIB_FACTOR = 1024 * KIB_FACTOR
+private val GB_FACTOR = 1000 * MB_FACTOR
+private val GIB_FACTOR = 1024 * MIB_FACTOR
+
+fun parseHumanReadableByteCount(arg0: String): Double? {
+ val spaceNdx = arg0.indexOf(" ")
+ val ret = java.lang.Double.parseDouble(arg0.substring(0, spaceNdx))
+ when (arg0.substring(spaceNdx + 1)) {
+ "GB" -> return ret * GB_FACTOR
+ "GiB" -> return ret * GIB_FACTOR
+ "MB" -> return ret * MB_FACTOR
+ "MiB" -> return ret * MIB_FACTOR
+ "KB" -> return ret * KB_FACTOR
+ "KiB" -> return ret * KIB_FACTOR
+ }
+ return null
+}
+
+
+fun String?.nullIfBlank(): String? = if(isNullOrBlank())
+ null
+else
+ this
+
+fun ignore(expr: () -> T): T? {
+ return try { expr() } catch (t: Throwable) { null }
+}
+
+fun Set>.forEach(action: (K, V) -> Unit) {
+ forEach { action(it.key, it.value) }
+}
\ No newline at end of file
diff --git a/app/src/main/java/exh/metadata/MetdataCopier.kt b/app/src/main/java/exh/metadata/MetdataCopier.kt
new file mode 100644
index 000000000..c2369ac73
--- /dev/null
+++ b/app/src/main/java/exh/metadata/MetdataCopier.kt
@@ -0,0 +1,94 @@
+package exh.metadata
+
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.util.UrlUtil
+import exh.metadata.models.ExGalleryMetadata
+import exh.metadata.models.Tag
+import exh.plusAssign
+import java.text.SimpleDateFormat
+import java.util.*
+
+/**
+ * Copies gallery metadata to a manga object
+ */
+
+private const val ARTIST_NAMESPACE = "artist"
+private const val AUTHOR_NAMESPACE = "author"
+
+private val ONGOING_SUFFIX = arrayOf(
+ "[ongoing]",
+ "(ongoing)",
+ "{ongoing}"
+)
+
+val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
+
+fun ExGalleryMetadata.copyTo(manga: Manga) {
+ exh?.let {
+ manga.source = if(it)
+ 2
+ else
+ 1
+ }
+ url?.let { manga.url = it }
+ thumbnailUrl?.let { manga.thumbnail_url = it }
+ title?.let { manga.title = it }
+
+ //Set artist (if we can find one)
+ tags[ARTIST_NAMESPACE]?.let {
+ if(it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
+ }
+ //Set author (if we can find one)
+ tags[AUTHOR_NAMESPACE]?.let {
+ if(it.isNotEmpty()) manga.author = it.joinToString(transform = Tag::name)
+ }
+ //Set genre
+ genre?.let { manga.genre = it }
+
+ //Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
+ //We default to completed
+ manga.status = Manga.COMPLETED
+ title?.let { t ->
+ ONGOING_SUFFIX.find {
+ t.endsWith(it, ignoreCase = true)
+ }?.let {
+ manga.status = Manga.ONGOING
+ }
+ }
+
+ //Build a nice looking description out of what we know
+ val titleDesc = StringBuilder()
+ title?.let { titleDesc += "Title: $it\n" }
+ altTitle?.let { titleDesc += "Japanese Title: $it\n" }
+
+ val detailsDesc = StringBuilder()
+ uploader?.let { detailsDesc += "Uploader: $it\n" }
+ datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" }
+ visible?.let { detailsDesc += "Visible: $it\n" }
+ language?.let {
+ detailsDesc += "Language: $it"
+ if(translated == true) detailsDesc += " TR"
+ detailsDesc += "\n"
+ }
+ size?.let { detailsDesc += "File Size: ${humanReadableByteCount(it, true)}\n" }
+ length?.let { detailsDesc += "Length: $it pages\n" }
+ favorites?.let { detailsDesc += "Favorited: $it times\n" }
+ averageRating?.let {
+ detailsDesc += "Rating: $it"
+ ratingCount?.let { detailsDesc += " ($it)" }
+ detailsDesc += "\n"
+ }
+
+ val tagsDesc = StringBuilder("Tags:\n")
+ //BiConsumer only available in Java 8, don't bother calling forEach directly on 'tags'
+ tags.entries.forEach { namespace, tags ->
+ if(tags.isNotEmpty()) {
+ val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
+ tagsDesc += "▪ $namespace: $joinedTags\n"
+ }
+ }
+
+ manga.description = listOf(titleDesc, detailsDesc, tagsDesc)
+ .filter { it.isNotBlank() }
+ .joinToString(separator = "\n")
+}
diff --git a/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt b/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
new file mode 100644
index 000000000..afee603df
--- /dev/null
+++ b/app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
@@ -0,0 +1,52 @@
+package exh.metadata.models
+
+import android.net.Uri
+import java.util.*
+
+/**
+ * Gallery metadata storage model
+ */
+
+class ExGalleryMetadata {
+ var url: String? = null
+
+ var exh: Boolean? = null
+
+ var title: String? = null
+ var altTitle: String? = null
+
+ var thumbnailUrl: String? = null
+
+ var genre: String? = null
+
+ var uploader: String? = null
+ var datePosted: Long? = null
+ var parent: String? = null
+ var visible: String? = null
+ var language: String? = null
+ var translated: Boolean? = null
+ var size: Long? = null
+ var length: Int? = null
+ var favorites: Int? = null
+ var ratingCount: Int? = null
+ var averageRating: Double? = null
+
+ //Being specific about which classes are used in generics to make deserialization easier
+ var tags: HashMap> = HashMap()
+
+ private fun splitGalleryUrl()
+ = url?.let {
+ Uri.parse(it).pathSegments.filterNot(String::isNullOrBlank)
+ }
+
+ fun galleryId() = splitGalleryUrl()?.let { it[it.size - 2] }
+
+ fun galleryToken() =
+ splitGalleryUrl()?.last()
+
+ fun galleryUniqueIdentifier() = exh?.let { exh ->
+ url?.let {
+ "${if(exh) "EXH" else "EX"}-${galleryId()}-${galleryToken()}"
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/exh/metadata/models/Tag.kt b/app/src/main/java/exh/metadata/models/Tag.kt
new file mode 100644
index 000000000..9b78b0c35
--- /dev/null
+++ b/app/src/main/java/exh/metadata/models/Tag.kt
@@ -0,0 +1,7 @@
+package exh.metadata.models
+
+/**
+ * Simple tag model
+ */
+
+data class Tag(var name: String, var light: Boolean)
diff --git a/app/src/main/java/exh/ui/login/LoginActivity.kt b/app/src/main/java/exh/ui/login/LoginActivity.kt
new file mode 100644
index 000000000..31cec32bf
--- /dev/null
+++ b/app/src/main/java/exh/ui/login/LoginActivity.kt
@@ -0,0 +1,182 @@
+package exh.ui.login
+
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.view.MenuItem
+import android.webkit.CookieManager
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
+import kotlinx.android.synthetic.main.eh_activity_login.*
+import kotlinx.android.synthetic.main.toolbar.*
+import timber.log.Timber
+import uy.kohesive.injekt.injectLazy
+import java.net.HttpCookie
+
+/**
+ * LoginActivity
+ */
+
+class LoginActivity : BaseActivity() {
+
+ val preferenceManager: PreferencesHelper by injectLazy()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setAppTheme()
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.eh_activity_login)
+
+ setup()
+
+ setupToolbar(toolbar, backNavigation = false)
+ }
+
+ fun setup() {
+ btn_cancel.setOnClickListener { onBackPressed() }
+ btn_recheck.setOnClickListener { webview.loadUrl("http://exhentai.org/") }
+
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ CookieManager.getInstance().removeAllCookies {
+ runOnUiThread {
+ startWebview()
+ }
+ }
+ } else {
+ CookieManager.getInstance().removeAllCookie()
+ startWebview()
+ }
+ }
+
+ fun startWebview() {
+ webview.settings.javaScriptEnabled = true
+ webview.settings.domStorageEnabled = true
+
+ webview.loadUrl("https://forums.e-hentai.org/index.php?act=Login")
+
+ webview.setWebViewClient(object : WebViewClient() {
+ override fun onPageFinished(view: WebView, url: String) {
+ super.onPageFinished(view, url)
+ Timber.d(url)
+ val parsedUrl = Uri.parse(url)
+ if(parsedUrl.host.equals("forums.e-hentai.org", ignoreCase = true)) {
+ //Hide distracting content
+ view.loadUrl(HIDE_JS)
+
+ //Check login result
+ if(parsedUrl.getQueryParameter("code")?.toInt() != 0) {
+ if(checkLoginCookies(url)) view.loadUrl("http://exhentai.org/")
+ }
+ } else if(parsedUrl.host.equals("exhentai.org", ignoreCase = true)) {
+ //At ExHentai, check that everything worked out...
+ if(applyExHentaiCookies(url)) {
+ preferenceManager.enableExhentai().set(true)
+ onBackPressed()
+ }
+ }
+ }
+ })
+ }
+
+ /**
+ * Check if we are logged in
+ */
+ fun checkLoginCookies(url: String): Boolean {
+ getCookies(url)?.let { parsed ->
+ return parsed.filter {
+ (it.name.equals(MEMBER_ID_COOKIE, ignoreCase = true)
+ || it.name.equals(PASS_HASH_COOKIE, ignoreCase = true))
+ && it.value.isNotBlank()
+ }.count() >= 2
+ }
+ return false
+ }
+
+ /**
+ * Parse cookies at ExHentai
+ */
+ fun applyExHentaiCookies(url: String): Boolean {
+ getCookies(url)?.let { parsed ->
+
+ var memberId: String? = null
+ var passHash: String? = null
+ var igneous: String? = null
+
+ parsed.forEach {
+ when (it.name.toLowerCase()) {
+ MEMBER_ID_COOKIE -> memberId = it.value
+ PASS_HASH_COOKIE -> passHash = it.value
+ IGNEOUS_COOKIE -> igneous = it.value
+ }
+ }
+
+ //Missing a cookie
+ if (memberId == null || passHash == null || igneous == null) return false
+
+ //Update prefs
+ preferenceManager.memberIdVal().set(memberId)
+ preferenceManager.passHashVal().set(passHash)
+ preferenceManager.igneousVal().set(igneous)
+
+ return true
+ }
+ return false
+ }
+
+ fun getCookies(url: String): List?
+ = CookieManager.getInstance().getCookie(url)?.let {
+ it.split("; ").flatMap {
+ HttpCookie.parse(it)
+ }
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> onBackPressed()
+ else -> return super.onOptionsItemSelected(item)
+ }
+ return true
+ }
+
+ companion object {
+ const val MEMBER_ID_COOKIE = "ipb_member_id"
+ const val PASS_HASH_COOKIE = "ipb_pass_hash"
+ const val IGNEOUS_COOKIE = "igneous"
+
+ const val HIDE_JS = """
+ javascript:(function () {
+ document.getElementsByTagName('body')[0].style.visibility = 'hidden';
+ document.getElementsByName('submit')[0].style.visibility = 'visible';
+ document.querySelector('td[width="60%"][valign="top"]').style.visibility = 'visible';
+
+ function hide(e) {if(e !== null && e !== undefined) e.style.display = 'none';}
+
+ hide(document.querySelector(".errorwrap"));
+ hide(document.querySelector('td[width="40%"][valign="top"]'));
+ var child = document.querySelector(".page").querySelector('div');
+ child.style.padding = null;
+ var ft = child.querySelectorAll('table');
+ var fd = child.parentNode.querySelectorAll('div > div');
+ var fh = document.querySelector('#border').querySelectorAll('td > table');
+ hide(ft[0]);
+ hide(ft[1]);
+ hide(fd[1]);
+ hide(fd[2]);
+ hide(child.querySelector('br'));
+ var error = document.querySelector(".page > div > .borderwrap");
+ if(error !== null) error.style.visibility = 'visible';
+ hide(fh[0]);
+ hide(fh[1]);
+ hide(document.querySelector("#gfooter"));
+ hide(document.querySelector(".copyright"));
+ document.querySelectorAll("td").forEach(function(e) {
+ e.style.color = "white";
+ });
+ var pc = document.querySelector(".postcolor");
+ if(pc !== null) pc.style.color = "#26353F";
+ })()
+ """
+ }
+}
diff --git a/app/src/main/res/drawable/eh_ic_ehlogo_red_24dp.xml b/app/src/main/res/drawable/eh_ic_ehlogo_red_24dp.xml
new file mode 100644
index 000000000..ee5aca6a2
--- /dev/null
+++ b/app/src/main/res/drawable/eh_ic_ehlogo_red_24dp.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/eh_activity_login.xml b/app/src/main/res/layout/eh_activity_login.xml
new file mode 100644
index 000000000..104324cd3
--- /dev/null
+++ b/app/src/main/res/layout/eh_activity_login.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index efb13c44f..74f1a341b 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -1,6 +1,4 @@
- Tachiyomi
-
Nombre
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index a46f7c849..6d5c30dbc 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -1,6 +1,4 @@
- Tachiyomi
-
Nome
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index cb2df4d23..b9c26f586 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -1,7 +1,5 @@
- Tachiyomi
-
Nome
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4a1746d1e..2a38646b5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,5 +1,5 @@
- Tachiyomi
+ TachiyomiEH
Name
@@ -375,4 +375,7 @@
No wifi connection available
No network connection available
+
+ Login
+ E-Hentai
diff --git a/app/src/main/res/xml/eh_pref_eh.xml b/app/src/main/res/xml/eh_pref_eh.xml
new file mode 100644
index 000000000..85ff98603
--- /dev/null
+++ b/app/src/main/res/xml/eh_pref_eh.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index dcb7393f8..00fb38c08 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,6 +10,9 @@ buildscript {
classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
+
+ //Firebase (EH)
+ classpath 'com.google.gms:google-services:3.0.0'
}
}
diff --git a/gradle.properties b/gradle.properties
index 1d3591c8a..c23c5049e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -15,4 +15,6 @@
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
\ No newline at end of file
+# org.gradle.parallel=true
+android.enableBuildCache=true
+kotlin.incremental=true
\ No newline at end of file