Add MangaDex only implementation of Mangadex Follows list

Add login dialog that pops up whenever you are not logged in when trying to browse MangaDex
Remove attempts at porting over chapter read history from older galleries to new ones
Disable latest for ExHentai, it was browse without buttons anyway
This commit is contained in:
Jobobby04 2020-09-11 23:12:13 -04:00
parent 8928aa77eb
commit b93298c411
63 changed files with 1492 additions and 163 deletions

View File

@ -15,9 +15,9 @@ import java.util.Date
interface ChapterQueries : DbProvider {
// SY -->
fun getChapters(manga: Manga) = getChaptersByMangaId(manga.id)
fun getChapters(manga: Manga) = getChapters(manga.id)
fun getChaptersByMangaId(mangaId: Long?) = db.get()
fun getChapters(mangaId: Long?) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(
Query.builder()

View File

@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.notification
@ -65,7 +66,7 @@ class LibraryUpdateNotifier(private val context: Context) {
* @param current the current progress.
* @param total the total progress.
*/
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
fun showProgressNotification(manga: /* SY --> */ SManga /* SY <-- */, current: Int, total: Int) {
val title = if (preferences.hideNotificationContent()) {
context.getString(R.string.notification_check_updates)
} else {

View File

@ -23,6 +23,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.library.LibraryGroup
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
@ -34,9 +35,16 @@ import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
import exh.MERGED_SOURCE_ID
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.metadata.metadata.base.insertFlatMetadata
import exh.source.EnhancedHttpSource.Companion.getMainSource
import exh.util.asObservable
import exh.util.await
import exh.util.awaitSingle
import exh.util.nullIfBlank
import java.io.File
import java.util.Date
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.runBlocking
import rx.Observable
@ -81,7 +89,10 @@ class LibraryUpdateService(
enum class Target {
CHAPTERS, // Manga chapters
COVERS, // Manga covers
TRACKING // Tracking metadata
TRACKING, // Tracking metadata
// SY -->
SYNC_FOLLOWS // MangaDex specific, pull mangadex manga in reading, rereading
// SY <--
}
companion object {
@ -215,6 +226,9 @@ class LibraryUpdateService(
Target.CHAPTERS -> updateChapterList(mangaList)
Target.COVERS -> updateCovers(mangaList)
Target.TRACKING -> updateTrackings(mangaList)
// SY -->
Target.SYNC_FOLLOWS -> syncFollows()
// SY <--
}
}
.subscribeOn(Schedulers.io())
@ -433,9 +447,34 @@ class LibraryUpdateService(
.subscribe()
}
return /* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() }
else /* SY <-- */ source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) }
// SY -->
if (source.getMainSource() is MangaDex) {
val tracks = db.getTracks(manga).executeAsBlocking()
if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) {
var track = trackManager.mdList.createInitialTracker(manga)
track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() }
db.insertTrack(track).executeAsBlocking()
}
}
// SY <--
return (
/* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() }
else /* SY <-- */ source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) }
// SY -->
)
.doOnNext {
if (source.getMainSource() is MangaDex) {
val tracks = db.getTracks(manga).executeAsBlocking()
if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) {
var track = trackManager.mdList.createInitialTracker(manga)
track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() }
db.insertTrack(track).executeAsBlocking()
}
}
}
// SY <--
}
private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
@ -501,6 +540,48 @@ class LibraryUpdateService(
}
}
// SY -->
// filter all follows from Mangadex and only add reading or rereading manga to library
private fun syncFollows(): Observable<LibraryManga> {
val count = AtomicInteger(0)
val mangaDex = MdUtil.getEnabledMangaDex(preferences, sourceManager)!!
return mangaDex.fetchAllFollows(true)
.asObservable()
.map { listManga ->
listManga.filter { (_, metadata) ->
metadata.follow_status == FollowStatus.RE_READING.int || metadata.follow_status == FollowStatus.READING.int
}
}
.doOnNext { listManga ->
listManga.forEach { (networkManga, metadata) ->
notifier.showProgressNotification(networkManga, count.andIncrement, listManga.size)
var dbManga = db.getManga(networkManga.url, mangaDex.id)
.executeAsBlocking()
if (dbManga == null) {
dbManga = Manga.create(
networkManga.url,
networkManga.title,
mangaDex.id
)
dbManga.date_added = Date().time
}
dbManga.copyFrom(networkManga)
dbManga.favorite = true
val id = db.insertManga(dbManga).executeAsBlocking().insertedId()
if (id != null) {
metadata.mangaId = id
db.insertFlatMetadata(metadata.flatten()).await()
}
}
}
.doOnCompleted {
notifier.cancelProgressNotification()
}
.map { LibraryManga() }
}
// SY <--
/**
* Writes basic file of update errors to cache dir.
*/

View File

@ -291,6 +291,10 @@ object PreferenceKeys {
const val mangaDexLowQualityCovers = "manga_dex_low_quality_covers"
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
const val preferredMangaDexId = "preferred_mangaDex_id"
const val dataSaver = "data_saver"
const val ignoreJpeg = "ignore_jpeg"

View File

@ -396,6 +396,10 @@ class PreferencesHelper(val context: Context) {
fun mangaDexLowQualityCovers() = flowPrefs.getBoolean(Keys.mangaDexLowQualityCovers, false)
fun mangaDexForceLatestCovers() = flowPrefs.getBoolean(Keys.mangaDexForceLatestCovers, false)
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)

View File

@ -4,6 +4,7 @@ import android.content.Context
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
@ -16,8 +17,8 @@ class TrackManager(context: Context) {
const val SHIKIMORI = 4
const val BANGUMI = 5
// SY --> Mangadex from Neko todo
const val MDLIST = 60
// SY --> Mangadex from Neko
const val MDLIST = 6
// SY <--
// SY -->
@ -31,6 +32,8 @@ class TrackManager(context: Context) {
// SY <--
}
val mdList = MdList(context, MDLIST)
val myAnimeList = MyAnimeList(context, MYANIMELIST)
val aniList = Anilist(context, ANILIST)
@ -41,11 +44,11 @@ class TrackManager(context: Context) {
val bangumi = Bangumi(context, BANGUMI)
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi)
val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi)
fun getService(id: Int) = services.find { it.id == id }
fun hasLoggedServices() = services.any { it.isLogged }
fun hasLoggedServices(isMangaDexManga: Boolean = true) = services.any { it.isLogged && ((it.id == MDLIST && isMangaDexManga) || it.id != MDLIST) }
// SY -->
fun mapTrackingOrder(status: String, context: Context): Int {

View File

@ -0,0 +1,136 @@
package eu.kanade.tachiyomi.data.track.mdlist
import android.content.Context
import android.graphics.Color
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.asObservable
import exh.util.floor
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.runBlocking
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
class MdList(private val context: Context, id: Int) : TrackService(id) {
private val mdex by lazy { MdUtil.getEnabledMangaDex() }
private val db: DatabaseHelper by injectLazy()
override val name = "MDList"
override fun getLogo(): Int {
return R.drawable.ic_tracker_mangadex_logo
}
override fun getLogoColor(): Int {
return Color.rgb(43, 48, 53)
}
override fun getStatusList(): List<Int> {
return FollowStatus.values().map { it.int }
}
override fun getStatus(status: Int): String =
context.resources.getStringArray(R.array.md_follows_options).asList()[status]
override fun getScoreList() = IntRange(0, 10).map(Int::toString)
override fun displayScore(track: Track) = track.score.toInt().toString()
override fun add(track: Track): Observable<Track> {
return update(track)
}
override fun update(track: Track): Observable<Track> {
val mdex = mdex ?: throw Exception("Mangadex not enabled")
return Observable.defer {
db.getManga(track.tracking_url.substringAfter(".org"), mdex.id)
.asRxObservable()
.map { manga ->
val mangaMetadata = db.getFlatMetadataForManga(manga.id!!).executeAsBlocking()?.raise(MangaDexSearchMetadata::class) ?: throw Exception("Invalid manga metadata")
val followStatus = FollowStatus.fromInt(track.status)!!
// allow follow status to update
if (mangaMetadata.follow_status != followStatus.int) {
runBlocking { mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus).collect() }
mangaMetadata.follow_status = followStatus.int
db.insertFlatMetadata(mangaMetadata.flatten()).await()
}
if (track.score.toInt() > 0) {
runBlocking { mdex.updateRating(track).collect() }
}
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
if (followStatus != FollowStatus.UNFOLLOWED) {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = FollowStatus.COMPLETED.int
}
runBlocking { mdex.updateReadingProgress(track).collect() }
} else if (track.last_chapter_read != 0) {
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
track.last_chapter_read = 0
}
track
}
}
}
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
override fun bind(track: Track): Observable<Track> {
val mdex = mdex ?: throw Exception("Mangadex not enabled")
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
.doOnNext { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = if (remoteTrack.total_chapters == 0) {
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
} else {
remoteTrack.total_chapters
}
update(track)
}
}
override fun refresh(track: Track): Observable<Track> {
val mdex = mdex ?: throw Exception("Mangadex not enabled")
return mdex.fetchTrackingInfo(track.tracking_url).asObservable()
.map { remoteTrack ->
track.copyPersonalFrom(remoteTrack)
track.total_chapters = if (remoteTrack.total_chapters == 0) {
db.getChapters(track.manga_id).executeAsBlocking().maxOfOrNull { it.chapter_number }?.floor() ?: remoteTrack.total_chapters
} else {
remoteTrack.total_chapters
}
track
}
}
fun createInitialTracker(manga: Manga): Track {
val track = Track.create(TrackManager.MDLIST)
track.manga_id = manga.id!!
track.status = FollowStatus.UNFOLLOWED.int
track.tracking_url = MdUtil.baseUrl + manga.url
track.title = manga.title
return track
}
override fun search(query: String): Observable<List<TrackSearch>> = throw Exception("not used")
override fun login(username: String, password: String): Completable = throw Exception("not used")
override val isLogged = mdex?.isLogged() ?: false
}

View File

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.source.online
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.Controller
interface BrowseSourceFilterHeader {
fun getFilterHeader(controller: Controller): RecyclerView.Adapter<*>
}

View File

@ -0,0 +1,30 @@
package eu.kanade.tachiyomi.source.online
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.utils.FollowStatus
import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.coroutines.flow.Flow
import rx.Observable
interface FollowsSource {
fun fetchFollows(): Observable<MangasPage>
/**
* Returns a list of all Follows retrieved by Coroutines
*
* @param SManga all smanga found for user
*/
fun fetchAllFollows(forceHd: Boolean = false): Flow<List<Pair<SManga, RaisedSearchMetadata>>>
/**
* updates the follow status for a manga
*/
fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Flow<Boolean>
/**
* Get a MdList Track of the manga
*/
fun fetchTrackingInfo(url: String): Flow<Track>
}

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.source.online
import android.app.Activity
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
interface LoginSource {
val needsLogin: Boolean
fun isLogged(): Boolean
fun getLoginDialog(source: Source, activity: Activity): DialogController
suspend fun login(username: String, password: String, twoFactorCode: String = ""): Boolean
suspend fun logout(): Boolean
}

View File

@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.source.online
import kotlinx.coroutines.flow.Flow
interface RandomMangaSource {
fun fetchRandomMangaUrl(): Flow<String>
}

View File

