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 { interface ChapterQueries : DbProvider {
// SY --> // 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) .listOfObjects(Chapter::class.java)
.withQuery( .withQuery(
Query.builder() 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.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.ui.main.MainActivity
import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
@ -65,7 +66,7 @@ class LibraryUpdateNotifier(private val context: Context) {
* @param current the current progress. * @param current the current progress.
* @param total the total 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()) { val title = if (preferences.hideNotificationContent()) {
context.getString(R.string.notification_check_updates) context.getString(R.string.notification_check_updates)
} else { } 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.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga 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.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.library.LibraryGroup import eu.kanade.tachiyomi.ui.library.LibraryGroup
import eu.kanade.tachiyomi.util.chapter.NoChaptersException 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 eu.kanade.tachiyomi.util.system.isServiceRunning
import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES
import exh.MERGED_SOURCE_ID 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.asObservable
import exh.util.await
import exh.util.awaitSingle
import exh.util.nullIfBlank import exh.util.nullIfBlank
import java.io.File import java.io.File
import java.util.Date
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import rx.Observable import rx.Observable
@ -81,7 +89,10 @@ class LibraryUpdateService(
enum class Target { enum class Target {
CHAPTERS, // Manga chapters CHAPTERS, // Manga chapters
COVERS, // Manga covers COVERS, // Manga covers
TRACKING // Tracking metadata TRACKING, // Tracking metadata
// SY -->
SYNC_FOLLOWS // MangaDex specific, pull mangadex manga in reading, rereading
// SY <--
} }
companion object { companion object {
@ -215,6 +226,9 @@ class LibraryUpdateService(
Target.CHAPTERS -> updateChapterList(mangaList) Target.CHAPTERS -> updateChapterList(mangaList)
Target.COVERS -> updateCovers(mangaList) Target.COVERS -> updateCovers(mangaList)
Target.TRACKING -> updateTrackings(mangaList) Target.TRACKING -> updateTrackings(mangaList)
// SY -->
Target.SYNC_FOLLOWS -> syncFollows()
// SY <--
} }
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -433,9 +447,34 @@ class LibraryUpdateService(
.subscribe() .subscribe()
} }
return /* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() } // 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) else /* SY <-- */ source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) } .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> { 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. * 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 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 dataSaver = "data_saver"
const val ignoreJpeg = "ignore_jpeg" const val ignoreJpeg = "ignore_jpeg"

View File

@ -396,6 +396,10 @@ class PreferencesHelper(val context: Context) {
fun mangaDexLowQualityCovers() = flowPrefs.getBoolean(Keys.mangaDexLowQualityCovers, false) 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 dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, 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.anilist.Anilist
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu 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.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
@ -16,8 +17,8 @@ class TrackManager(context: Context) {
const val SHIKIMORI = 4 const val SHIKIMORI = 4
const val BANGUMI = 5 const val BANGUMI = 5
// SY --> Mangadex from Neko todo // SY --> Mangadex from Neko
const val MDLIST = 60 const val MDLIST = 6
// SY <-- // SY <--
// SY --> // SY -->
@ -31,6 +32,8 @@ class TrackManager(context: Context) {
// SY <-- // SY <--
} }
val mdList = MdList(context, MDLIST)
val myAnimeList = MyAnimeList(context, MYANIMELIST) val myAnimeList = MyAnimeList(context, MYANIMELIST)
val aniList = Anilist(context, ANILIST) val aniList = Anilist(context, ANILIST)
@ -41,11 +44,11 @@ class TrackManager(context: Context) {
val bangumi = Bangumi(context, BANGUMI) 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 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 --> // SY -->
fun mapTrackingOrder(status: String, context: Context): Int { 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" get() = "https://$domain"
override val lang = "all" override val lang = "all"
override val supportsLatest = true override val supportsLatest = !exh
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val updateHelper: EHentaiUpdateHelper by injectLazy() private val updateHelper: EHentaiUpdateHelper by injectLazy()

View File

@ -1,41 +1,78 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.app.Activity
import android.content.Context import android.content.Context
import android.net.Uri 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.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga 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.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.source.online.MetadataSource 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.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.manga.MangaController 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.ApiChapterParser
import exh.md.handlers.ApiMangaParser import exh.md.handlers.ApiMangaParser
import exh.md.handlers.FollowsHandler
import exh.md.handlers.MangaHandler import exh.md.handlers.MangaHandler
import exh.md.handlers.MangaPlusHandler import exh.md.handlers.MangaPlusHandler
import exh.md.utils.FollowStatus
import exh.md.utils.MdLang import exh.md.utils.MdLang
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog
import kotlin.reflect.KClass 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 kotlinx.serialization.ExperimentalSerializationApi
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaDex(delegate: HttpSource, val context: Context) : class MangaDex(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
MetadataSource<MangaDexSearchMetadata, Response>, MetadataSource<MangaDexSearchMetadata, Response>,
UrlImportableSource { UrlImportableSource,
FollowsSource,
LoginSource,
BrowseSourceFilterHeader,
RandomMangaSource {
override val lang: String = delegate.lang 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 { private val mdLang by lazy {
MdLang.values().find { it.lang == lang }?.dexLang ?: lang 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> = override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) { urlImportFetchSearchManga(context, query) {
ImportIdToMdId(query) {
super.fetchSearchManga(page, query, filters) super.fetchSearchManga(page, query, filters)
} }
}
override fun mapUrlToMangaUrl(uri: Uri): String? { override fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") { return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
"/manga/${uri.pathSegments[1]}/" MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt())
} else { } else {
null null
} }
} }
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { 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>> { 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 @ExperimentalSerializationApi
@ -96,6 +135,130 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) { 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.LocalSource
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource 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.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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 eu.kanade.tachiyomi.widget.EmptyView
import exh.EXHSavedSearch import exh.EXHSavedSearch
import exh.isEhBasedSource import exh.isEhBasedSource
import exh.source.EnhancedHttpSource.Companion.getMainSource
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.main_activity.root_coordinator import kotlinx.android.synthetic.main.main_activity.root_coordinator
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -187,6 +189,14 @@ open class BrowseSourceController(bundle: Bundle) :
setupRecycler(view) setupRecycler(view)
binding.progress.isVisible = true 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() { open fun initFilterSheet() {
@ -205,6 +215,8 @@ open class BrowseSourceController(bundle: Bundle) :
filterSheet = SourceFilterSheet( filterSheet = SourceFilterSheet(
activity!!, activity!!,
// SY --> // SY -->
this,
presenter.source,
presenter.loadSearches(), presenter.loadSearches(),
// SY <-- // SY <--
onFilterClicked = { 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.ui.browse.source.filter.TriStateSectionItem
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import exh.EXHSavedSearch import exh.EXHSavedSearch
import exh.isEhBasedSource
import java.lang.RuntimeException import java.lang.RuntimeException
import java.util.Date import java.util.Date
import rx.Observable import rx.Observable
@ -188,7 +187,7 @@ open class BrowseSourcePresenter(
// SY <-- // SY <--
.doOnNext { initializeMangas(it.second) } .doOnNext { initializeMangas(it.second) }
// SY --> // 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 <-- // SY <--
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribeReplay( .subscribeReplay(

View File

@ -4,11 +4,15 @@ import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.widget.StateImageViewTarget 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.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.progress
import kotlinx.android.synthetic.main.source_comfortable_grid_item.thumbnail import kotlinx.android.synthetic.main.source_comfortable_grid_item.thumbnail
import kotlinx.android.synthetic.main.source_comfortable_grid_item.title 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) 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) { override fun setImage(manga: Manga) {
// For rounded corners // For rounded corners
card.clipToOutline = true card.clipToOutline = true

View File

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

View File

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

View File

@ -1,13 +1,18 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.View import android.view.View
import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.widget.StateImageViewTarget 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.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.progress
import kotlinx.android.synthetic.main.source_compact_grid_item.thumbnail import kotlinx.android.synthetic.main.source_compact_grid_item.thumbnail
import kotlinx.android.synthetic.main.source_compact_grid_item.title 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) 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) { override fun setImage(manga: Manga) {
// For rounded corners // For rounded corners
card.clipToOutline = true card.clipToOutline = true

View File

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

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.browse.source.browse package eu.kanade.tachiyomi.ui.browse.source.browse
import android.view.View import android.view.View
import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners 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.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.util.system.getResourceColor 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.thumbnail
import kotlinx.android.synthetic.main.source_list_item.title import kotlinx.android.synthetic.main.source_list_item.title
@ -44,6 +48,17 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
setImage(manga) 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) { override fun setImage(manga: Manga) {
GlideApp.with(view.context).clear(thumbnail) GlideApp.with(view.context).clear(thumbnail)

View File

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

View File

@ -49,6 +49,7 @@ import exh.PERV_EDEN_EN_SOURCE_ID
import exh.PERV_EDEN_IT_SOURCE_ID import exh.PERV_EDEN_IT_SOURCE_ID
import exh.favorites.FavoritesIntroDialog import exh.favorites.FavoritesIntroDialog
import exh.favorites.FavoritesSyncStatus import exh.favorites.FavoritesSyncStatus
import exh.mangaDexSourceIds
import exh.nHentaiSourceIds import exh.nHentaiSourceIds
import exh.ui.LoaderManager import exh.ui.LoaderManager
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -530,6 +531,9 @@ class LibraryController(
it.source == PERV_EDEN_EN_SOURCE_ID || it.source == PERV_EDEN_EN_SOURCE_ID ||
it.source == PERV_EDEN_IT_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 <-- // SY <--
} }
return false return false
@ -556,6 +560,7 @@ class LibraryController(
PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds) PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds)
} }
R.id.action_clean -> cleanTitles() R.id.action_clean -> cleanTitles()
R.id.action_push_to_mdlist -> pushToMdList()
// SY <-- // SY <--
else -> return false else -> return false
} }
@ -658,6 +663,13 @@ class LibraryController(
presenter.cleanTitles(mangas) presenter.cleanTitles(mangas)
destroyActionModeIfNeeded() destroyActionModeIfNeeded()
} }
private fun pushToMdList() {
val mangas = selectedMangas.filter {
it.source in mangaDexSourceIds
}.toList()
presenter.syncMangaToDex(mangas)
}
// SY <-- // SY <--
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) { 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.EXH_SOURCE_ID
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.favorites.FavoritesSyncHelper import exh.favorites.FavoritesSyncHelper
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.util.await import exh.util.await
import exh.util.isLewd import exh.util.isLewd
import exh.util.nullIfBlank import exh.util.nullIfBlank
import java.util.Collections import java.util.Collections
import java.util.Comparator import java.util.Comparator
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.flow.singleOrNull
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import rx.Observable import rx.Observable
@ -194,6 +197,7 @@ class LibraryPresenter(
} }
if (filterTracked != STATE_IGNORE) { if (filterTracked != STATE_IGNORE) {
val tracks = db.getTracks(item.manga).executeAsBlocking() 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 if (filterTracked == STATE_INCLUDE && tracks.isEmpty()) return@f false
else if (filterTracked == STATE_EXCLUDE && tracks.isNotEmpty()) 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 <-- // SY <--
/** /**
@ -612,6 +626,9 @@ class LibraryPresenter(
LibraryGroup.BY_STATUS -> { LibraryGroup.BY_STATUS -> {
grouping += Triple(SManga.ONGOING.toString(), SManga.ONGOING, context.getString(R.string.ongoing)) 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.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.COMPLETED.toString(), SManga.COMPLETED, context.getString(R.string.completed))
grouping += Triple(SManga.UNKNOWN.toString(), SManga.UNKNOWN, context.getString(R.string.unknown)) 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.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource 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.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController 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.MERGED_SOURCE_ID
import exh.isEhBasedSource import exh.isEhBasedSource
import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.FlatMetadata
import exh.source.EnhancedHttpSource.Companion.getMainSource
import java.io.IOException import java.io.IOException
import kotlin.math.min import kotlin.math.min
import kotlinx.android.synthetic.main.main_activity.root_coordinator import kotlinx.android.synthetic.main.main_activity.root_coordinator
@ -254,9 +255,9 @@ class MangaController :
adapters += mangaInfoAdapter adapters += mangaInfoAdapter
val thisSourceAsLewdSource = presenter.source.getMetadataSource() val mainSource = presenter.source.getMainSource()
if (thisSourceAsLewdSource != null) { if (mainSource is MetadataSource<*, *>) {
mangaMetaInfoAdapter = thisSourceAsLewdSource.getDescriptionAdapter(this) mangaMetaInfoAdapter = mainSource.getDescriptionAdapter(this)
mangaMetaInfoAdapter?.let { adapters += it } mangaMetaInfoAdapter?.let { adapters += it }
} }
mangaInfoItemAdapter = MangaInfoItemAdapter(this, fromSource) mangaInfoItemAdapter = MangaInfoItemAdapter(this, fromSource)
@ -277,7 +278,7 @@ class MangaController :
binding.recycler.adapter = ConcatAdapter(adapters) binding.recycler.adapter = ConcatAdapter(adapters)
binding.recycler.layoutManager = LinearLayoutManager(view.context) 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 <-- // SY <--
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
chaptersAdapter?.fastScroller = binding.fastScroller chaptersAdapter?.fastScroller = binding.fastScroller
@ -481,9 +482,9 @@ class MangaController :
// SY --> // SY -->
fun onNextMetaInfo(flatMetadata: FlatMetadata) { fun onNextMetaInfo(flatMetadata: FlatMetadata) {
val thisSourceAsLewdSource = presenter.source.getMetadataSource() val mainSource = presenter.source.getMainSource()
if (thisSourceAsLewdSource != null) { if (mainSource is MetadataSource<*, *>) {
presenter.meta = flatMetadata.raise(thisSourceAsLewdSource.metaClass) presenter.meta = flatMetadata.raise(mainSource.metaClass)
mangaMetaInfoAdapter?.notifyDataSetChanged() 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.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.MetadataSource 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.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
@ -37,11 +37,12 @@ import exh.MERGED_SOURCE_ID
import exh.debug.DebugToggles import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper import exh.eh.EHentaiUpdateHelper
import exh.isEhBasedSource import exh.isEhBasedSource
import exh.md.utils.FollowStatus
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.source.EnhancedHttpSource import exh.source.EnhancedHttpSource.Companion.getMainSource
import exh.util.asObservable import exh.util.asObservable
import exh.util.await import exh.util.await
import exh.util.trimOrNull import exh.util.trimOrNull
@ -122,7 +123,7 @@ class MangaPresenter(
super.onCreate(savedState) super.onCreate(savedState)
// SY --> // 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") }) 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> { 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 Observable.just(0)
} }
return db.getTracks(manga).asRxObservable() return db.getTracks(manga).asRxObservable()
.map { tracks -> .map { tracks ->
val loggedServices = trackManager.services.filter { it.isLogged }.map { it.id } val loggedServices = trackManager.services.filter { it.isLogged /* SY --> */ && ((it.id == TrackManager.MDLIST && sourceIsMangaDex) || it.id != TrackManager.MDLIST) /* SY <-- */ }.map { it.id }
tracks.filter { it.sync_id in loggedServices } tracks
// SY -->
.filterNot { it.sync_id == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int }
// SY <--
.filter { it.sync_id in loggedServices }
} }
.map { it.size } .map { it.size }
} }
@ -244,7 +252,7 @@ class MangaPresenter(
} }
// SY --> // SY -->
.doOnNext { .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") }) 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.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource 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.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.view.setTooltip import eu.kanade.tachiyomi.util.view.setTooltip
import exh.MERGED_SOURCE_ID import exh.MERGED_SOURCE_ID
import exh.source.EnhancedHttpSource.Companion.getMainSource
import exh.util.SourceTagsUtil import exh.util.SourceTagsUtil
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -106,7 +108,10 @@ class MangaInfoHeaderAdapter(
} }
with(binding.btnTracking) { with(binding.btnTracking) {
if (trackManager.hasLoggedServices()) { // SY -->
val sourceIsMangaDex = source.let { it.getMainSource() is MangaDex }
// SY <--
if (trackManager.hasLoggedServices(/* SY --> */sourceIsMangaDex/* SY <-- */)) {
isVisible = true isVisible = true
if (trackCount > 0) { if (trackCount > 0) {

View File

@ -9,6 +9,7 @@ import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga 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.data.track.model.TrackSearch
import eu.kanade.tachiyomi.databinding.TrackControllerBinding import eu.kanade.tachiyomi.databinding.TrackControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
@ -91,6 +92,10 @@ class TrackController :
val atLeastOneLink = trackings.any { it.track != null } val atLeastOneLink = trackings.any { it.track != null }
adapter?.items = trackings adapter?.items = trackings
binding.swipeRefresh.isEnabled = atLeastOneLink binding.swipeRefresh.isEnabled = atLeastOneLink
if (presenter.needsRefresh) {
presenter.needsRefresh = false
presenter.refresh()
}
} }
fun onSearchResults(results: List<TrackSearch>) { fun onSearchResults(results: List<TrackSearch>) {
@ -126,6 +131,9 @@ class TrackController :
override fun onSetClick(position: Int) { override fun onSetClick(position: Int) {
val item = adapter?.getItem(position) ?: return 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) 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.data.track.TrackService
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.mangaDexSourceIds
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -35,6 +36,10 @@ class TrackPresenter(
private var refreshSubscription: Subscription? = null private var refreshSubscription: Subscription? = null
// SY -->
var needsRefresh = false
// SY <--
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
fetchTrackings() fetchTrackings()
@ -50,10 +55,36 @@ class TrackPresenter(
} }
} }
.observeOn(AndroidSchedulers.mainThread()) .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 } .doOnNext { trackList = it }
.subscribeLatestCache(TrackController::onNextTrackings) .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() { fun refresh() {
refreshSubscription?.let { remove(it) } refreshSubscription?.let { remove(it) }
refreshSubscription = Observable.from(trackList) 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.preference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import exh.md.utils.MdUtil
class SettingsMainController : SettingsController() { class SettingsMainController : SettingsController() {
@ -80,6 +81,14 @@ class SettingsMainController : SettingsController() {
onClick { navigateTo(SettingsEhController()) } 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 <-- // SY <--
preference { preference {
iconRes = R.drawable.ic_code_24dp 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 eu.kanade.tachiyomi.source.online.HttpSource
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXH_SOURCE_ID import exh.EXH_SOURCE_ID
import exh.debug.DebugToggles
import java.util.Date import java.util.Date
import java.util.TreeSet import java.util.TreeSet
import uy.kohesive.injekt.Injekt 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 // <-- EXH

View File

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

View File

@ -152,7 +152,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope {
return@mapNotNull null return@mapNotNull null
} }
val chapter = db.getChaptersByMangaId(manga.id!!).asRxSingle().await().minByOrNull { val chapter = db.getChapters(manga.id!!).asRxSingle().await().minByOrNull {
it.date_upload 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.RaisedTag
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.floor
import java.util.Date import java.util.Date
import kotlin.math.floor
import okhttp3.Response import okhttp3.Response
import rx.Completable import rx.Completable
import rx.Single import rx.Single
@ -41,7 +41,7 @@ class ApiMangaParser(private val langs: List<String>) {
* *
* Will also save the metadata to the DB if possible * 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 mangaId = (manga as? Manga)?.id
val metaObservable = if (mangaId != null) { val metaObservable = if (mangaId != null) {
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions // 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 { return metaObservable.map {
parseIntoMetadata(it, input) parseIntoMetadata(it, input, forceLatestCover)
it.copyTo(manga) it.copyTo(manga)
it it
}.flatMapCompletable { }.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) { with(metadata) {
try { try {
val networkApiManga = MdUtil.jsonParser.decodeFromString(ApiMangaSerializer.serializer(), input.body!!.string()) 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()) mdId = MdUtil.getMangaId(input.request.url.toString())
mdUrl = input.request.url.toString() mdUrl = input.request.url.toString()
title = MdUtil.cleanString(networkManga.title) 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) description = MdUtil.cleanDescription(networkManga.description)
author = MdUtil.cleanString(networkManga.author) author = MdUtil.cleanString(networkManga.author)
artist = MdUtil.cleanString(networkManga.artist) artist = MdUtil.cleanString(networkManga.artist)
lang_flag = networkManga.lang_flag lang_flag = networkManga.lang_flag
val lastChapter = networkManga.last_chapter?.toFloatOrNull() last_chapter_number = networkManga.last_chapter?.toFloatOrNull()?.floor()
lastChapter?.let {
last_chapter_number = floor(it).toInt()
}
networkManga.rating?.let { networkManga.rating?.let {
rating = it.bayesian ?: it.mean rating = it.bayesian ?: it.mean
@ -107,10 +110,16 @@ class ApiMangaParser(private val langs: List<String>) {
status = tempStatus status = tempStatus
} }
val demographic = FilterHandler.demographics().filter { it.id == networkManga.demographic }.firstOrNull()
val genres = val genres =
networkManga.genres.mapNotNull { FilterHandler.allTypes[it.toString()] } networkManga.genres.mapNotNull { FilterHandler.allTypes[it.toString()] }
.toMutableList() .toMutableList()
if (demographic != null) {
genres.add(0, demographic.name)
}
if (networkManga.hentai == 1) { if (networkManga.hentai == 1) {
genres.add("Hentai") genres.add("Hentai")
} }
@ -135,7 +144,9 @@ class ApiMangaParser(private val langs: List<String>) {
if (filteredChapters.isEmpty() || serializer.manga.last_chapter.isNullOrEmpty()) { if (filteredChapters.isEmpty() || serializer.manga.last_chapter.isNullOrEmpty()) {
return false 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)) { if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) {
filteredChapters.firstOrNull()?.let { filteredChapters.firstOrNull()?.let {
if (isOneShot(it.value, finalChapterNumber)) { if (isOneShot(it.value, finalChapterNumber)) {
@ -144,7 +155,7 @@ class ApiMangaParser(private val langs: List<String>) {
} }
} }
val removeOneshots = filteredChapters.filter { !it.value.chapter.isNullOrBlank() } 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>> { 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 ((status == 2 || status == 3)) {
if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) || if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) ||
networkChapter.chapter == finalChapterNumber networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0
) { ) {
chapterName.add("[END]") 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("81", "Virtual Reality"),
Tag("82", "Zombies"), Tag("82", "Zombies"),
Tag("83", "Incest"), Tag("83", "Incest"),
Tag("84", "Mafia") Tag("84", "Mafia"),
Tag("85", "Villainess")
).sortedWith(compareBy { it.name }) ).sortedWith(compareBy { it.name })
val allTypes = (contentType() + formats() + genre() + themes()).map { it.id to it.name }.toMap() 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.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable 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.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import exh.md.handlers.serializers.FollowsPageResult 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.baseUrl
import exh.md.utils.MdUtil.Companion.getMangaId import exh.md.utils.MdUtil.Companion.getMangaId
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import kotlin.math.floor import exh.util.floor
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
// Unused, kept for future featues todo
class FollowsHandler(val client: OkHttpClient, val headers: Headers, val preferences: PreferencesHelper) { class FollowsHandler(val client: OkHttpClient, val headers: Headers, val preferences: PreferencesHelper) {
/** /**
* fetch follows by page * fetch follows by page
*/ */
fun fetchFollows(page: Int): Observable<MetadataMangasPage> { fun fetchFollows(): Observable<MangasPage> {
return client.newCall(followsListRequest(page)) return client.newCall(followsListRequest())
.asObservable() .asObservable()
.map { response -> .map { response ->
followsParseMangaPage(response) followsParseMangaPage(response)
@ -96,9 +95,9 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
val follow = result.first() val follow = result.first()
track.status = follow.follow_type track.status = follow.follow_type
if (result[0].chapter.isNotBlank()) { 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 track.title = follow.title
} }
return track return track
@ -107,11 +106,8 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
/**build Request for follows page /**build Request for follows page
* *
*/ */
private fun followsListRequest(page: Int): Request { private fun followsListRequest(): Request {
val url = "${MdUtil.baseUrl}${MdUtil.followsAllApi}".toHttpUrlOrNull()!!.newBuilder() return GET("$baseUrl${MdUtil.followsAllApi}", headers, CacheControl.FORCE_NETWORK)
.addQueryParameter("page", page.toString())
return GET(url.toString(), headers, CacheControl.FORCE_NETWORK)
} }
/** /**
@ -126,7 +122,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
title = MdUtil.cleanString(result.title) title = MdUtil.cleanString(result.title)
mdUrl = "/manga/${result.manga_id}/" mdUrl = "/manga/${result.manga_id}/"
thumbnail_url = MdUtil.formThumbUrl(manga.url, lowQualityCovers) 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 * 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) { return withContext(Dispatchers.IO) {
val listManga = mutableListOf<SManga>() val listManga = mutableListOf<Pair<SManga, MangaDexSearchMetadata>>()
loop@ for (i in 1..10000) { val response = client.newCall(followsListRequest()).execute()
val response = client.newCall(followsListRequest(i))
.execute()
val mangasPage = followsParseMangaPage(response, forceHd) val mangasPage = followsParseMangaPage(response, forceHd)
listManga.addAll(
if (mangasPage.mangas.isNotEmpty()) { mangasPage.mangas.mapIndexed { index, sManga ->
listManga.addAll(mangasPage.mangas) sManga to mangasPage.mangasMetadata[index] as MangaDexSearchMetadata
}
if (!mangasPage.hasNextPage) {
break@loop
}
} }
)
listManga listManga
} }
} }
@ -227,7 +218,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
suspend fun fetchTrackingInfo(url: String): Track { suspend fun fetchTrackingInfo(url: String): Track {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val request = GET( val request = GET(
"${MdUtil.baseUrl}${MdUtil.followsMangaApi}" + getMangaId(url), "$baseUrl${MdUtil.followsMangaApi}" + getMangaId(url),
headers, headers,
CacheControl.FORCE_NETWORK CacheControl.FORCE_NETWORK
) )

View File

@ -3,10 +3,13 @@ package exh.md.handlers
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
@ -14,7 +17,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import rx.Observable 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 // TODO make use of this
suspend fun fetchMangaAndChapterDetails(manga: SManga): Pair<SManga, List<SChapter>> { 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} ") 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) val chapterList = parser.chapterListParse(jsonData)
Pair( Pair(
manga, manga,
@ -48,7 +51,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
suspend fun fetchMangaDetails(manga: SManga): SManga { suspend fun fetchMangaDetails(manga: SManga): SManga {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val response = client.newCall(apiRequest(manga)).execute() val response = client.newCall(apiRequest(manga)).execute()
ApiMangaParser(langs).parseToManga(manga, response).await() ApiMangaParser(langs).parseToManga(manga, response, forceLatestCovers).await()
manga.apply { manga.apply {
initialized = true initialized = true
} }
@ -59,7 +62,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li
return client.newCall(apiRequest(manga)) return client.newCall(apiRequest(manga))
.asObservableSuccess() .asObservableSuccess()
.flatMap { .flatMap {
ApiMangaParser(langs).parseToManga(manga, it).andThen( ApiMangaParser(langs).parseToManga(manga, it, forceLatestCovers).andThen(
Observable.just( Observable.just(
manga.apply { manga.apply {
initialized = true 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()) return client.newCall(randomMangaRequest())
.asObservableSuccess() .asObservableSuccess()
.map { response -> .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 { private fun randomMangaRequest(): Request {
return GET(MdUtil.baseUrl + MdUtil.randMangaPage) 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 -> val mangas = document.select(popularMangaSelector).map { element ->
popularMangaFromElement(element) popularMangaFromElement(element)
}.distinct() }.distinctBy { it.url }
val hasNextPage = popularMangaNextPageSelector.let { selector -> val hasNextPage = popularMangaNextPageSelector.let { selector ->
document.select(selector).first() document.select(selector).first()

View File

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

View File

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

View File

@ -1,12 +1,17 @@
package exh.md.utils 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga 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.URI
import java.net.URISyntaxException import java.net.URISyntaxException
import kotlin.math.floor
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MdUtil { class MdUtil {
@ -63,10 +68,15 @@ class MdUtil {
"[b][u]Spanish", "[b][u]Spanish",
"[Espa&ntilde;ol]:", "[Espa&ntilde;ol]:",
"[b] Spanish: [/ b]", "[b] Spanish: [/ b]",
"정보",
"Spanish/Espa&ntilde;ol", "Spanish/Espa&ntilde;ol",
"Espa&ntilde;ol / Spanish", "Espa&ntilde;ol / Spanish",
"Italian / Italiano", "Italian / Italiano",
"Italian/Italiano",
"\r\n\r\nItalian\r\n",
"Pasta-Pizza-Mandolino/Italiano", "Pasta-Pizza-Mandolino/Italiano",
"Persian /فارسی",
"Farsi/Persian/",
"Polish / polski", "Polish / polski",
"Polish / Polski", "Polish / Polski",
"Polish Summary / Polski Opis", "Polish Summary / Polski Opis",
@ -89,6 +99,7 @@ class MdUtil {
"French - Français:", "French - Français:",
"Turkish / T&uuml;rk&ccedil;e", "Turkish / T&uuml;rk&ccedil;e",
"Turkish/T&uuml;rk&ccedil;e", "Turkish/T&uuml;rk&ccedil;e",
"T&uuml;rk&ccedil;e",
"[b][u]Chinese", "[b][u]Chinese",
"Arabic / العربية", "Arabic / العربية",
"العربية", "العربية",
@ -191,11 +202,11 @@ class MdUtil {
}.sortedByDescending { it.chapter_number } }.sortedByDescending { it.chapter_number }
remove0ChaptersFromCount.firstOrNull()?.let { remove0ChaptersFromCount.firstOrNull()?.let {
val chpNumber = floor(it.chapter_number).toInt() val chpNumber = it.chapter_number.floor()
val allChapters = (1..chpNumber).toMutableSet() val allChapters = (1..chpNumber).toMutableSet()
remove0ChaptersFromCount.forEach { remove0ChaptersFromCount.forEach {
allChapters.remove(floor(it.chapter_number).toInt()) allChapters.remove(it.chapter_number.floor())
} }
if (allChapters.size <= 0) return null if (allChapters.size <= 0) return null
@ -203,6 +214,25 @@ class MdUtil {
} }
return null 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( private val EH_INTERCEPTORS: Map<Long, List<EHInterceptor>> = mapOf(
EH_UNIVERSAL_INTERCEPTOR to listOf( EH_UNIVERSAL_INTERCEPTOR to listOf(
CAPTCHA_DETECTION_PATCH // Auto captcha detection 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 package exh.source
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@ -234,4 +235,21 @@ class EnhancedHttpSource(
originalSource 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 androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.databinding.MetadataViewItemBinding import eu.kanade.tachiyomi.databinding.MetadataViewItemBinding
import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.copyToClipboard
import kotlin.math.floor import exh.util.floor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -43,7 +43,7 @@ class MetadataViewAdapter(private var data: List<Pair<String, String>>) :
private var dataPosition: Int? = null private var dataPosition: Int? = null
fun bind(position: Int) { fun bind(position: Int) {
if (data.isEmpty() || !binding.infoText.text.isNullOrBlank()) return 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.text = if (position % 2 == 0) data[dataPosition!!].first else data[dataPosition!!].second
binding.infoText.clicks() binding.infoText.clicks()
.onEach { .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.databinding.MetadataViewControllerBinding
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager 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.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.source.EnhancedHttpSource.Companion.getMainSource
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -73,9 +74,9 @@ class MetadataViewController : NucleusController<MetadataViewControllerBinding,
} }
fun onNextMetaInfo(flatMetadata: FlatMetadata) { fun onNextMetaInfo(flatMetadata: FlatMetadata) {
val thisSourceAsLewdSource = presenter.source.getMetadataSource() val mainSource = presenter.source.getMainSource()
if (thisSourceAsLewdSource != null) { if (mainSource is MetadataSource<*, *>) {
presenter.meta = flatMetadata.raise(thisSourceAsLewdSource.metaClass) 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:iconTint="?attr/colorOnPrimary"
app:showAsAction="ifRoom" /> 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> </menu>

View File

@ -5,4 +5,14 @@
<item>@string/clean_read_downloads</item> <item>@string/clean_read_downloads</item>
<item>@string/clean_read_manga_not_in_library</item> <item>@string/clean_read_manga_not_in_library</item>
</string-array> </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> </resources>

View File

@ -521,4 +521,20 @@
<string name="manga_info_manga">Info manga:</string> <string name="manga_info_manga">Info manga:</string>
<string name="toggle_dedupe">Toggle dedupe</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> </resources>