mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Add EH code.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,4 +6,5 @@
|
||||
.idea/
|
||||
*iml
|
||||
*.iml
|
||||
*/build
|
||||
*/build
|
||||
/mainframer.sh
|
||||
|
3
app/.gitignore
vendored
3
app/.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
/build
|
||||
*iml
|
||||
*.iml
|
||||
custom.gradle
|
||||
custom.gradle
|
||||
google-services.json
|
@ -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'
|
||||
|
5
app/proguard-rules.pro
vendored
5
app/proguard-rules.pro
vendored
@ -95,4 +95,7 @@
|
||||
-dontwarn org.yaml.snakeyaml.**
|
||||
|
||||
# Duktape
|
||||
-keep class com.squareup.duktape.** { *; }
|
||||
-keep class com.squareup.duktape.** { *; }
|
||||
|
||||
# [EH]
|
||||
-keep class exh.** { *; }
|
@ -98,6 +98,13 @@
|
||||
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
|
||||
android:value="GlideModule" />
|
||||
|
||||
<!-- EH -->
|
||||
<activity
|
||||
android:name="exh.ui.login.LoginActivity"
|
||||
android:label="@string/label_login"
|
||||
android:parentActivityName=".ui.setting.SettingsActivity" >
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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<Source> = 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<Source> {
|
||||
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<Int, Source> = hashMapOf<Int, Source>().apply {
|
||||
createEHSources().forEach { put(it.id, it) }
|
||||
createOnlineSourceList().forEach { put(it.id, it) }
|
||||
|
||||
val parsersDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
||||
|
@ -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 {
|
||||
|
@ -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<List<Chapter>>
|
||||
= 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<List<String>> {
|
||||
val urls = mutableListOf<String>()
|
||||
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<Int, String>::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<OnlineSource.Filter>): 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<Filter>)
|
||||
= "$baseUrl$QUERY_PREFIX${buildGenreString(filters)}&f_search=${URLEncoder.encode(query, "UTF-8")}"
|
||||
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||
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<Chapter>) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||
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> {
|
||||
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<String, String> = 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<String, String>)
|
||||
= 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"
|
||||
}
|
||||
}
|
@ -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<Filter>): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response, page: MangasPage, query: String, filters: List<Filter>) {
|
||||
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<Chapter>) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response, pages: MutableList<Page>) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>>
|
||||
= 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<ExGalleryMetadata>.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<Filter>)
|
||||
= 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"
|
||||
|
||||
}
|
@ -23,7 +23,7 @@ interface GithubService {
|
||||
}
|
||||
}
|
||||
|
||||
@GET("/repos/inorichi/tachiyomi/releases/latest")
|
||||
@GET("/repos/NerdNumber9/tachiyomi/releases/latest")
|
||||
fun getLatestVersion(): Observable<GithubRelease>
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
||||
|
191
app/src/main/java/exh/FavoritesSyncManager.java
Normal file
191
app/src/main/java/exh/FavoritesSyncManager.java
Normal file
@ -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<String, List<Manga>> favorites = favResponse.favs;
|
||||
List<Category> ourCategories = new ArrayList<>(db.getCategories().executeAsBlocking());
|
||||
List<Manga> ourMangas = new ArrayList<>(db.getMangas().executeAsBlocking());
|
||||
//Add required categories (categories do not sync upwards)
|
||||
List<Category> 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<Category, PutResult> result : db.insertCategories(categoriesToInsert).executeAsBlocking().results().entrySet()) {
|
||||
if(result.getValue().wasInserted()) {
|
||||
result.getKey().setId(result.getValue().insertedId().intValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
//Build category map
|
||||
Map<String, Category> categoryMap = new HashMap<>();
|
||||
for (Category category : ourCategories) {
|
||||
categoryMap.put(category.getName(), category);
|
||||
}
|
||||
//Insert new mangas
|
||||
List<Manga> mangaToInsert = new ArrayList<>();
|
||||
Map<Manga, Category> mangaToSetCategories = new HashMap<>();
|
||||
for (Map.Entry<String, List<Manga>> 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<Manga, PutResult> results : db.insertMangas(mangaToInsert).executeAsBlocking().results().entrySet()) {
|
||||
if(results.getValue().wasInserted()) {
|
||||
results.getKey().setId(results.getValue().insertedId());
|
||||
}
|
||||
}
|
||||
for(Map.Entry<Manga, Category> entry : mangaToSetCategories.entrySet()) {
|
||||
db.setMangaCategories(Collections.singletonList(MangaCategory.Companion.create(entry.getKey(), entry.getValue())),
|
||||
Collections.singletonList(entry.getKey()));
|
||||
}
|
||||
//Determines what
|
||||
/*Map<Integer, List<Manga>> toUpload = new HashMap<>();
|
||||
for (Manga manga : ourMangas) {
|
||||
if(manga.getFavorite()) {
|
||||
boolean remoteHasManga = false;
|
||||
for (List<Manga> remoteMangas : favorites.values()) {
|
||||
for (Manga remoteManga : remoteMangas) {
|
||||
if (remoteManga.getUrl().equals(manga.getUrl())) {
|
||||
remoteHasManga = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!remoteHasManga) {
|
||||
List<Category> mangaCategories = db.getCategoriesForManga(manga).executeAsBlocking();
|
||||
for (Category category : mangaCategories) {
|
||||
int categoryIndex = favResponse.favCategories.indexOf(category.getName());
|
||||
if (categoryIndex >= 0) {
|
||||
List<Manga> 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<Integer, List<Manga>> entry : toUpload.entrySet()) {
|
||||
FormBody.Builder formBody = new FormBody.Builder()
|
||||
.add("ddact", "fav" + entry.getKey());
|
||||
for(Manga manga : entry.getValue()) {
|
||||
List<String> 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());
|
||||
}*/
|
||||
// }
|
||||
}
|
3
app/src/main/java/exh/StringBuilderExtensions.kt
Normal file
3
app/src/main/java/exh/StringBuilderExtensions.kt
Normal file
@ -0,0 +1,3 @@
|
||||
package exh
|
||||
|
||||
operator fun StringBuilder.plusAssign(other: String) { append(other) }
|
22
app/src/main/java/exh/metadata/MetadataHelper.kt
Normal file
22
app/src/main/java/exh/metadata/MetadataHelper.kt
Normal file
@ -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<ExGalleryMetadata>(galleryUniqueIdentifier())
|
||||
}
|
||||
|
||||
fun getAllGalleries() = exGalleryBook().allKeys.map {
|
||||
exGalleryBook().read<ExGalleryMetadata>(it)
|
||||
}
|
||||
|
||||
fun exGalleryBook() = Paper.book("gallery-ex")!!
|
||||
}
|
47
app/src/main/java/exh/metadata/MetadataUtil.kt
Normal file
47
app/src/main/java/exh/metadata/MetadataUtil.kt
Normal file
@ -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 <T> ignore(expr: () -> T): T? {
|
||||
return try { expr() } catch (t: Throwable) { null }
|
||||
}
|
||||
|
||||
fun <K,V> Set<Map.Entry<K,V>>.forEach(action: (K, V) -> Unit) {
|
||||
forEach { action(it.key, it.value) }
|
||||
}
|
94
app/src/main/java/exh/metadata/MetdataCopier.kt
Normal file
94
app/src/main/java/exh/metadata/MetdataCopier.kt
Normal file
@ -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")
|
||||
}
|
52
app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
Normal file
52
app/src/main/java/exh/metadata/models/ExGalleryMetadata.kt
Normal file
@ -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<String, ArrayList<Tag>> = 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()}"
|
||||
}
|
||||
}
|
||||
}
|
7
app/src/main/java/exh/metadata/models/Tag.kt
Normal file
7
app/src/main/java/exh/metadata/models/Tag.kt
Normal file
@ -0,0 +1,7 @@
|
||||
package exh.metadata.models
|
||||
|
||||
/**
|
||||
* Simple tag model
|
||||
*/
|
||||
|
||||
data class Tag(var name: String, var light: Boolean)
|
182
app/src/main/java/exh/ui/login/LoginActivity.kt
Normal file
182
app/src/main/java/exh/ui/login/LoginActivity.kt
Normal file
@ -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<HttpCookie>?
|
||||
= 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";
|
||||
})()
|
||||
"""
|
||||
}
|
||||
}
|
42
app/src/main/res/drawable/eh_ic_ehlogo_red_24dp.xml
Normal file
42
app/src/main/res/drawable/eh_ic_ehlogo_red_24dp.xml
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="8"
|
||||
android:viewportHeight="7">
|
||||
|
||||
<!-- Crafted by hand, command by command -->
|
||||
<group
|
||||
android:translateX="0.8"
|
||||
android:translateY="0.7"
|
||||
android:scaleX="0.8"
|
||||
android:scaleY="0.8">
|
||||
<path
|
||||
android:fillColor="#660611"
|
||||
android:pathData="M 0 0 h 3 v 1 h -3 Z" />
|
||||
<path
|
||||
android:fillColor="#660611"
|
||||
android:pathData="M 0 1 v 2 h 1 v -2 Z" />
|
||||
<path
|
||||
android:fillColor="#660611"
|
||||
android:pathData="M 0 3 h 2.25 v 1 h -2.25 Z" />
|
||||
<path
|
||||
android:fillColor="#660611"
|
||||
android:pathData="M 0 4 v 2 h 1 v -2 Z" />
|
||||
<path
|
||||
android:fillColor="#660611"
|
||||
android:pathData="M 0 6 h 3 v 1 h -3 Z" />
|
||||
<path
|
||||
android:fillColor="#660611"
|
||||
android:pathData="M 3 3 h 1 v 1 h -1" />
|
||||
<path
|
||||
android:fillColor="#660611"
|
||||
android:pathData="M 4.75 0 h 1 v 7 h -1 Z" />
|
||||
<path
|
||||
android:fillColor="#660611"
|
||||
android:pathData="M 5.75 3 h 1.25 v 1 h -1.25 Z" />
|
||||
<path
|
||||
android:fillColor="#660611"
|
||||
android:pathData="M 7 0 h 1 v 7 h -1 Z" />
|
||||
</group>
|
||||
</vector>
|
67
app/src/main/res/layout/eh_activity_login.xml
Normal file
67
app/src/main/res/layout/eh_activity_login.xml
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.design.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/linearLayout"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="horizontal"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
android:layout_width="0dp"
|
||||
android:id="@+id/linearLayout">
|
||||
|
||||
<Button
|
||||
android:text="@android:string/cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/btn_cancel"
|
||||
android:layout_weight="1"
|
||||
style="@style/Widget.AppCompat.Button.Borderless" />
|
||||
|
||||
<Button
|
||||
android:text="Recheck"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/btn_recheck"
|
||||
android:layout_weight="1"
|
||||
style="@style/Widget.AppCompat.Button.Borderless" />
|
||||
</LinearLayout>
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</android.support.design.widget.CoordinatorLayout>
|
@ -1,6 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">Tachiyomi</string>
|
||||
|
||||
<string name="name">Nombre</string>
|
||||
|
||||
<!-- Activities and fragments labels (toolbar title) -->
|
||||
|
@ -1,6 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">Tachiyomi</string>
|
||||
|
||||
<string name="name">Nome</string>
|
||||
|
||||
<!-- Activities and fragments labels (toolbar title) -->
|
||||
|
@ -1,7 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Tachiyomi</string>
|
||||
|
||||
<string name="name">Nome</string>
|
||||
|
||||
<!-- Activities and fragments labels (toolbar title) -->
|
||||
|
@ -1,5 +1,5 @@
|
||||
<resources>
|
||||
<string name="app_name">Tachiyomi</string>
|
||||
<string name="app_name">TachiyomiEH</string>
|
||||
|
||||
<string name="name">Name</string>
|
||||
|
||||
@ -375,4 +375,7 @@
|
||||
<string name="download_notifier_text_only_wifi">No wifi connection available</string>
|
||||
<string name="download_notifier_no_network">No network connection available</string>
|
||||
|
||||
<!-- EH -->
|
||||
<string name="label_login">Login</string>
|
||||
<string name="pref_category_eh">E-Hentai</string>
|
||||
</resources>
|
||||
|
29
app/src/main/res/xml/eh_pref_eh.xml
Normal file
29
app/src/main/res/xml/eh_pref_eh.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceScreen
|
||||
android:icon="@drawable/eh_ic_ehlogo_red_24dp"
|
||||
android:key="eh_screen"
|
||||
android:persistent="false"
|
||||
android:title="@string/pref_category_eh"
|
||||
app:asp_tintEnabled="true">
|
||||
|
||||
<SwitchPreference
|
||||
android:persistent="false"
|
||||
android:title="Enable ExHentai"
|
||||
android:summaryOff="Requires login"
|
||||
android:key="enable_exhentai"
|
||||
android:defaultValue="false" />
|
||||
|
||||
<SwitchPreference
|
||||
android:dependency="enable_exhentai"
|
||||
android:defaultValue="true"
|
||||
android:key="secure_exh"
|
||||
android:title="Secure ExHentai"
|
||||
android:summary="Use the HTTPS version of ExHentai. Uncheck if ExHentai is not working." />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
</PreferenceScreen>
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
# org.gradle.parallel=true
|
||||
android.enableBuildCache=true
|
||||
kotlin.incremental=true
|
Reference in New Issue
Block a user