Suggestions settings
This commit is contained in:
@@ -135,6 +135,12 @@ class AppSettings(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
val isSuggestionsEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_SUGGESTIONS, false)
|
||||
|
||||
val isSuggestionsExcludeNsfw: Boolean
|
||||
get() = prefs.getBoolean(KEY_SUGGESTIONS_EXCLUDE_NSFW, false)
|
||||
|
||||
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
|
||||
when (format) {
|
||||
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
@@ -224,6 +230,8 @@ class AppSettings(context: Context) {
|
||||
const val KEY_REVERSE_CHAPTERS = "reverse_chapters"
|
||||
const val KEY_HISTORY_EXCLUDE_NSFW = "history_exclude_nsfw"
|
||||
const val KEY_PAGES_NUMBERS = "pages_numbers"
|
||||
const val KEY_SUGGESTIONS = "suggestions"
|
||||
const val KEY_SUGGESTIONS_EXCLUDE_NSFW = "suggestions_exclude_nsfw"
|
||||
|
||||
// About
|
||||
const val KEY_APP_UPDATE = "app_update"
|
||||
|
||||
@@ -91,7 +91,7 @@ class HistoryRepository(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAllTags(): List<MangaTag> {
|
||||
return db.historyDao.findAllTags().map { x -> x.toMangaTag() }
|
||||
suspend fun getAllTags(): Set<MangaTag> {
|
||||
return db.historyDao.findAllTags().mapToSet { x -> x.toMangaTag() }
|
||||
}
|
||||
}
|
||||
@@ -43,12 +43,15 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
import org.koitharu.kotatsu.settings.AppUpdateChecker
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
||||
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
|
||||
import org.koitharu.kotatsu.tracker.ui.FeedFragment
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
import org.koitharu.kotatsu.utils.ext.*
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.hideKeyboard
|
||||
import org.koitharu.kotatsu.utils.ext.measureHeight
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
|
||||
class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
NavigationView.OnNavigationItemSelectedListener, AppBarOwner,
|
||||
@@ -116,6 +119,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
viewModel.onError.observe(this, this::onError)
|
||||
viewModel.isLoading.observe(this, this::onLoadingStateChanged)
|
||||
viewModel.remoteSources.observe(this, this::updateSideMenu)
|
||||
viewModel.isSuggestionsEnabled.observe(this, this::setSuggestionsEnabled)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
@@ -301,6 +305,14 @@ class MainActivity : BaseActivity<ActivityMainBinding>(),
|
||||
submenu.setGroupCheckable(R.id.group_remote_sources, true, true)
|
||||
}
|
||||
|
||||
private fun setSuggestionsEnabled(isEnabled: Boolean) {
|
||||
val item = binding.navigationView.menu.findItem(R.id.nav_suggestions) ?: return
|
||||
if (!isEnabled && item.isChecked) {
|
||||
binding.navigationView.setCheckedItem(R.id.nav_history)
|
||||
}
|
||||
item.isVisible = isEnabled
|
||||
}
|
||||
|
||||
private fun openDefaultSection() {
|
||||
when (viewModel.defaultSection) {
|
||||
AppSection.LOCAL -> {
|
||||
|
||||
@@ -21,6 +21,12 @@ class MainViewModel(
|
||||
val onOpenReader = SingleLiveEvent<Manga>()
|
||||
var defaultSection by settings::defaultSection
|
||||
|
||||
val isSuggestionsEnabled = settings.observe()
|
||||
.filter { it == AppSettings.KEY_SUGGESTIONS }
|
||||
.onStart { emit("") }
|
||||
.map { settings.isSuggestionsEnabled }
|
||||
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||
|
||||
val remoteSources = settings.observe()
|
||||
.filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN }
|
||||
.onStart { emit("") }
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreference
|
||||
import androidx.preference.TwoStatePreference
|
||||
import kotlinx.coroutines.launch
|
||||
import leakcanary.LeakCanary
|
||||
import org.koin.android.ext.android.inject
|
||||
@@ -56,7 +56,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
entryValues = ListMode.values().names()
|
||||
setDefaultValueCompat(ListMode.GRID.name)
|
||||
}
|
||||
findPreference<SwitchPreference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible =
|
||||
findPreference<Preference>(AppSettings.KEY_DYNAMIC_THEME)?.isVisible =
|
||||
AppSettings.isDynamicColorAvailable
|
||||
findPreference<ListPreference>(AppSettings.KEY_DATE_FORMAT)?.run {
|
||||
entryValues = arrayOf("", "MM/dd/yy", "dd/MM/yy", "yyyy-MM-dd", "dd MMM yyyy", "MMM dd, yyyy")
|
||||
@@ -72,12 +72,15 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
setDefaultValueCompat("")
|
||||
summary = "%s"
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_LOCAL_STORAGE)?.bindStorageName()
|
||||
findPreference<SwitchPreference>(AppSettings.KEY_PROTECT_APP)?.isChecked =
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)?.isChecked =
|
||||
!settings.appPassword.isNullOrEmpty()
|
||||
settings.subscribe(this)
|
||||
}
|
||||
@@ -114,15 +117,20 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
|
||||
}
|
||||
AppSettings.KEY_HIDE_TOOLBAR -> {
|
||||
findPreference<SwitchPreference>(key)?.setSummary(R.string.restart_required)
|
||||
findPreference<Preference>(key)?.setSummary(R.string.restart_required)
|
||||
}
|
||||
AppSettings.KEY_LOCAL_STORAGE -> {
|
||||
findPreference<Preference>(key)?.bindStorageName()
|
||||
}
|
||||
AppSettings.KEY_APP_PASSWORD -> {
|
||||
findPreference<SwitchPreference>(AppSettings.KEY_PROTECT_APP)
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
}
|
||||
AppSettings.KEY_SUGGESTIONS -> {
|
||||
findPreference<Preference>(AppSettings.KEY_SUGGESTIONS)?.setSummary(
|
||||
if (settings.isSuggestionsEnabled) R.string.enabled else R.string.disabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +156,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
||||
true
|
||||
}
|
||||
AppSettings.KEY_PROTECT_APP -> {
|
||||
val pref = (preference as? SwitchPreference ?: return false)
|
||||
val pref = (preference as? TwoStatePreference ?: return false)
|
||||
if (pref.isChecked) {
|
||||
pref.isChecked = false
|
||||
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
||||
|
||||
class SuggestionsSettingsFragment : BasePreferenceFragment(R.string.suggestions),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val repository by inject<SuggestionRepository>(mode = LazyThreadSafetyMode.NONE)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_suggestions)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
settings.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == AppSettings.KEY_SUGGESTIONS && settings.isSuggestionsEnabled) {
|
||||
onSuggestionsEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSuggestionsEnabled() {
|
||||
lifecycleScope.launch {
|
||||
if (repository.isEmpty()) {
|
||||
SuggestionsWorker.startNow(context ?: return@launch)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ abstract class SuggestionDao {
|
||||
@Query("SELECT * FROM suggestions ORDER BY relevance DESC")
|
||||
abstract fun observeAll(): Flow<List<SuggestionWithManga>>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM suggestions")
|
||||
abstract suspend fun count(): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
abstract suspend fun insert(entity: SuggestionEntity): Long
|
||||
|
||||
|
||||
@@ -20,6 +20,14 @@ class SuggestionRepository(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clear() {
|
||||
db.suggestionDao.deleteAll()
|
||||
}
|
||||
|
||||
suspend fun isEmpty(): Boolean {
|
||||
return db.suggestionDao.count() == 0
|
||||
}
|
||||
|
||||
suspend fun replace(suggestions: Iterable<MangaSuggestion>) {
|
||||
db.withTransaction {
|
||||
db.suggestionDao.deleteAll()
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.list.ui.MangaListFragment
|
||||
|
||||
class SuggestionsFragment : MangaListFragment() {
|
||||
|
||||
override val viewModel by viewModel<SuggestionsViewModel>(mode = LazyThreadSafetyMode.NONE)
|
||||
override val viewModel by viewModel<SuggestionsViewModel>()
|
||||
override val isSwipeRefreshEnabled = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koitharu.kotatsu.core.model.Manga
|
||||
import org.koitharu.kotatsu.core.model.SortOrder
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion
|
||||
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
|
||||
@@ -18,10 +19,25 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
|
||||
|
||||
private val suggestionRepository by inject<SuggestionRepository>()
|
||||
private val historyRepository by inject<HistoryRepository>()
|
||||
private val appSettings by inject<AppSettings>()
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
override suspend fun doWork(): Result = try {
|
||||
val count = doWorkImpl()
|
||||
Result.success(workDataOf(DATA_COUNT to count))
|
||||
} catch (t: Throwable) {
|
||||
Result.failure()
|
||||
}
|
||||
|
||||
private suspend fun doWorkImpl(): Int {
|
||||
if (!appSettings.isSuggestionsEnabled) {
|
||||
suggestionRepository.clear()
|
||||
return 0
|
||||
}
|
||||
val rawResults = ArrayList<Manga>()
|
||||
val allTags = historyRepository.getAllTags()
|
||||
if (allTags.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
val tagsBySources = allTags.groupBy { x -> x.source }
|
||||
for ((source, tags) in tagsBySources) {
|
||||
val repo = mangaRepositoryOf(source)
|
||||
@@ -33,23 +49,31 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
|
||||
)
|
||||
}
|
||||
}
|
||||
suggestionRepository.replace(
|
||||
rawResults.distinctBy { manga ->
|
||||
manga.id
|
||||
}.map { manga ->
|
||||
val jointTags = manga.tags intersect allTags
|
||||
MangaSuggestion(
|
||||
manga = manga,
|
||||
relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(),
|
||||
)
|
||||
}
|
||||
)
|
||||
return Result.success()
|
||||
if (appSettings.isSuggestionsExcludeNsfw) {
|
||||
rawResults.removeAll { it.isNsfw }
|
||||
}
|
||||
if (rawResults.isEmpty()) {
|
||||
return 0
|
||||
}
|
||||
val suggestions = rawResults.distinctBy { manga ->
|
||||
manga.id
|
||||
}.map { manga ->
|
||||
val jointTags = manga.tags intersect allTags
|
||||
MangaSuggestion(
|
||||
manga = manga,
|
||||
relevance = (jointTags.size / manga.tags.size.toDouble()).pow(2.0).toFloat(),
|
||||
)
|
||||
}.sortedBy { it.relevance }.take(LIMIT)
|
||||
suggestionRepository.replace(suggestions)
|
||||
return suggestions.size
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "suggestions"
|
||||
private const val TAG_ONESHOT = "suggestions_oneshot"
|
||||
private const val LIMIT = 140
|
||||
private const val DATA_COUNT = "count"
|
||||
|
||||
fun setup(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
@@ -64,5 +88,17 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) :
|
||||
WorkManager.getInstance(context)
|
||||
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
|
||||
}
|
||||
|
||||
fun startNow(context: Context) {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
val request = OneTimeWorkRequestBuilder<SuggestionsWorker>()
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG_ONESHOT)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueue(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import org.koitharu.kotatsu.utils.PendingIntentCompat
|
||||
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
|
||||
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
|
||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -237,6 +236,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
private const val DATA_PROGRESS = "progress"
|
||||
private const val DATA_TOTAL = "total"
|
||||
private const val TAG = "tracking"
|
||||
private const val TAG_ONESHOT = "tracking_oneshot"
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private fun createNotificationChannel(context: Context) {
|
||||
@@ -277,7 +277,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||
.build()
|
||||
val request = OneTimeWorkRequestBuilder<TrackWorker>()
|
||||
.setConstraints(constraints)
|
||||
.addTag(TAG)
|
||||
.addTag(TAG_ONESHOT)
|
||||
.build()
|
||||
WorkManager.getInstance(context)
|
||||
.enqueue(request)
|
||||
|
||||
@@ -249,4 +249,12 @@
|
||||
<string name="available_sources">Доступные источники</string>
|
||||
<string name="dynamic_theme">Динамическая тема</string>
|
||||
<string name="dynamic_theme_summary">Применяет тему приложения, основанную на цветовой палитре обоев на устройстве</string>
|
||||
<string name="suggestions">Рекомендации</string>
|
||||
<string name="suggestions_enable">Включить рекомендации</string>
|
||||
<string name="suggestions_summary">Предлагать мангу на основе Ваших предпочтений</string>
|
||||
<string name="suggestions_info">Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы</string>
|
||||
<string name="text_suggestion_holder">Начните читать мангу, чтобы получать персональные предложения</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Не предлагать NSFW мангу</string>
|
||||
<string name="enabled">Включено</string>
|
||||
<string name="disabled">Выключено</string>
|
||||
</resources>
|
||||
@@ -251,6 +251,12 @@
|
||||
<string name="dynamic_theme">Dynamic theme</string>
|
||||
<string name="dynamic_theme_summary">Applies a theme created on the color scheme of your wallpaper</string>
|
||||
<string name="importing_progress">Importing manga: %1$d of %2$d</string>
|
||||
<string name="suggestions">Suggestions</string>
|
||||
<string name="text_suggestion_holder">Start reading manga and you will get personalized suggestions</string>
|
||||
<string name="suggestions">Suggestions</string>
|
||||
<string name="suggestions_enable">Enable suggestions</string>
|
||||
<string name="suggestions_summary">Suggest manga based on your preferences</string>
|
||||
<string name="suggestions_info">All data is analyzed locally on this device. There is no transfer of your personal data to any services</string>
|
||||
<string name="text_suggestion_holder">Start reading manga and you will get personalized suggestions</string>
|
||||
<string name="exclude_nsfw_from_suggestions">Do not suggest NSFW manga</string>
|
||||
<string name="enabled">Enabled</string>
|
||||
<string name="disabled">Disabled</string>
|
||||
</resources>
|
||||
@@ -61,6 +61,13 @@
|
||||
app:allowDividerAbove="true"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.SuggestionsSettingsFragment"
|
||||
android:key="suggestions"
|
||||
android:persistent="false"
|
||||
android:title="@string/suggestions"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:key="local_storage"
|
||||
android:title="@string/manga_save_location"
|
||||
@@ -71,7 +78,7 @@
|
||||
android:title="@string/history_and_cache"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreference
|
||||
<SwitchPreferenceCompat
|
||||
android:key="protect_app"
|
||||
android:persistent="false"
|
||||
android:summary="@string/protect_application_summary"
|
||||
|
||||
27
app/src/main/res/xml/pref_suggestions.xml
Normal file
27
app/src/main/res/xml/pref_suggestions.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="suggestions"
|
||||
android:summary="@string/suggestions_summary"
|
||||
android:title="@string/suggestions_enable"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:dependency="suggestions"
|
||||
android:key="suggestions_exclude_nsfw"
|
||||
android:title="@string/exclude_nsfw_from_suggestions"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/ic_info_outline"
|
||||
android:key="track_warning"
|
||||
android:persistent="false"
|
||||
android:selectable="false"
|
||||
android:summary="@string/suggestions_info"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
Reference in New Issue
Block a user