From caa1e1ef09e96aa3e3a118a27d040451f2633e9e Mon Sep 17 00:00:00 2001 From: NerdNumber9 Date: Mon, 2 Jan 2017 18:02:10 -0500 Subject: [PATCH] Add EH code. --- .gitignore | 3 +- app/.gitignore | 3 +- app/build.gradle | 24 +- app/proguard-rules.pro | 5 +- app/src/main/AndroidManifest.xml | 7 + app/src/main/java/eu/kanade/tachiyomi/App.kt | 2 + .../data/preference/PreferencesHelper.kt | 9 + .../tachiyomi/data/source/SourceManager.kt | 49 ++- .../tachiyomi/data/source/model/Page.kt | 2 +- .../data/source/online/all/EHentai.kt | 343 ++++++++++++++++++ .../data/source/online/all/EHentaiMetadata.kt | 135 +++++++ .../tachiyomi/data/updater/GithubService.kt | 2 +- .../tachiyomi/ui/setting/SettingsActivity.kt | 1 + .../ui/setting/SettingsEhFragment.kt | 52 +++ .../tachiyomi/ui/setting/SettingsFragment.kt | 1 + .../main/java/exh/FavoritesSyncManager.java | 191 ++++++++++ .../main/java/exh/StringBuilderExtensions.kt | 3 + .../main/java/exh/metadata/MetadataHelper.kt | 22 ++ .../main/java/exh/metadata/MetadataUtil.kt | 47 +++ .../main/java/exh/metadata/MetdataCopier.kt | 94 +++++ .../exh/metadata/models/ExGalleryMetadata.kt | 52 +++ app/src/main/java/exh/metadata/models/Tag.kt | 7 + .../main/java/exh/ui/login/LoginActivity.kt | 182 ++++++++++ .../res/drawable/eh_ic_ehlogo_red_24dp.xml | 42 +++ app/src/main/res/layout/eh_activity_login.xml | 67 ++++ app/src/main/res/values-es/strings.xml | 2 - app/src/main/res/values-it/strings.xml | 2 - app/src/main/res/values-pt/strings.xml | 2 - app/src/main/res/values/strings.xml | 5 +- app/src/main/res/xml/eh_pref_eh.xml | 29 ++ build.gradle | 3 + gradle.properties | 4 +- 32 files changed, 1363 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/online/all/EHentai.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/source/online/all/EHentaiMetadata.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsEhFragment.kt create mode 100644 app/src/main/java/exh/FavoritesSyncManager.java create mode 100644 app/src/main/java/exh/StringBuilderExtensions.kt create mode 100644 app/src/main/java/exh/metadata/MetadataHelper.kt create mode 100644 app/src/main/java/exh/metadata/MetadataUtil.kt create mode 100644 app/src/main/java/exh/metadata/MetdataCopier.kt create mode 100644 app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt create mode 100644 app/src/main/java/exh/metadata/models/Tag.kt create mode 100644 app/src/main/java/exh/ui/login/LoginActivity.kt create mode 100644 app/src/main/res/drawable/eh_ic_ehlogo_red_24dp.xml create mode 100644 app/src/main/res/layout/eh_activity_login.xml create mode 100644 app/src/main/res/xml/eh_pref_eh.xml 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 @@ + + + + + + + + + + + + + + + + + +