@ -91,7 +91,7 @@ class EHentai(
get() = "https://$domain"
override val lang = "all"
override val supportsLatest = true
override val supportsLatest = !exh
private val preferences: PreferencesHelper by injectLazy()
private val updateHelper: EHentaiUpdateHelper by injectLazy()

View File

@ -1,41 +1,78 @@
package eu.kanade.tachiyomi.source.online.all
import android.app.Activity
import android.content.Context
import android.net.Uri
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.BrowseSourceFilterHeader
import eu.kanade.tachiyomi.source.online.FollowsSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.GalleryAddEvent
import exh.GalleryAdder
import exh.md.MangaDexFabHeaderAdapter
import exh.md.handlers.ApiChapterParser
import exh.md.handlers.ApiMangaParser
import exh.md.handlers.FollowsHandler
import exh.md.handlers.MangaHandler
import exh.md.handlers.MangaPlusHandler
import exh.md.utils.FollowStatus
import exh.md.utils.MdLang
import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog
import kotlin.reflect.KClass
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaDex(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
MetadataSource<MangaDexSearchMetadata, Response>,
UrlImportableSource {
UrlImportableSource,
FollowsSource,
LoginSource,
BrowseSourceFilterHeader,
RandomMangaSource {
override val lang: String = delegate.lang
override val headers: Headers
get() = super.headers.newBuilder().apply {
add("X-Requested-With", "XMLHttpRequest")
add("Referer", MdUtil.baseUrl)
}.build()
private val mdLang by lazy {
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
}
@ -44,25 +81,27 @@ class MangaDex(delegate: HttpSource, val context: Context) :
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) {
super.fetchSearchManga(page, query, filters)
ImportIdToMdId(query) {
super.fetchSearchManga(page, query, filters)
}
}
override fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
"/manga/${uri.pathSegments[1]}/"
MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt())
} else {
null
}
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return MangaHandler(client, headers, listOf(mdLang)).fetchMangaDetailsObservable(manga)
return MangaHandler(client, headers, listOf(mdLang), Injekt.get<PreferencesHelper>().mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga)
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return MangaHandler(client, headers, listOf(mdLang)).fetchChapterListObservable(manga)
return MangaHandler(client, headers, listOf(mdLang), Injekt.get<PreferencesHelper>().mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga)
}
@ExperimentalSerializationApi
@ -96,6 +135,130 @@ class MangaDex(delegate: HttpSource, val context: Context) :
}
override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
ApiMangaParser(listOf(mdLang)).parseIntoMetadata(metadata, input)
ApiMangaParser(listOf(mdLang)).parseIntoMetadata(metadata, input, Injekt.get<PreferencesHelper>().mangaDexForceLatestCovers().get())
}
override fun fetchFollows(): Observable<MangasPage> {
return FollowsHandler(client, headers, Injekt.get()).fetchFollows()
}
override val needsLogin: Boolean = true
override fun getLoginDialog(source: Source, activity: Activity): DialogController {
return MangadexLoginDialog(source as MangaDex, activity)
}
override fun isLogged(): Boolean {
val httpUrl = MdUtil.baseUrl.toHttpUrlOrNull()!!
return network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
}
override suspend fun login(
username: String,
password: String,
twoFactorCode: String
): Boolean {
return withContext(Dispatchers.IO) {
val formBody = FormBody.Builder()
.add("login_username", username)
.add("login_password", password)
.add("no_js", "1")
.add("remember_me", "1")
twoFactorCode.let {
formBody.add("two_factor", it)
}
val response = client.newCall(
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
headers,
formBody.build()
)
).execute()
response.body!!.string().isEmpty()
}
}
override suspend fun logout(): Boolean {
return withContext(Dispatchers.IO) {
// https://mangadex.org/ajax/actions.ajax.php?function=logout
val httpUrl = MdUtil.baseUrl.toHttpUrlOrNull()!!
val listOfDexCookies = network.cookieManager.get(httpUrl)
val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
val token = cookie?.value
if (token.isNullOrEmpty()) {
return@withContext true
}
val result = client.newCall(
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
).execute()
val resultStr = result.body!!.string()
if (resultStr.contains("success", true)) {
network.cookieManager.remove(httpUrl)
Injekt.get<TrackManager>().mdList.logout()
return@withContext true
}
false
}
}
override fun fetchAllFollows(forceHd: Boolean): Flow<List<Pair<SManga, MangaDexSearchMetadata>>> {
return flow { emit(FollowsHandler(client, headers, Injekt.get()).fetchAllFollows(forceHd)) }
}
fun updateReadingProgress(track: Track): Flow<Boolean> {
return flow { FollowsHandler(client, headers, Injekt.get()).updateReadingProgress(track) }
}
fun updateRating(track: Track): Flow<Boolean> {
return flow { FollowsHandler(client, headers, Injekt.get()).updateRating(track) }
}
override fun fetchTrackingInfo(url: String): Flow<Track> {
return flow {
if (!isLogged()) {
throw Exception("Not Logged in")
}
emit(FollowsHandler(client, headers, Injekt.get()).fetchTrackingInfo(url))
}
}
override fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Flow<Boolean> {
return flow { emit(FollowsHandler(client, headers, Injekt.get()).updateFollowStatus(mangaID, followStatus)) }
}
override fun getFilterHeader(controller: Controller): MangaDexFabHeaderAdapter {
return MangaDexFabHeaderAdapter(controller, this)
}
override fun fetchRandomMangaUrl(): Flow<String> {
return MangaHandler(client, headers, listOf(mdLang)).fetchRandomMangaId()
}
private fun ImportIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
when {
query.toIntOrNull() != null -> {
Observable.fromCallable {
// MdUtil.
val res = GalleryAdder().addGallery(context, MdUtil.baseUrl + MdUtil.mapMdIdToMangaUrl(query.toInt()), false, this)
MangasPage(
(
if (res is GalleryAddEvent.Success) {
listOf(res.manga)
} else {
emptyList()
}
),
false
)
}
}
else -> fail()
}
companion object {
private const val REMEMBER_ME = "mangadex_rememberme_token"
}
}

View File

