From b93298c411d2aac12c5857a478c8114b5e68b160 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Fri, 11 Sep 2020 23:12:13 -0400 Subject: [PATCH] Add MangaDex only implementation of Mangadex Follows list Add login dialog that pops up whenever you are not logged in when trying to browse MangaDex Remove attempts at porting over chapter read history from older galleries to new ones Disable latest for ExHentai, it was browse without buttons anyway --- .../data/database/queries/ChapterQueries.kt | 4 +- .../data/library/LibraryUpdateNotifier.kt | 3 +- .../data/library/LibraryUpdateService.kt | 89 ++++++++- .../data/preference/PreferenceKeys.kt | 4 + .../data/preference/PreferencesHelper.kt | 4 + .../tachiyomi/data/track/TrackManager.kt | 11 +- .../tachiyomi/data/track/mdlist/MdList.kt | 136 ++++++++++++++ .../source/online/BrowseSourceFilterHeader.kt | 8 + .../tachiyomi/source/online/FollowsSource.kt | 30 +++ .../tachiyomi/source/online/LoginSource.kt | 17 ++ .../source/online/RandomMangaSource.kt | 7 + .../tachiyomi/source/online/all/EHentai.kt | 2 +- .../tachiyomi/source/online/all/MangaDex.kt | 175 +++++++++++++++++- .../source/browse/BrowseSourceController.kt | 12 ++ .../source/browse/BrowseSourcePresenter.kt | 3 +- .../browse/SourceComfortableGridHolder.kt | 15 ++ .../browse/SourceEnhancedEHentaiListHolder.kt | 24 +-- .../browse/source/browse/SourceFilterSheet.kt | 22 ++- .../browse/source/browse/SourceGridHolder.kt | 16 ++ .../ui/browse/source/browse/SourceHolder.kt | 5 + .../ui/browse/source/browse/SourceItem.kt | 23 ++- .../browse/source/browse/SourceListHolder.kt | 15 ++ .../ui/browse/source/index/IndexController.kt | 2 + .../tachiyomi/ui/library/LibraryController.kt | 12 ++ .../tachiyomi/ui/library/LibraryPresenter.kt | 17 ++ .../tachiyomi/ui/manga/MangaController.kt | 17 +- .../tachiyomi/ui/manga/MangaPresenter.kt | 22 ++- .../ui/manga/info/MangaInfoHeaderAdapter.kt | 7 +- .../ui/manga/track/TrackController.kt | 8 + .../ui/manga/track/TrackPresenter.kt | 31 ++++ .../ui/setting/SettingsMainController.kt | 9 + .../ui/setting/SettingsMangaDexController.kt | 100 ++++++++++ .../util/chapter/ChapterSourceSync.kt | 13 -- .../main/java/exh/eh/EHentaiUpdateHelper.kt | 2 +- .../main/java/exh/eh/EHentaiUpdateWorker.kt | 2 +- .../java/exh/md/MangaDexFabHeaderAdapter.kt | 56 ++++++ .../md/follows/MangaDexFollowsController.kt | 39 ++++ .../exh/md/follows/MangaDexFollowsPager.kt | 21 +++ .../md/follows/MangaDexFollowsPresenter.kt | 18 ++ .../java/exh/md/handlers/ApiMangaParser.kt | 35 ++-- .../main/java/exh/md/handlers/CoverHandler.kt | 26 --- .../java/exh/md/handlers/FilterHandler.kt | 3 +- .../java/exh/md/handlers/FollowsHandler.kt | 45 ++--- .../main/java/exh/md/handlers/MangaHandler.kt | 20 +- .../java/exh/md/handlers/PopularHandler.kt | 2 +- .../java/exh/md/handlers/SearchHandler.kt | 2 +- .../serializers/ApiMangaSerializer.kt | 2 + app/src/main/java/exh/md/utils/MdUtil.kt | 36 +++- app/src/main/java/exh/patch/NetworkPatches.kt | 7 +- .../java/exh/source/EnhancedHttpSource.kt | 18 ++ .../exh/ui/metadata/MetadataViewAdapter.kt | 4 +- .../exh/ui/metadata/MetadataViewController.kt | 9 +- app/src/main/java/exh/util/Math.kt | 5 + .../preference/MangaDexLoginPreference.kt | 54 ++++++ .../widget/preference/MangadexLoginDialog.kt | 115 ++++++++++++ .../widget/preference/MangadexLogoutDialog.kt | 49 +++++ .../res/drawable/ic_tracker_mangadex_logo.xml | 16 ++ .../main/res/layout/pref_item_mangadex.xml | 61 ++++++ .../pref_site_login_two_factor_auth.xml | 82 ++++++++ .../layout/source_filter_mangadex_header.xml | 30 +++ app/src/main/res/menu/library_selection.xml | 7 + app/src/main/res/values/arrays_sy.xml | 10 + app/src/main/res/values/strings_sy.xml | 16 ++ 63 files changed, 1492 insertions(+), 163 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/BrowseSourceFilterHeader.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/FollowsSource.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/LoginSource.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/online/RandomMangaSource.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMangaDexController.kt create mode 100644 app/src/main/java/exh/md/MangaDexFabHeaderAdapter.kt create mode 100644 app/src/main/java/exh/md/follows/MangaDexFollowsController.kt create mode 100644 app/src/main/java/exh/md/follows/MangaDexFollowsPager.kt create mode 100644 app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt delete mode 100644 app/src/main/java/exh/md/handlers/CoverHandler.kt create mode 100644 app/src/main/java/exh/util/Math.kt create mode 100644 app/src/main/java/exh/widget/preference/MangaDexLoginPreference.kt create mode 100644 app/src/main/java/exh/widget/preference/MangadexLoginDialog.kt create mode 100644 app/src/main/java/exh/widget/preference/MangadexLogoutDialog.kt create mode 100644 app/src/main/res/drawable/ic_tracker_mangadex_logo.xml create mode 100644 app/src/main/res/layout/pref_item_mangadex.xml create mode 100644 app/src/main/res/layout/pref_site_login_two_factor_auth.xml create mode 100644 app/src/main/res/layout/source_filter_mangadex_header.xml 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