Implement Neko similar manga, Mangadex only recommendations
This commit is contained in:
parent
3f1dede133
commit
eb3a987826
@ -328,6 +328,9 @@ dependencies {
|
|||||||
// RatingBar (SY)
|
// RatingBar (SY)
|
||||||
implementation 'me.zhanghai.android.materialratingbar:library:1.4.0'
|
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 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
|
|
||||||
implementation 'com.google.guava:guava:29.0-android'
|
implementation 'com.google.guava:guava:29.0-android'
|
||||||
|
@ -153,6 +153,10 @@
|
|||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<!-- EH -->
|
<!-- EH -->
|
||||||
|
<service
|
||||||
|
android:name="exh.md.similar.SimilarUpdateService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="exh.eh.EHentaiUpdateWorker"
|
android:name="exh.eh.EHentaiUpdateWorker"
|
||||||
android:permission="android.permission.BIND_JOB_SERVICE"
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
@ -21,6 +21,9 @@ import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
|||||||
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
|
import exh.md.similar.sql.mappers.SimilarTypeMapping
|
||||||
|
import exh.md.similar.sql.models.MangaSimilar
|
||||||
|
import exh.md.similar.sql.queries.SimilarQueries
|
||||||
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
import exh.merged.sql.mappers.MergedMangaTypeMapping
|
||||||
import exh.merged.sql.models.MergedMangaReference
|
import exh.merged.sql.models.MergedMangaReference
|
||||||
import exh.merged.sql.queries.MergedQueries
|
import exh.merged.sql.queries.MergedQueries
|
||||||
@ -39,7 +42,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
|||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context) :
|
open class DatabaseHelper(context: Context) :
|
||||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries /* SY --> */, 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)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
@ -59,6 +62,7 @@ open class DatabaseHelper(context: Context) :
|
|||||||
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
|
||||||
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
|
||||||
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
.addTypeMapping(MergedMangaReference::class.java, MergedMangaTypeMapping())
|
||||||
|
.addTypeMapping(MangaSimilar::class.java, SimilarTypeMapping())
|
||||||
// SY <--
|
// SY <--
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
@ -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.MangaCategoryTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
||||||
|
import exh.md.similar.sql.tables.SimilarTable
|
||||||
import exh.merged.sql.tables.MergedTable
|
import exh.merged.sql.tables.MergedTable
|
||||||
import exh.metadata.sql.tables.SearchMetadataTable
|
import exh.metadata.sql.tables.SearchMetadataTable
|
||||||
import exh.metadata.sql.tables.SearchTagTable
|
import exh.metadata.sql.tables.SearchTagTable
|
||||||
@ -24,7 +25,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
/**
|
/**
|
||||||
* Version of the database.
|
* 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) {
|
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
||||||
@ -39,6 +40,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(SearchTagTable.createTableQuery)
|
execSQL(SearchTagTable.createTableQuery)
|
||||||
execSQL(SearchTitleTable.createTableQuery)
|
execSQL(SearchTitleTable.createTableQuery)
|
||||||
execSQL(MergedTable.createTableQuery)
|
execSQL(MergedTable.createTableQuery)
|
||||||
|
execSQL(SimilarTable.createTableQuery)
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
// DB indexes
|
// DB indexes
|
||||||
@ -55,6 +57,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
execSQL(SearchTitleTable.createMangaIdIndexQuery)
|
||||||
execSQL(SearchTitleTable.createTitleIndexQuery)
|
execSQL(SearchTitleTable.createTitleIndexQuery)
|
||||||
execSQL(MergedTable.createIndexQuery)
|
execSQL(MergedTable.createIndexQuery)
|
||||||
|
execSQL(SimilarTable.createMangaIdIndexQuery)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,6 +74,10 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
|||||||
db.execSQL(MergedTable.createTableQuery)
|
db.execSQL(MergedTable.createTableQuery)
|
||||||
db.execSQL(MergedTable.createIndexQuery)
|
db.execSQL(MergedTable.createIndexQuery)
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 5) {
|
||||||
|
db.execSQL(SimilarTable.createTableQuery)
|
||||||
|
db.execSQL(SimilarTable.createMangaIdIndexQuery)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
@ -25,6 +25,7 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import exh.md.similar.SimilarUpdateService
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -100,6 +101,9 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
markAsRead(urls, mangaId)
|
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 {
|
companion object {
|
||||||
private const val NAME = "NotificationReceiver"
|
private const val NAME = "NotificationReceiver"
|
||||||
|
|
||||||
@ -298,6 +314,11 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
// Value containing chapter url.
|
// Value containing chapter url.
|
||||||
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_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
|
* 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)
|
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 <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,15 @@ object Notifications {
|
|||||||
const val ID_BACKUP_COMPLETE = -502
|
const val ID_BACKUP_COMPLETE = -502
|
||||||
const val ID_RESTORE_COMPLETE = -504
|
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(
|
private val deprecatedChannels = listOf(
|
||||||
"downloader_channel",
|
"downloader_channel",
|
||||||
"backup_restore_complete_channel"
|
"backup_restore_complete_channel"
|
||||||
@ -143,6 +152,13 @@ object Notifications {
|
|||||||
group = GROUP_BACKUP_RESTORE
|
group = GROUP_BACKUP_RESTORE
|
||||||
setShowBadge(false)
|
setShowBadge(false)
|
||||||
setSound(null, null)
|
setSound(null, null)
|
||||||
|
},
|
||||||
|
NotificationChannel(
|
||||||
|
CHANNEL_SIMILAR,
|
||||||
|
context.getString(R.string.similar),
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
).apply {
|
||||||
|
setShowBadge(false)
|
||||||
}
|
}
|
||||||
).forEach(context.notificationManager::createNotificationChannel)
|
).forEach(context.notificationManager::createNotificationChannel)
|
||||||
|
|
||||||
|
@ -309,6 +309,12 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val mangaDexForceLatestCovers = "manga_dex_force_latest_covers"
|
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 preferredMangaDexId = "preferred_mangaDex_id"
|
||||||
|
|
||||||
const val dataSaver = "data_saver"
|
const val dataSaver = "data_saver"
|
||||||
|
@ -431,6 +431,14 @@ class PreferencesHelper(val context: Context) {
|
|||||||
|
|
||||||
fun preferredMangaDexId() = flowPrefs.getString(Keys.preferredMangaDexId, "0")
|
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 dataSaver() = flowPrefs.getBoolean(Keys.dataSaver, false)
|
||||||
|
|
||||||
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
|
fun ignoreJpeg() = flowPrefs.getBoolean(Keys.ignoreJpeg, false)
|
||||||
|
@ -6,6 +6,7 @@ import android.content.SharedPreferences
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import com.bluelinelabs.conductor.Controller
|
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.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
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.FollowsHandler
|
||||||
import exh.md.handlers.MangaHandler
|
import exh.md.handlers.MangaHandler
|
||||||
import exh.md.handlers.MangaPlusHandler
|
import exh.md.handlers.MangaPlusHandler
|
||||||
|
import exh.md.handlers.SimilarHandler
|
||||||
import exh.md.utils.FollowStatus
|
import exh.md.utils.FollowStatus
|
||||||
import exh.md.utils.MdLang
|
import exh.md.utils.MdLang
|
||||||
import exh.md.utils.MdUtil
|
import exh.md.utils.MdUtil
|
||||||
@ -257,6 +259,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
|||||||
return MangaHandler(client, headers, listOf(mdLang)).fetchRandomMangaId()
|
return MangaHandler(client, headers, listOf(mdLang)).fetchRandomMangaId()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
|
||||||
|
return SimilarHandler(preferences, useLowQualityThumbnail()).fetchSimilar(manga)
|
||||||
|
}
|
||||||
|
|
||||||
private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
|
private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
|
||||||
when {
|
when {
|
||||||
query.toIntOrNull() != null -> {
|
query.toIntOrNull() != null -> {
|
||||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
|
|||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
@ -10,7 +9,6 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
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.model.FilterList
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
import eu.kanade.tachiyomi.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.FabController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
@ -55,9 +54,9 @@ import eu.kanade.tachiyomi.util.view.snack
|
|||||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
import eu.kanade.tachiyomi.widget.EmptyView
|
import eu.kanade.tachiyomi.widget.EmptyView
|
||||||
import exh.isEhBasedSource
|
import exh.isEhBasedSource
|
||||||
|
import exh.md.similar.ui.EnableMangaDexSimilarDialogController
|
||||||
import exh.savedsearches.EXHSavedSearch
|
import exh.savedsearches.EXHSavedSearch
|
||||||
import exh.source.EnhancedHttpSource.Companion.getMainSource
|
import exh.source.EnhancedHttpSource.Companion.getMainSource
|
||||||
import kotlinx.android.parcel.Parcelize
|
|
||||||
import kotlinx.android.synthetic.main.main_activity.root_coordinator
|
import kotlinx.android.synthetic.main.main_activity.root_coordinator
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.drop
|
import kotlinx.coroutines.flow.drop
|
||||||
@ -109,17 +108,10 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
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.
|
* Adapter containing the list of manga from the catalogue.
|
||||||
*/
|
*/
|
||||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
/* SY --> */ protected /* SY <-- */ var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||||
|
|
||||||
private var actionFab: ExtendedFloatingActionButton? = null
|
private var actionFab: ExtendedFloatingActionButton? = null
|
||||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
||||||
@ -154,12 +146,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
override fun getTitle(): String? {
|
||||||
// SY -->
|
return presenter.source.name
|
||||||
return when (mode) {
|
|
||||||
Mode.CATALOGUE -> presenter.source.name
|
|
||||||
Mode.RECOMMENDS -> recommendsConfig!!.title
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createPresenter(): BrowseSourcePresenter {
|
override fun createPresenter(): BrowseSourcePresenter {
|
||||||
@ -167,7 +154,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
return BrowseSourcePresenter(
|
return BrowseSourcePresenter(
|
||||||
args.getLong(SOURCE_ID_KEY),
|
args.getLong(SOURCE_ID_KEY),
|
||||||
args.getString(SEARCH_QUERY_KEY),
|
args.getString(SEARCH_QUERY_KEY),
|
||||||
recommendsMangaId = if (mode == Mode.RECOMMENDS) recommendsConfig?.mangaId else null,
|
|
||||||
filters = args.getString(FILTERS_CONFIG_KEY)
|
filters = args.getString(FILTERS_CONFIG_KEY)
|
||||||
)
|
)
|
||||||
// SY <--
|
// SY <--
|
||||||
@ -192,6 +178,11 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
val mainSource = presenter.source.getMainSource()
|
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()) {
|
if (mainSource is LoginSource && mainSource.needsLogin && !mainSource.isLogged()) {
|
||||||
val dialog = mainSource.getLoginDialog(mainSource, activity!!)
|
val dialog = mainSource.getLoginDialog(mainSource, activity!!)
|
||||||
dialog.showDialog(router)
|
dialog.showDialog(router)
|
||||||
@ -200,12 +191,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
open fun initFilterSheet() {
|
open fun initFilterSheet() {
|
||||||
// SY -->
|
|
||||||
if (mode == Mode.RECOMMENDS) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
if (presenter.sourceFilters.isEmpty()) {
|
if (presenter.sourceFilters.isEmpty()) {
|
||||||
// SY -->
|
// SY -->
|
||||||
actionFab?.text = activity!!.getString(R.string.saved_searches)
|
actionFab?.text = activity!!.getString(R.string.saved_searches)
|
||||||
@ -455,10 +440,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
}
|
}
|
||||||
menu.findItem(displayItem).isChecked = true
|
menu.findItem(displayItem).isChecked = true
|
||||||
// SY -->
|
// SY -->
|
||||||
if (mode == Mode.RECOMMENDS) {
|
|
||||||
menu.findItem(R.id.action_search).isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) {
|
if (preferences.enhancedEHentaiView().get() && presenter.source.isEhBasedSource()) {
|
||||||
menu.findItem(R.id.action_display_mode).isVisible = false
|
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
|
menu.findItem(R.id.action_open_in_web_view).isVisible = isHttpSource
|
||||||
|
|
||||||
val isLocalSource = presenter.source is LocalSource
|
val isLocalSource = presenter.source is LocalSource
|
||||||
// SY -->
|
menu.findItem(R.id.action_local_source_help).isVisible = isLocalSource
|
||||||
menu.findItem(R.id.action_local_source_help).isVisible = isLocalSource && mode == Mode.CATALOGUE
|
|
||||||
|
|
||||||
|
// SY -->
|
||||||
menu.findItem(R.id.action_settings).isVisible = presenter.source is ConfigurableSource
|
menu.findItem(R.id.action_settings).isVisible = presenter.source is ConfigurableSource
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
@ -555,19 +536,14 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
*
|
*
|
||||||
* @param error the error received.
|
* @param error the error received.
|
||||||
*/
|
*/
|
||||||
fun onAddPageError(error: Throwable) {
|
/* SY --> */ open /* SY <-- */fun onAddPageError(error: Throwable) {
|
||||||
// SY -->
|
// SY -->
|
||||||
XLog.w("> Failed to load next catalogue page!", error)
|
XLog.w("> Failed to load next catalogue page!", error)
|
||||||
|
XLog.w(
|
||||||
if (mode == Mode.CATALOGUE) {
|
"> (source.id: %s, source.name: %s)",
|
||||||
XLog.w(
|
presenter.source.id,
|
||||||
"> (source.id: %s, source.name: %s)",
|
presenter.source.name
|
||||||
presenter.source.id,
|
)
|
||||||
presenter.source.name
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
XLog.w("> Recommendations")
|
|
||||||
}
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
val adapter = adapter ?: return
|
val adapter = adapter ?: return
|
||||||
@ -590,7 +566,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
if (adapter.isEmpty) {
|
if (adapter.isEmpty) {
|
||||||
val actions = emptyList<EmptyView.Action>().toMutableList()
|
val actions = emptyList<EmptyView.Action>().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() })
|
actions += EmptyView.Action(R.string.local_source_help_guide, View.OnClickListener { openLocalSourceHelpGuide() })
|
||||||
} else {
|
} else {
|
||||||
actions += EmptyView.Action(R.string.action_retry, retryAction)
|
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 {
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||||
// SY -->
|
router.pushController(
|
||||||
when (mode) {
|
MangaController(
|
||||||
Mode.CATALOGUE -> {
|
item.manga,
|
||||||
router.pushController(
|
true,
|
||||||
MangaController(
|
args.getParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA)
|
||||||
item.manga,
|
).withFadeTransaction()
|
||||||
true,
|
)
|
||||||
args.getParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA)
|
|
||||||
).withFadeTransaction()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Mode.RECOMMENDS -> openSmartSearch(item.manga.originalTitle)
|
|
||||||
}
|
|
||||||
// SY <--
|
|
||||||
return false
|
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.
|
* Called when a manga is long clicked.
|
||||||
*
|
*
|
||||||
@ -774,9 +730,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
* @param position the position of the element clicked.
|
* @param position the position of the element clicked.
|
||||||
*/
|
*/
|
||||||
override fun onItemLongClick(position: Int) {
|
override fun onItemLongClick(position: Int) {
|
||||||
// SY -->
|
|
||||||
if (mode == Mode.RECOMMENDS) return
|
|
||||||
// SY <--
|
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: 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))
|
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 {
|
companion object {
|
||||||
const val SOURCE_ID_KEY = "sourceId"
|
const val SOURCE_ID_KEY = "sourceId"
|
||||||
const val SEARCH_QUERY_KEY = "searchQuery"
|
const val SEARCH_QUERY_KEY = "searchQuery"
|
||||||
@ -869,7 +812,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
|||||||
// SY -->
|
// SY -->
|
||||||
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
|
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
|
||||||
const val FILTERS_CONFIG_KEY = "filters"
|
const val FILTERS_CONFIG_KEY = "filters"
|
||||||
const val RECOMMENDS_CONFIG = "RECOMMENDS_CONFIG"
|
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,6 @@ open class BrowseSourcePresenter(
|
|||||||
private val sourceId: Long,
|
private val sourceId: Long,
|
||||||
private val searchQuery: String? = null,
|
private val searchQuery: String? = null,
|
||||||
// SY -->
|
// SY -->
|
||||||
private val recommendsMangaId: Long? = null,
|
|
||||||
private val filters: String? = null,
|
private val filters: String? = null,
|
||||||
// SY <--
|
// SY <--
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
@ -145,10 +144,6 @@ open class BrowseSourcePresenter(
|
|||||||
query = savedState.getString(::query.name, "")
|
query = savedState.getString(::query.name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recommendsMangaId != null) {
|
|
||||||
manga = db.getManga(recommendsMangaId).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
restartPager(/* SY -->*/ filters = if (allDefault) this.appliedFilters else sourceFilters /* SY <--*/)
|
restartPager(/* SY -->*/ filters = if (allDefault) this.appliedFilters else sourceFilters /* SY <--*/)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,11 +165,7 @@ open class BrowseSourcePresenter(
|
|||||||
subscribeToMangaInitializer()
|
subscribeToMangaInitializer()
|
||||||
|
|
||||||
// Create a new pager.
|
// Create a new pager.
|
||||||
// SY -->
|
pager = createPager(query, filters)
|
||||||
pager = if (recommendsMangaId != null) RecommendsPager(
|
|
||||||
manga ?: throw Exception("Could not get Manga")
|
|
||||||
) else createPager(query, filters)
|
|
||||||
// SY <--
|
|
||||||
|
|
||||||
val sourceId = source.id
|
val sourceId = source.id
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.recyclerview.widget.ConcatAdapter
|
import androidx.recyclerview.widget.ConcatAdapter
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.ControllerChangeHandler
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
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.glide.toMangaThumbnail
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.databinding.MangaControllerBinding
|
import eu.kanade.tachiyomi.databinding.MangaControllerBinding
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
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.FabController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
|
||||||
@ -96,7 +100,9 @@ import eu.kanade.tachiyomi.util.view.shrinkOnScroll
|
|||||||
import eu.kanade.tachiyomi.util.view.snack
|
import eu.kanade.tachiyomi.util.view.snack
|
||||||
import exh.MERGED_SOURCE_ID
|
import exh.MERGED_SOURCE_ID
|
||||||
import exh.isEhBasedSource
|
import exh.isEhBasedSource
|
||||||
|
import exh.md.similar.ui.MangaDexSimilarController
|
||||||
import exh.metadata.metadata.base.FlatMetadata
|
import exh.metadata.metadata.base.FlatMetadata
|
||||||
|
import exh.recs.RecommendsController
|
||||||
import exh.source.EnhancedHttpSource.Companion.getMainSource
|
import exh.source.EnhancedHttpSource.Companion.getMainSource
|
||||||
import kotlinx.android.synthetic.main.main_activity.root_coordinator
|
import kotlinx.android.synthetic.main.main_activity.root_coordinator
|
||||||
import kotlinx.android.synthetic.main.main_activity.toolbar
|
import kotlinx.android.synthetic.main.main_activity.toolbar
|
||||||
@ -748,15 +754,25 @@ class MangaController :
|
|||||||
|
|
||||||
// AZ -->
|
// AZ -->
|
||||||
fun openRecommends() {
|
fun openRecommends() {
|
||||||
val recommendsConfig = BrowseSourceController.RecommendsConfig(presenter.manga.originalTitle, presenter.manga.id)
|
val source = presenter.source.getMainSource()
|
||||||
|
if (source is MangaDex && preferences.mangadexSimilarEnabled().get()) {
|
||||||
router?.pushController(
|
MaterialDialog(activity!!)
|
||||||
BrowseSourceController(
|
.title(R.string.az_recommends)
|
||||||
bundleOf(
|
.listItemsSingleChoice(
|
||||||
BrowseSourceController.RECOMMENDS_CONFIG to recommendsConfig
|
items = listOf(
|
||||||
)
|
"MangaDex similar",
|
||||||
).withFadeTransaction()
|
"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 <--
|
// AZ <--
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ class SettingsMainController : SettingsController() {
|
|||||||
preference {
|
preference {
|
||||||
iconRes = R.drawable.ic_tracker_mangadex_logo_24dp
|
iconRes = R.drawable.ic_tracker_mangadex_logo_24dp
|
||||||
iconTint = tintColor
|
iconTint = tintColor
|
||||||
titleRes = R.string.mangadex_specific_settings
|
titleRes = R.string.pref_category_mangadex
|
||||||
onClick { navigateTo(SettingsMangaDexController()) }
|
onClick { navigateTo(SettingsMangaDexController()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
package eu.kanade.tachiyomi.ui.setting
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.util.preference.defaultValue
|
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.listPreference
|
||||||
|
import eu.kanade.tachiyomi.util.preference.onChange
|
||||||
import eu.kanade.tachiyomi.util.preference.onClick
|
import eu.kanade.tachiyomi.util.preference.onClick
|
||||||
import eu.kanade.tachiyomi.util.preference.preference
|
import eu.kanade.tachiyomi.util.preference.preference
|
||||||
|
import eu.kanade.tachiyomi.util.preference.preferenceCategory
|
||||||
import eu.kanade.tachiyomi.util.preference.summaryRes
|
import eu.kanade.tachiyomi.util.preference.summaryRes
|
||||||
import eu.kanade.tachiyomi.util.preference.switchPreference
|
import eu.kanade.tachiyomi.util.preference.switchPreference
|
||||||
import eu.kanade.tachiyomi.util.preference.titleRes
|
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.md.utils.MdUtil
|
||||||
import exh.widget.preference.MangaDexLoginPreference
|
import exh.widget.preference.MangaDexLoginPreference
|
||||||
import exh.widget.preference.MangadexLoginDialog
|
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) {
|
override fun siteLoginDialogClosed(source: Source) {
|
||||||
|
@ -4,7 +4,7 @@ import eu.kanade.tachiyomi.source.model.FilterList
|
|||||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
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.BrowseSourcePresenter
|
||||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
||||||
import exh.source.EnhancedHttpSource
|
import exh.source.EnhancedHttpSource.Companion.getMainSource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Presenter of [MangaDexFollowsController]. Inherit BrowseCataloguePresenter.
|
* Presenter of [MangaDexFollowsController]. Inherit BrowseCataloguePresenter.
|
||||||
@ -12,7 +12,7 @@ import exh.source.EnhancedHttpSource
|
|||||||
class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
|
class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
|
||||||
|
|
||||||
override fun createPager(query: String, filters: FilterList): Pager {
|
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)
|
return MangaDexFollowsPager(sourceAsMangaDex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,41 @@
|
|||||||
package exh.md.handlers
|
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.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSimilarImpl
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
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 exh.md.utils.MdUtil
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
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<MangasPage> {
|
fun fetchSimilar(manga: Manga): Observable<MangasPage> {
|
||||||
|
|
||||||
// Parse the Mangadex id from the URL
|
// Parse the Mangadex id from the URL
|
||||||
val mangaid = MdUtil.getMangaId(manga.url).toLong()
|
return Observable.just(MdUtil.getMangaId(manga.url).toLong())
|
||||||
|
.flatMap { mangaId ->
|
||||||
val lowQualityCovers = preferences.mangaDexLowQualityCovers().get()
|
val db = Injekt.get<DatabaseHelper>()
|
||||||
|
db.getSimilar(mangaId).asRxObservable()
|
||||||
// Get our current database
|
}.map { similarMangaDb: MangaSimilar? ->
|
||||||
val db = Injekt.get<DatabaseHelper>()
|
similarMangaDb?.let { mangaSimilar ->
|
||||||
val similarMangaDb = db.getSimilar(mangaid).executeAsBlocking() ?: return Observable.just(MangasPage(mutableListOf(), false))
|
val similarMangaTitles = mangaSimilar.matched_titles.split(MangaSimilarImpl.DELIMITER)
|
||||||
|
val similarMangaIds = mangaSimilar.matched_ids.split(MangaSimilarImpl.DELIMITER)
|
||||||
// Check if we have a result
|
val similarMangas = similarMangaIds.mapIndexed { index, similarId ->
|
||||||
|
SManga.create().apply {
|
||||||
val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER)
|
title = similarMangaTitles[index]
|
||||||
val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER)
|
url = "/manga/$similarId/"
|
||||||
|
thumbnail_url = MdUtil.formThumbUrl(url, useLowQualityCovers)
|
||||||
val similarMangas = similarMangaIds.mapIndexed { index, similarId ->
|
}
|
||||||
SManga.create().apply {
|
}
|
||||||
title = similarMangaTitles[index]
|
MangasPage(similarMangas, false)
|
||||||
url = "/manga/$similarId/"
|
} ?: MangasPage(mutableListOf(), false)
|
||||||
thumbnail_url = MdUtil.formThumbUrl(url, lowQualityCovers)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Return the matches
|
|
||||||
return Observable.just(MangasPage(similarMangas, false))
|
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
47
app/src/main/java/exh/md/similar/SimilarHttpService.kt
Normal file
47
app/src/main/java/exh/md/similar/SimilarHttpService.kt
Normal file
@ -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<ResponseBody>
|
||||||
|
}
|
74
app/src/main/java/exh/md/similar/SimilarUpdateJob.kt
Normal file
74
app/src/main/java/exh/md/similar/SimilarUpdateJob.kt
Normal file
@ -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<PreferencesHelper>()
|
||||||
|
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<SimilarUpdateJob>().setConstraints(constraints).build())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally build the periodic request
|
||||||
|
val request = PeriodicWorkRequestBuilder<SimilarUpdateJob>(
|
||||||
|
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<SimilarUpdateJob>().build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
324
app/src/main/java/exh/md/similar/SimilarUpdateService.kt
Normal file
324
app/src/main/java/exh/md/similar/SimilarUpdateService.kt
Normal file
@ -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<SimilarFromJson> {
|
||||||
|
val reader = JsonReader.of(destinationFile.source().buffer())
|
||||||
|
|
||||||
|
var processingManga = false
|
||||||
|
var processingTitles = false
|
||||||
|
var mangaId: String? = null
|
||||||
|
var similarIds = mutableListOf<String>()
|
||||||
|
var similarTitles = mutableListOf<String>()
|
||||||
|
val similars = mutableListOf<SimilarFromJson>()
|
||||||
|
|
||||||
|
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<String>, val similarTitles: List<String>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<MangaSimilar>(
|
||||||
|
SimilarPutResolver(),
|
||||||
|
SimilarGetResolver(),
|
||||||
|
SimilarDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class SimilarPutResolver : DefaultPutResolver<MangaSimilar>() {
|
||||||
|
|
||||||
|
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<MangaSimilar>() {
|
||||||
|
|
||||||
|
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<MangaSimilar>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: MangaSimilar) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_ID = ?")
|
||||||
|
.whereArgs(obj.id)
|
||||||
|
.build()
|
||||||
|
}
|
31
app/src/main/java/exh/md/similar/sql/models/MangaSimilar.kt
Normal file
31
app/src/main/java/exh/md/similar/sql/models/MangaSimilar.kt
Normal file
@ -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
|
||||||
|
}
|
@ -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 = "|*|"
|
||||||
|
}
|
||||||
|
}
|
@ -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<MangaSimilar>) = db.put().objects(similarList).prepare()
|
||||||
|
|
||||||
|
fun deleteAllSimilar() = db.delete()
|
||||||
|
.byQuery(
|
||||||
|
DeleteQuery.builder()
|
||||||
|
.table(SimilarTable.TABLE)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
.prepare()
|
||||||
|
}
|
27
app/src/main/java/exh/md/similar/sql/tables/SimilarTable.kt
Normal file
27
app/src/main/java/exh/md/similar/sql/tables/SimilarTable.kt
Normal file
@ -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)"
|
||||||
|
}
|
@ -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<PreferencesHelper>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
29
app/src/main/java/exh/md/similar/ui/MangaDexSimilarPager.kt
Normal file
29
app/src/main/java/exh/md/similar/ui/MangaDexSimilarPager.kt
Normal file
@ -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<MangasPage> {
|
||||||
|
return source.fetchMangaSimilar(manga)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnNext {
|
||||||
|
if (it.mangas.isNotEmpty()) {
|
||||||
|
onPageReceived(it)
|
||||||
|
} else {
|
||||||
|
throw NoResultsException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
70
app/src/main/java/exh/recs/RecommendsController.kt
Normal file
70
app/src/main/java/exh/recs/RecommendsController.kt
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -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.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SMangaImpl
|
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 exh.util.mangaType
|
import exh.util.mangaType
|
||||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
23
app/src/main/java/exh/recs/RecommendsPresenter.kt
Normal file
23
app/src/main/java/exh/recs/RecommendsPresenter.kt
Normal file
@ -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!!)
|
||||||
|
}
|
||||||
|
}
|
@ -32,6 +32,7 @@
|
|||||||
<string name="pref_category_all_sources">All Sources</string>
|
<string name="pref_category_all_sources">All Sources</string>
|
||||||
<string name="pref_category_eh">E-Hentai</string>
|
<string name="pref_category_eh">E-Hentai</string>
|
||||||
<string name="pref_category_fork">Fork Settings</string>
|
<string name="pref_category_fork">Fork Settings</string>
|
||||||
|
<string name="pref_category_mangadex">MangaDex</string>
|
||||||
|
|
||||||
|
|
||||||
<!-- EH Settings -->
|
<!-- EH Settings -->
|
||||||
@ -573,4 +574,37 @@
|
|||||||
<string name="metadata_corrupted">Metadata corrupted, please refresh the manga</string>
|
<string name="metadata_corrupted">Metadata corrupted, please refresh the manga</string>
|
||||||
<string name="no_scanlators">No scanlators available</string>
|
<string name="no_scanlators">No scanlators available</string>
|
||||||
|
|
||||||
|
<!-- Similar -->
|
||||||
|
<string name="similar">Similar manga</string>
|
||||||
|
<string name="similar_loading_percent">Updating similar manga (%1$d / %2$d updated)</string>
|
||||||
|
<string name="similar_loading_complete">Updating similar manga complete</string>
|
||||||
|
<string name="similar_loading_complete_error">Error trying to load/process similar manga</string>
|
||||||
|
<string name="similar_loading_progress_start">Downloading similar manga data file…</string>
|
||||||
|
<string name="similar_ask_to_enable_title">Enable Similar Manga?</string>
|
||||||
|
<string name="similar_ask_to_enable">
|
||||||
|
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.
|
||||||
|
</string>
|
||||||
|
<string name="similar_ask_to_enable_yes">Enable</string>
|
||||||
|
<string name="similar_ask_to_enable_no">Skip</string>
|
||||||
|
<string name="similar_settings">Similar Manga Settings</string>
|
||||||
|
<string name="similar_screen">Show similar manga</string>
|
||||||
|
<string name="similar_manually_update">Pull latest database</string>
|
||||||
|
<string name="similar_manually_toast">Starting manual update</string>
|
||||||
|
<string name="similar_manually_update_message">
|
||||||
|
Download the latest similar manga database.
|
||||||
|
This is around 9MB in size and is updated daily.
|
||||||
|
</string>
|
||||||
|
<string name="similar_update_fequency">Similar update frequency</string>
|
||||||
|
<string name="similar_screen_summary_message">
|
||||||
|
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.
|
||||||
|
</string>
|
||||||
|
<string name="similar_credit_message">
|
||||||
|
For more information and to view the source code:\n%s
|
||||||
|
</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user