diff --git a/app/build.gradle b/app/build.gradle
index f0138949c..11237c419 100755
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -328,6 +328,9 @@ dependencies {
// RatingBar (SY)
implementation 'me.zhanghai.android.materialratingbar:library:1.4.0'
+ // JsonReader for similar manga
+ implementation 'com.squareup.moshi:moshi:1.11.0'
+
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'com.google.guava:guava:29.0-android'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 500556c11..2b500aeac 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -153,6 +153,10 @@
android:exported="false" />
+
+
*/, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries /* SY <-- */ {
+ MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, SearchMetadataQueries, SearchTagQueries, SearchTitleQueries, MergedQueries, SimilarQueries /* SY <-- */ {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME)
@@ -59,6 +62,7 @@ open class DatabaseHelper(context: Context) :
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
+ .addTypeMapping(MangaSimilar::class.java, SimilarTypeMapping())
// SY <--
.build()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt
index ab0229260..784762063 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.database.tables.TrackTable
+import exh.md.similar.sql.tables.SimilarTable
import exh.merged.sql.tables.MergedTable
import exh.metadata.sql.tables.SearchMetadataTable
import exh.metadata.sql.tables.SearchTagTable
@@ -24,7 +25,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/**
* Version of the database.
*/
- const val DATABASE_VERSION = /* SY --> */ 4 /* SY <-- */
+ const val DATABASE_VERSION = /* SY --> */ 5 /* SY <-- */
}
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@@ -39,6 +40,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(SearchTagTable.createTableQuery)
execSQL(SearchTitleTable.createTableQuery)
execSQL(MergedTable.createTableQuery)
+ execSQL(SimilarTable.createTableQuery)
// SY <--
// DB indexes
@@ -55,6 +57,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
execSQL(SearchTitleTable.createMangaIdIndexQuery)
execSQL(SearchTitleTable.createTitleIndexQuery)
execSQL(MergedTable.createIndexQuery)
+ execSQL(SimilarTable.createMangaIdIndexQuery)
// SY <--
}
@@ -71,6 +74,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(MergedTable.createTableQuery)
db.execSQL(MergedTable.createIndexQuery)
}
+ if (oldVersion < 5) {
+ db.execSQL(SimilarTable.createTableQuery)
+ db.execSQL(SimilarTable.createMangaIdIndexQuery)
+ }
}
override fun onConfigure(db: SupportSQLiteDatabase) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
index 4960327b9..4d3ae1845 100755
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
@@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
+import exh.md.similar.SimilarUpdateService
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@@ -100,6 +101,9 @@ class NotificationReceiver : BroadcastReceiver() {
markAsRead(urls, mangaId)
}
}
+ // SY -->
+ ACTION_CANCEL_SIMILAR_UPDATE -> cancelSimilarUpdate(context)
+ // SY <--
}
}
@@ -241,6 +245,18 @@ class NotificationReceiver : BroadcastReceiver() {
}
}
+ // SY -->
+ /**
+ * Method called when user wants to stop a similar manga update
+ *
+ * @param context context of application
+ */
+ private fun cancelSimilarUpdate(context: Context) {
+ SimilarUpdateService.stop(context)
+ Handler().post { dismissNotification(context, Notifications.ID_SIMILAR_PROGRESS) }
+ }
+ // SY <--
+
companion object {
private const val NAME = "NotificationReceiver"
@@ -298,6 +314,11 @@ class NotificationReceiver : BroadcastReceiver() {
// Value containing chapter url.
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
+ // Sy -->
+ // Called to cancel similar manga update.
+ private const val ACTION_CANCEL_SIMILAR_UPDATE = "$ID.$NAME.CANCEL_SIMILAR_UPDATE"
+ // SY <--
+
/**
* Returns a [PendingIntent] that resumes the download of a chapter
*
@@ -548,5 +569,20 @@ class NotificationReceiver : BroadcastReceiver() {
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
+
+ // SY -->
+ /**
+ * Returns [PendingIntent] that starts a service which stops the similar update
+ *
+ * @param context context of application
+ * @return [PendingIntent]
+ */
+ internal fun cancelSimilarUpdatePendingBroadcast(context: Context): PendingIntent {
+ val intent = Intent(context, NotificationReceiver::class.java).apply {
+ action = ACTION_CANCEL_SIMILAR_UPDATE
+ }
+ return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+ // SY <--
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
index b55b53fed..32c7ad2e1 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
@@ -62,6 +62,15 @@ object Notifications {
const val ID_BACKUP_COMPLETE = -502
const val ID_RESTORE_COMPLETE = -504
+ // SY -->
+ /**
+ * Notification channel and ids used for backup and restore.
+ */
+ const val CHANNEL_SIMILAR = "similar_channel"
+ const val ID_SIMILAR_PROGRESS = -601
+ const val ID_SIMILAR_COMPLETE = -602
+ // SY <--
+
private val deprecatedChannels = listOf(
"downloader_channel",
"backup_restore_complete_channel"
@@ -143,6 +152,13 @@ object Notifications {
group = GROUP_BACKUP_RESTORE
setShowBadge(false)
setSound(null, null)
+ },
+ NotificationChannel(
+ CHANNEL_SIMILAR,
+ context.getString(R.string.similar),
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ setShowBadge(false)
}
).forEach(context.notificationManager::createNotificationChannel)
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 14a0ee3e0..3b0c40478 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
@@ -309,6 +309,12 @@ object PreferenceKeys {
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
+ const val mangadexSimilarEnabled = "pref_related_show_tab_key"
+
+ const val mangadexSimilarUpdateInterval = "related_update_interval"
+
+ const val mangadexSimilarOnlyOverWifi = "pref_simular_only_over_wifi_key"
+
const val preferredMangaDexId = "preferred_mangaDex_id"
const val dataSaver = "data_saver"
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 3303e1e2a..17de26ff4 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
@@ -431,6 +431,14 @@ class PreferencesHelper(val context: Context) {
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
+ fun mangadexSimilarEnabled() = flowPrefs.getBoolean(Keys.mangadexSimilarEnabled, false)
+
+ fun shownMangaDexSimilarAskDialog() = flowPrefs.getBoolean("shown_similar_ask_dialog", false)
+
+ fun mangadexSimilarOnlyOverWifi() = flowPrefs.getBoolean(Keys.mangadexSimilarOnlyOverWifi, true)
+
+ fun mangadexSimilarUpdateInterval() = flowPrefs.getInt(Keys.mangadexSimilarUpdateInterval, 2)
+
fun dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
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 86dea0cd6..b39d18316 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
@@ -6,6 +6,7 @@ import android.content.SharedPreferences
import android.net.Uri
import androidx.core.text.HtmlCompat
import com.bluelinelabs.conductor.Controller
+import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
@@ -35,6 +36,7 @@ import exh.md.handlers.ApiMangaParser
import exh.md.handlers.FollowsHandler
import exh.md.handlers.MangaHandler
import exh.md.handlers.MangaPlusHandler
+import exh.md.handlers.SimilarHandler
import exh.md.utils.FollowStatus
import exh.md.utils.MdLang
import exh.md.utils.MdUtil
@@ -257,6 +259,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return MangaHandler(client, headers, listOf(mdLang)).fetchRandomMangaId()
}
+ fun fetchMangaSimilar(manga: Manga): Observable {
+ return SimilarHandler(preferences, useLowQualityThumbnail()).fetchSimilar(manga)
+ }
+
private fun importIdToMdId(query: String, fail: () -> Observable): Observable =
when {
query.toIntOrNull() != null -> {
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 1ce14be35..805a0cd99 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
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
import android.content.res.Configuration
import android.os.Bundle
-import android.os.Parcelable
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
@@ -10,7 +9,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
-import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
@@ -37,6 +35,7 @@ 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.source.online.all.MangaDex
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,9 +54,9 @@ import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView
import exh.isEhBasedSource
+import exh.md.similar.ui.EnableMangaDexSimilarDialogController
import exh.savedsearches.EXHSavedSearch
import exh.source.EnhancedHttpSource.Companion.getMainSource
-import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.main_activity.root_coordinator
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop
@@ -109,17 +108,10 @@ open class BrowseSourceController(bundle: Bundle) :
private val preferences: PreferencesHelper by injectLazy()
- // SY -->
- private val recommendsConfig: RecommendsConfig? = args.getParcelable(RECOMMENDS_CONFIG)
- // SY <--
-
- // AZ -->
- private val mode = if (recommendsConfig == null) Mode.CATALOGUE else Mode.RECOMMENDS
- // AZ <--
/**
* Adapter containing the list of manga from the catalogue.
*/
- private var adapter: FlexibleAdapter>? = null
+ /* SY --> */ protected /* SY <-- */ var adapter: FlexibleAdapter>? = null
private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
@@ -154,12 +146,7 @@ open class BrowseSourceController(bundle: Bundle) :
}
override fun getTitle(): String? {
- // SY -->
- return when (mode) {
- Mode.CATALOGUE -> presenter.source.name
- Mode.RECOMMENDS -> recommendsConfig!!.title
- }
- // SY <--
+ return presenter.source.name
}
override fun createPresenter(): BrowseSourcePresenter {
@@ -167,7 +154,6 @@ open class BrowseSourceController(bundle: Bundle) :
return BrowseSourcePresenter(
args.getLong(SOURCE_ID_KEY),
args.getString(SEARCH_QUERY_KEY),
- recommendsMangaId = if (mode == Mode.RECOMMENDS) recommendsConfig?.mangaId else null,
filters = args.getString(FILTERS_CONFIG_KEY)
)
// SY <--
@@ -192,6 +178,11 @@ open class BrowseSourceController(bundle: Bundle) :
// SY -->
val mainSource = presenter.source.getMainSource()
+ if (mainSource is MangaDex && !preferences.mangadexSimilarEnabled().get() && !preferences.shownMangaDexSimilarAskDialog().get()) {
+ EnableMangaDexSimilarDialogController().showDialog(router)
+ preferences.shownMangaDexSimilarAskDialog().set(true)
+ }
+
if (mainSource is LoginSource && mainSource.needsLogin && !mainSource.isLogged()) {
val dialog = mainSource.getLoginDialog(mainSource, activity!!)
dialog.showDialog(router)
@@ -200,12 +191,6 @@ open class BrowseSourceController(bundle: Bundle) :
}
open fun initFilterSheet() {
- // SY -->
- if (mode == Mode.RECOMMENDS) {
- return
- }
- // SY <--
-
if (presenter.sourceFilters.isEmpty()) {
// SY -->
actionFab?.text = activity!!.getString(R.string.saved_searches)
@@ -455,10 +440,6 @@ open class BrowseSourceController(bundle: Bundle) :
}
menu.findItem(displayItem).isChecked = true
// SY -->
- if (mode == Mode.RECOMMENDS) {
- menu.findItem(R.id.action_search).isVisible = false
- }
-
if (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) {
menu.findItem(R.id.action_display_mode).isVisible = false
}
@@ -472,9 +453,9 @@ open class BrowseSourceController(bundle: Bundle) :
menu.findItem(R.id.action_open_in_web_view).isVisible = isHttpSource
val isLocalSource = presenter.source is LocalSource
- // SY -->
- menu.findItem(R.id.action_local_source_help).isVisible = isLocalSource && mode == Mode.CATALOGUE
+ menu.findItem(R.id.action_local_source_help).isVisible = isLocalSource
+ // SY -->
menu.findItem(R.id.action_settings).isVisible = presenter.source is ConfigurableSource
// SY <--
}
@@ -555,19 +536,14 @@ open class BrowseSourceController(bundle: Bundle) :
*
* @param error the error received.
*/
- fun onAddPageError(error: Throwable) {
+ /* SY --> */ open /* SY <-- */fun onAddPageError(error: Throwable) {
// SY -->
XLog.w("> Failed to load next catalogue page!", error)
-
- if (mode == Mode.CATALOGUE) {
- XLog.w(
- "> (source.id: %s, source.name: %s)",
- presenter.source.id,
- presenter.source.name
- )
- } else {
- XLog.w("> Recommendations")
- }
+ XLog.w(
+ "> (source.id: %s, source.name: %s)",
+ presenter.source.id,
+ presenter.source.name
+ )
// SY <--
val adapter = adapter ?: return
@@ -590,7 +566,7 @@ open class BrowseSourceController(bundle: Bundle) :
if (adapter.isEmpty) {
val actions = emptyList().toMutableList()
- if (presenter.source is LocalSource /* SY --> */ && mode == Mode.CATALOGUE /* SY <-- */) {
+ if (presenter.source is LocalSource) {
actions += EmptyView.Action(R.string.local_source_help_guide, View.OnClickListener { openLocalSourceHelpGuide() })
} else {
actions += EmptyView.Action(R.string.action_retry, retryAction)
@@ -734,36 +710,16 @@ open class BrowseSourceController(bundle: Bundle) :
*/
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
- // SY -->
- when (mode) {
- Mode.CATALOGUE -> {
- router.pushController(
- MangaController(
- item.manga,
- true,
- args.getParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA)
- ).withFadeTransaction()
- )
- }
- Mode.RECOMMENDS -> openSmartSearch(item.manga.originalTitle)
- }
- // SY <--
+ router.pushController(
+ MangaController(
+ item.manga,
+ true,
+ args.getParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA)
+ ).withFadeTransaction()
+ )
return false
}
- // AZ -->
- private fun openSmartSearch(title: String) {
- val smartSearchConfig = SourceController.SmartSearchConfig(title)
- router.pushController(
- SourceController(
- bundleOf(
- SourceController.SMART_SEARCH_CONFIG to smartSearchConfig
- )
- ).withFadeTransaction()
- )
- }
-
- // AZ <--
/**
* Called when a manga is long clicked.
*
@@ -774,9 +730,6 @@ open class BrowseSourceController(bundle: Bundle) :
* @param position the position of the element clicked.
*/
override fun onItemLongClick(position: Int) {
- // SY -->
- if (mode == Mode.RECOMMENDS) return
- // SY <--
val activity = activity ?: return
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
@@ -852,16 +805,6 @@ open class BrowseSourceController(bundle: Bundle) :
activity?.toast(activity?.getString(R.string.manga_added_library))
}
- // SY -->
- @Parcelize
- data class RecommendsConfig(val title: String, val mangaId: Long?) : Parcelable
-
- enum class Mode {
- CATALOGUE,
- RECOMMENDS
- }
- // SY <--
-
companion object {
const val SOURCE_ID_KEY = "sourceId"
const val SEARCH_QUERY_KEY = "searchQuery"
@@ -869,7 +812,6 @@ open class BrowseSourceController(bundle: Bundle) :
// SY -->
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
const val FILTERS_CONFIG_KEY = "filters"
- const val RECOMMENDS_CONFIG = "RECOMMENDS_CONFIG"
// SY <--
}
}
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 9184a92b3..526a0f461 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
@@ -59,7 +59,6 @@ open class BrowseSourcePresenter(
private val sourceId: Long,
private val searchQuery: String? = null,
// SY -->
- private val recommendsMangaId: Long? = null,
private val filters: String? = null,
// SY <--
private val sourceManager: SourceManager = Injekt.get(),
@@ -145,10 +144,6 @@ open class BrowseSourcePresenter(
query = savedState.getString(::query.name, "")
}
- if (recommendsMangaId != null) {
- manga = db.getManga(recommendsMangaId).executeAsBlocking()
- }
-
restartPager(/* SY -->*/ filters = if (allDefault) this.appliedFilters else sourceFilters /* SY <--*/)
}
@@ -170,11 +165,7 @@ open class BrowseSourcePresenter(
subscribeToMangaInitializer()
// Create a new pager.
- // SY -->
- pager = if (recommendsMangaId != null) RecommendsPager(
- manga ?: throw Exception("Could not get Manga")
- ) else createPager(query, filters)
- // SY <--
+ pager = createPager(query, filters)
val sourceId = source.id
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 54186e462..2250e0150 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
@@ -31,6 +31,8 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.list.listItemsSingleChoice
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bumptech.glide.load.engine.DiskCacheStrategy
@@ -50,11 +52,13 @@ import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.MangaControllerBinding
+import eu.kanade.tachiyomi.source.CatalogueSource
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
+import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
@@ -96,7 +100,9 @@ import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack
import exh.MERGED_SOURCE_ID
import exh.isEhBasedSource
+import exh.md.similar.ui.MangaDexSimilarController
import exh.metadata.metadata.base.FlatMetadata
+import exh.recs.RecommendsController
import exh.source.EnhancedHttpSource.Companion.getMainSource
import kotlinx.android.synthetic.main.main_activity.root_coordinator
import kotlinx.android.synthetic.main.main_activity.toolbar
@@ -748,15 +754,25 @@ class MangaController :
// AZ -->
fun openRecommends() {
- val recommendsConfig = BrowseSourceController.RecommendsConfig(presenter.manga.originalTitle, presenter.manga.id)
-
- router?.pushController(
- BrowseSourceController(
- bundleOf(
- BrowseSourceController.RECOMMENDS_CONFIG to recommendsConfig
- )
- ).withFadeTransaction()
- )
+ val source = presenter.source.getMainSource()
+ if (source is MangaDex && preferences.mangadexSimilarEnabled().get()) {
+ MaterialDialog(activity!!)
+ .title(R.string.az_recommends)
+ .listItemsSingleChoice(
+ items = listOf(
+ "MangaDex similar",
+ "Community recommendations"
+ )
+ ) { _, index, _ ->
+ when (index) {
+ 0 -> router.pushController(MangaDexSimilarController(presenter.manga, source).withFadeTransaction())
+ 1 -> router.pushController(RecommendsController(presenter.manga, source).withFadeTransaction())
+ }
+ }
+ .show()
+ } else if (source is CatalogueSource) {
+ router.pushController(RecommendsController(presenter.manga, source).withFadeTransaction())
+ }
}
// AZ <--
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 5484ba6be..c6e3d25f3 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
@@ -90,7 +90,7 @@ class SettingsMainController : SettingsController() {
preference {
iconRes = R.drawable.ic_tracker_mangadex_logo_24dp
iconTint = tintColor
- titleRes = R.string.mangadex_specific_settings
+ titleRes = R.string.pref_category_mangadex
onClick { navigateTo(SettingsMangaDexController()) }
}
}
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
index 83e275244..aedee2a29 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMangaDexController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMangaDexController.kt
@@ -1,17 +1,25 @@
package eu.kanade.tachiyomi.ui.setting
+import android.content.Intent
+import androidx.core.net.toUri
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.entriesRes
+import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.listPreference
+import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
+import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
+import eu.kanade.tachiyomi.util.system.toast
+import exh.md.similar.SimilarUpdateJob
import exh.md.utils.MdUtil
import exh.widget.preference.MangaDexLoginPreference
import exh.widget.preference.MangadexLoginDialog
@@ -85,6 +93,76 @@ class SettingsMangaDexController :
)
}
}
+
+ preferenceCategory {
+ titleRes = R.string.similar_settings
+
+ preference {
+ key = "pref_similar_screen"
+ titleRes = R.string.similar_screen
+ summaryRes = R.string.similar_screen_summary_message
+ isIconSpaceReserved = true
+ }
+
+ switchPreference {
+ key = PreferenceKeys.mangadexSimilarEnabled
+ titleRes = R.string.similar_screen
+ defaultValue = false
+ onClick {
+ SimilarUpdateJob.setupTask(context)
+ }
+ }
+
+ switchPreference {
+ key = PreferenceKeys.mangadexSimilarOnlyOverWifi
+ titleRes = R.string.pref_download_only_over_wifi
+ defaultValue = true
+ onClick {
+ SimilarUpdateJob.setupTask(context, true)
+ }
+ }
+
+ preference {
+ key = "pref_similar_manually_update"
+ titleRes = R.string.similar_manually_update
+ summaryRes = R.string.similar_manually_update_message
+ onClick {
+ SimilarUpdateJob.doWorkNow(context)
+ context.toast(R.string.similar_manually_toast)
+ }
+ }
+
+ intListPreference {
+ key = PreferenceKeys.mangadexSimilarUpdateInterval
+ titleRes = R.string.similar_update_fequency
+ entriesRes = arrayOf(
+ R.string.update_never,
+ R.string.update_24hour,
+ R.string.update_48hour,
+ R.string.update_weekly,
+ R.string.update_monthly
+ )
+ entryValues = arrayOf("0", "1", "2", "7", "30")
+ defaultValue = "2"
+
+ onChange {
+ SimilarUpdateJob.setupTask(context, true)
+ true
+ }
+ }
+
+ preference {
+ key = "similar_credits"
+ title = "Credits"
+ val url = "https://github.com/goldbattle/MangadexRecomendations"
+ summary = context.getString(R.string.similar_credit_message, url)
+ onClick {
+ val intent = Intent(Intent.ACTION_VIEW, url.toUri())
+ startActivity(intent)
+ }
+ isIconSpaceReserved = true
+ }
+ }
}
override fun siteLoginDialogClosed(source: Source) {
diff --git a/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt b/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt
index 57ea7b4dc..23cf7fcc3 100644
--- a/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt
+++ b/app/src/main/java/exh/md/follows/MangaDexFollowsPresenter.kt
@@ -4,7 +4,7 @@ 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
+import exh.source.EnhancedHttpSource.Companion.getMainSource
/**
* Presenter of [MangaDexFollowsController]. Inherit BrowseCataloguePresenter.
@@ -12,7 +12,7 @@ import exh.source.EnhancedHttpSource
class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
override fun createPager(query: String, filters: FilterList): Pager {
- val sourceAsMangaDex = (source as EnhancedHttpSource).enhancedSource as MangaDex
+ val sourceAsMangaDex = source.getMainSource() as MangaDex
return MangaDexFollowsPager(sourceAsMangaDex)
}
}
diff --git a/app/src/main/java/exh/md/handlers/SimilarHandler.kt b/app/src/main/java/exh/md/handlers/SimilarHandler.kt
index c245e974a..0288a8ad9 100644
--- a/app/src/main/java/exh/md/handlers/SimilarHandler.kt
+++ b/app/src/main/java/exh/md/handlers/SimilarHandler.kt
@@ -1,47 +1,41 @@
package exh.md.handlers
-// todo make this work
-/*import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaSimilarImpl
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
+import exh.md.similar.sql.models.MangaSimilar
+import exh.md.similar.sql.models.MangaSimilarImpl
import exh.md.utils.MdUtil
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-class SimilarHandler(val preferences: PreferencesHelper) {
+class SimilarHandler(val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) {
- *//**
- * fetch our similar mangas
- *//*
+ /*
+ * fetch our similar mangas
+ */
fun fetchSimilar(manga: Manga): Observable {
-
// Parse the Mangadex id from the URL
- val mangaid = MdUtil.getMangaId(manga.url).toLong()
-
- val lowQualityCovers = preferences.mangaDexLowQualityCovers().get()
-
- // Get our current database
- val db = Injekt.get()
- val similarMangaDb = db.getSimilar(mangaid).executeAsBlocking() ?: return Observable.just(MangasPage(mutableListOf(), false))
-
- // Check if we have a result
-
- val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER)
- val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER)
-
- val similarMangas = similarMangaIds.mapIndexed { index, similarId ->
- SManga.create().apply {
- title = similarMangaTitles[index]
- url = "/manga/$similarId/"
- thumbnail_url = MdUtil.formThumbUrl(url, lowQualityCovers)
+ return Observable.just(MdUtil.getMangaId(manga.url).toLong())
+ .flatMap { mangaId ->
+ val db = Injekt.get()
+ db.getSimilar(mangaId).asRxObservable()
+ }.map { similarMangaDb: MangaSimilar? ->
+ similarMangaDb?.let { mangaSimilar ->
+ val similarMangaTitles = mangaSimilar.matched_titles.split(MangaSimilarImpl.DELIMITER)
+ val similarMangaIds = mangaSimilar.matched_ids.split(MangaSimilarImpl.DELIMITER)
+ val similarMangas = similarMangaIds.mapIndexed { index, similarId ->
+ SManga.create().apply {
+ title = similarMangaTitles[index]
+ url = "/manga/$similarId/"
+ thumbnail_url = MdUtil.formThumbUrl(url, useLowQualityCovers)
+ }
+ }
+ MangasPage(similarMangas, false)
+ } ?: MangasPage(mutableListOf(), false)
}
- }
-
- // Return the matches
- return Observable.just(MangasPage(similarMangas, false))
}
-}*/
+}
diff --git a/app/src/main/java/exh/md/similar/SimilarHttpService.kt b/app/src/main/java/exh/md/similar/SimilarHttpService.kt
new file mode 100644
index 000000000..e36219e9f
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/SimilarHttpService.kt
@@ -0,0 +1,47 @@
+package exh.md.similar
+
+import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
+import eu.kanade.tachiyomi.network.NetworkHelper
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.Json
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.ResponseBody
+import retrofit2.Call
+import retrofit2.Retrofit
+import retrofit2.http.GET
+import retrofit2.http.Streaming
+import uy.kohesive.injekt.injectLazy
+
+interface SimilarHttpService {
+ companion object {
+ private val client by lazy {
+ val network: NetworkHelper by injectLazy()
+ network.client.newBuilder()
+ // unzip interceptor which will add the correct headers
+ .addNetworkInterceptor { chain ->
+ val originalResponse = chain.proceed(chain.request())
+ originalResponse.newBuilder()
+ .header("Content-Encoding", "gzip")
+ .header("Content-Type", "application/json")
+ .build()
+ }
+ .build()
+ }
+
+ @ExperimentalSerializationApi
+ fun create(): SimilarHttpService {
+ // actual builder, which will parse the underlying json file
+ val adapter = Retrofit.Builder()
+ .baseUrl("https://raw.githubusercontent.com")
+ .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
+ .client(client)
+ .build()
+
+ return adapter.create(SimilarHttpService::class.java)
+ }
+ }
+
+ @Streaming
+ @GET("/goldbattle/MangadexRecomendations/master/output/mangas_compressed.json.gz")
+ fun getSimilarResults(): Call
+}
diff --git a/app/src/main/java/exh/md/similar/SimilarUpdateJob.kt b/app/src/main/java/exh/md/similar/SimilarUpdateJob.kt
new file mode 100644
index 000000000..38b9983b6
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/SimilarUpdateJob.kt
@@ -0,0 +1,74 @@
+package exh.md.similar
+
+import android.content.Context
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.concurrent.TimeUnit
+
+class SimilarUpdateJob(private val context: Context, workerParams: WorkerParameters) :
+ Worker(context, workerParams) {
+
+ override fun doWork(): Result {
+ SimilarUpdateService.start(context)
+ return Result.success()
+ }
+
+ companion object {
+ const val TAG = "RelatedUpdate"
+
+ fun setupTask(context: Context, skipInitial: Boolean = false) {
+ val preferences = Injekt.get()
+ val enabled = preferences.mangadexSimilarEnabled().get()
+ val interval = preferences.mangadexSimilarUpdateInterval().get()
+ if (enabled) {
+ // We are enabled, so construct the constraints
+ val wifiRestriction = if (preferences.mangadexSimilarOnlyOverWifi().get()) {
+ NetworkType.UNMETERED
+ } else {
+ NetworkType.CONNECTED
+ }
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(wifiRestriction)
+ .build()
+
+ // If we are not skipping the initial then run it right now
+ // Note that we won't run it if the constraints are not satisfied
+ if (!skipInitial) {
+ WorkManager.getInstance(context).enqueue(OneTimeWorkRequestBuilder().setConstraints(constraints).build())
+ }
+
+ // Finally build the periodic request
+ val request = PeriodicWorkRequestBuilder(
+ interval.toLong(),
+ TimeUnit.DAYS,
+ 1,
+ TimeUnit.HOURS
+ )
+ .addTag(TAG)
+ .setConstraints(constraints)
+ .build()
+
+ if (interval > 0) {
+ WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
+ } else {
+ WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
+ }
+ } else {
+ WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
+ }
+ }
+
+ fun doWorkNow(context: Context) {
+ WorkManager.getInstance(context).enqueue(OneTimeWorkRequestBuilder().build())
+ }
+ }
+}
diff --git a/app/src/main/java/exh/md/similar/SimilarUpdateService.kt b/app/src/main/java/exh/md/similar/SimilarUpdateService.kt
new file mode 100644
index 000000000..4091b0da7
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/SimilarUpdateService.kt
@@ -0,0 +1,324 @@
+package exh.md.similar
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.graphics.BitmapFactory
+import android.os.PowerManager
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import androidx.core.text.isDigitsOnly
+import com.elvishew.xlog.XLog
+import com.squareup.moshi.JsonReader
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.notification.NotificationReceiver
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.util.system.isServiceRunning
+import eu.kanade.tachiyomi.util.system.notificationManager
+import exh.md.similar.sql.models.MangaSimilarImpl
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okio.buffer
+import okio.sink
+import okio.source
+import retrofit2.awaitResponse
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+class SimilarUpdateService(
+ val db: DatabaseHelper = Injekt.get()
+) : Service() {
+
+ /**
+ * Wake lock that will be held until the service is destroyed.
+ */
+ private lateinit var wakeLock: PowerManager.WakeLock
+
+ var similarServiceScope = CoroutineScope(Dispatchers.IO + Job())
+
+ /**
+ * Subscription where the update is done.
+ */
+ private var job: Job? = null
+
+ /**
+ * Pending intent of action that cancels the library update
+ */
+ private val cancelIntent by lazy {
+ NotificationReceiver.cancelSimilarUpdatePendingBroadcast(this)
+ }
+
+ private val progressNotification by lazy {
+ NotificationCompat.Builder(this, Notifications.CHANNEL_SIMILAR)
+ .setLargeIcon(BitmapFactory.decodeResource(this.resources, R.mipmap.ic_launcher))
+ .setSmallIcon(R.drawable.ic_tachi)
+ .setOngoing(true)
+ .setContentTitle(getString(R.string.similar_loading_progress_start))
+ .setAutoCancel(true)
+ .addAction(
+ R.drawable.ic_close_24dp,
+ getString(android.R.string.cancel),
+ cancelIntent
+ )
+ }
+
+ /**
+ * Method called when the service is created. It injects dagger dependencies and acquire
+ * the wake lock.
+ */
+ override fun onCreate() {
+ super.onCreate()
+ startForeground(Notifications.ID_SIMILAR_PROGRESS, progressNotification.build())
+ wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "SimilarUpdateService:WakeLock"
+ )
+ wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
+ }
+
+ /**
+ * Method called when the service is destroyed. It destroys subscriptions and releases the wake
+ * lock.
+ */
+ override fun onDestroy() {
+ job?.cancel()
+ similarServiceScope.cancel()
+ if (wakeLock.isHeld) {
+ wakeLock.release()
+ }
+ super.onDestroy()
+ }
+
+ /**
+ * This method needs to be implemented, but it's not used/needed.
+ */
+ override fun onBind(intent: Intent) = null
+
+ /**
+ * Method called when the service receives an intent.
+ *
+ * @param intent the start intent from.
+ * @param flags the flags of the command.
+ * @param startId the start id of this command.
+ * @return the start value of the command.
+ */
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent == null) return Service.START_NOT_STICKY
+
+ // Unsubscribe from any previous subscription if needed.
+ job?.cancel()
+ val handler = CoroutineExceptionHandler { _, exception ->
+ XLog.e(exception)
+ stopSelf(startId)
+ showResultNotification(true)
+ cancelProgressNotification()
+ }
+ job = similarServiceScope.launch(handler) {
+ updateSimilar()
+ }
+ job?.invokeOnCompletion { stopSelf(startId) }
+
+ return START_REDELIVER_INTENT
+ }
+
+ /**
+ * Method that updates the similar database for manga
+ */
+ private suspend fun updateSimilar() = withContext(Dispatchers.IO) {
+ val response = SimilarHttpService.create().getSimilarResults().awaitResponse()
+ if (!response.isSuccessful) {
+ throw Exception("Error trying to download similar file")
+ }
+ val destinationFile = File(filesDir, "neko-similar.json")
+ val buffer = withContext(Dispatchers.IO) { destinationFile.sink().buffer() }
+
+ // write json to file
+ response.body()?.byteStream()?.source()?.use { input ->
+ buffer.use { output ->
+ output.writeAll(input)
+ }
+ }
+
+ val listSimilar = getSimilar(destinationFile)
+
+ // Delete the old similar table
+ db.deleteAllSimilar().executeAsBlocking()
+
+ val totalManga = listSimilar.size
+
+ // Loop through each and insert into the database
+
+ val dataToInsert = listSimilar.mapIndexed { index, similarFromJson ->
+ showProgressNotification(index, totalManga)
+
+ if (similarFromJson.similarIds.size != similarFromJson.similarTitles.size) {
+ return@mapIndexed null
+ }
+
+ val similar = MangaSimilarImpl()
+ similar.id = index.toLong()
+ similar.manga_id = similarFromJson.id.toLong()
+ similar.matched_ids = similarFromJson.similarIds.joinToString(MangaSimilarImpl.DELIMITER)
+ similar.matched_titles = similarFromJson.similarTitles.joinToString(MangaSimilarImpl.DELIMITER)
+ return@mapIndexed similar
+ }.filterNotNull()
+
+ showProgressNotification(dataToInsert.size, totalManga)
+
+ if (dataToInsert.isNotEmpty()) {
+ db.insertSimilar(dataToInsert).executeAsBlocking()
+ }
+ destinationFile.delete()
+ showResultNotification(!this.isActive)
+ cancelProgressNotification()
+ }
+
+ private fun getSimilar(destinationFile: File): List {
+ val reader = JsonReader.of(destinationFile.source().buffer())
+
+ var processingManga = false
+ var processingTitles = false
+ var mangaId: String? = null
+ var similarIds = mutableListOf()
+ var similarTitles = mutableListOf()
+ val similars = mutableListOf()
+
+ while (reader.peek() != JsonReader.Token.END_DOCUMENT) {
+ val nextToken = reader.peek()
+
+ if (JsonReader.Token.BEGIN_OBJECT == nextToken) {
+ reader.beginObject()
+ } else if (JsonReader.Token.NAME == nextToken) {
+ val name = reader.nextName()
+ if (!processingManga && name.isDigitsOnly()) {
+ processingManga = true
+ // similar add id
+ mangaId = name
+ } else if (name == "m_titles") {
+ processingTitles = true
+ }
+ } else if (JsonReader.Token.BEGIN_ARRAY == nextToken) {
+ reader.beginArray()
+ } else if (JsonReader.Token.END_ARRAY == nextToken) {
+ reader.endArray()
+ if (processingTitles) {
+ processingManga = false
+ processingTitles = false
+ similars.add(SimilarFromJson(mangaId!!, similarIds.toList(), similarTitles.toList()))
+ mangaId = null
+ similarIds = mutableListOf()
+ similarTitles = mutableListOf()
+ }
+ } else if (JsonReader.Token.NUMBER == nextToken) {
+ similarIds.add(reader.nextInt().toString())
+ } else if (JsonReader.Token.STRING == nextToken) {
+ if (processingTitles) {
+ similarTitles.add(reader.nextString())
+ }
+ } else if (JsonReader.Token.END_OBJECT == nextToken) {
+ reader.endObject()
+ }
+ }
+
+ return similars
+ }
+
+ data class SimilarFromJson(val id: String, val similarIds: List, val similarTitles: List)
+
+ /**
+ * Shows the notification containing the currently updating manga and the progress.
+ *
+ * @param current the current progress.
+ * @param total the total progress.
+ */
+ private fun showProgressNotification(current: Int, total: Int) {
+ notificationManager.notify(
+ Notifications.ID_SIMILAR_PROGRESS,
+ progressNotification
+ .setContentTitle(
+ getString(
+ R.string.similar_loading_percent,
+ current,
+ total
+ )
+ )
+ .setProgress(total, current, false)
+ .build()
+ )
+ }
+
+ /**
+ * Shows the notification containing the result of the update done by the service.
+ *
+ * @param error if the result was a error.
+ */
+ private fun showResultNotification(error: Boolean = false) {
+ val title = if (error) {
+ getString(R.string.similar_loading_complete_error)
+ } else {
+ getString(
+ R.string.similar_loading_complete
+ )
+ }
+
+ val result = NotificationCompat.Builder(this, Notifications.CHANNEL_SIMILAR)
+ .setContentTitle(title)
+ .setLargeIcon(BitmapFactory.decodeResource(this.resources, R.mipmap.ic_launcher))
+ .setSmallIcon(R.drawable.ic_tachi)
+ .setAutoCancel(true)
+ NotificationManagerCompat.from(this)
+ .notify(Notifications.ID_SIMILAR_COMPLETE, result.build())
+ }
+
+ /**
+ * Cancels the progress notification.
+ */
+ private fun cancelProgressNotification() {
+ notificationManager.cancel(Notifications.ID_SIMILAR_PROGRESS)
+ }
+
+ companion object {
+
+ /**
+ * Returns the status of the service.
+ *
+ * @param context the application context.
+ * @return true if the service is running, false otherwise.
+ */
+ fun isRunning(context: Context): Boolean {
+ return context.isServiceRunning(SimilarUpdateService::class.java)
+ }
+
+ /**
+ * Starts the service. It will be started only if there isn't another instance already
+ * running.
+ *
+ * @param context the application context.
+ */
+ fun start(context: Context) {
+ if (!isRunning(context)) {
+ val intent = Intent(context, SimilarUpdateService::class.java)
+ ContextCompat.startForegroundService(context, intent)
+ }
+ }
+
+ /**
+ * Stops the service.
+ *
+ * @param context the application context.
+ */
+ fun stop(context: Context) {
+ context.stopService(Intent(context, SimilarUpdateService::class.java))
+ }
+ }
+}
diff --git a/app/src/main/java/exh/md/similar/sql/mappers/SimilarTypeMapping.kt b/app/src/main/java/exh/md/similar/sql/mappers/SimilarTypeMapping.kt
new file mode 100644
index 000000000..53283be6b
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/sql/mappers/SimilarTypeMapping.kt
@@ -0,0 +1,63 @@
+package exh.md.similar.sql.mappers
+
+import android.content.ContentValues
+import android.database.Cursor
+import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
+import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
+import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
+import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
+import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
+import com.pushtorefresh.storio.sqlite.queries.InsertQuery
+import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
+import exh.md.similar.sql.models.MangaSimilar
+import exh.md.similar.sql.models.MangaSimilarImpl
+import exh.md.similar.sql.tables.SimilarTable.COL_ID
+import exh.md.similar.sql.tables.SimilarTable.COL_MANGA_ID
+import exh.md.similar.sql.tables.SimilarTable.COL_MANGA_SIMILAR_MATCHED_IDS
+import exh.md.similar.sql.tables.SimilarTable.COL_MANGA_SIMILAR_MATCHED_TITLES
+import exh.md.similar.sql.tables.SimilarTable.TABLE
+
+class SimilarTypeMapping : SQLiteTypeMapping(
+ SimilarPutResolver(),
+ SimilarGetResolver(),
+ SimilarDeleteResolver()
+)
+
+class SimilarPutResolver : DefaultPutResolver() {
+
+ override fun mapToInsertQuery(obj: MangaSimilar) = InsertQuery.builder()
+ .table(TABLE)
+ .build()
+
+ override fun mapToUpdateQuery(obj: MangaSimilar) = UpdateQuery.builder()
+ .table(TABLE)
+ .where("$COL_ID = ?")
+ .whereArgs(obj.id)
+ .build()
+
+ override fun mapToContentValues(obj: MangaSimilar) = ContentValues(4).apply {
+ put(COL_ID, obj.id)
+ put(COL_MANGA_ID, obj.manga_id)
+ put(COL_MANGA_SIMILAR_MATCHED_IDS, obj.matched_ids)
+ put(COL_MANGA_SIMILAR_MATCHED_TITLES, obj.matched_titles)
+ }
+}
+
+class SimilarGetResolver : DefaultGetResolver() {
+
+ override fun mapFromCursor(cursor: Cursor): MangaSimilar = MangaSimilarImpl().apply {
+ id = cursor.getLong(cursor.getColumnIndex(COL_ID))
+ manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
+ matched_ids = cursor.getString(cursor.getColumnIndex(COL_MANGA_SIMILAR_MATCHED_IDS))
+ matched_titles = cursor.getString(cursor.getColumnIndex(COL_MANGA_SIMILAR_MATCHED_TITLES))
+ }
+}
+
+class SimilarDeleteResolver : DefaultDeleteResolver() {
+
+ override fun mapToDeleteQuery(obj: MangaSimilar) = DeleteQuery.builder()
+ .table(TABLE)
+ .where("$COL_ID = ?")
+ .whereArgs(obj.id)
+ .build()
+}
diff --git a/app/src/main/java/exh/md/similar/sql/models/MangaSimilar.kt b/app/src/main/java/exh/md/similar/sql/models/MangaSimilar.kt
new file mode 100644
index 000000000..03d050bc9
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/sql/models/MangaSimilar.kt
@@ -0,0 +1,31 @@
+package exh.md.similar.sql.models
+
+import java.io.Serializable
+
+/**
+ * Object containing the history statistics of a chapter
+ */
+interface MangaSimilar : Serializable {
+
+ /**
+ * Id of this similar manga object.
+ */
+ var id: Long?
+
+ /**
+ * Id of matching manga
+ */
+ var manga_id: Long?
+
+ /**
+ * JSONArray.toString() list of ids for this manga
+ * Example: [3467, 5907, 21052, 2141, 6139, 5602, 3999]
+ */
+ var matched_ids: String
+
+ /**
+ * JSONArray.toString() list of titles for this manga
+ * Example: [Title1, Title2, ..., Title10]
+ */
+ var matched_titles: String
+}
diff --git a/app/src/main/java/exh/md/similar/sql/models/MangaSimilarImpl.kt b/app/src/main/java/exh/md/similar/sql/models/MangaSimilarImpl.kt
new file mode 100644
index 000000000..07297b4c1
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/sql/models/MangaSimilarImpl.kt
@@ -0,0 +1,32 @@
+package exh.md.similar.sql.models
+
+class MangaSimilarImpl : MangaSimilar {
+
+ override var id: Long? = null
+
+ override var manga_id: Long? = null
+
+ override lateinit var matched_ids: String
+
+ override lateinit var matched_titles: String
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || javaClass != other.javaClass) return false
+
+ other as MangaSimilar
+
+ if (id != other.id) return false
+ if (manga_id != other.manga_id) return false
+ if (matched_ids != other.matched_ids) return false
+ return matched_titles != other.matched_titles
+ }
+
+ override fun hashCode(): Int {
+ return id.hashCode() + manga_id.hashCode()
+ }
+
+ companion object {
+ const val DELIMITER = "|*|"
+ }
+}
diff --git a/app/src/main/java/exh/md/similar/sql/queries/SimilarQueries.kt b/app/src/main/java/exh/md/similar/sql/queries/SimilarQueries.kt
new file mode 100644
index 000000000..ad430078c
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/sql/queries/SimilarQueries.kt
@@ -0,0 +1,42 @@
+package exh.md.similar.sql.queries
+
+import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
+import com.pushtorefresh.storio.sqlite.queries.Query
+import eu.kanade.tachiyomi.data.database.DbProvider
+import exh.md.similar.sql.models.MangaSimilar
+import exh.md.similar.sql.tables.SimilarTable
+
+interface SimilarQueries : DbProvider {
+
+ fun getAllSimilar() = db.get()
+ .listOfObjects(MangaSimilar::class.java)
+ .withQuery(
+ Query.builder()
+ .table(SimilarTable.TABLE)
+ .build()
+ )
+ .prepare()
+
+ fun getSimilar(manga_id: Long) = db.get()
+ .`object`(MangaSimilar::class.java)
+ .withQuery(
+ Query.builder()
+ .table(SimilarTable.TABLE)
+ .where("${SimilarTable.COL_MANGA_ID} = ?")
+ .whereArgs(manga_id)
+ .build()
+ )
+ .prepare()
+
+ fun insertSimilar(similar: MangaSimilar) = db.put().`object`(similar).prepare()
+
+ fun insertSimilar(similarList: List) = db.put().objects(similarList).prepare()
+
+ fun deleteAllSimilar() = db.delete()
+ .byQuery(
+ DeleteQuery.builder()
+ .table(SimilarTable.TABLE)
+ .build()
+ )
+ .prepare()
+}
diff --git a/app/src/main/java/exh/md/similar/sql/tables/SimilarTable.kt b/app/src/main/java/exh/md/similar/sql/tables/SimilarTable.kt
new file mode 100644
index 000000000..937f8f1df
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/sql/tables/SimilarTable.kt
@@ -0,0 +1,27 @@
+package exh.md.similar.sql.tables
+
+object SimilarTable {
+
+ const val TABLE = "manga_related"
+
+ const val COL_ID = "_id"
+
+ const val COL_MANGA_ID = "manga_id"
+
+ const val COL_MANGA_SIMILAR_MATCHED_IDS = "matched_ids"
+
+ const val COL_MANGA_SIMILAR_MATCHED_TITLES = "matched_titles"
+
+ val createTableQuery: String
+ get() =
+ """CREATE TABLE $TABLE(
+ $COL_ID INTEGER NOT NULL PRIMARY KEY,
+ $COL_MANGA_ID INTEGER NOT NULL,
+ $COL_MANGA_SIMILAR_MATCHED_IDS TEXT NOT NULL,
+ $COL_MANGA_SIMILAR_MATCHED_TITLES TEXT NOT NULL,
+ UNIQUE ($COL_ID) ON CONFLICT REPLACE
+ )"""
+
+ val createMangaIdIndexQuery: String
+ get() = "CREATE INDEX ${TABLE}_${COL_MANGA_SIMILAR_MATCHED_IDS}_index ON $TABLE($COL_MANGA_SIMILAR_MATCHED_IDS)"
+}
diff --git a/app/src/main/java/exh/md/similar/ui/EnableMangaDexSimilarDialogController.kt b/app/src/main/java/exh/md/similar/ui/EnableMangaDexSimilarDialogController.kt
new file mode 100644
index 000000000..cc2d80e69
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/ui/EnableMangaDexSimilarDialogController.kt
@@ -0,0 +1,27 @@
+package exh.md.similar.ui
+
+import android.app.Dialog
+import android.os.Bundle
+import com.afollestad.materialdialogs.MaterialDialog
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.base.controller.DialogController
+import exh.md.similar.SimilarUpdateJob
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class EnableMangaDexSimilarDialogController : DialogController() {
+
+ override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+ val activity = activity!!
+ val preferences = Injekt.get()
+ return MaterialDialog(activity)
+ .title(text = activity.getString(R.string.similar_ask_to_enable_title))
+ .message(R.string.similar_ask_to_enable)
+ .negativeButton(R.string.similar_ask_to_enable_no, activity.getString(R.string.similar_ask_to_enable_no))
+ .positiveButton(R.string.similar_ask_to_enable_yes, activity.getString(R.string.similar_ask_to_enable_yes)) {
+ preferences.mangadexSimilarEnabled().set(true)
+ SimilarUpdateJob.setupTask(activity)
+ }
+ }
+}
diff --git a/app/src/main/java/exh/md/similar/ui/MangaDexSimilarController.kt b/app/src/main/java/exh/md/similar/ui/MangaDexSimilarController.kt
new file mode 100644
index 000000000..2ebd94f08
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/ui/MangaDexSimilarController.kt
@@ -0,0 +1,55 @@
+package exh.md.similar.ui
+
+import android.os.Bundle
+import android.view.Menu
+import androidx.core.os.bundleOf
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+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 MangaDexSimilarController(bundle: Bundle) : BrowseSourceController(bundle) {
+
+ constructor(manga: Manga, source: CatalogueSource) : this(
+ bundleOf(
+ MANGA_ID to manga.id!!,
+ SOURCE_ID_KEY to source.id
+ )
+ )
+
+ override fun getTitle(): String? {
+ return view?.context?.getString(R.string.similar)
+ }
+
+ override fun createPresenter(): BrowseSourcePresenter {
+ return MangaDexSimilarPresenter(args.getLong(MANGA_ID), 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 similar
+ }
+
+ override fun onItemLongClick(position: Int) {
+ return
+ }
+
+ override fun onAddPageError(error: Throwable) {
+ super.onAddPageError(error)
+ binding.emptyView.show("No Similar Manga found")
+ }
+
+ companion object {
+ const val MANGA_ID = "manga_id"
+ }
+}
diff --git a/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPager.kt b/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPager.kt
new file mode 100644
index 000000000..190d2e0bf
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPager.kt
@@ -0,0 +1,29 @@
+package exh.md.similar.ui
+
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.source.model.MangasPage
+import eu.kanade.tachiyomi.source.online.all.MangaDex
+import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
+import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+
+/**
+ * MangaDexSimilarPager inherited from the general Pager.
+ */
+class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() {
+
+ override fun requestNext(): Observable {
+ return source.fetchMangaSimilar(manga)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .doOnNext {
+ if (it.mangas.isNotEmpty()) {
+ onPageReceived(it)
+ } else {
+ throw NoResultsException()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPresenter.kt b/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPresenter.kt
new file mode 100644
index 000000000..dfffee18a
--- /dev/null
+++ b/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPresenter.kt
@@ -0,0 +1,26 @@
+package exh.md.similar.ui
+
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+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.Companion.getMainSource
+import uy.kohesive.injekt.injectLazy
+
+/**
+ * Presenter of [MangaDexSimilarController]. Inherit BrowseCataloguePresenter.
+ */
+class MangaDexSimilarPresenter(val mangaId: Long, sourceId: Long) : BrowseSourcePresenter(sourceId) {
+
+ var manga: Manga? = null
+
+ val db: DatabaseHelper by injectLazy()
+
+ override fun createPager(query: String, filters: FilterList): Pager {
+ val sourceAsMangaDex = source.getMainSource() as MangaDex
+ this.manga = db.getManga(mangaId).executeAsBlocking()
+ return MangaDexSimilarPager(manga!!, sourceAsMangaDex)
+ }
+}
diff --git a/app/src/main/java/exh/recs/RecommendsController.kt b/app/src/main/java/exh/recs/RecommendsController.kt
new file mode 100644
index 000000000..8f21d2b05
--- /dev/null
+++ b/app/src/main/java/exh/recs/RecommendsController.kt
@@ -0,0 +1,70 @@
+package exh.recs
+
+import android.os.Bundle
+import android.view.Menu
+import android.view.View
+import androidx.core.os.bundleOf
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.source.CatalogueSource
+import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
+import eu.kanade.tachiyomi.ui.browse.source.SourceController
+import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
+import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem
+
+/**
+ * Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
+ */
+class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) {
+
+ constructor(manga: Manga, source: CatalogueSource) : this(
+ bundleOf(
+ MANGA_ID to manga.id!!,
+ SOURCE_ID_KEY to source.id
+ )
+ )
+
+ override fun getTitle(): String? {
+ return (presenter as? RecommendsPresenter)?.manga?.title
+ }
+
+ override fun createPresenter(): RecommendsPresenter {
+ return RecommendsPresenter(args.getLong(MANGA_ID), 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 recs
+ }
+
+ override fun onItemClick(view: View, position: Int): Boolean {
+ val item = adapter?.getItem(position) as? SourceItem ?: return false
+ openSmartSearch(item.manga.originalTitle)
+ return true
+ }
+
+ private fun openSmartSearch(title: String) {
+ val smartSearchConfig = SourceController.SmartSearchConfig(title)
+ router.pushController(
+ SourceController(
+ bundleOf(
+ SourceController.SMART_SEARCH_CONFIG to smartSearchConfig
+ )
+ ).withFadeTransaction()
+ )
+ }
+
+ override fun onItemLongClick(position: Int) {
+ return
+ }
+
+ companion object {
+ const val MANGA_ID = "manga_id"
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/RecommendsPager.kt b/app/src/main/java/exh/recs/RecommendsPager.kt
similarity index 99%
rename from app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/RecommendsPager.kt
rename to app/src/main/java/exh/recs/RecommendsPager.kt
index 0eac0104c..7cf539e4b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/RecommendsPager.kt
+++ b/app/src/main/java/exh/recs/RecommendsPager.kt
@@ -1,9 +1,10 @@
-package eu.kanade.tachiyomi.ui.browse.source.browse
+package exh.recs
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SMangaImpl
+import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import exh.util.MangaType
import exh.util.mangaType
import kotlinx.coroutines.CoroutineExceptionHandler
diff --git a/app/src/main/java/exh/recs/RecommendsPresenter.kt b/app/src/main/java/exh/recs/RecommendsPresenter.kt
new file mode 100644
index 000000000..586e9eff6
--- /dev/null
+++ b/app/src/main/java/exh/recs/RecommendsPresenter.kt
@@ -0,0 +1,23 @@
+package exh.recs
+
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.source.model.FilterList
+import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
+import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
+import uy.kohesive.injekt.injectLazy
+
+/**
+ * Presenter of [RecommendsController]. Inherit BrowseCataloguePresenter.
+ */
+class RecommendsPresenter(val mangaId: Long, sourceId: Long) : BrowseSourcePresenter(sourceId) {
+
+ var manga: Manga? = null
+
+ val db: DatabaseHelper by injectLazy()
+
+ override fun createPager(query: String, filters: FilterList): Pager {
+ this.manga = db.getManga(mangaId).executeAsBlocking()
+ return RecommendsPager(manga!!)
+ }
+}
diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml
index 2d9470001..b7009d676 100644
--- a/app/src/main/res/values/strings_sy.xml
+++ b/app/src/main/res/values/strings_sy.xml
@@ -32,6 +32,7 @@
All Sources
E-Hentai
Fork Settings
+ MangaDex
@@ -573,4 +574,37 @@
Metadata corrupted, please refresh the manga
No scanlators available
+
+ Similar manga
+ Updating similar manga (%1$d / %2$d updated)
+ Updating similar manga complete
+ Error trying to load/process similar manga
+ Downloading similar manga data file…
+ Enable Similar Manga?
+
+ Would you like to enable similar manga recommendations?
+ This will download approximately 9 MB of data if enabled right now.
+ You can always enable it in the Settings / Mangadex menu.
+
+ Enable
+ Skip
+ Similar Manga Settings
+ Show similar manga
+ Pull latest database
+ Starting manual update
+
+ Download the latest similar manga database.
+ This is around 9MB in size and is updated daily.
+
+ Similar update frequency
+
+ This is a feature where one can get manga recommendations.
+ This is a recommendation system outside of MangaDex, and works by matching by genres,
+ demographics, content type, themes, and then using term frequency–inverse document frequency (tf–idf) to get the
+ similarity of two manga\'s descriptions. When enabled this file will download immediately!! The file is about 9 MB in size.
+
+
+ For more information and to view the source code:\n%s
+
+