diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt index 2ba875be0..49135168e 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/ChapterQueries.kt @@ -15,9 +15,9 @@ import java.util.Date interface ChapterQueries : DbProvider { // SY --> - fun getChapters(manga: Manga) = getChaptersByMangaId(manga.id) + fun getChapters(manga: Manga) = getChapters(manga.id) - fun getChaptersByMangaId(mangaId: Long?) = db.get() + fun getChapters(mangaId: Long?) = db.get() .listOfObjects(Chapter::class.java) .withQuery( Query.builder() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 8982730e0..da1cc3944 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.util.lang.chop import eu.kanade.tachiyomi.util.system.notification @@ -65,7 +66,7 @@ class LibraryUpdateNotifier(private val context: Context) { * @param current the current progress. * @param total the total progress. */ - fun showProgressNotification(manga: Manga, current: Int, total: Int) { + fun showProgressNotification(manga: /* SY --> */ SManga /* SY <-- */, current: Int, total: Int) { val title = if (preferences.hideNotificationContent()) { context.getString(R.string.notification_check_updates) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index c3bb1dc43..ef8b4b5d1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -23,6 +23,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.library.LibraryGroup import eu.kanade.tachiyomi.util.chapter.NoChaptersException @@ -34,9 +35,16 @@ import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.isServiceRunning import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES import exh.MERGED_SOURCE_ID +import exh.md.utils.FollowStatus +import exh.md.utils.MdUtil +import exh.metadata.metadata.base.insertFlatMetadata +import exh.source.EnhancedHttpSource.Companion.getMainSource import exh.util.asObservable +import exh.util.await +import exh.util.awaitSingle import exh.util.nullIfBlank import java.io.File +import java.util.Date import java.util.concurrent.atomic.AtomicInteger import kotlinx.coroutines.runBlocking import rx.Observable @@ -81,7 +89,10 @@ class LibraryUpdateService( enum class Target { CHAPTERS, // Manga chapters COVERS, // Manga covers - TRACKING // Tracking metadata + TRACKING, // Tracking metadata + // SY --> + SYNC_FOLLOWS // MangaDex specific, pull mangadex manga in reading, rereading + // SY <-- } companion object { @@ -215,6 +226,9 @@ class LibraryUpdateService( Target.CHAPTERS -> updateChapterList(mangaList) Target.COVERS -> updateCovers(mangaList) Target.TRACKING -> updateTrackings(mangaList) + // SY --> + Target.SYNC_FOLLOWS -> syncFollows() + // SY <-- } } .subscribeOn(Schedulers.io()) @@ -433,9 +447,34 @@ class LibraryUpdateService( .subscribe() } - return /* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() } - else /* SY <-- */ source.fetchChapterList(manga) - .map { syncChaptersWithSource(db, it, manga, source) } + // SY --> + if (source.getMainSource() is MangaDex) { + val tracks = db.getTracks(manga).executeAsBlocking() + if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) { + var track = trackManager.mdList.createInitialTracker(manga) + track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() } + db.insertTrack(track).executeAsBlocking() + } + } + // SY <-- + + return ( + /* SY --> */ if (source is MergedSource) runBlocking { source.fetchChaptersAndSync(manga, false).asObservable() } + else /* SY <-- */ source.fetchChapterList(manga) + .map { syncChaptersWithSource(db, it, manga, source) } + // SY --> + ) + .doOnNext { + if (source.getMainSource() is MangaDex) { + val tracks = db.getTracks(manga).executeAsBlocking() + if (tracks.isEmpty() || tracks.all { it.sync_id != TrackManager.MDLIST }) { + var track = trackManager.mdList.createInitialTracker(manga) + track = runBlocking { trackManager.mdList.refresh(track).awaitSingle() } + db.insertTrack(track).executeAsBlocking() + } + } + } + // SY <-- } private fun updateCovers(mangaToUpdate: List): Observable { @@ -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 { + 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. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 9970fdaea..03c1bbd56 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -291,6 +291,10 @@ object PreferenceKeys { const val mangaDexLowQualityCovers = "manga_dex_low_quality_covers" + const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers" + + const val preferredMangaDexId = "preferred_mangaDex_id" + const val dataSaver = "data_saver" const val ignoreJpeg = "ignore_jpeg" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 081273b95..dbbbc3784 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -396,6 +396,10 @@ class PreferencesHelper(val context: Context) { fun mangaDexLowQualityCovers() = flowPrefs.getBoolean(Keys.mangaDexLowQualityCovers, false) + fun mangaDexForceLatestCovers() = flowPrefs.getBoolean(Keys.mangaDexForceLatestCovers, false) + + fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0") + fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false) fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt index 2644bb12a..db3024fcd 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -4,6 +4,7 @@ import android.content.Context import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.kitsu.Kitsu +import eu.kanade.tachiyomi.data.track.mdlist.MdList import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.data.track.shikimori.Shikimori @@ -16,8 +17,8 @@ class TrackManager(context: Context) { const val SHIKIMORI = 4 const val BANGUMI = 5 - // SY --> Mangadex from Neko todo - const val MDLIST = 60 + // SY --> Mangadex from Neko + const val MDLIST = 6 // SY <-- // SY --> @@ -31,6 +32,8 @@ class TrackManager(context: Context) { // SY <-- } + val mdList = MdList(context, MDLIST) + val myAnimeList = MyAnimeList(context, MYANIMELIST) val aniList = Anilist(context, ANILIST) @@ -41,11 +44,11 @@ class TrackManager(context: Context) { val bangumi = Bangumi(context, BANGUMI) - val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi) + val services = listOf(mdList, myAnimeList, aniList, kitsu, shikimori, bangumi) fun getService(id: Int) = services.find { it.id == id } - fun hasLoggedServices() = services.any { it.isLogged } + fun hasLoggedServices(isMangaDexManga: Boolean = true) = services.any { it.isLogged && ((it.id == MDLIST && isMangaDexManga) || it.id != MDLIST) } // SY --> fun mapTrackingOrder(status: String, context: Context): Int { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt new file mode 100644 index 000000000..23862469a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt @@ -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 { + 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 { + return update(track) + } + + override fun update(track: Track): Observable { + 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 { + 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 { + 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> = throw Exception("not used") + + override fun login(username: String, password: String): Completable = throw Exception("not used") + + override val isLogged = mdex?.isLogged() ?: false +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/BrowseSourceFilterHeader.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/BrowseSourceFilterHeader.kt new file mode 100644 index 000000000..436cf2a41 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/BrowseSourceFilterHeader.kt @@ -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<*> +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/FollowsSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/FollowsSource.kt new file mode 100644 index 000000000..9f3e73ae8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/FollowsSource.kt @@ -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 + + /** + * Returns a list of all Follows retrieved by Coroutines + * + * @param SManga all smanga found for user + */ + fun fetchAllFollows(forceHd: Boolean = false): Flow>> + + /** + * updates the follow status for a manga + */ + fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Flow + + /** + * Get a MdList Track of the manga + */ + fun fetchTrackingInfo(url: String): Flow +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt new file mode 100644 index 000000000..9c29ea001 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt @@ -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 +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/RandomMangaSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/RandomMangaSource.kt new file mode 100644 index 000000000..03de01af5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/RandomMangaSource.kt @@ -0,0 +1,7 @@ +package eu.kanade.tachiyomi.source.online + +import kotlinx.coroutines.flow.Flow + +interface RandomMangaSource { + fun fetchRandomMangaUrl(): Flow +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt index ea7ae849a..c1aaed0d9 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/EHentai.kt @@ -91,7 +91,7 @@ class EHentai( get() = "https://$domain" override val lang = "all" - override val supportsLatest = true + override val supportsLatest = !exh private val preferences: PreferencesHelper by injectLazy() private val updateHelper: EHentaiUpdateHelper by injectLazy() diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt index 5251df4cf..9f7538a11 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt @@ -1,41 +1,78 @@ package eu.kanade.tachiyomi.source.online.all +import android.app.Activity import android.content.Context import android.net.Uri +import com.bluelinelabs.conductor.Controller +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.BrowseSourceFilterHeader +import eu.kanade.tachiyomi.source.online.FollowsSource import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.MetadataSource +import eu.kanade.tachiyomi.source.online.RandomMangaSource import eu.kanade.tachiyomi.source.online.UrlImportableSource +import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.manga.MangaController +import exh.GalleryAddEvent +import exh.GalleryAdder +import exh.md.MangaDexFabHeaderAdapter import exh.md.handlers.ApiChapterParser import exh.md.handlers.ApiMangaParser +import exh.md.handlers.FollowsHandler import exh.md.handlers.MangaHandler import exh.md.handlers.MangaPlusHandler +import exh.md.utils.FollowStatus import exh.md.utils.MdLang import exh.md.utils.MdUtil import exh.metadata.metadata.MangaDexSearchMetadata import exh.source.DelegatedHttpSource import exh.ui.metadata.adapters.MangaDexDescriptionAdapter import exh.util.urlImportFetchSearchManga +import exh.widget.preference.MangadexLoginDialog import kotlin.reflect.KClass +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import okhttp3.CacheControl +import okhttp3.FormBody +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.Request import okhttp3.Response import rx.Observable +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get class MangaDex(delegate: HttpSource, val context: Context) : DelegatedHttpSource(delegate), MetadataSource, - UrlImportableSource { + UrlImportableSource, + FollowsSource, + LoginSource, + BrowseSourceFilterHeader, + RandomMangaSource { override val lang: String = delegate.lang + override val headers: Headers + get() = super.headers.newBuilder().apply { + add("X-Requested-With", "XMLHttpRequest") + add("Referer", MdUtil.baseUrl) + }.build() + private val mdLang by lazy { MdLang.values().find { it.lang == lang }?.dexLang ?: lang } @@ -44,25 +81,27 @@ class MangaDex(delegate: HttpSource, val context: Context) : override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = urlImportFetchSearchManga(context, query) { - super.fetchSearchManga(page, query, filters) + ImportIdToMdId(query) { + super.fetchSearchManga(page, query, filters) + } } override fun mapUrlToMangaUrl(uri: Uri): String? { val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") { - "/manga/${uri.pathSegments[1]}/" + MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt()) } else { null } } override fun fetchMangaDetails(manga: SManga): Observable { - return MangaHandler(client, headers, listOf(mdLang)).fetchMangaDetailsObservable(manga) + return MangaHandler(client, headers, listOf(mdLang), Injekt.get().mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga) } override fun fetchChapterList(manga: SManga): Observable> { - return MangaHandler(client, headers, listOf(mdLang)).fetchChapterListObservable(manga) + return MangaHandler(client, headers, listOf(mdLang), Injekt.get().mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga) } @ExperimentalSerializationApi @@ -96,6 +135,130 @@ class MangaDex(delegate: HttpSource, val context: Context) : } override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) { - ApiMangaParser(listOf(mdLang)).parseIntoMetadata(metadata, input) + ApiMangaParser(listOf(mdLang)).parseIntoMetadata(metadata, input, Injekt.get().mangaDexForceLatestCovers().get()) + } + + override fun fetchFollows(): Observable { + 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().mdList.logout() + return@withContext true + } + + false + } + } + + override fun fetchAllFollows(forceHd: Boolean): Flow>> { + return flow { emit(FollowsHandler(client, headers, Injekt.get()).fetchAllFollows(forceHd)) } + } + + fun updateReadingProgress(track: Track): Flow { + return flow { FollowsHandler(client, headers, Injekt.get()).updateReadingProgress(track) } + } + + fun updateRating(track: Track): Flow { + return flow { FollowsHandler(client, headers, Injekt.get()).updateRating(track) } + } + + override fun fetchTrackingInfo(url: String): Flow { + 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 { + 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 { + return MangaHandler(client, headers, listOf(mdLang)).fetchRandomMangaId() + } + + private fun ImportIdToMdId(query: String, fail: () -> Observable): Observable = + 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" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index 67d0111b4..df60d55d5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction @@ -55,6 +56,7 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.EmptyView import exh.EXHSavedSearch import exh.isEhBasedSource +import exh.source.EnhancedHttpSource.Companion.getMainSource import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.main_activity.root_coordinator import kotlinx.coroutines.Job @@ -187,6 +189,14 @@ open class BrowseSourceController(bundle: Bundle) : setupRecycler(view) binding.progress.isVisible = true + + // SY --> + val mainSource = presenter.source.getMainSource() + if (mainSource is LoginSource && mainSource.needsLogin && !mainSource.isLogged()) { + val dialog = mainSource.getLoginDialog(mainSource, activity!!) + dialog.showDialog(router) + } + // SY <-- } open fun initFilterSheet() { @@ -205,6 +215,8 @@ open class BrowseSourceController(bundle: Bundle) : filterSheet = SourceFilterSheet( activity!!, // SY --> + this, + presenter.source, presenter.loadSearches(), // SY <-- onFilterClicked = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt index 45c434e02..16d1a3731 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt @@ -38,7 +38,6 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem import eu.kanade.tachiyomi.util.removeCovers import exh.EXHSavedSearch -import exh.isEhBasedSource import java.lang.RuntimeException import java.util.Date import rx.Observable @@ -188,7 +187,7 @@ open class BrowseSourcePresenter( // SY <-- .doOnNext { initializeMangas(it.second) } // SY --> - .map { triple -> triple.first to triple.second.mapIndexed { index, manga -> SourceItem(manga, sourceDisplayMode, if (prefs.enhancedEHentaiView().get() && source.isEhBasedSource()) triple.third?.getOrNull(index) else null) } } + .map { triple -> triple.first to triple.second.mapIndexed { index, manga -> SourceItem(manga, sourceDisplayMode, triple.third?.getOrNull(index)) } } // SY <-- .observeOn(AndroidSchedulers.mainThread()) .subscribeReplay( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt index 1ac0e71fe..6953bccf4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt @@ -4,11 +4,15 @@ import android.view.View import androidx.core.view.isVisible import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.widget.StateImageViewTarget +import exh.metadata.metadata.MangaDexSearchMetadata +import exh.metadata.metadata.base.RaisedSearchMetadata import kotlinx.android.synthetic.main.source_comfortable_grid_item.card +import kotlinx.android.synthetic.main.source_comfortable_grid_item.local_text import kotlinx.android.synthetic.main.source_comfortable_grid_item.progress import kotlinx.android.synthetic.main.source_comfortable_grid_item.thumbnail import kotlinx.android.synthetic.main.source_comfortable_grid_item.title @@ -43,6 +47,17 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F setImage(manga) } + // SY --> + override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { + if (metadata is MangaDexSearchMetadata) { + metadata.follow_status?.let { + local_text.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it] + local_text.isVisible = true + } + } + } + // SY <-- + override fun setImage(manga: Manga) { // For rounded corners card.clipToOutline = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceEnhancedEHentaiListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceEnhancedEHentaiListHolder.kt index 02f0e1d53..d75b810a2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceEnhancedEHentaiListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceEnhancedEHentaiListHolder.kt @@ -56,7 +56,7 @@ class SourceEnhancedEHentaiListHolder(private val view: View, adapter: FlexibleA setImage(manga) } - fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { + override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { if (metadata !is EHentaiSearchMetadata) return if (metadata.uploader != null) { @@ -64,17 +64,17 @@ class SourceEnhancedEHentaiListHolder(private val view: View, adapter: FlexibleA } val pair = when (metadata.genre) { - "doujinshi" -> Pair(SourceTagsUtil.DOUJINSHI_COLOR, R.string.doujinshi) - "manga" -> Pair(SourceTagsUtil.MANGA_COLOR, R.string.manga) - "artistcg" -> Pair(SourceTagsUtil.ARTIST_CG_COLOR, R.string.artist_cg) - "gamecg" -> Pair(SourceTagsUtil.GAME_CG_COLOR, R.string.game_cg) - "western" -> Pair(SourceTagsUtil.WESTERN_COLOR, R.string.western) - "non-h" -> Pair(SourceTagsUtil.NON_H_COLOR, R.string.non_h) - "imageset" -> Pair(SourceTagsUtil.IMAGE_SET_COLOR, R.string.image_set) - "cosplay" -> Pair(SourceTagsUtil.COSPLAY_COLOR, R.string.cosplay) - "asianporn" -> Pair(SourceTagsUtil.ASIAN_PORN_COLOR, R.string.asian_porn) - "misc" -> Pair(SourceTagsUtil.MISC_COLOR, R.string.misc) - else -> Pair("", 0) + "doujinshi" -> SourceTagsUtil.DOUJINSHI_COLOR to R.string.doujinshi + "manga" -> SourceTagsUtil.MANGA_COLOR to R.string.manga + "artistcg" -> SourceTagsUtil.ARTIST_CG_COLOR to R.string.artist_cg + "gamecg" -> SourceTagsUtil.GAME_CG_COLOR to R.string.game_cg + "western" -> SourceTagsUtil.WESTERN_COLOR to R.string.western + "non-h" -> SourceTagsUtil.NON_H_COLOR to R.string.non_h + "imageset" -> SourceTagsUtil.IMAGE_SET_COLOR to R.string.image_set + "cosplay" -> SourceTagsUtil.COSPLAY_COLOR to R.string.cosplay + "asianporn" -> SourceTagsUtil.ASIAN_PORN_COLOR to R.string.asian_porn + "misc" -> SourceTagsUtil.MISC_COLOR to R.string.misc + else -> "" to 0 } if (pair.first.isNotBlank()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt index f00fb488d..16ed32051 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceFilterSheet.kt @@ -8,18 +8,24 @@ import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bluelinelabs.conductor.Controller import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.chip.Chip import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.databinding.SourceFilterSheetBinding -import eu.kanade.tachiyomi.util.view.inflate +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.source.online.BrowseSourceFilterHeader import eu.kanade.tachiyomi.widget.SimpleNavigationView import exh.EXHSavedSearch +import exh.source.EnhancedHttpSource.Companion.getMainSource class SourceFilterSheet( activity: Activity, // SY --> + controller: Controller, + source: CatalogueSource, searches: List = emptyList(), // SY <-- onFilterClicked: () -> Unit, @@ -34,7 +40,7 @@ class SourceFilterSheet( private var filterNavView: FilterNavigationView init { - filterNavView = FilterNavigationView(activity /* SY --> */, searches = searches/* SY <-- */) + filterNavView = FilterNavigationView(activity /* SY --> */, searches = searches, source = source, controller = controller/* SY <-- */) filterNavView.onFilterClicked = { onFilterClicked() this.dismiss() @@ -66,7 +72,7 @@ class SourceFilterSheet( } // SY <-- - class FilterNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null /* SY --> */, searches: List = emptyList()/* SY <-- */) : + class FilterNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null /* SY --> */, searches: List = emptyList(), source: CatalogueSource? = null, controller: Controller? = null/* SY <-- */) : SimpleNavigationView(context, attrs) { var onFilterClicked = {} @@ -79,6 +85,8 @@ class SourceFilterSheet( var onSavedSearchDeleteClicked: (Int, String) -> Unit = { _, _ -> } + val adapters = mutableListOf>() + private val savedSearchesAdapter = SavedSearchesAdapter(getSavedSearchesChips(searches)) // SY <-- @@ -88,7 +96,13 @@ class SourceFilterSheet( init { // SY --> - recycler.adapter = ConcatAdapter(savedSearchesAdapter, adapter) + val mainSource = source?.getMainSource() + if (mainSource is BrowseSourceFilterHeader && controller != null) { + adapters += mainSource.getFilterHeader(controller) + } + adapters += savedSearchesAdapter + adapters += adapter + recycler.adapter = ConcatAdapter(adapters) // SY <-- recycler.setHasFixedSize(true) (binding.root.getChildAt(1) as ViewGroup).addView(recycler) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt index ecff1e23d..6bfbf3fb3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt @@ -1,13 +1,18 @@ package eu.kanade.tachiyomi.ui.browse.source.browse import android.view.View +import androidx.core.view.isVisible import com.bumptech.glide.load.engine.DiskCacheStrategy import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.widget.StateImageViewTarget +import exh.metadata.metadata.MangaDexSearchMetadata +import exh.metadata.metadata.base.RaisedSearchMetadata import kotlinx.android.synthetic.main.source_compact_grid_item.card +import kotlinx.android.synthetic.main.source_compact_grid_item.local_text import kotlinx.android.synthetic.main.source_compact_grid_item.progress import kotlinx.android.synthetic.main.source_compact_grid_item.thumbnail import kotlinx.android.synthetic.main.source_compact_grid_item.title @@ -39,6 +44,17 @@ open class SourceGridHolder(private val view: View, private val adapter: Flexibl setImage(manga) } + // SY --> + override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { + if (metadata is MangaDexSearchMetadata) { + metadata.follow_status?.let { + local_text.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it] + local_text.isVisible = true + } + } + } + // SY <-- + override fun setImage(manga: Manga) { // For rounded corners card.clipToOutline = true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt index 90743afda..3533170d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt @@ -4,6 +4,7 @@ import android.view.View import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import exh.metadata.metadata.base.RaisedSearchMetadata /** * Generic class used to hold the displayed data of a manga in the catalogue. @@ -29,4 +30,8 @@ abstract class SourceHolder(view: View, adapter: FlexibleAdapter<*>) : * @param manga the manga to bind. */ abstract fun setImage(manga: Manga) + + // SY --> + abstract fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) + // SY <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt index 96cf52812..a399b5fab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt @@ -13,27 +13,38 @@ import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode +import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.widget.AutofitRecyclerView +import exh.EH_SOURCE_ID +import exh.EXH_SOURCE_ID import exh.metadata.metadata.base.RaisedSearchMetadata import kotlinx.android.synthetic.main.source_compact_grid_item.view.card import kotlinx.android.synthetic.main.source_compact_grid_item.view.gradient +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy class SourceItem(val manga: Manga, private val displayMode: Preference /* SY --> */, private val metadata: RaisedSearchMetadata? = null /* SY <-- */) : AbstractFlexibleItem() { + // SY --> + val preferences: PreferencesHelper by injectLazy() + // SY <-- override fun getLayoutRes(): Int { - return /* SY --> */ if (metadata == null) /* SY <-- */ when (displayMode.get()) { + return /* SY --> */ if ((manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) && preferences.enhancedEHentaiView().get()) R.layout.source_enhanced_ehentai_list_item + else /* SY <-- */ when (displayMode.get()) { DisplayMode.COMPACT_GRID -> R.layout.source_compact_grid_item DisplayMode.COMFORTABLE_GRID, /* SY --> */ DisplayMode.NO_TITLE_GRID /* SY <-- */ -> R.layout.source_comfortable_grid_item DisplayMode.LIST -> R.layout.source_list_item - } /* SY --> */ else R.layout.source_enhanced_ehentai_list_item /* SY <-- */ + } } override fun createViewHolder( view: View, adapter: FlexibleAdapter> ): SourceHolder { - return /* SY --> */ if (metadata == null) /* SY <-- */ when (displayMode.get()) { + return /* SY --> */ if ((manga.source == EH_SOURCE_ID || manga.source == EXH_SOURCE_ID) && preferences.enhancedEHentaiView().get()) { + SourceEnhancedEHentaiListHolder(view, adapter) + } else /* SY <-- */ when (displayMode.get()) { DisplayMode.COMPACT_GRID -> { val parent = adapter.recyclerView as AutofitRecyclerView val coverHeight = parent.itemWidth / 3 * 4 @@ -60,11 +71,7 @@ class SourceItem(val manga: Manga, private val displayMode: Preference { SourceListHolder(view, adapter) } - // SY --> - } else { - SourceEnhancedEHentaiListHolder(view, adapter) } - // SY <-- } override fun bindViewHolder( @@ -76,7 +83,7 @@ class SourceItem(val manga: Manga, private val displayMode: Preference if (metadata != null) { - (holder as? SourceEnhancedEHentaiListHolder)?.onSetMetadataValues(manga, metadata) + holder.onSetMetadataValues(manga, metadata) } // SY <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt index 8df3a899b..cfebb9c7c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.browse.source.browse import android.view.View +import androidx.core.view.isVisible import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.bitmap.CenterCrop import com.bumptech.glide.load.resource.bitmap.RoundedCorners @@ -11,6 +12,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.util.system.getResourceColor +import exh.metadata.metadata.MangaDexSearchMetadata +import exh.metadata.metadata.base.RaisedSearchMetadata +import kotlinx.android.synthetic.main.source_list_item.local_text import kotlinx.android.synthetic.main.source_list_item.thumbnail import kotlinx.android.synthetic.main.source_list_item.title @@ -44,6 +48,17 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) : setImage(manga) } + // SY --> + override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { + if (metadata is MangaDexSearchMetadata) { + metadata.follow_status?.let { + local_text.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it] + local_text.isVisible = true + } + } + } + // SY <-- + override fun setImage(manga: Manga) { GlideApp.with(view.context).clear(thumbnail) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt index 928a1cdcb..e326210b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/index/IndexController.kt @@ -198,6 +198,8 @@ open class IndexController : filterSheet = SourceFilterSheet( activity!!, // SY --> + this, + presenter.source, presenter.loadSearches(), // SY <-- onFilterClicked = { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index c9e47f9b5..aba08c0a5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -49,6 +49,7 @@ import exh.PERV_EDEN_EN_SOURCE_ID import exh.PERV_EDEN_IT_SOURCE_ID import exh.favorites.FavoritesIntroDialog import exh.favorites.FavoritesSyncStatus +import exh.mangaDexSourceIds import exh.nHentaiSourceIds import exh.ui.LoaderManager import java.util.concurrent.TimeUnit @@ -530,6 +531,9 @@ class LibraryController( it.source == PERV_EDEN_EN_SOURCE_ID || it.source == PERV_EDEN_IT_SOURCE_ID } + binding.actionToolbar.findItem(R.id.action_push_to_mdlist)?.isVisible = selectedMangas.any { + it.source in mangaDexSourceIds + } // SY <-- } return false @@ -556,6 +560,7 @@ class LibraryController( PreMigrationController.navigateToMigration(skipPre, router, selectedMangaIds) } R.id.action_clean -> cleanTitles() + R.id.action_push_to_mdlist -> pushToMdList() // SY <-- else -> return false } @@ -658,6 +663,13 @@ class LibraryController( presenter.cleanTitles(mangas) destroyActionModeIfNeeded() } + + private fun pushToMdList() { + val mangas = selectedMangas.filter { + it.source in mangaDexSourceIds + }.toList() + presenter.syncMangaToDex(mangas) + } // SY <-- override fun updateCategoriesForMangas(mangas: List, categories: List) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index facac6ec3..164be76b0 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -30,11 +30,14 @@ import exh.EH_SOURCE_ID import exh.EXH_SOURCE_ID import exh.MERGED_SOURCE_ID import exh.favorites.FavoritesSyncHelper +import exh.md.utils.FollowStatus +import exh.md.utils.MdUtil import exh.util.await import exh.util.isLewd import exh.util.nullIfBlank import java.util.Collections import java.util.Comparator +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.singleOrNull import kotlinx.coroutines.runBlocking import rx.Observable @@ -194,6 +197,7 @@ class LibraryPresenter( } if (filterTracked != STATE_IGNORE) { val tracks = db.getTracks(item.manga).executeAsBlocking() + .filterNot { it.sync_id == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int } if (filterTracked == STATE_INCLUDE && tracks.isEmpty()) return@f false else if (filterTracked == STATE_EXCLUDE && tracks.isNotEmpty()) return@f false } @@ -501,6 +505,16 @@ class LibraryPresenter( } } } + + fun syncMangaToDex(mangaList: List) { + launchIO { + MdUtil.getEnabledMangaDex(preferences)?.let { mdex -> + mangaList.forEach { + mdex.updateFollowStatus(MdUtil.getMangaId(it.url), FollowStatus.READING).collect() + } + } + } + } // SY <-- /** @@ -612,6 +626,9 @@ class LibraryPresenter( LibraryGroup.BY_STATUS -> { grouping += Triple(SManga.ONGOING.toString(), SManga.ONGOING, context.getString(R.string.ongoing)) grouping += Triple(SManga.LICENSED.toString(), SManga.LICENSED, context.getString(R.string.licensed)) + grouping += Triple(SManga.CANCELLED.toString(), SManga.CANCELLED, context.getString(R.string.cancelled)) + grouping += Triple(SManga.HIATUS.toString(), SManga.HIATUS, context.getString(R.string.hiatus)) + grouping += Triple(SManga.PUBLICATION_COMPLETE.toString(), SManga.PUBLICATION_COMPLETE, context.getString(R.string.publication_complete)) grouping += Triple(SManga.COMPLETED.toString(), SManga.COMPLETED, context.getString(R.string.completed)) grouping += Triple(SManga.UNKNOWN.toString(), SManga.UNKNOWN, context.getString(R.string.unknown)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 7970ad342..38963d1e4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -50,7 +50,7 @@ import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.getMetadataSource +import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController @@ -92,6 +92,7 @@ import eu.kanade.tachiyomi.util.view.snack import exh.MERGED_SOURCE_ID import exh.isEhBasedSource import exh.metadata.metadata.base.FlatMetadata +import exh.source.EnhancedHttpSource.Companion.getMainSource import java.io.IOException import kotlin.math.min import kotlinx.android.synthetic.main.main_activity.root_coordinator @@ -254,9 +255,9 @@ class MangaController : adapters += mangaInfoAdapter - val thisSourceAsLewdSource = presenter.source.getMetadataSource() - if (thisSourceAsLewdSource != null) { - mangaMetaInfoAdapter = thisSourceAsLewdSource.getDescriptionAdapter(this) + val mainSource = presenter.source.getMainSource() + if (mainSource is MetadataSource<*, *>) { + mangaMetaInfoAdapter = mainSource.getDescriptionAdapter(this) mangaMetaInfoAdapter?.let { adapters += it } } mangaInfoItemAdapter = MangaInfoItemAdapter(this, fromSource) @@ -277,7 +278,7 @@ class MangaController : binding.recycler.adapter = ConcatAdapter(adapters) binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.addItemDecoration(ChapterDividerItemDecoration(view.context, if ((!preferences.recommendsInOverflow().get() || smartSearchConfig != null) && thisSourceAsLewdSource != null) 4 else if (!preferences.recommendsInOverflow().get() || smartSearchConfig != null || thisSourceAsLewdSource != null) 3 else 2)) + binding.recycler.addItemDecoration(ChapterDividerItemDecoration(view.context, if ((!preferences.recommendsInOverflow().get() || smartSearchConfig != null) && mainSource is MetadataSource<*, *>) 4 else if (!preferences.recommendsInOverflow().get() || smartSearchConfig != null || mainSource is MetadataSource<*, *>) 3 else 2)) // SY <-- binding.recycler.setHasFixedSize(true) chaptersAdapter?.fastScroller = binding.fastScroller @@ -481,9 +482,9 @@ class MangaController : // SY --> fun onNextMetaInfo(flatMetadata: FlatMetadata) { - val thisSourceAsLewdSource = presenter.source.getMetadataSource() - if (thisSourceAsLewdSource != null) { - presenter.meta = flatMetadata.raise(thisSourceAsLewdSource.metaClass) + val mainSource = presenter.source.getMainSource() + if (mainSource is MetadataSource<*, *>) { + presenter.meta = flatMetadata.raise(mainSource.metaClass) mangaMetaInfoAdapter?.notifyDataSetChanged() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 2c88ca3b6..336957b76 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -21,7 +21,7 @@ import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.MetadataSource -import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.isMetadataSource +import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem @@ -37,11 +37,12 @@ import exh.MERGED_SOURCE_ID import exh.debug.DebugToggles import exh.eh.EHentaiUpdateHelper import exh.isEhBasedSource +import exh.md.utils.FollowStatus import exh.merged.sql.models.MergedMangaReference import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.getFlatMetadataForManga -import exh.source.EnhancedHttpSource +import exh.source.EnhancedHttpSource.Companion.getMainSource import exh.util.asObservable import exh.util.await import exh.util.trimOrNull @@ -122,7 +123,7 @@ class MangaPresenter( super.onCreate(savedState) // SY --> - if (manga.initialized && source.isMetadataSource()) { + if (manga.initialized && source.getMainSource() is MetadataSource<*, *>) { getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") }) } @@ -207,14 +208,21 @@ class MangaPresenter( } private fun getTrackingObservable(): Observable { - if (!trackManager.hasLoggedServices()) { + // SY --> + val sourceIsMangaDex = source.getMainSource() is MangaDex + // SY <-- + if (!trackManager.hasLoggedServices(/* SY --> */sourceIsMangaDex/* SY <-- */)) { return Observable.just(0) } return db.getTracks(manga).asRxObservable() .map { tracks -> - val loggedServices = trackManager.services.filter { it.isLogged }.map { it.id } - tracks.filter { it.sync_id in loggedServices } + val loggedServices = trackManager.services.filter { it.isLogged /* SY --> */ && ((it.id == TrackManager.MDLIST && sourceIsMangaDex) || it.id != TrackManager.MDLIST) /* SY <-- */ }.map { it.id } + tracks + // SY --> + .filterNot { it.sync_id == TrackManager.MDLIST && it.status == FollowStatus.UNFOLLOWED.int } + // SY <-- + .filter { it.sync_id in loggedServices } } .map { it.size } } @@ -244,7 +252,7 @@ class MangaPresenter( } // SY --> .doOnNext { - if (source is MetadataSource<*, *> || (source is EnhancedHttpSource && source.enhancedSource is MetadataSource<*, *>)) { + if (source.getMainSource() is MetadataSource<*, *>) { getMangaMetaObservable().subscribeLatestCache({ view, flatMetadata -> if (flatMetadata != null) view.onNextMetaInfo(flatMetadata) else XLog.d("Invalid metadata") }) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt index 5327a47a6..472ac87ea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt @@ -19,10 +19,12 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.view.setTooltip import exh.MERGED_SOURCE_ID +import exh.source.EnhancedHttpSource.Companion.getMainSource import exh.util.SourceTagsUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -106,7 +108,10 @@ class MangaInfoHeaderAdapter( } with(binding.btnTracking) { - if (trackManager.hasLoggedServices()) { + // SY --> + val sourceIsMangaDex = source.let { it.getMainSource() is MangaDex } + // SY <-- + if (trackManager.hasLoggedServices(/* SY --> */sourceIsMangaDex/* SY <-- */)) { isVisible = true if (trackCount > 0) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt index f73b25036..4d2b6c48c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt @@ -9,6 +9,7 @@ import androidx.core.net.toUri import androidx.recyclerview.widget.LinearLayoutManager import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.databinding.TrackControllerBinding import eu.kanade.tachiyomi.ui.base.controller.NucleusController @@ -91,6 +92,10 @@ class TrackController : val atLeastOneLink = trackings.any { it.track != null } adapter?.items = trackings binding.swipeRefresh.isEnabled = atLeastOneLink + if (presenter.needsRefresh) { + presenter.needsRefresh = false + presenter.refresh() + } } fun onSearchResults(results: List) { @@ -126,6 +131,9 @@ class TrackController : override fun onSetClick(position: Int) { val item = adapter?.getItem(position) ?: return + // SY --> Kill search for now until cesco puts MdList into stable + if (item.service.id == TrackManager.MDLIST) return + // SY <-- TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt index 85d28c55b..a18911b97 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt @@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.system.toast +import exh.mangaDexSourceIds import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -35,6 +36,10 @@ class TrackPresenter( private var refreshSubscription: Subscription? = null + // SY --> + var needsRefresh = false + // SY <-- + override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) fetchTrackings() @@ -50,10 +55,36 @@ class TrackPresenter( } } .observeOn(AndroidSchedulers.mainThread()) + // SY --> + .map { trackItems -> + if (manga.source in mangaDexSourceIds) { + val mdTrack = trackItems.firstOrNull { it.service.id == TrackManager.MDLIST } + when { + mdTrack == null -> { + needsRefresh = true + trackItems + createMdListTrack() + } + mdTrack.track == null -> { + needsRefresh = true + trackItems - mdTrack + createMdListTrack() + } + else -> trackItems + } + } else trackItems + } + // SY <-- .doOnNext { trackList = it } .subscribeLatestCache(TrackController::onNextTrackings) } + // SY --> + private fun createMdListTrack(): TrackItem { + val track = trackManager.mdList.createInitialTracker(manga) + track.id = db.insertTrack(track).executeAsBlocking().insertedId() + return TrackItem(track, trackManager.mdList) + } + // SY <-- + fun refresh() { refreshSubscription?.let { remove(it) } refreshSubscription = Observable.from(trackList) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index 6d2de3a52..b15d231ed 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.preference import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.system.getResourceColor +import exh.md.utils.MdUtil class SettingsMainController : SettingsController() { @@ -80,6 +81,14 @@ class SettingsMainController : SettingsController() { onClick { navigateTo(SettingsEhController()) } } } + if (MdUtil.getEnabledMangaDex(preferences) != null) { + preference { + iconRes = R.drawable.ic_tracker_mangadex_logo + iconTint = tintColor + titleRes = R.string.mangadex_specific_settings + onClick { navigateTo(SettingsMangaDexController()) } + } + } // SY <-- preference { iconRes = R.drawable.ic_code_24dp diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMangaDexController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMangaDexController.kt new file mode 100644 index 000000000..2262971b0 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMangaDexController.kt @@ -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" + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt index fa2e12ad2..f72554bad 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSourceSync.kt @@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.online.HttpSource import exh.EH_SOURCE_ID import exh.EXH_SOURCE_ID -import exh.debug.DebugToggles import java.util.Date import java.util.TreeSet import uy.kohesive.injekt.Injekt @@ -145,18 +144,6 @@ fun syncChaptersWithSource( } } } - if (dbChapters.isEmpty() && !DebugToggles.INCLUDE_ONLY_ROOT_WHEN_LOADING_EXH_VERSIONS.enabled) { - val readChapters = db.getChaptersReadByUrls(finalAdded.map { it.url }).executeAsBlocking() - val readChapterUrls = readChapters.map { it.url } - if (readChapters.isNotEmpty()) { - toAdd.filter { it.url in readChapterUrls }.onEach { chapter -> - readChapters.firstOrNull { it.url == chapter.url }?.let { - chapter.read = it.read - chapter.last_page_read = it.last_page_read - } - } - } - } } // <-- EXH diff --git a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt index d7ae5887b..21ae9dd29 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateHelper.kt @@ -42,7 +42,7 @@ class EHentaiUpdateHelper(context: Context) { mangaIds.map { mangaId -> Single.zip( db.getManga(mangaId).asRxSingle(), - db.getChaptersByMangaId(mangaId).asRxSingle() + db.getChapters(mangaId).asRxSingle() ) { manga, chapters -> ChapterChain(manga, chapters) }.toObservable().filter { diff --git a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt index 646560c18..49e063491 100644 --- a/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt +++ b/app/src/main/java/exh/eh/EHentaiUpdateWorker.kt @@ -152,7 +152,7 @@ class EHentaiUpdateWorker : JobService(), CoroutineScope { return@mapNotNull null } - val chapter = db.getChaptersByMangaId(manga.id!!).asRxSingle().await().minByOrNull { + val chapter = db.getChapters(manga.id!!).asRxSingle().await().minByOrNull { it.date_upload } diff --git a/app/src/main/java/exh/md/MangaDexFabHeaderAdapter.kt b/app/src/main/java/exh/md/MangaDexFabHeaderAdapter.kt new file mode 100644 index 000000000..3b17f03c4 --- /dev/null +++ b/app/src/main/java/exh/md/MangaDexFabHeaderAdapter.kt @@ -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() { + + 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) + } + } +} diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt new file mode 100644 index 000000000..cb469c4d1 --- /dev/null +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsController.kt @@ -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 + } +} diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsPager.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsPager.kt new file mode 100644 index 000000000..f9d725c5b --- /dev/null +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsPager.kt @@ -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 { + return source.fetchFollows() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { onPageReceived(it) } + } +} diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt new file mode 100644 index 000000000..57ea7b4dc --- /dev/null +++ b/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt @@ -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) + } +} diff --git a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt index 3a92d3c56..8e669fe3c 100644 --- a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt +++ b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt @@ -17,8 +17,8 @@ import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.insertFlatMetadata +import exh.util.floor import java.util.Date -import kotlin.math.floor import okhttp3.Response import rx.Completable import rx.Single @@ -41,7 +41,7 @@ class ApiMangaParser(private val langs: List) { * * Will also save the metadata to the DB if possible */ - fun parseToManga(manga: SManga, input: Response): Completable { + fun parseToManga(manga: SManga, input: Response, forceLatestCover: Boolean): Completable { val mangaId = (manga as? Manga)?.id val metaObservable = if (mangaId != null) { // We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions @@ -55,7 +55,7 @@ class ApiMangaParser(private val langs: List) { } return metaObservable.map { - parseIntoMetadata(it, input) + parseIntoMetadata(it, input, forceLatestCover) it.copyTo(manga) it }.flatMapCompletable { @@ -66,7 +66,7 @@ class ApiMangaParser(private val langs: List) { } } - fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) { + fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, forceLatestCover: Boolean) { with(metadata) { try { val networkApiManga = MdUtil.jsonParser.decodeFromString(ApiMangaSerializer.serializer(), input.body!!.string()) @@ -74,15 +74,18 @@ class ApiMangaParser(private val langs: List) { mdId = MdUtil.getMangaId(input.request.url.toString()) mdUrl = input.request.url.toString() title = MdUtil.cleanString(networkManga.title) - thumbnail_url = MdUtil.cdnUrl + MdUtil.removeTimeParamUrl(networkManga.cover_url) + val coverList = networkManga.covers + thumbnail_url = MdUtil.cdnUrl + + if (forceLatestCover && coverList.isNotEmpty()) { + coverList.last() + } else { + MdUtil.removeTimeParamUrl(networkManga.cover_url) + } description = MdUtil.cleanDescription(networkManga.description) author = MdUtil.cleanString(networkManga.author) artist = MdUtil.cleanString(networkManga.artist) lang_flag = networkManga.lang_flag - val lastChapter = networkManga.last_chapter?.toFloatOrNull() - lastChapter?.let { - last_chapter_number = floor(it).toInt() - } + last_chapter_number = networkManga.last_chapter?.toFloatOrNull()?.floor() networkManga.rating?.let { rating = it.bayesian ?: it.mean @@ -107,10 +110,16 @@ class ApiMangaParser(private val langs: List) { status = tempStatus } + val demographic = FilterHandler.demographics().filter { it.id == networkManga.demographic }.firstOrNull() + val genres = networkManga.genres.mapNotNull { FilterHandler.allTypes[it.toString()] } .toMutableList() + if (demographic != null) { + genres.add(0, demographic.name) + } + if (networkManga.hentai == 1) { genres.add("Hentai") } @@ -135,7 +144,9 @@ class ApiMangaParser(private val langs: List) { if (filteredChapters.isEmpty() || serializer.manga.last_chapter.isNullOrEmpty()) { return false } - val finalChapterNumber = serializer.manga.last_chapter!! + // just to fix the stupid lint + val lastMangaChapter: String? = serializer.manga.last_chapter + val finalChapterNumber = lastMangaChapter!! if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) { filteredChapters.firstOrNull()?.let { if (isOneShot(it.value, finalChapterNumber)) { @@ -144,7 +155,7 @@ class ApiMangaParser(private val langs: List) { } } 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> { @@ -269,7 +280,7 @@ class ApiMangaParser(private val langs: List) { } if ((status == 2 || status == 3)) { if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) || - networkChapter.chapter == finalChapterNumber + networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0 ) { chapterName.add("[END]") } diff --git a/app/src/main/java/exh/md/handlers/CoverHandler.kt b/app/src/main/java/exh/md/handlers/CoverHandler.kt deleted file mode 100644 index 6443e120d..000000000 --- a/app/src/main/java/exh/md/handlers/CoverHandler.kt +++ /dev/null @@ -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 { - 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" } - } - } -} diff --git a/app/src/main/java/exh/md/handlers/FilterHandler.kt b/app/src/main/java/exh/md/handlers/FilterHandler.kt index 3e5467eb0..1ed7f65c6 100644 --- a/app/src/main/java/exh/md/handlers/FilterHandler.kt +++ b/app/src/main/java/exh/md/handlers/FilterHandler.kt @@ -171,7 +171,8 @@ class FilterHandler { Tag("81", "Virtual Reality"), Tag("82", "Zombies"), Tag("83", "Incest"), - Tag("84", "Mafia") + Tag("84", "Mafia"), + Tag("85", "Villainess") ).sortedWith(compareBy { it.name }) val allTypes = (contentType() + formats() + genre() + themes()).map { it.id to it.name }.toMap() diff --git a/app/src/main/java/exh/md/handlers/FollowsHandler.kt b/app/src/main/java/exh/md/handlers/FollowsHandler.kt index e3266afa8..bfd51e065 100644 --- a/app/src/main/java/exh/md/handlers/FollowsHandler.kt +++ b/app/src/main/java/exh/md/handlers/FollowsHandler.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservable +import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage import eu.kanade.tachiyomi.source.model.SManga import exh.md.handlers.serializers.FollowsPageResult @@ -16,26 +17,24 @@ import exh.md.utils.MdUtil import exh.md.utils.MdUtil.Companion.baseUrl import exh.md.utils.MdUtil.Companion.getMangaId import exh.metadata.metadata.MangaDexSearchMetadata -import kotlin.math.floor +import exh.util.floor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.CacheControl import okhttp3.FormBody import okhttp3.Headers -import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import rx.Observable -// Unused, kept for future featues todo class FollowsHandler(val client: OkHttpClient, val headers: Headers, val preferences: PreferencesHelper) { /** * fetch follows by page */ - fun fetchFollows(page: Int): Observable { - return client.newCall(followsListRequest(page)) + fun fetchFollows(): Observable { + return client.newCall(followsListRequest()) .asObservable() .map { response -> followsParseMangaPage(response) @@ -96,9 +95,9 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere val follow = result.first() track.status = follow.follow_type if (result[0].chapter.isNotBlank()) { - track.last_chapter_read = floor(follow.chapter.toFloat()).toInt() + track.last_chapter_read = follow.chapter.toFloat().floor() } - track.tracking_url = MdUtil.baseUrl + follow.manga_id.toString() + track.tracking_url = baseUrl + follow.manga_id.toString() track.title = follow.title } return track @@ -107,11 +106,8 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere /**build Request for follows page * */ - private fun followsListRequest(page: Int): Request { - val url = "${MdUtil.baseUrl}${MdUtil.followsAllApi}".toHttpUrlOrNull()!!.newBuilder() - .addQueryParameter("page", page.toString()) - - return GET(url.toString(), headers, CacheControl.FORCE_NETWORK) + private fun followsListRequest(): Request { + return GET("$baseUrl${MdUtil.followsAllApi}", headers, CacheControl.FORCE_NETWORK) } /** @@ -126,7 +122,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere title = MdUtil.cleanString(result.title) mdUrl = "/manga/${result.manga_id}/" thumbnail_url = MdUtil.formThumbUrl(manga.url, lowQualityCovers) - follow_status = FollowStatus.fromInt(result.follow_type)?.ordinal + follow_status = FollowStatus.fromInt(result.follow_type)?.int } } @@ -205,21 +201,16 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere /** * fetch all manga from all possible pages */ - suspend fun fetchAllFollows(forceHd: Boolean): List { + suspend fun fetchAllFollows(forceHd: Boolean): List> { return withContext(Dispatchers.IO) { - val listManga = mutableListOf() - loop@ for (i in 1..10000) { - val response = client.newCall(followsListRequest(i)) - .execute() - val mangasPage = followsParseMangaPage(response, forceHd) - - if (mangasPage.mangas.isNotEmpty()) { - listManga.addAll(mangasPage.mangas) + val listManga = mutableListOf>() + val response = client.newCall(followsListRequest()).execute() + val mangasPage = followsParseMangaPage(response, forceHd) + listManga.addAll( + mangasPage.mangas.mapIndexed { index, sManga -> + sManga to mangasPage.mangasMetadata[index] as MangaDexSearchMetadata } - if (!mangasPage.hasNextPage) { - break@loop - } - } + ) listManga } } @@ -227,7 +218,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere suspend fun fetchTrackingInfo(url: String): Track { return withContext(Dispatchers.IO) { val request = GET( - "${MdUtil.baseUrl}${MdUtil.followsMangaApi}" + getMangaId(url), + "$baseUrl${MdUtil.followsMangaApi}" + getMangaId(url), headers, CacheControl.FORCE_NETWORK ) diff --git a/app/src/main/java/exh/md/handlers/MangaHandler.kt b/app/src/main/java/exh/md/handlers/MangaHandler.kt index fc39f81cf..c1f3f414a 100644 --- a/app/src/main/java/exh/md/handlers/MangaHandler.kt +++ b/app/src/main/java/exh/md/handlers/MangaHandler.kt @@ -3,10 +3,13 @@ package exh.md.handlers import com.elvishew.xlog.XLog import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import exh.md.utils.MdUtil import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext import okhttp3.CacheControl import okhttp3.Headers @@ -14,7 +17,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import rx.Observable -class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: List) { +class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: List, val forceLatestCovers: Boolean = false) { // TODO make use of this suspend fun fetchMangaAndChapterDetails(manga: SManga): Pair> { @@ -28,7 +31,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li throw Exception("Error from MangaDex Response code ${response.code} ") } - parser.parseToManga(manga, response).await() + parser.parseToManga(manga, response, forceLatestCovers).await() val chapterList = parser.chapterListParse(jsonData) Pair( manga, @@ -48,7 +51,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li suspend fun fetchMangaDetails(manga: SManga): SManga { return withContext(Dispatchers.IO) { val response = client.newCall(apiRequest(manga)).execute() - ApiMangaParser(langs).parseToManga(manga, response).await() + ApiMangaParser(langs).parseToManga(manga, response, forceLatestCovers).await() manga.apply { initialized = true } @@ -59,7 +62,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li return client.newCall(apiRequest(manga)) .asObservableSuccess() .flatMap { - ApiMangaParser(langs).parseToManga(manga, it).andThen( + ApiMangaParser(langs).parseToManga(manga, it, forceLatestCovers).andThen( Observable.just( manga.apply { initialized = true @@ -84,7 +87,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li } } - fun fetchRandomMangaId(): Observable { + fun fetchRandomMangaIdObservable(): Observable { return client.newCall(randomMangaRequest()) .asObservableSuccess() .map { response -> @@ -92,6 +95,13 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li } } + fun fetchRandomMangaId(): Flow { + return flow { + val response = client.newCall(randomMangaRequest()).await() + emit(ApiMangaParser(langs).randomMangaIdParse(response)) + } + } + private fun randomMangaRequest(): Request { return GET(MdUtil.baseUrl + MdUtil.randMangaPage) } diff --git a/app/src/main/java/exh/md/handlers/PopularHandler.kt b/app/src/main/java/exh/md/handlers/PopularHandler.kt index 96998e6ce..9a1e1689e 100644 --- a/app/src/main/java/exh/md/handlers/PopularHandler.kt +++ b/app/src/main/java/exh/md/handlers/PopularHandler.kt @@ -42,7 +42,7 @@ class PopularHandler(val client: OkHttpClient, private val headers: Headers) { val mangas = document.select(popularMangaSelector).map { element -> popularMangaFromElement(element) - }.distinct() + }.distinctBy { it.url } val hasNextPage = popularMangaNextPageSelector.let { selector -> document.select(selector).first() diff --git a/app/src/main/java/exh/md/handlers/SearchHandler.kt b/app/src/main/java/exh/md/handlers/SearchHandler.kt index 8a0af588f..2a56d0293 100644 --- a/app/src/main/java/exh/md/handlers/SearchHandler.kt +++ b/app/src/main/java/exh/md/handlers/SearchHandler.kt @@ -33,7 +33,7 @@ class SearchHandler(val client: OkHttpClient, private val headers: Headers, val .map { response -> val details = SManga.create() details.url = "/manga/$realQuery/" - ApiMangaParser(langs).parseToManga(details, response).await() + ApiMangaParser(langs).parseToManga(details, response, preferences.mangaDexForceLatestCovers().get()).await() MangasPage(listOf(details), false) } } diff --git a/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt index c2a3f90d4..7484f62a8 100644 --- a/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt +++ b/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt @@ -15,7 +15,9 @@ data class MangaSerializer( val author: String, val cover_url: String, val description: String, + val demographic: String, val genres: List, + val covers: List, val hentai: Int, val lang_flag: String, val lang_name: String, diff --git a/app/src/main/java/exh/md/utils/MdUtil.kt b/app/src/main/java/exh/md/utils/MdUtil.kt index 91f5a1e7b..786d4d1ff 100644 --- a/app/src/main/java/exh/md/utils/MdUtil.kt +++ b/app/src/main/java/exh/md/utils/MdUtil.kt @@ -1,12 +1,17 @@ package exh.md.utils +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.all.MangaDex +import exh.util.floor import java.net.URI import java.net.URISyntaxException -import kotlin.math.floor import kotlinx.serialization.json.Json import org.jsoup.parser.Parser +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get class MdUtil { @@ -63,10 +68,15 @@ class MdUtil { "[b][u]Spanish", "[Español]:", "[b] Spanish: [/ b]", + "정보", "Spanish/Español", "Español / Spanish", "Italian / Italiano", + "Italian/Italiano", + "\r\n\r\nItalian\r\n", "Pasta-Pizza-Mandolino/Italiano", + "Persian /فارسی", + "Farsi/Persian/", "Polish / polski", "Polish / Polski", "Polish Summary / Polski Opis", @@ -89,6 +99,7 @@ class MdUtil { "French - Français:", "Turkish / Türkçe", "Turkish/Türkçe", + "Türkçe", "[b][u]Chinese", "Arabic / العربية", "العربية", @@ -191,11 +202,11 @@ class MdUtil { }.sortedByDescending { it.chapter_number } remove0ChaptersFromCount.firstOrNull()?.let { - val chpNumber = floor(it.chapter_number).toInt() + val chpNumber = it.chapter_number.floor() val allChapters = (1..chpNumber).toMutableSet() remove0ChaptersFromCount.forEach { - allChapters.remove(floor(it.chapter_number).toInt()) + allChapters.remove(it.chapter_number.floor()) } if (allChapters.size <= 0) return null @@ -203,6 +214,25 @@ class MdUtil { } return null } + + fun getEnabledMangaDex(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): MangaDex? { + return getEnabledMangaDexs(preferences, sourceManager).let { mangadexs -> + val preferredMangaDexId = preferences.preferredMangaDexId().get().toLongOrNull() + mangadexs.firstOrNull { preferredMangaDexId != null && preferredMangaDexId != 0L && it.id == preferredMangaDexId } ?: mangadexs.firstOrNull() + } + } + + fun getEnabledMangaDexs(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): List { + 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/" } } diff --git a/app/src/main/java/exh/patch/NetworkPatches.kt b/app/src/main/java/exh/patch/NetworkPatches.kt index b1e96bd4c..ab635664e 100644 --- a/app/src/main/java/exh/patch/NetworkPatches.kt +++ b/app/src/main/java/exh/patch/NetworkPatches.kt @@ -36,10 +36,5 @@ private const val EH_UNIVERSAL_INTERCEPTOR = -1L private val EH_INTERCEPTORS: Map> = mapOf( EH_UNIVERSAL_INTERCEPTOR to listOf( CAPTCHA_DETECTION_PATCH // Auto captcha detection - ), - - // MangaDex login support - *MANGADEX_SOURCE_IDS.map { id -> - id to listOf(MANGADEX_LOGIN_PATCH) - }.toTypedArray() + ) ) diff --git a/app/src/main/java/exh/source/EnhancedHttpSource.kt b/app/src/main/java/exh/source/EnhancedHttpSource.kt index 9fd2d142d..3213297ac 100644 --- a/app/src/main/java/exh/source/EnhancedHttpSource.kt +++ b/app/src/main/java/exh/source/EnhancedHttpSource.kt @@ -1,6 +1,7 @@ package exh.source import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page @@ -234,4 +235,21 @@ class EnhancedHttpSource( originalSource } } + + companion object { + fun Source.getMainSource(): Source { + return if (this is EnhancedHttpSource) { + this.source() + } else { + this + } + } + fun Source.getOriginalSource(): Source { + return if (this is EnhancedHttpSource) { + this.originalSource + } else { + this + } + } + } } diff --git a/app/src/main/java/exh/ui/metadata/MetadataViewAdapter.kt b/app/src/main/java/exh/ui/metadata/MetadataViewAdapter.kt index 160c92251..a121d12cd 100644 --- a/app/src/main/java/exh/ui/metadata/MetadataViewAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/MetadataViewAdapter.kt @@ -6,7 +6,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import eu.kanade.tachiyomi.databinding.MetadataViewItemBinding import eu.kanade.tachiyomi.util.system.copyToClipboard -import kotlin.math.floor +import exh.util.floor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -43,7 +43,7 @@ class MetadataViewAdapter(private var data: List>) : private var dataPosition: Int? = null fun bind(position: Int) { if (data.isEmpty() || !binding.infoText.text.isNullOrBlank()) return - dataPosition = floor(position / 2F).toInt() + dataPosition = (position / 2F).floor() binding.infoText.text = if (position % 2 == 0) data[dataPosition!!].first else data[dataPosition!!].second binding.infoText.clicks() .onEach { diff --git a/app/src/main/java/exh/ui/metadata/MetadataViewController.kt b/app/src/main/java/exh/ui/metadata/MetadataViewController.kt index 39fc51d65..ce10a81da 100644 --- a/app/src/main/java/exh/ui/metadata/MetadataViewController.kt +++ b/app/src/main/java/exh/ui/metadata/MetadataViewController.kt @@ -10,11 +10,12 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.databinding.MetadataViewControllerBinding import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.online.MetadataSource.Companion.getMetadataSource +import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.manga.MangaController import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.RaisedSearchMetadata +import exh.source.EnhancedHttpSource.Companion.getMainSource import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -73,9 +74,9 @@ class MetadataViewController : NucleusController) { + presenter.meta = flatMetadata.raise(mainSource.metaClass) } } diff --git a/app/src/main/java/exh/util/Math.kt b/app/src/main/java/exh/util/Math.kt new file mode 100644 index 000000000..b142a4578 --- /dev/null +++ b/app/src/main/java/exh/util/Math.kt @@ -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() diff --git a/app/src/main/java/exh/widget/preference/MangaDexLoginPreference.kt b/app/src/main/java/exh/widget/preference/MangaDexLoginPreference.kt new file mode 100644 index 000000000..97ff61712 --- /dev/null +++ b/app/src/main/java/exh/widget/preference/MangaDexLoginPreference.kt @@ -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() + } +} diff --git a/app/src/main/java/exh/widget/preference/MangadexLoginDialog.kt b/app/src/main/java/exh/widget/preference/MangadexLoginDialog.kt new file mode 100644 index 000000000..5446da47c --- /dev/null +++ b/app/src/main/java/exh/widget/preference/MangadexLoginDialog.kt @@ -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().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().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) + } +} diff --git a/app/src/main/java/exh/widget/preference/MangadexLogoutDialog.kt b/app/src/main/java/exh/widget/preference/MangadexLogoutDialog.kt new file mode 100644 index 000000000..b07e10f31 --- /dev/null +++ b/app/src/main/java/exh/widget/preference/MangadexLogoutDialog.kt @@ -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) + } +} diff --git a/app/src/main/res/drawable/ic_tracker_mangadex_logo.xml b/app/src/main/res/drawable/ic_tracker_mangadex_logo.xml new file mode 100644 index 000000000..597e2baae --- /dev/null +++ b/app/src/main/res/drawable/ic_tracker_mangadex_logo.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/pref_item_mangadex.xml b/app/src/main/res/layout/pref_item_mangadex.xml new file mode 100644 index 000000000..2a2fbc3d8 --- /dev/null +++ b/app/src/main/res/layout/pref_item_mangadex.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/pref_site_login_two_factor_auth.xml b/app/src/main/res/layout/pref_site_login_two_factor_auth.xml new file mode 100644 index 000000000..99776ef8d --- /dev/null +++ b/app/src/main/res/layout/pref_site_login_two_factor_auth.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/source_filter_mangadex_header.xml b/app/src/main/res/layout/source_filter_mangadex_header.xml new file mode 100644 index 000000000..ce771f94c --- /dev/null +++ b/app/src/main/res/layout/source_filter_mangadex_header.xml @@ -0,0 +1,30 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/library_selection.xml b/app/src/main/res/menu/library_selection.xml index a3ceb4c74..9a60c67c8 100755 --- a/app/src/main/res/menu/library_selection.xml +++ b/app/src/main/res/menu/library_selection.xml @@ -51,4 +51,11 @@ app:iconTint="?attr/colorOnPrimary" app:showAsAction="ifRoom" /> + + diff --git a/app/src/main/res/values/arrays_sy.xml b/app/src/main/res/values/arrays_sy.xml index 06ca2a26e..250a722d1 100644 --- a/app/src/main/res/values/arrays_sy.xml +++ b/app/src/main/res/values/arrays_sy.xml @@ -5,4 +5,14 @@ @string/clean_read_downloads @string/clean_read_manga_not_in_library + + + @string/md_follows_unfollowed + @string/reading + @string/completed + @string/on_hold + @string/plan_to_read + @string/dropped + @string/repeating + \ No newline at end of file diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index 76783db66..8d3c85d52 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -521,4 +521,20 @@ Info manga: Toggle dedupe + + Unfollowed + MangaDex settings + Sync Mangadex manga into Neko + Pulls reading/rereading manga from Mangadex into your Neko library + Use low quality thumbnails + Use latest uploaded cover + When enabled, it uses the latest uploaded manga cover under the /covers url instead of using the cover on MangaDex\'s manga page + Preferred MangaDex source + Set your chosen mangadex source, this will be used for follows and a bunch more features around the app + 2FA Code + Fields cannot be blank + Add to MangaDex follows + MangaDex follows + Random + \ No newline at end of file