@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@ -55,6 +56,7 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView
import exh.EXHSavedSearch
import exh.isEhBasedSource
import exh.source.EnhancedHttpSource.Companion.getMainSource
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.main_activity.root_coordinator
import kotlinx.coroutines.Job
@ -187,6 +189,14 @@ open class BrowseSourceController(bundle: Bundle) :
setupRecycler(view)
binding.progress.isVisible = true
// SY -->
val mainSource = presenter.source.getMainSource()
if (mainSource is LoginSource && mainSource.needsLogin && !mainSource.isLogged()) {
val dialog = mainSource.getLoginDialog(mainSource, activity!!)
dialog.showDialog(router)
}
// SY <--
}
open fun initFilterSheet() {
@ -205,6 +215,8 @@ open class BrowseSourceController(bundle: Bundle) :
filterSheet = SourceFilterSheet(
activity!!,
// SY -->
this,
presenter.source,
presenter.loadSearches(),
// SY <--
onFilterClicked = {

View File

@ -38,7 +38,6 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
import eu.kanade.tachiyomi.util.removeCovers
import exh.EXHSavedSearch
import exh.isEhBasedSource
import java.lang.RuntimeException
import java.util.Date
import rx.Observable
@ -188,7 +187,7 @@ open class BrowseSourcePresenter(
// SY <--
.doOnNext { initializeMangas(it.second) }
// SY -->
.map { triple -> triple.first to triple.second.mapIndexed { index, manga -> SourceItem(manga, sourceDisplayMode, if (prefs.enhancedEHentaiView().get() && source.isEhBasedSource()) triple.third?.getOrNull(index) else null) } }
.map { triple -> triple.first to triple.second.mapIndexed { index, manga -> SourceItem(manga, sourceDisplayMode, triple.third?.getOrNull(index)) } }
// SY <--
.observeOn(AndroidSchedulers.mainThread())
.subscribeReplay(

View File

@ -4,11 +4,15 @@ import android.view.View
import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.widget.StateImageViewTarget
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.android.synthetic.main.source_comfortable_grid_item.card
import kotlinx.android.synthetic.main.source_comfortable_grid_item.local_text
import kotlinx.android.synthetic.main.source_comfortable_grid_item.progress
import kotlinx.android.synthetic.main.source_comfortable_grid_item.thumbnail
import kotlinx.android.synthetic.main.source_comfortable_grid_item.title
@ -43,6 +47,17 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
setImage(manga)
}
// SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) {
metadata.follow_status?.let {
local_text.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
local_text.isVisible = true
}
}
}
// SY <--
override fun setImage(manga: Manga) {
// For rounded corners
card.clipToOutline = true

View File

@ -56,7 +56,7 @@ class SourceEnhancedEHentaiListHolder(private val view: View, adapter: FlexibleA
setImage(manga)
}
fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata !is EHentaiSearchMetadata) return
if (metadata.uploader != null) {
@ -64,17 +64,17 @@ class SourceEnhancedEHentaiListHolder(private val view: View, adapter: FlexibleA
}
val pair = when (metadata.genre) {
"doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi)
"manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga)
"artistcg" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.artist_cg)
"gamecg" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.game_cg)
"western" -> Pair(SourceTagsUtil.WESTERN_COLOR, R.string.western)
"non-h" -> Pair(SourceTagsUtil.NON_H_COLOR, R.string.non_h)
"imageset" -> Pair(SourceTagsUtil.IMAGE_SET_COLOR, R.string.image_set)
"cosplay" -> Pair(SourceTagsUtil.COSPLAY_COLOR, R.string.cosplay)
"asianporn" -> Pair(SourceTagsUtil.ASIAN_PORN_COLOR, R.string.asian_porn)
"misc" -> Pair(SourceTagsUtil.MISC_COLOR, R.string.misc)
else -> Pair("", 0)
"doujinshi" -> SourceTagsUtil.DOUJINSHI_COLOR to R.string.doujinshi
"manga" -> SourceTagsUtil.MANGA_COLOR to R.string.manga
"artistcg" -> SourceTagsUtil.ARTIST_CG_COLOR to R.string.artist_cg
"gamecg" -> SourceTagsUtil.GAME_CG_COLOR to R.string.game_cg
"western" -> SourceTagsUtil.WESTERN_COLOR to R.string.western
"non-h" -> SourceTagsUtil.NON_H_COLOR to R.string.non_h
"imageset" -> SourceTagsUtil.IMAGE_SET_COLOR to R.string.image_set
"cosplay" -> SourceTagsUtil.COSPLAY_COLOR to R.string.cosplay
"asianporn" -> SourceTagsUtil.ASIAN_PORN_COLOR to R.string.asian_porn
"misc" -> SourceTagsUtil.MISC_COLOR to R.string.misc
else -> "" to 0
}
if (pair.first.isNotBlank()) {

View File

@ -8,18 +8,24 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.Controller
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.chip.Chip
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.databinding.SourceFilterSheetBinding
import eu.kanade.tachiyomi.util.view.inflate
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.BrowseSourceFilterHeader
import eu.kanade.tachiyomi.widget.SimpleNavigationView
import exh.EXHSavedSearch
import exh.source.EnhancedHttpSource.Companion.getMainSource
class SourceFilterSheet(
activity: Activity,
// SY -->
controller: Controller,
source: CatalogueSource,
searches: List<EXHSavedSearch> = emptyList(),
// SY <--
onFilterClicked: () -> Unit,
@ -34,7 +40,7 @@ class SourceFilterSheet(
private var filterNavView: FilterNavigationView
init {
filterNavView = FilterNavigationView(activity /* SY --> */, searches = searches/* SY <-- */)
filterNavView = FilterNavigationView(activity /* SY --> */, searches = searches, source = source, controller = controller/* SY <-- */)
filterNavView.onFilterClicked = {
onFilterClicked()
this.dismiss()
@ -66,7 +72,7 @@ class SourceFilterSheet(
}
// SY <--
class FilterNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null /* SY --> */, searches: List<EXHSavedSearch> = emptyList()/* SY <-- */) :
class FilterNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null /* SY --> */, searches: List<EXHSavedSearch> = emptyList(), source: CatalogueSource? = null, controller: Controller? = null/* SY <-- */) :
SimpleNavigationView(context, attrs) {
var onFilterClicked = {}
@ -79,6 +85,8 @@ class SourceFilterSheet(
var onSavedSearchDeleteClicked: (Int, String) -> Unit = { _, _ -> }
val adapters = mutableListOf<RecyclerView.Adapter<*>>()
private val savedSearchesAdapter = SavedSearchesAdapter(getSavedSearchesChips(searches))
// SY <--
@ -88,7 +96,13 @@ class SourceFilterSheet(
init {
// SY -->
recycler.adapter = ConcatAdapter(savedSearchesAdapter, adapter)
val mainSource = source?.getMainSource()
if (mainSource is BrowseSourceFilterHeader && controller != null) {
adapters += mainSource.getFilterHeader(controller)
}
adapters += savedSearchesAdapter
adapters += adapter
recycler.adapter = ConcatAdapter(adapters)
// SY <--
recycler.setHasFixedSize(true)
(binding.root.getChildAt(1) as ViewGroup).addView(recycler)

View File

@ -1,13 +1,18 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.View
import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.widget.StateImageViewTarget
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.android.synthetic.main.source_compact_grid_item.card
import kotlinx.android.synthetic.main.source_compact_grid_item.local_text
import kotlinx.android.synthetic.main.source_compact_grid_item.progress
import kotlinx.android.synthetic.main.source_compact_grid_item.thumbnail
import kotlinx.android.synthetic.main.source_compact_grid_item.title
@ -39,6 +44,17 @@ open class SourceGridHolder(private val view: View, private val adapter: Flexibl
setImage(manga)
}
// SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) {
metadata.follow_status?.let {
local_text.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
local_text.isVisible = true
}
}
}
// SY <--
override fun setImage(manga: Manga) {
// For rounded corners
card.clipToOutline = true

View File

@ -4,6 +4,7 @@ import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import exh.metadata.metadata.base.RaisedSearchMetadata
/**
* Generic class used to hold the displayed data of a manga in the catalogue.
@ -29,4 +30,8 @@ abstract class SourceHolder(view: View, adapter: FlexibleAdapter<*>) :
* @param manga the manga to bind.
*/
abstract fun setImage(manga: Manga)
// SY -->
abstract fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata)
// SY <--
}

View File

@ -13,27 +13,38 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.android.synthetic.main.source_compact_grid_item.view.card
import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class SourceItem(val manga: Manga, private val displayMode: Preference<DisplayMode> /* SY --> */, private val metadata: RaisedSearchMetadata? = null /* SY <-- */) :
AbstractFlexibleItem<SourceHolder>() {
// SY -->
val preferences: PreferencesHelper by injectLazy()
// SY <--
override fun getLayoutRes(): Int {
return /* SY --> */ if (metadata == null) /* SY <-- */ when (displayMode.get()) {
return /* SY --> */ if ((manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) && preferences.enhancedEHentaiView().get()) R.layout.source_enhanced_ehentai_list_item
else /* SY <-- */ when (displayMode.get()) {
DisplayMode.COMPACT_GRID -> R.layout.source_compact_grid_item
DisplayMode.COMFORTABLE_GRID, /* SY --> */ DisplayMode.NO_TITLE_GRID /* SY <-- */ -> R.layout.source_comfortable_grid_item
DisplayMode.LIST -> R.layout.source_list_item
} /* SY --> */ else R.layout.source_enhanced_ehentai_list_item /* SY <-- */
}
}
override fun createViewHolder(
view: View,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
): SourceHolder {
return /* SY --> */ if (metadata == null) /* SY <-- */ when (displayMode.get()) {
return /* SY --> */ if ((manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) && preferences.enhancedEHentaiView().get()) {
SourceEnhancedEHentaiListHolder(view, adapter)
} else /* SY <-- */ when (displayMode.get()) {
DisplayMode.COMPACT_GRID -> {
val parent = adapter.recyclerView as AutofitRecyclerView
val coverHeight = parent.itemWidth / 3 * 4
@ -60,11 +71,7 @@ class SourceItem(val manga: Manga, private val displayMode: Preference<DisplayMo
DisplayMode.LIST -> {
SourceListHolder(view, adapter)
}
// SY -->
} else {
SourceEnhancedEHentaiListHolder(view, adapter)
}
// SY <--
}
override fun bindViewHolder(
@ -76,7 +83,7 @@ class SourceItem(val manga: Manga, private val displayMode: Preference<DisplayMo
holder.onSetValues(manga)
// SY -->
if (metadata != null) {
(holder as? SourceEnhancedEHentaiListHolder)?.onSetMetadataValues(manga, metadata)
holder.onSetMetadataValues(manga, metadata)
}
// SY <--
}

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.View
import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
@ -11,6 +12,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.util.system.getResourceColor
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import kotlinx.android.synthetic.main.source_list_item.local_text
import kotlinx.android.synthetic.main.source_list_item.thumbnail
import kotlinx.android.synthetic.main.source_list_item.title
@ -44,6 +48,17 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
setImage(manga)
}
// SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) {
metadata.follow_status?.let {
local_text.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
local_text.isVisible = true
}
}
}
// SY <--
override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(thumbnail)

View File

@ -198,6 +198,8 @@ open class IndexController :
filterSheet = SourceFilterSheet(
activity!!,
// SY -->
this,
presenter.source,
presenter.loadSearches(),
// SY <--
onFilterClicked = {

View File

@ -49,6 +49,7 @@ import exh.PERV_EDEN_EN_SOURCE_ID
import exh.PERV_EDEN_IT_SOURCE_ID
import exh.favorites.FavoritesIntroDialog
import exh.favorites.FavoritesSyncStatus
import exh.mangaDexSourceIds
import exh.nHentaiSourceIds
import exh.ui.LoaderManager
import java.util.concurrent.TimeUnit
@ -530,6 +531,9 @@ class LibraryController(
it.source == PERV_EDEN_EN_SOURCE_ID ||
it.source == PERV_EDEN_IT_SOURCE_ID
}
binding.actionToolbar.findItem(R.id.action_push_to_mdlist)?.isVisible = selectedMangas.any {
it.source in mangaDexSourceIds
}
// SY <--
}
return false
@ -556,6 +560,7 @@ class LibraryController(
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
}
R.id.action_clean -> cleanTitles()
R.id.action_push_to_mdlist -> pushToMdList()
// SY <--
else -> return false
}
@ -658,6 +663,13 @@ class LibraryController(
presenter.cleanTitles(mangas)
destroyActionModeIfNeeded()
}
private fun pushToMdList() {
val mangas = selectedMangas.filter {
it.source in mangaDexSourceIds
}.toList()
presenter.syncMangaToDex(mangas)
}
// SY <--
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {

View File

@ -30,11 +30,14 @@ import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.MERGED_SOURCE_ID
import exh.favorites.FavoritesSyncHelper
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.util.await
import exh.util.isLewd
import exh.util.nullIfBlank
import java.util.Collections
import java.util.Comparator
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.runBlocking
import rx.Observable
@ -194,6 +197,7 @@ class LibraryPresenter(
}
if (filterTracked != STATE_IGNORE) {
val tracks = db.getTracks(item.manga).executeAsBlocking()
.filterNot { it.sync_id == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int }
if (filterTracked == STATE_INCLUDE && tracks.isEmpty()) return@f false
else if (filterTracked == STATE_EXCLUDE && tracks.isNotEmpty()) return@f false
}
@ -501,6 +505,16 @@ class LibraryPresenter(
}
}
}
fun syncMangaToDex(mangaList: List<Manga>) {
launchIO {
MdUtil.getEnabledMangaDex(preferences)?.let { mdex ->
mangaList.forEach {
mdex.updateFollowStatus(MdUtil.getMangaId(it.url), FollowStatus.READING).collect()
}
}
}
}
// SY <--
/**
@ -612,6 +626,9 @@ class LibraryPresenter(
LibraryGroup.BY_STATUS -> {
grouping += Triple(SManga.ONGOING.toString(), SManga.ONGOING, context.getString(R.string.ongoing))
grouping += Triple(SManga.LICENSED.toString(), SManga.LICENSED, context.getString(R.string.licensed))
grouping += Triple(SManga.CANCELLED.toString(), SManga.CANCELLED, context.getString(R.string.cancelled))
grouping += Triple(SManga.HIATUS.toString(), SManga.HIATUS, context.getString(R.string.hiatus))
grouping += Triple(SManga.PUBLICATION_COMPLETE.toString(), SManga.PUBLICATION_COMPLETE, context.getString(R.string.publication_complete))
grouping += Triple(SManga.COMPLETED.toString(), SManga.COMPLETED, context.getString(R.string.completed))
grouping += Triple(SManga.UNKNOWN.toString(), SManga.UNKNOWN, context.getString(R.string.unknown))
}

View File

@ -50,7 +50,7 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.getMetadataSource
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
@ -92,6 +92,7 @@ import eu.kanade.tachiyomi.util.view.snack
import exh.MERGED_SOURCE_ID
import exh.isEhBasedSource
import exh.metadata.metadata.base.FlatMetadata
import exh.source.EnhancedHttpSource.Companion.getMainSource
import java.io.IOException
import kotlin.math.min
import kotlinx.android.synthetic.main.main_activity.root_coordinator
@ -254,9 +255,9 @@ class MangaController :
adapters += mangaInfoAdapter
val thisSourceAsLewdSource = presenter.source.getMetadataSource()
if (thisSourceAsLewdSource != null) {
mangaMetaInfoAdapter = thisSourceAsLewdSource.getDescriptionAdapter(this)
val mainSource = presenter.source.getMainSource()
if (mainSource is MetadataSource<*, *>) {
mangaMetaInfoAdapter = mainSource.getDescriptionAdapter(this)
mangaMetaInfoAdapter?.let { adapters += it }
}
mangaInfoItemAdapter = MangaInfoItemAdapter(this, fromSource)
@ -277,7 +278,7 @@ class MangaController :
binding.recycler.adapter = ConcatAdapter(adapters)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.addItemDecoration(ChapterDividerItemDecoration(view.context, if ((!preferences.recommendsInOverflow().get() || smartSearchConfig != null) && thisSourceAsLewdSource != null) 4 else if (!preferences.recommendsInOverflow().get() || smartSearchConfig != null || thisSourceAsLewdSource != null) 3 else 2))
binding.recycler.addItemDecoration(ChapterDividerItemDecoration(view.context, if ((!preferences.recommendsInOverflow().get() || smartSearchConfig != null) && mainSource is MetadataSource<*, *>) 4 else if (!preferences.recommendsInOverflow().get() || smartSearchConfig != null || mainSource is MetadataSource<*, *>) 3 else 2))
// SY <--
binding.recycler.setHasFixedSize(true)
chaptersAdapter?.fastScroller = binding.fastScroller
@ -481,9 +482,9 @@ class MangaController :
// SY -->
fun onNextMetaInfo(flatMetadata: FlatMetadata) {
val thisSourceAsLewdSource = presenter.source.getMetadataSource()
if (thisSourceAsLewdSource != null) {
presenter.meta = flatMetadata.raise(thisSourceAsLewdSource.metaClass)
val mainSource = presenter.source.getMainSource()
if (mainSource is MetadataSource<*, *>) {
presenter.meta = flatMetadata.raise(mainSource.metaClass)
mangaMetaInfoAdapter?.notifyDataSetChanged()
}
}

View File

@ -21,7 +21,7 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.isMetadataSource
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
@ -37,11 +37,12 @@ import exh.MERGED_SOURCE_ID
import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper
import exh.isEhBasedSource
import exh.md.utils.FollowStatus
import exh.merged.sql.models.MergedMangaReference
import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.source.EnhancedHttpSource
import exh.source.EnhancedHttpSource.Companion.getMainSource
import exh.util.asObservable
import exh.util.await
import exh.util.trimOrNull
@ -122,7 +123,7 @@ class MangaPresenter(
super.onCreate(savedState)
// SY -->
if (manga.initialized && source.isMetadataSource()) {
if (manga.initialized && source.getMainSource() is MetadataSource<*, *>) {
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") })
}
@ -207,14 +208,21 @@ class MangaPresenter(
}
private fun getTrackingObservable(): Observable<Int> {
if (!trackManager.hasLoggedServices()) {
// SY -->
val sourceIsMangaDex = source.getMainSource() is MangaDex
// SY <--
if (!trackManager.hasLoggedServices(/* SY --> */sourceIsMangaDex/* SY <-- */)) {
return Observable.just(0)
}
return db.getTracks(manga).asRxObservable()
.map { tracks ->
val loggedServices = trackManager.services.filter { it.isLogged }.map { it.id }
tracks.filter { it.sync_id in loggedServices }
val loggedServices = trackManager.services.filter { it.isLogged /* SY --> */ && ((it.id == TrackManager.MDLIST && sourceIsMangaDex) || it.id != TrackManager.MDLIST) /* SY <-- */ }.map { it.id }
tracks
// SY -->
.filterNot { it.sync_id == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int }
// SY <--
.filter { it.sync_id in loggedServices }
}
.map { it.size }
}
@ -244,7 +252,7 @@ class MangaPresenter(
}
// SY -->
.doOnNext {
if (source is MetadataSource<*, *> || (source is EnhancedHttpSource && source.enhancedSource is MetadataSource<*, *>)) {
if (source.getMainSource() is MetadataSource<*, *>) {
getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") })
}
}

View File

@ -19,10 +19,12 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.view.setTooltip
import exh.MERGED_SOURCE_ID
import exh.source.EnhancedHttpSource.Companion.getMainSource
import exh.util.SourceTagsUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -106,7 +108,10 @@ class MangaInfoHeaderAdapter(
}
with(binding.btnTracking) {
if (trackManager.hasLoggedServices()) {
// SY -->
val sourceIsMangaDex = source.let { it.getMainSource() is MangaDex }
// SY <--
if (trackManager.hasLoggedServices(/* SY --> */sourceIsMangaDex/* SY <-- */)) {
isVisible = true
if (trackCount > 0) {

View File

@ -9,6 +9,7 @@ import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
@ -91,6 +92,10 @@ class TrackController :
val atLeastOneLink = trackings.any { it.track != null }
adapter?.items = trackings
binding.swipeRefresh.isEnabled = atLeastOneLink
if (presenter.needsRefresh) {
presenter.needsRefresh = false
presenter.refresh()
}
}
fun onSearchResults(results: List<TrackSearch>) {
@ -126,6 +131,9 @@ class TrackController :
override fun onSetClick(position: Int) {
val item = adapter?.getItem(position) ?: return
// SY --> Kill search for now until cesco puts MdList into stable
if (item.service.id == TrackManager.MDLIST) return
// SY <--
TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER)
}

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.system.toast
import exh.mangaDexSourceIds
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -35,6 +36,10 @@ class TrackPresenter(
private var refreshSubscription: Subscription? = null
// SY -->
var needsRefresh = false
// SY <--
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
fetchTrackings()
@ -50,10 +55,36 @@ class TrackPresenter(
}
}
.observeOn(AndroidSchedulers.mainThread())
// SY -->
.map { trackItems ->
if (manga.source in mangaDexSourceIds) {
val mdTrack = trackItems.firstOrNull { it.service.id == TrackManager.MDLIST }
when {
mdTrack == null -> {
needsRefresh = true
trackItems + createMdListTrack()
}
mdTrack.track == null -> {
needsRefresh = true
trackItems - mdTrack + createMdListTrack()
}
else -> trackItems
}
} else trackItems
}
// SY <--
.doOnNext { trackList = it }
.subscribeLatestCache(TrackController::onNextTrackings)
}
// SY -->
private fun createMdListTrack(): TrackItem {
val track = trackManager.mdList.createInitialTracker(manga)
track.id = db.insertTrack(track).executeAsBlocking().insertedId()
return TrackItem(track, trackManager.mdList)
}
// SY <--
fun refresh() {
refreshSubscription?.let { remove(it) }
refreshSubscription = Observable.from(trackList)

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor
import exh.md.utils.MdUtil
class SettingsMainController : SettingsController() {
@ -80,6 +81,14 @@ class SettingsMainController : SettingsController() {
onClick { navigateTo(SettingsEhController()) }
}
}
if (MdUtil.getEnabledMangaDex(preferences) != null) {
preference {
iconRes = R.drawable.ic_tracker_mangadex_logo
iconTint = tintColor
titleRes = R.string.mangadex_specific_settings
onClick { navigateTo(SettingsMangaDexController()) }
}
}
// SY <--
preference {
iconRes = R.drawable.ic_code_24dp

View File

@ -0,0 +1,100 @@
package eu.kanade.tachiyomi.ui.setting
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.listPreference
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import exh.md.utils.MdUtil
import exh.widget.preference.MangaDexLoginPreference
import exh.widget.preference.MangadexLoginDialog
import exh.widget.preference.MangadexLogoutDialog
class SettingsMangaDexController :
SettingsController(),
MangadexLoginDialog.Listener,
MangadexLogoutDialog.Listener {
private val mdex by lazy { MdUtil.getEnabledMangaDex() }
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
titleRes = R.string.mangadex_specific_settings
if (mdex == null) router.popCurrentController()
val sourcePreference = MangaDexLoginPreference(context, mdex!!).apply {
title = mdex!!.name + " Login"
key = getSourceKey(source.id)
setOnLoginClickListener {
if (mdex!!.isLogged()) {
val dialog = MangadexLogoutDialog(source)
dialog.targetController = this@SettingsMangaDexController
dialog.showDialog(router)
} else {
val dialog = MangadexLoginDialog(source, activity)
dialog.targetController = this@SettingsMangaDexController
dialog.showDialog(router)
}
}
}
preferenceScreen.addPreference(sourcePreference)
listPreference {
titleRes = R.string.mangadex_preffered_source
summaryRes = R.string.mangadex_preffered_source_summary
key = PreferenceKeys.preferredMangaDexId
val mangaDexs = MdUtil.getEnabledMangaDexs()
entries = mangaDexs.map { it.toString() }.toTypedArray()
entryValues = mangaDexs.map { it.id.toString() }.toTypedArray()
/*setOnPreferenceChangeListener { preference, newValue ->
preferences.preferredMangaDexId().set((newValue as? String)?.toLongOrNull() ?: 0)
true
}*/
}
switchPreference {
key = PreferenceKeys.mangaDexLowQualityCovers
titleRes = R.string.mangadex_low_quality_covers
defaultValue = false
}
switchPreference {
key = PreferenceKeys.mangaDexForceLatestCovers
titleRes = R.string.mangadex_use_latest_cover
summaryRes = R.string.mangadex_use_latest_cover_summary
defaultValue = false
}
preference {
titleRes = R.string.mangadex_sync_follows_to_library
summaryRes = R.string.mangadex_sync_follows_to_library_summary
onClick {
LibraryUpdateService.start(
context,
target = LibraryUpdateService.Target.SYNC_FOLLOWS
)
}
}
}
override fun siteLoginDialogClosed(source: Source) {
val pref = findPreference(getSourceKey(source.id)) as? MangaDexLoginPreference
pref?.notifyChanged()
}
override fun siteLogoutDialogClosed(source: Source) {
val pref = findPreference(getSourceKey(source.id)) as? MangaDexLoginPreference
pref?.notifyChanged()
}
private fun getSourceKey(sourceId: Long): String {
return "source_$sourceId"
}
}

View File

@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.online.HttpSource
import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID
import exh.debug.DebugToggles
import java.util.Date
import java.util.TreeSet
import uy.kohesive.injekt.Injekt
@ -145,18 +144,6 @@ fun syncChaptersWithSource(
}
}
}
if (dbChapters.isEmpty() && !DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) {
val readChapters = db.getChaptersReadByUrls(finalAdded.map { it.url }).executeAsBlocking()
val readChapterUrls = readChapters.map { it.url }
if (readChapters.isNotEmpty()) {
toAdd.filter { it.url in readChapterUrls }.onEach { chapter ->
readChapters.firstOrNull { it.url == chapter.url }?.let {
chapter.read = it.read
chapter.last_page_read = it.last_page_read
}
}
}
}
}
// <-- EXH

View File

@ -42,7 +42,7 @@ class EHentaiUpdateHelper(context: Context) {
mangaIds.map { mangaId ->
Single.zip(
db.getManga(mangaId).asRxSingle(),
db.getChaptersByMangaId(mangaId).asRxSingle()
db.getChapters(mangaId).asRxSingle()
) { manga, chapters ->
ChapterChain(manga, chapters)
}.toObservable().filter {

View File

@ -152,7 +152,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
return@mapNotNull null
}
val chapter = db.getChaptersByMangaId(manga.id!!).asRxSingle().await().minByOrNull {
val chapter = db.getChapters(manga.id!!).asRxSingle().await().minByOrNull {
it.date_upload
}

View File

@ -0,0 +1,56 @@
package exh.md
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.databinding.SourceFilterMangadexHeaderBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import exh.md.follows.MangaDexFollowsController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.singleOrNull
import reactivecircus.flowbinding.android.view.clicks
class MangaDexFabHeaderAdapter(val controller: Controller, val source: CatalogueSource) :
RecyclerView.Adapter<MangaDexFabHeaderAdapter.SavedSearchesViewHolder>() {
private lateinit var binding: SourceFilterMangadexHeaderBinding
private val scope = CoroutineScope(Job() + Dispatchers.Main)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SavedSearchesViewHolder {
binding = SourceFilterMangadexHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SavedSearchesViewHolder(binding.root)
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: SavedSearchesViewHolder, position: Int) {
holder.bind()
}
inner class SavedSearchesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind() {
binding.mangadexFollows.clicks()
.onEach {
controller.router.replaceTopController(MangaDexFollowsController(source).withFadeTransaction())
}
.launchIn(scope)
binding.mangadexRandom.clicks()
.onEach {
(source as? RandomMangaSource)?.fetchRandomMangaUrl()?.singleOrNull()?.let { randomMangaId ->
controller.router.replaceTopController(BrowseSourceController(source, randomMangaId).withFadeTransaction())
}
}
.launchIn(scope)
}
}
}

View File

@ -0,0 +1,39 @@
package exh.md.follows
import android.os.Bundle
import android.view.Menu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
/**
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
*/
class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle) {
constructor(source: CatalogueSource) : this(
Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
}
)
override fun getTitle(): String? {
return view?.context?.getString(R.string.mangadex_follows)
}
override fun createPresenter(): BrowseSourcePresenter {
return MangaDexFollowsPresenter(args.getLong(SOURCE_ID_KEY))
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
menu.findItem(R.id.action_search).isVisible = false
menu.findItem(R.id.action_open_in_web_view).isVisible = false
menu.findItem(R.id.action_settings).isVisible = false
}
override fun initFilterSheet() {
// No-op: we don't allow filtering in latest
}
}

View File

@ -0,0 +1,21 @@
package exh.md.follows
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
/**
* LatestUpdatesPager inherited from the general Pager.
*/
class MangaDexFollowsPager(val source: MangaDex) : Pager() {
override fun requestNext(): Observable<MangasPage> {
return source.fetchFollows()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { onPageReceived(it) }
}
}

View File

@ -0,0 +1,18 @@
package exh.md.follows
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import exh.source.EnhancedHttpSource
/**
* Presenter of [MangaDexFollowsController]. Inherit BrowseCataloguePresenter.
*/
class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
val sourceAsMangaDex = (source as EnhancedHttpSource).enhancedSource as MangaDex
return MangaDexFollowsPager(sourceAsMangaDex)
}
}

View File

@ -17,8 +17,8 @@ import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedTag
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.floor
import java.util.Date
import kotlin.math.floor
import okhttp3.Response
import rx.Completable
import rx.Single
@ -41,7 +41,7 @@ class ApiMangaParser(private val langs: List<String>) {
*
* Will also save the metadata to the DB if possible
*/
fun parseToManga(manga: SManga, input: Response): Completable {
fun parseToManga(manga: SManga, input: Response, forceLatestCover: Boolean): Completable {
val mangaId = (manga as? Manga)?.id
val metaObservable = if (mangaId != null) {
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
@ -55,7 +55,7 @@ class ApiMangaParser(private val langs: List<String>) {
}
return metaObservable.map {
parseIntoMetadata(it, input)
parseIntoMetadata(it, input, forceLatestCover)
it.copyTo(manga)
it
}.flatMapCompletable {
@ -66,7 +66,7 @@ class ApiMangaParser(private val langs: List<String>) {
}
}
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, forceLatestCover: Boolean) {
with(metadata) {
try {
val networkApiManga = MdUtil.jsonParser.decodeFromString(ApiMangaSerializer.serializer(), input.body!!.string())
@ -74,15 +74,18 @@ class ApiMangaParser(private val langs: List<String>) {
mdId = MdUtil.getMangaId(input.request.url.toString())
mdUrl = input.request.url.toString()
title = MdUtil.cleanString(networkManga.title)
thumbnail_url = MdUtil.cdnUrl + MdUtil.removeTimeParamUrl(networkManga.cover_url)
val coverList = networkManga.covers
thumbnail_url = MdUtil.cdnUrl +
if (forceLatestCover && coverList.isNotEmpty()) {
coverList.last()
} else {
MdUtil.removeTimeParamUrl(networkManga.cover_url)
}
description = MdUtil.cleanDescription(networkManga.description)
author = MdUtil.cleanString(networkManga.author)
artist = MdUtil.cleanString(networkManga.artist)
lang_flag = networkManga.lang_flag
val lastChapter = networkManga.last_chapter?.toFloatOrNull()
lastChapter?.let {
last_chapter_number = floor(it).toInt()
}
last_chapter_number = networkManga.last_chapter?.toFloatOrNull()?.floor()
networkManga.rating?.let {
rating = it.bayesian ?: it.mean
@ -107,10 +110,16 @@ class ApiMangaParser(private val langs: List<String>) {
status = tempStatus
}
val demographic = FilterHandler.demographics().filter { it.id == networkManga.demographic }.firstOrNull()
val genres =
networkManga.genres.mapNotNull { FilterHandler.allTypes[it.toString()] }
.toMutableList()
if (demographic != null) {
genres.add(0, demographic.name)
}
if (networkManga.hentai == 1) {
genres.add("Hentai")
}
@ -135,7 +144,9 @@ class ApiMangaParser(private val langs: List<String>) {
if (filteredChapters.isEmpty() || serializer.manga.last_chapter.isNullOrEmpty()) {
return false
}
val finalChapterNumber = serializer.manga.last_chapter!!
// just to fix the stupid lint
val lastMangaChapter: String? = serializer.manga.last_chapter
val finalChapterNumber = lastMangaChapter!!
if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) {
filteredChapters.firstOrNull()?.let {
if (isOneShot(it.value, finalChapterNumber)) {
@ -144,7 +155,7 @@ class ApiMangaParser(private val langs: List<String>) {
}
}
val removeOneshots = filteredChapters.filter { !it.value.chapter.isNullOrBlank() }
return removeOneshots.size.toString() == floor(finalChapterNumber.toDouble()).toInt().toString()
return removeOneshots.size.toString() == finalChapterNumber.toDouble().floor().toString()
}
private fun filterChapterForChecking(serializer: ApiMangaSerializer): List<Map.Entry<String, ChapterSerializer>> {
@ -269,7 +280,7 @@ class ApiMangaParser(private val langs: List<String>) {
}
if ((status == 2 || status == 3)) {
if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) ||
networkChapter.chapter == finalChapterNumber
networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0
) {
chapterName.add("[END]")
}

View File

@ -1,26 +0,0 @@
package exh.md.handlers
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.handlers.serializers.CoversResult
import exh.md.utils.MdUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
// Unused, look into what its used for todo
class CoverHandler(val client: OkHttpClient, val headers: Headers) {
suspend fun getCovers(manga: SManga): List<String> {
return withContext(Dispatchers.IO) {
val response = client.newCall(GET("${MdUtil.baseUrl}${MdUtil.coversApi}${MdUtil.getMangaId(manga.url)}", headers, CacheControl.FORCE_NETWORK)).execute()
val result = MdUtil.jsonParser.decodeFromString(
CoversResult.serializer(),
response.body!!.string()
)
result.covers.map { "${MdUtil.baseUrl}$it" }
}
}
}

View File

@ -171,7 +171,8 @@ class FilterHandler {
Tag("81", "Virtual Reality"),
Tag("82", "Zombies"),
Tag("83", "Incest"),
Tag("84", "Mafia")
Tag("84", "Mafia"),
Tag("85", "Villainess")
).sortedWith(compareBy { it.name })
val allTypes = (contentType() + formats() + genre() + themes()).map { it.id to it.name }.toMap()

View File

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.handlers.serializers.FollowsPageResult
@ -16,26 +17,24 @@ import exh.md.utils.MdUtil
import exh.md.utils.MdUtil.Companion.baseUrl
import exh.md.utils.MdUtil.Companion.getMangaId
import exh.metadata.metadata.MangaDexSearchMetadata
import kotlin.math.floor
import exh.util.floor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
// Unused, kept for future featues todo
class FollowsHandler(val client: OkHttpClient, val headers: Headers, val preferences: PreferencesHelper) {
/**
* fetch follows by page
*/
fun fetchFollows(page: Int): Observable<MetadataMangasPage> {
return client.newCall(followsListRequest(page))
fun fetchFollows(): Observable<MangasPage> {
return client.newCall(followsListRequest())
.asObservable()
.map { response ->
followsParseMangaPage(response)
@ -96,9 +95,9 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
val follow = result.first()
track.status = follow.follow_type
if (result[0].chapter.isNotBlank()) {
track.last_chapter_read = floor(follow.chapter.toFloat()).toInt()
track.last_chapter_read = follow.chapter.toFloat().floor()
}
track.tracking_url = MdUtil.baseUrl + follow.manga_id.toString()
track.tracking_url = baseUrl + follow.manga_id.toString()
track.title = follow.title
}
return track
@ -107,11 +106,8 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
/**build Request for follows page
*
*/
private fun followsListRequest(page: Int): Request {
val url = "${MdUtil.baseUrl}${MdUtil.followsAllApi}".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("page", page.toString())
return GET(url.toString(), headers, CacheControl.FORCE_NETWORK)
private fun followsListRequest(): Request {
return GET("$baseUrl${MdUtil.followsAllApi}", headers, CacheControl.FORCE_NETWORK)
}
/**
@ -126,7 +122,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
title = MdUtil.cleanString(result.title)
mdUrl = "/manga/${result.manga_id}/"
thumbnail_url = MdUtil.formThumbUrl(manga.url, lowQualityCovers)
follow_status = FollowStatus.fromInt(result.follow_type)?.ordinal
follow_status = FollowStatus.fromInt(result.follow_type)?.int
}
}
@ -205,21 +201,16 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
/**
* fetch all manga from all possible pages
*/
suspend fun fetchAllFollows(forceHd: Boolean): List<SManga> {
suspend fun fetchAllFollows(forceHd: Boolean): List<Pair<SManga, MangaDexSearchMetadata>> {
return withContext(Dispatchers.IO) {
val listManga = mutableListOf<SManga>()
loop@ for (i in 1..10000) {
val response = client.newCall(followsListRequest(i))
.execute()
val mangasPage = followsParseMangaPage(response, forceHd)
if (mangasPage.mangas.isNotEmpty()) {
listManga.addAll(mangasPage.mangas)
val listManga = mutableListOf<Pair<SManga, MangaDexSearchMetadata>>()
val response = client.newCall(followsListRequest()).execute()
val mangasPage = followsParseMangaPage(response, forceHd)
listManga.addAll(
mangasPage.mangas.mapIndexed { index, sManga ->
sManga to mangasPage.mangasMetadata[index] as MangaDexSearchMetadata
}
if (!mangasPage.hasNextPage) {
break@loop
}
}
)
listManga
}
}
@ -227,7 +218,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
suspend fun fetchTrackingInfo(url: String): Track {
return withContext(Dispatchers.IO) {
val request = GET(
"${MdUtil.baseUrl}${MdUtil.followsMangaApi}" + getMangaId(url),
"$baseUrl${MdUtil.followsMangaApi}" + getMangaId(url),
headers,
CacheControl.FORCE_NETWORK
)

View File

@ -3,10 +3,13 @@ package exh.md.handlers
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.utils.MdUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
import okhttp3.CacheControl
import okhttp3.Headers
@ -14,7 +17,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import rx.Observable
class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: List<String>) {
class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: List<String>, val forceLatestCovers: Boolean = false) {
// TODO make use of this
suspend fun fetchMangaAndChapterDetails(manga: SManga): Pair<SManga, List<SChapter>> {
@ -28,7 +31,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
throw Exception("Error from MangaDex Response code ${response.code} ")
}
parser.parseToManga(manga, response).await()
parser.parseToManga(manga, response, forceLatestCovers).await()
val chapterList = parser.chapterListParse(jsonData)
Pair(
manga,
@ -48,7 +51,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
suspend fun fetchMangaDetails(manga: SManga): SManga {
return withContext(Dispatchers.IO) {
val response = client.newCall(apiRequest(manga)).execute()
ApiMangaParser(langs).parseToManga(manga, response).await()
ApiMangaParser(langs).parseToManga(manga, response, forceLatestCovers).await()
manga.apply {
initialized = true
}
@ -59,7 +62,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
return client.newCall(apiRequest(manga))
.asObservableSuccess()
.flatMap {
ApiMangaParser(langs).parseToManga(manga, it).andThen(
ApiMangaParser(langs).parseToManga(manga, it, forceLatestCovers).andThen(
Observable.just(
manga.apply {
initialized = true
@ -84,7 +87,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
}
}
fun fetchRandomMangaId(): Observable<String> {
fun fetchRandomMangaIdObservable(): Observable<String> {
return client.newCall(randomMangaRequest())
.asObservableSuccess()
.map { response ->
@ -92,6 +95,13 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
}
}
fun fetchRandomMangaId(): Flow<String> {
return flow {
val response = client.newCall(randomMangaRequest()).await()
emit(ApiMangaParser(langs).randomMangaIdParse(response))
}
}
private fun randomMangaRequest(): Request {
return GET(MdUtil.baseUrl + MdUtil.randMangaPage)
}

View File

@ -42,7 +42,7 @@ class PopularHandler(val client: OkHttpClient, private val headers: Headers) {
val mangas = document.select(popularMangaSelector).map { element ->
popularMangaFromElement(element)
}.distinct()
}.distinctBy { it.url }
val hasNextPage = popularMangaNextPageSelector.let { selector ->
document.select(selector).first()

View File

@ -33,7 +33,7 @@ class SearchHandler(val client: OkHttpClient, private val headers: Headers, val
.map { response ->
val details = SManga.create()
details.url = "/manga/$realQuery/"
ApiMangaParser(langs).parseToManga(details, response).await()
ApiMangaParser(langs).parseToManga(details, response, preferences.mangaDexForceLatestCovers().get()).await()
MangasPage(listOf(details), false)
}
}

View File

@ -15,7 +15,9 @@ data class MangaSerializer(
val author: String,
val cover_url: String,
val description: String,
val demographic: String,
val genres: List<Int>,
val covers: List<String>,
val hentai: Int,
val lang_flag: String,
val lang_name: String,

View File

@ -1,12 +1,17 @@
package exh.md.utils
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MangaDex
import exh.util.floor
import java.net.URI
import java.net.URISyntaxException
import kotlin.math.floor
import kotlinx.serialization.json.Json
import org.jsoup.parser.Parser
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MdUtil {
@ -63,10 +68,15 @@ class MdUtil {
"[b][u]Spanish",
"[Espa&ntilde;ol]:",
"[b] Spanish: [/ b]",
"정보",
"Spanish/Espa&ntilde;ol",
"Espa&ntilde;ol / Spanish",
"Italian / Italiano",
"Italian/Italiano",
"\r\n\r\nItalian\r\n",
"Pasta-Pizza-Mandolino/Italiano",
"Persian /فارسی",
"Farsi/Persian/",
"Polish / polski",
"Polish / Polski",
"Polish Summary / Polski Opis",
@ -89,6 +99,7 @@ class MdUtil {
"French - Français:",
"Turkish / T&uuml;rk&ccedil;e",
"Turkish/T&uuml;rk&ccedil;e",
"T&uuml;rk&ccedil;e",
"[b][u]Chinese",
"Arabic / العربية",
"العربية",
@ -191,11 +202,11 @@ class MdUtil {
}.sortedByDescending { it.chapter_number }
remove0ChaptersFromCount.firstOrNull()?.let {
val chpNumber = floor(it.chapter_number).toInt()
val chpNumber = it.chapter_number.floor()
val allChapters = (1..chpNumber).toMutableSet()
remove0ChaptersFromCount.forEach {
allChapters.remove(floor(it.chapter_number).toInt())
allChapters.remove(it.chapter_number.floor())
}
if (allChapters.size <= 0) return null
@ -203,6 +214,25 @@ class MdUtil {
}
return null
}
fun getEnabledMangaDex(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): MangaDex? {
return getEnabledMangaDexs(preferences, sourceManager).let { mangadexs ->
val preferredMangaDexId = preferences.preferredMangaDexId().get().toLongOrNull()
mangadexs.firstOrNull { preferredMangaDexId != null && preferredMangaDexId != 0L && it.id == preferredMangaDexId } ?: mangadexs.firstOrNull()
}
}
fun getEnabledMangaDexs(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): List<MangaDex> {
val languages = preferences.enabledLanguages().get()
val disabledSourceIds = preferences.disabledSources().get()
return sourceManager.getDelegatedCatalogueSources()
.filter { it.lang in languages }
.filterNot { it.id.toString() in disabledSourceIds }
.filterIsInstance(MangaDex::class.java)
}
fun mapMdIdToMangaUrl(id: Int) = "/manga/$id/"
}
}

View File

@ -36,10 +36,5 @@ private const val EH_UNIVERSAL_INTERCEPTOR = -1L
private val EH_INTERCEPTORS: Map<Long, List<EHInterceptor>> = mapOf(
EH_UNIVERSAL_INTERCEPTOR to listOf(
CAPTCHA_DETECTION_PATCH // Auto captcha detection
),
// MangaDex login support
*MANGADEX_SOURCE_IDS.map { id ->
id to listOf(MANGADEX_LOGIN_PATCH)
}.toTypedArray()
)
)

View File

@ -1,6 +1,7 @@
package exh.source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -234,4 +235,21 @@ class EnhancedHttpSource(
originalSource
}
}
companion object {
fun Source.getMainSource(): Source {
return if (this is EnhancedHttpSource) {
this.source()
} else {
this
}
}
fun Source.getOriginalSource(): Source {
return if (this is EnhancedHttpSource) {
this.originalSource
} else {
this
}
}
}
}

View File

@ -6,7 +6,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.databinding.MetadataViewItemBinding
import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlin.math.floor
import exh.util.floor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -43,7 +43,7 @@ class MetadataViewAdapter(private var data: List<Pair<String, String>>) :
private var dataPosition: Int? = null
fun bind(position: Int) {
if (data.isEmpty() || !binding.infoText.text.isNullOrBlank()) return
dataPosition = floor(position / 2F).toInt()
dataPosition = (position / 2F).floor()
binding.infoText.text = if (position % 2 == 0) data[dataPosition!!].first else data[dataPosition!!].second
binding.infoText.clicks()
.onEach {

View File

@ -10,11 +10,12 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.MetadataViewControllerBinding
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.getMetadataSource
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.EnhancedHttpSource.Companion.getMainSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -73,9 +74,9 @@ class MetadataViewController : NucleusController<MetadataViewControllerBinding,
}
fun onNextMetaInfo(flatMetadata: FlatMetadata) {
val thisSourceAsLewdSource = presenter.source.getMetadataSource()
if (thisSourceAsLewdSource != null) {
presenter.meta = flatMetadata.raise(thisSourceAsLewdSource.metaClass)
val mainSource = presenter.source.getMainSource()
if (mainSource is MetadataSource<*, *>) {
presenter.meta = flatMetadata.raise(mainSource.metaClass)
}
}

View File

@ -0,0 +1,5 @@
package exh.util
fun Float.floor(): Int = kotlin.math.floor(this).toInt()
fun Double.floor(): Int = kotlin.math.floor(this).toInt()

View File

@ -0,0 +1,54 @@
package exh.widget.preference
import android.content.Context
import android.util.AttributeSet
import androidx.core.view.isVisible
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlinx.android.synthetic.main.pref_item_mangadex.view.*
class MangaDexLoginPreference @JvmOverloads constructor(
context: Context,
val source: MangaDex,
attrs: AttributeSet? = null
) : Preference(context, attrs) {
init {
layoutResource = R.layout.pref_item_mangadex
}
private var onLoginClick: () -> Unit = {}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
holder.itemView.setOnClickListener {
onLoginClick()
}
val loginFrame = holder.itemView.login_frame
val color = if (source.isLogged()) {
context.getResourceColor(R.attr.colorAccent)
} else {
context.getResourceColor(R.attr.colorSecondary)
}
holder.itemView.login.setImageResource(R.drawable.ic_outline_people_alt_24dp)
holder.itemView.login.drawable.setTint(color)
loginFrame.isVisible = true
loginFrame.setOnClickListener {
onLoginClick()
}
}
fun setOnLoginClickListener(block: () -> Unit) {
onLoginClick = block
}
// Make method public
public override fun notifyChanged() {
super.notifyChanged()
}
}

View File

@ -0,0 +1,115 @@
package exh.widget.preference
import android.app.Activity
import android.app.Dialog
import android.os.Bundle
import android.view.View
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.preference.LoginDialogPreference
import exh.md.utils.MdUtil
import kotlinx.android.synthetic.main.pref_account_login.view.login
import kotlinx.android.synthetic.main.pref_account_login.view.password
import kotlinx.android.synthetic.main.pref_account_login.view.username
import kotlinx.android.synthetic.main.pref_site_login_two_factor_auth.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangadexLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle = bundle) {
val source by lazy { MdUtil.getEnabledMangaDex() }
val service = Injekt.get<TrackManager>().mdList
val scope = CoroutineScope(Job() + Dispatchers.Main)
constructor(source: MangaDex, activity: Activity? = null) : this(
Bundle().apply {
putLong(
"key",
source.id
)
}
)
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val dialog = MaterialDialog(activity!!).apply {
customView(R.layout.pref_site_login_two_factor_auth, scrollable = false)
}
onViewCreated(dialog.view)
return dialog
}
override fun setCredentialsOnView(view: View) = with(view) {
username.setText(service.getUsername())
password.setText(service.getPassword())
}
override fun checkLogin() {
v?.apply {
if (username.text.isNullOrBlank() || password.text.isNullOrBlank() || (two_factor_check.isChecked && two_factor_edit.text.isNullOrBlank())) {
errorResult()
context.toast(R.string.fields_cannot_be_blank)
return
}
login.progress = 1
dialog?.setCancelable(false)
dialog?.setCanceledOnTouchOutside(false)
scope.launch {
try {
val result = source?.login(
username.text.toString(),
password.text.toString(),
two_factor_edit.text.toString()
) ?: false
if (result) {
dialog?.dismiss()
preferences.setTrackCredentials(Injekt.get<TrackManager>().mdList, username.toString(), password.toString())
context.toast(R.string.login_success)
} else {
errorResult()
}
} catch (error: Exception) {
errorResult()
error.message?.let { context.toast(it) }
}
}
}
}
private fun errorResult() {
v?.apply {
dialog?.setCancelable(true)
dialog?.setCanceledOnTouchOutside(true)
login.progress = -1
login.setText(R.string.unknown_error)
}
}
override fun onDialogClosed() {
super.onDialogClosed()
if (activity != null) {
(activity as? Listener)?.siteLoginDialogClosed(source!!)
} else {
(targetController as? Listener)?.siteLoginDialogClosed(source!!)
}
}
interface Listener {
fun siteLoginDialogClosed(source: Source)
}
}

View File

@ -0,0 +1,49 @@
package exh.widget.preference
import android.app.Dialog
import android.os.Bundle
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.toast
import exh.md.utils.MdUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.injectLazy
class MangadexLogoutDialog(bundle: Bundle? = null) : DialogController(bundle) {
val source by lazy { MdUtil.getEnabledMangaDex() }
val trackManager: TrackManager by injectLazy()
constructor(source: Source) : this(Bundle().apply { putLong("key", source.id) })
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.title(R.string.logout)
.positiveButton(R.string.logout) {
launchNow {
source?.let { source ->
val loggedOut = withContext(Dispatchers.IO) { source.logout() }
if (loggedOut) {
trackManager.mdList.logout()
activity?.toast(R.string.logout_success)
(targetController as? Listener)?.siteLogoutDialogClosed(source)
} else {
activity?.toast(R.string.unknown_error)
}
} ?: activity!!.toast("Mangadex not enabled")
}
}
.negativeButton(android.R.string.cancel)
}
interface Listener {
fun siteLogoutDialogClosed(source: Source)
}
}

View File

@ -0,0 +1,16 @@
<vector android:height="32dp" android:viewportHeight="130"
android:viewportWidth="150" android:width="36.923077dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#F79421" android:pathData="M131.158,81.768H30.498c-1.235,0 -2.236,-1.002 -2.236,-2.236c0,-1.235 1.001,-2.237 2.236,-2.237h100.66c1.234,0 2.236,1.002 2.236,2.237C133.395,80.766 132.393,81.768 131.158,81.768"/>
<path android:fillColor="#F79421" android:pathData="M131.158,90.248H30.498c-1.235,0 -2.236,-1 -2.236,-2.236c0,-1.234 1.001,-2.236 2.236,-2.236h100.66c1.234,0 2.236,1.002 2.236,2.236C133.395,89.248 132.393,90.248 131.158,90.248"/>
<path android:fillColor="#F79421" android:pathData="M114.684,87.732l0,14.961l6.404,-3.711l6.408,3.713l0,-14.963z"/>
<path android:fillColor="#272B30" android:pathData="M118.807,40.464h16.834v9.333h-16.834z"/>
<path android:fillColor="#F1F1F1" android:pathData="M144.688,52.169c-0.084,-0.072 -0.168,-0.14 -0.252,-0.212c-1.922,-1.671 -3.565,-3.655 -4.84,-5.874c-0.012,-0.022 -0.022,-0.045 -0.033,-0.067h-0.006c-1.408,-2.487 -2.365,-5.265 -2.75,-8.221c-0.019,-0.179 -0.039,-0.358 -0.058,-0.537c0,-0.011 -0.004,-0.022 -0.004,-0.033c-0.162,-1.04 -0.608,-1.99 -1.248,-2.772c-0.062,-0.068 -0.123,-0.14 -0.183,-0.207c-5.282,-5.93 -12.308,-10.278 -20.271,-12.229c-0.018,-0.38 -0.027,-0.765 -0.027,-1.151c0,-4.415 1.135,-8.562 3.125,-12.172c0.11,-0.202 0.172,-0.436 0.172,-0.688c0,-0.799 -0.643,-1.442 -1.44,-1.442c-0.034,0 -0.067,0 -0.101,0.006c-0.084,0.005 -0.168,0.022 -0.246,0.039C106.482,8.28 97.4,12.796 90.141,19.301c-3.06,2.739 -5.797,5.83 -8.145,9.211c-0.988,1.419 -1.905,2.884 -2.749,4.398c-0.504,-0.274 -1.019,-0.536 -1.538,-0.788c-1.632,-0.804 -3.314,-1.514 -5.046,-2.135c-5.193,-1.861 -10.787,-2.873 -16.622,-2.873c-1.531,0 -3.051,0.073 -4.549,0.213c-5.634,0.509 -10.999,1.973 -15.923,4.225C19.653,38.823 8.28,54.316 6.883,72.596c-0.101,1.264 -0.151,2.543 -0.151,3.829v12.362c0,2.137 0.318,4.197 0.905,6.137c1.599,5.281 5.203,9.686 9.926,12.334c2.906,1.633 6.237,2.6 9.786,2.689h31.404c0.156,-0.012 0.313,-0.018 0.469,-0.018c0.151,0 0.308,0.006 0.459,0.018c0.05,0.004 0.1,0.004 0.15,0.012c0.084,0.006 0.163,0.01 0.247,0.021c1.933,0.225 3.638,1.186 4.828,2.6c0.184,0.217 0.363,0.451 0.526,0.697c0.592,0.9 0.994,1.936 1.145,3.045c0.011,0.053 0.017,0.107 0.023,0.164c0.033,0.229 0.05,0.463 0.05,0.703c0.005,0.057 0.005,0.117 0.005,0.174c0,0.051 0,0.096 -0.005,0.141v0.016c0.016,0.922 0.201,1.799 0.531,2.611c0.592,1.475 1.643,2.715 2.979,3.547c1.134,0.705 2.47,1.107 3.9,1.107c3.617,0 6.635,-2.594 7.283,-6.029c0.062,-0.281 0.095,-0.57 0.117,-0.867c0.006,-0.1 0.013,-0.195 0.013,-0.297c0.004,-0.078 0.004,-0.15 0.004,-0.229v-0.184c0,-2.533 -0.425,-4.97 -1.201,-7.232c-1.129,-3.287 -3.006,-6.227 -5.433,-8.617c-2.588,-2.551 -5.796,-4.473 -9.378,-5.518c-1.978,-0.576 -4.08,-0.89 -6.248,-0.89H33.039c-0.062,0.007 -0.129,0.007 -0.19,0.007c-0.062,0 -0.129,0 -0.19,-0.007C26.83,94.83 22.09,90.268 21.71,84.518c-0.017,-0.252 -0.028,-0.504 -0.028,-0.754c0,-6.17 4.996,-11.168 11.167,-11.168h99.816c2.576,0 4.762,-1.688 5.517,-4.012c0.021,-0.09 0.051,-0.186 0.078,-0.273c0.346,-1.123 1.033,-2.102 1.934,-2.817c0.006,-0.006 0.006,-0.006 0.012,-0.006c0.072,-0.061 0.15,-0.117 0.225,-0.167c0.1,-0.078 0.207,-0.146 0.313,-0.212c2.47,-1.845 4.162,-4.673 4.516,-7.897c0.051,-0.414 0.072,-0.839 0.072,-1.264C145.33,54.624 145.105,53.354 144.688,52.169M131.352,47.715c-0.24,0 -0.47,-0.062 -0.67,-0.167c-0.979,-0.565 -2.091,-0.923 -3.275,-1.018c-0.213,-0.017 -0.426,-0.028 -0.643,-0.028c-0.521,0 -1.029,0.05 -1.521,0.151c-0.812,0.151 -1.576,0.436 -2.275,0.822c-0.012,0.005 -0.021,0.016 -0.032,0.022c-0.045,0.017 -0.084,0.045 -0.123,0.072c-0.189,0.096 -0.402,0.146 -0.627,0.146c-0.775,0 -1.408,-0.631 -1.408,-1.408c0,-0.487 0.246,-0.911 0.621,-1.163c0.045,-0.033 0.09,-0.056 0.14,-0.084c0.905,-0.508 1.896,-0.888 2.94,-1.117c0.736,-0.163 1.502,-0.247 2.285,-0.247c1.682,0 3.274,0.386 4.688,1.085c0.185,0.084 0.362,0.179 0.542,0.279c0.051,0.028 0.095,0.056 0.14,0.084c0.375,0.252 0.62,0.682 0.62,1.163C132.754,47.084 132.123,47.715 131.352,47.715"/>
<path android:fillColor="#E6E6E6" android:pathData="M144.688,52.169c-0.084,-0.072 -0.168,-0.14 -0.252,-0.212c-1.922,-1.671 -3.565,-3.655 -4.84,-5.874c-0.012,-0.022 -0.022,-0.045 -0.033,-0.067h-0.006c-1.408,-2.487 -2.365,-5.265 -2.75,-8.221c-0.019,-0.179 -0.039,-0.358 -0.058,-0.537c0,-0.011 -0.004,-0.022 -0.004,-0.033c-0.162,-1.04 -0.608,-1.99 -1.248,-2.772c-0.062,-0.068 -0.123,-0.14 -0.183,-0.207c-5.282,-5.93 -12.308,-10.278 -20.271,-12.229c-0.018,-0.38 -0.027,-0.765 -0.027,-1.151c0,-4.415 1.135,-8.562 3.125,-12.172c0.11,-0.202 0.172,-0.436 0.172,-0.688c0,-0.799 -0.643,-1.442 -1.44,-1.442c-0.034,0 -0.067,0 -0.101,0.006c-0.084,0.005 -0.168,0.022 -0.246,0.039C106.482,8.28 97.4,12.796 90.141,19.301c-3.06,2.739 -5.797,5.83 -8.145,9.211c-0.988,1.419 -1.905,2.884 -2.749,4.398c-0.504,-0.274 -1.019,-0.536 -1.538,-0.788c-1.632,-0.804 -3.314,-1.514 -5.046,-2.135c-5.193,-1.861 -10.787,-2.873 -16.622,-2.873c-1.531,0 -3.051,0.073 -4.549,0.213c-5.634,0.509 -10.999,1.973 -15.923,4.225C19.653,38.823 8.28,54.316 6.883,72.596c-0.101,1.264 -0.151,2.543 -0.151,3.829v12.362c0,2.137 0.318,4.197 0.905,6.137c1.599,5.281 5.203,9.686 9.926,12.334c2.906,1.633 6.237,2.6 9.786,2.689h31.404c0.156,-0.012 0.313,-0.018 0.469,-0.018c0.151,0 0.308,0.006 0.459,0.018c0.05,0.004 0.1,0.004 0.15,0.012c0.084,0.006 0.163,0.01 0.247,0.021c1.933,0.225 3.638,1.186 4.828,2.6c0.184,0.217 0.363,0.451 0.526,0.697c0.592,0.9 0.994,1.936 1.145,3.045c0.011,0.053 0.017,0.107 0.023,0.164c0.033,0.229 0.05,0.463 0.05,0.703c0.005,0.057 0.005,0.117 0.005,0.174c0,0.051 0,0.096 -0.005,0.141v0.016c0.016,0.922 0.201,1.799 0.531,2.611c0.592,1.475 1.643,2.715 2.979,3.547c1.134,0.705 2.47,1.107 3.9,1.107c3.617,0 6.635,-2.594 7.283,-6.029c0.062,-0.281 0.095,-0.57 0.117,-0.867c0.006,-0.1 0.013,-0.195 0.013,-0.297c0.004,-0.078 0.004,-0.15 0.004,-0.229v-0.184c0,-2.533 -0.425,-4.97 -1.201,-7.232c-1.129,-3.287 -3.006,-6.227 -5.433,-8.617c-2.588,-2.551 -5.796,-4.473 -9.378,-5.518c-1.978,-0.576 -4.08,-0.89 -6.248,-0.89H33.039c-0.062,0.007 -0.129,0.007 -0.19,0.007c-0.062,0 -0.129,0 -0.19,-0.007C26.83,94.83 22.09,90.268 21.71,84.518c-0.017,-0.252 -0.028,-0.504 -0.028,-0.754c0,-6.17 4.996,-11.168 11.167,-11.168h99.816c2.576,0 4.762,-1.688 5.517,-4.012c0.021,-0.09 0.051,-0.186 0.078,-0.273c0.346,-1.123 1.033,-2.102 1.934,-2.817c0.006,-0.006 0.006,-0.006 0.012,-0.006c0.072,-0.061 0.15,-0.117 0.225,-0.167c0.1,-0.078 0.207,-0.146 0.313,-0.212c2.47,-1.845 4.162,-4.673 4.516,-7.897c0.051,-0.414 0.072,-0.839 0.072,-1.264C145.33,54.624 145.105,53.354 144.688,52.169M131.352,47.715c-0.24,0 -0.47,-0.062 -0.67,-0.167c-0.979,-0.565 -2.091,-0.923 -3.275,-1.018c-0.213,-0.017 -0.426,-0.028 -0.643,-0.028c-0.521,0 -1.029,0.05 -1.521,0.151c-0.812,0.151 -1.576,0.436 -2.275,0.822c-0.012,0.005 -0.021,0.016 -0.032,0.022c-0.045,0.017 -0.084,0.045 -0.123,0.072c-0.189,0.096 -0.402,0.146 -0.627,0.146c-0.775,0 -1.408,-0.631 -1.408,-1.408c0,-0.487 0.246,-0.911 0.621,-1.163c0.045,-0.033 0.09,-0.056 0.14,-0.084c0.905,-0.508 1.896,-0.888 2.94,-1.117c0.736,-0.163 1.502,-0.247 2.285,-0.247c1.682,0 3.274,0.386 4.688,1.085c0.185,0.084 0.362,0.179 0.542,0.279c0.051,0.028 0.095,0.056 0.14,0.084c0.375,0.252 0.62,0.682 0.62,1.163C132.754,47.084 132.123,47.715 131.352,47.715"/>
<path android:fillColor="#F79421" android:pathData="M79.398,32.655c2.688,12.921 13.898,22.615 27.328,22.615c7.903,0 15.037,-3.365 20.119,-8.763c-0.027,-0.001 -0.055,-0.004 -0.082,-0.004c-0.521,0 -1.029,0.05 -1.521,0.151c-0.812,0.15 -1.576,0.436 -2.275,0.821c-0.011,0.006 -0.021,0.017 -0.032,0.023c-0.045,0.016 -0.084,0.044 -0.123,0.072c-0.189,0.095 -0.402,0.146 -0.627,0.146c-0.775,0 -1.408,-0.632 -1.408,-1.409c0,-0.486 0.246,-0.911 0.621,-1.162c0.045,-0.034 0.09,-0.056 0.14,-0.084c0.905,-0.509 1.896,-0.889 2.94,-1.118c0.736,-0.162 1.502,-0.246 2.285,-0.246c0.769,0 1.516,0.084 2.237,0.238c2.392,-3.226 4.125,-6.988 5.006,-11.079c-5.11,-5.217 -11.638,-9.044 -18.964,-10.838c-0.018,-0.38 -0.027,-0.766 -0.027,-1.151c0,-4.416 1.135,-8.563 3.125,-12.173c0.11,-0.201 0.172,-0.435 0.172,-0.687c0,-0.799 -0.643,-1.442 -1.44,-1.442c-0.034,0 -0.067,0 -0.101,0.006c-0.084,0.005 -0.168,0.022 -0.246,0.039C106.48,8.28 97.4,12.797 90.141,19.302c-3.059,2.738 -5.797,5.829 -8.145,9.21C81.064,29.85 80.202,31.232 79.398,32.655"/>
<path android:fillColor="#272B30" android:pathData="M137.284,59.354c-1.214,-0.36 -2.421,-0.72 -3.65,-0.991c-1.218,-0.312 -2.446,-0.576 -3.679,-0.806c-2.467,-0.449 -4.947,-0.791 -7.438,-0.949c-2.492,-0.209 -4.99,-0.106 -7.478,0.086l-1.863,0.209c-0.62,0.087 -1.228,0.234 -1.845,0.344c-1.256,0.152 -2.43,0.608 -3.678,0.87c2.307,-1.034 4.759,-1.823 7.289,-2.186c2.521,-0.379 5.093,-0.51 7.638,-0.32c2.545,0.186 5.073,0.563 7.539,1.199C132.582,57.441 135.007,58.235 137.284,59.354"/>
<path android:fillColor="#272B30" android:pathData="M136.188,61.648c-1.254,-0.176 -2.5,-0.352 -3.758,-0.437c-1.252,-0.128 -2.505,-0.207 -3.759,-0.251c-2.506,-0.077 -5.009,-0.048 -7.496,0.167c-2.495,0.163 -4.95,0.636 -7.381,1.195l-1.813,0.484c-0.599,0.177 -1.179,0.413 -1.772,0.613c-1.218,0.337 -2.312,0.962 -3.506,1.407c2.127,-1.365 4.435,-2.509 6.883,-3.244c2.438,-0.75 4.96,-1.261 7.506,-1.451c2.543,-0.194 5.103,-0.197 7.634,0.065C131.251,60.456 133.77,60.88 136.188,61.648"/>
<path android:fillColor="#272B30" android:pathData="M136.016,64.321c-1.264,0.1 -2.518,0.196 -3.765,0.384c-1.248,0.146 -2.489,0.339 -3.724,0.567c-2.463,0.466 -4.9,1.035 -7.282,1.781c-2.401,0.698 -4.696,1.689 -6.949,2.762l-1.665,0.863c-0.547,0.303 -1.063,0.657 -1.599,0.98c-1.117,0.593 -2.05,1.439 -3.121,2.131c1.782,-1.791 3.789,-3.407 6.021,-4.652c2.219,-1.259 4.57,-2.303 7.016,-3.037c2.441,-0.74 4.938,-1.294 7.467,-1.585C130.939,64.221 133.488,64.093 136.016,64.321"/>
<path android:fillColor="#F27BAA" android:pathData="M144.436,51.957c-0.422,-0.366 -0.828,-0.751 -1.223,-1.147c-1.07,0.649 -1.79,1.822 -1.79,3.167c0,2.044 1.657,3.702 3.701,3.702c0.021,0 0.038,-0.002 0.058,-0.003c0.022,-0.156 0.059,-0.307 0.075,-0.465c0.05,-0.414 0.072,-0.838 0.072,-1.263c0,-1.325 -0.224,-2.594 -0.644,-3.779C144.604,52.097 144.52,52.03 144.436,51.957"/>
<path android:fillColor="#FFFFFF" android:pathData="M113.764,23.924c0,0.173 0,0.352 0.006,0.525c-4.504,1.828 -8.602,4.46 -12.105,7.713c-0.145,-0.995 -0.219,-2.007 -0.219,-3.041c0,-10.216 7.216,-18.75 16.834,-20.779C115.423,12.847 113.764,18.19 113.764,23.924"/>
<path android:fillColor="#F79421" android:pathData="M74.843,101.328c-0.49,-0.482 -1.01,-0.936 -1.543,-1.371c-5.61,1.143 -10.229,5.014 -12.41,10.168c1.596,0.367 2.996,1.242 4.016,2.453c0.184,0.219 0.363,0.453 0.525,0.699c0.593,0.9 0.995,1.934 1.146,3.047c0.011,0.049 0.017,0.105 0.022,0.162c0.034,0.229 0.051,0.463 0.051,0.703c0.005,0.057 0.005,0.119 0.005,0.174c0,0.049 0,0.094 -0.005,0.139v0.018c0.016,0.922 0.201,1.801 0.53,2.609c0.593,1.477 1.643,2.717 2.979,3.551c1.135,0.703 2.471,1.105 3.901,1.105c3.616,0 6.635,-2.594 7.283,-6.031c0.062,-0.279 0.095,-0.568 0.117,-0.865c0.006,-0.102 0.011,-0.195 0.011,-0.295c0.006,-0.08 0.006,-0.152 0.006,-0.23v-0.184c0,-2.533 -0.425,-4.971 -1.201,-7.232C79.146,106.66 77.268,103.721 74.843,101.328"/>
</vector>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:baselineAligned="false"
android:clipToPadding="false"
android:gravity="center_vertical"
android:minHeight="42dp"
android:paddingStart="?listPreferredItemPaddingStart"
android:paddingEnd="?listPreferredItemPaddingEnd"
tools:ignore="RtlHardcoded">
<LinearLayout
android:id="@android:id/widget_frame"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:clipToPadding="false"
android:gravity="start|center_vertical"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"/>
<TextView
android:id="@android:id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="marquee"
android:singleLine="true"
android:textAppearance="?textAppearanceListItem"/>
<!-- Hidden view -->
<TextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone" />
<LinearLayout
android:id="@+id/login_frame"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="-16dp"
android:clipToPadding="false"
android:gravity="end|center_vertical"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:visibility="gone">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/login" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/dialog_title"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
tools:text="Log in to MangaDex" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/username_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/username">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/two_factor_holder"
style="@style/Theme.Widget.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/two_factor"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/two_factor_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/two_factor_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/two_factor" />
<com.dd.processbutton.iml.ActionProcessButton
android:id="@+id/login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/login"
android:textColor="@android:color/white"
app:pb_textComplete="@string/login_success"
app:pb_textError="@string/invalid_login"
app:pb_textProgress="@string/loading" />
</LinearLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/mangadex_random"
style="@style/Theme.Widget.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/random"
android:visibility="visible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/mangadex_follows"
style="@style/Theme.Widget.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/mangadex_follows"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -51,4 +51,11 @@
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_push_to_mdlist"
android:icon="@drawable/baseline_swap_calls_24"
android:title="@string/mangadex_add_to_follows"
app:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom" />
</menu>

View File

@ -5,4 +5,14 @@
<item>@string/clean_read_downloads</item>
<item>@string/clean_read_manga_not_in_library</item>
</string-array>
<string-array name="md_follows_options">
<item>@string/md_follows_unfollowed</item>
<item>@string/reading</item>
<item>@string/completed</item>
<item>@string/on_hold</item>
<item>@string/plan_to_read</item>
<item>@string/dropped</item>
<item>@string/repeating</item>
</string-array>
</resources>

View File

@ -521,4 +521,20 @@
<string name="manga_info_manga">Info manga:</string>
<string name="toggle_dedupe">Toggle dedupe</string>
<!-- MangaDex -->
<string name="md_follows_unfollowed">Unfollowed</string>
<string name="mangadex_specific_settings">MangaDex settings</string>
<string name="mangadex_sync_follows_to_library">Sync Mangadex manga into Neko</string>
<string name="mangadex_sync_follows_to_library_summary">Pulls reading/rereading manga from Mangadex into your Neko library</string>
<string name="mangadex_low_quality_covers">Use low quality thumbnails</string>
<string name="mangadex_use_latest_cover">Use latest uploaded cover</string>
<string name="mangadex_use_latest_cover_summary">When enabled, it uses the latest uploaded manga cover under the /covers url instead of using the cover on MangaDex\'s manga page</string>
<string name="mangadex_preffered_source">Preferred MangaDex source</string>
<string name="mangadex_preffered_source_summary">Set your chosen mangadex source, this will be used for follows and a bunch more features around the app</string>
<string name="two_factor">2FA Code</string>
<string name="fields_cannot_be_blank">Fields cannot be blank</string>
<string name="mangadex_add_to_follows">Add to MangaDex follows</string>
<string name="mangadex_follows">MangaDex follows</string>
<string name="random">Random</string>
</resources>