Remove dead code

Mostly from settings rewrite, but some other things too.
This commit is contained in:
arkon
2022-10-16 12:40:56 -04:00
parent 5c5468f9af
commit 69cdba71eb
56 changed files with 6 additions and 4397 deletions

View File

@@ -1,401 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.provider.Settings
import android.webkit.WebStorage
import android.webkit.WebView
import androidx.core.net.toUri
import androidx.preference.PreferenceScreen
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.domain.manga.repository.MangaRepository
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.TabletUiMode
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_360
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.network.PREF_DOH_CONTROLD
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
import eu.kanade.tachiyomi.network.PREF_DOH_MULLVAD
import eu.kanade.tachiyomi.network.PREF_DOH_NJALLA
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.setting.database.ClearDatabaseController
import eu.kanade.tachiyomi.util.CrashLogUtil
import eu.kanade.tachiyomi.util.lang.launchNonCancellable
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.defaultValue
import eu.kanade.tachiyomi.util.preference.editTextPreference
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.listPreference
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
import logcat.LogPriority
import rikka.sui.Sui
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
class SettingsAdvancedController(
private val mangaRepository: MangaRepository = Injekt.get(),
) : SettingsController() {
private val network: NetworkHelper by injectLazy()
private val chapterCache: ChapterCache by injectLazy()
private val trackManager: TrackManager by injectLazy()
private val networkPreferences: NetworkPreferences by injectLazy()
private val libraryPreferences: LibraryPreferences by injectLazy()
private val uiPreferences: UiPreferences by injectLazy()
@SuppressLint("BatteryLife")
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_advanced
if (isDevFlavor.not()) {
switchPreference {
key = "acra.enable"
titleRes = R.string.pref_enable_acra
summaryRes = R.string.pref_acra_summary
defaultValue = true
}
}
preference {
key = "dump_crash_logs"
titleRes = R.string.pref_dump_crash_logs
summaryRes = R.string.pref_dump_crash_logs_summary
onClick {
viewScope.launchNonCancellable {
CrashLogUtil(context).dumpLogs()
}
}
}
switchPreference {
key = networkPreferences.verboseLogging().key()
titleRes = R.string.pref_verbose_logging
summaryRes = R.string.pref_verbose_logging_summary
defaultValue = isDevFlavor
onChange {
activity?.toast(R.string.requires_app_restart)
true
}
}
preferenceCategory {
titleRes = R.string.label_background_activity
preference {
key = "pref_disable_battery_optimization"
titleRes = R.string.pref_disable_battery_optimization
summaryRes = R.string.pref_disable_battery_optimization_summary
onClick {
val packageName: String = context.packageName
if (!context.powerManager.isIgnoringBatteryOptimizations(packageName)) {
try {
val intent = Intent().apply {
action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
data = "package:$packageName".toUri()
}
startActivity(intent)
} catch (e: ActivityNotFoundException) {
context.toast(R.string.battery_optimization_setting_activity_not_found)
}
} else {
context.toast(R.string.battery_optimization_disabled)
}
}
}
preference {
key = "pref_dont_kill_my_app"
title = "Don't kill my app!"
summaryRes = R.string.about_dont_kill_my_app
onClick {
openInBrowser("https://dontkillmyapp.com/")
}
}
}
preferenceCategory {
titleRes = R.string.label_data
preference {
key = CLEAR_CACHE_KEY
titleRes = R.string.pref_clear_chapter_cache
summary = context.getString(R.string.used_cache, chapterCache.readableSize)
onClick { clearChapterCache() }
}
switchPreference {
bindTo(libraryPreferences.autoClearChapterCache())
titleRes = R.string.pref_auto_clear_chapter_cache
}
preference {
key = "pref_clear_database"
titleRes = R.string.pref_clear_database
summaryRes = R.string.pref_clear_database_summary
onClick {
router.pushController(ClearDatabaseController())
}
}
}
preferenceCategory {
titleRes = R.string.label_network
preference {
key = "pref_clear_cookies"
titleRes = R.string.pref_clear_cookies
onClick {
network.cookieManager.removeAll()
activity?.toast(R.string.cookies_cleared)
}
}
preference {
key = "pref_clear_webview_data"
titleRes = R.string.pref_clear_webview_data
onClick { clearWebViewData() }
}
intListPreference {
key = networkPreferences.dohProvider().key()
titleRes = R.string.pref_dns_over_https
entries = arrayOf(
context.getString(R.string.disabled),
"Cloudflare",
"Google",
"AdGuard",
"Quad9",
"AliDNS",
"DNSPod",
"360",
"Quad 101",
"Mullvad",
"Control D",
"Njalla",
)
entryValues = arrayOf(
"-1",
PREF_DOH_CLOUDFLARE.toString(),
PREF_DOH_GOOGLE.toString(),
PREF_DOH_ADGUARD.toString(),
PREF_DOH_QUAD9.toString(),
PREF_DOH_ALIDNS.toString(),
PREF_DOH_DNSPOD.toString(),
PREF_DOH_360.toString(),
PREF_DOH_QUAD101.toString(),
PREF_DOH_MULLVAD.toString(),
PREF_DOH_CONTROLD.toString(),
PREF_DOH_NJALLA.toString(),
)
defaultValue = "-1"
summary = "%s"
onChange {
activity?.toast(R.string.requires_app_restart)
true
}
}
val defaultUserAgent = networkPreferences.defaultUserAgent()
editTextPreference {
key = defaultUserAgent.key()
titleRes = R.string.pref_user_agent_string
text = defaultUserAgent.get()
summary = network.defaultUserAgent
onChange {
if (it.toString().isBlank()) {
activity?.toast(R.string.error_user_agent_string_blank)
} else {
text = it.toString().trim()
activity?.toast(R.string.requires_app_restart)
}
false
}
}
preference {
key = "pref_reset_user_agent"
titleRes = R.string.pref_reset_user_agent_string
visibleIf(defaultUserAgent) { it != defaultUserAgent.defaultValue() }
onClick {
defaultUserAgent.delete()
activity?.toast(R.string.requires_app_restart)
}
}
}
preferenceCategory {
titleRes = R.string.label_library
preference {
key = "pref_refresh_library_covers"
titleRes = R.string.pref_refresh_library_covers
onClick { LibraryUpdateService.start(context, target = Target.COVERS) }
}
if (trackManager.hasLoggedServices()) {
preference {
key = "pref_refresh_library_tracking"
titleRes = R.string.pref_refresh_library_tracking
summaryRes = R.string.pref_refresh_library_tracking_summary
onClick { LibraryUpdateService.start(context, target = Target.TRACKING) }
}
}
preference {
key = "pref_reset_viewer_flags"
titleRes = R.string.pref_reset_viewer_flags
summaryRes = R.string.pref_reset_viewer_flags_summary
onClick { resetViewerFlags() }
}
}
preferenceCategory {
titleRes = R.string.label_extensions
listPreference {
bindTo(preferences.extensionInstaller())
titleRes = R.string.ext_installer_pref
summary = "%s"
// PackageInstaller doesn't work on MIUI properly for non-allowlisted apps
val values = if (DeviceUtil.isMiui) {
PreferenceValues.ExtensionInstaller.values()
.filter { it != PreferenceValues.ExtensionInstaller.PACKAGEINSTALLER }
} else {
PreferenceValues.ExtensionInstaller.values().toList()
}
entriesRes = values.map { it.titleResId }.toTypedArray()
entryValues = values.map { it.name }.toTypedArray()
onChange {
if (it == PreferenceValues.ExtensionInstaller.SHIZUKU.name &&
!(context.isPackageInstalled("moe.shizuku.privileged.api") || Sui.isSui())
) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.ext_installer_shizuku)
.setMessage(R.string.ext_installer_shizuku_unavailable_dialog)
.setPositiveButton(android.R.string.ok) { _, _ ->
openInBrowser("https://shizuku.rikka.app/download")
}
.setNegativeButton(android.R.string.cancel, null)
.show()
false
} else {
true
}
}
}
}
preferenceCategory {
titleRes = R.string.pref_category_display
listPreference {
bindTo(uiPreferences.tabletUiMode())
titleRes = R.string.pref_tablet_ui_mode
summary = "%s"
entriesRes = TabletUiMode.values().map { it.titleResId }.toTypedArray()
entryValues = TabletUiMode.values().map { it.name }.toTypedArray()
onChange {
activity?.toast(R.string.requires_app_restart)
true
}
}
}
}
private fun clearChapterCache() {
val activity = activity ?: return
viewScope.launchNonCancellable {
try {
val deletedFiles = chapterCache.clear()
withUIContext {
activity.toast(resources?.getString(R.string.cache_deleted, deletedFiles))
findPreference(CLEAR_CACHE_KEY)?.summary =
resources?.getString(R.string.used_cache, chapterCache.readableSize)
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
withUIContext { activity.toast(R.string.cache_delete_error) }
}
}
}
private fun clearWebViewData() {
val activity = activity ?: return
try {
WebView(activity).run {
setDefaultSettings()
clearCache(true)
clearFormData()
clearHistory()
clearSslPreferences()
}
WebStorage.getInstance().deleteAllData()
activity.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() }
activity.toast(R.string.webview_data_deleted)
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
activity.toast(R.string.cache_delete_error)
}
}
private fun resetViewerFlags() {
val activity = activity ?: return
viewScope.launchNonCancellable {
val success = mangaRepository.resetViewerFlags()
withUIContext {
val message = if (success) {
R.string.pref_reset_viewer_flags_success
} else {
R.string.pref_reset_viewer_flags_error
}
activity.toast(message)
}
}
}
}
private const val CLEAR_CACHE_KEY = "pref_clear_cache_key"

View File

@@ -1,173 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.core.app.ActivityCompat
import androidx.preference.PreferenceScreen
import eu.kanade.domain.ui.UiPreferences
import eu.kanade.domain.ui.model.AppTheme
import eu.kanade.domain.ui.model.ThemeMode
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.initThenAdd
import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.listPreference
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDynamicColorAvailable
import eu.kanade.tachiyomi.util.system.isTablet
import eu.kanade.tachiyomi.widget.preference.ThemesPreference
import uy.kohesive.injekt.injectLazy
import java.util.Date
class SettingsAppearanceController : SettingsController() {
private var themesPreference: ThemesPreference? = null
private val uiPreferences: UiPreferences by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_appearance
preferenceCategory {
titleRes = R.string.pref_category_theme
listPreference {
bindTo(uiPreferences.themeMode())
titleRes = R.string.pref_theme_mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
entriesRes = arrayOf(
R.string.theme_system,
R.string.theme_light,
R.string.theme_dark,
)
entryValues = arrayOf(
ThemeMode.SYSTEM.name,
ThemeMode.LIGHT.name,
ThemeMode.DARK.name,
)
} else {
entriesRes = arrayOf(
R.string.theme_light,
R.string.theme_dark,
)
entryValues = arrayOf(
ThemeMode.LIGHT.name,
ThemeMode.DARK.name,
)
}
summary = "%s"
}
themesPreference = initThenAdd(ThemesPreference(context)) {
bindTo(uiPreferences.appTheme())
titleRes = R.string.pref_app_theme
val appThemes = AppTheme.values().filter {
val monetFilter = if (it == AppTheme.MONET) {
DeviceUtil.isDynamicColorAvailable
} else {
true
}
it.titleResId != null && monetFilter
}
entries = appThemes
onChange {
activity?.let { ActivityCompat.recreate(it) }
true
}
}
switchPreference {
bindTo(uiPreferences.themeDarkAmoled())
titleRes = R.string.pref_dark_theme_pure_black
visibleIf(uiPreferences.themeMode()) { it != ThemeMode.LIGHT }
onChange {
activity?.let { ActivityCompat.recreate(it) }
true
}
}
}
if (context.isTablet()) {
preferenceCategory {
titleRes = R.string.pref_category_navigation
intListPreference {
bindTo(uiPreferences.sideNavIconAlignment())
titleRes = R.string.pref_side_nav_icon_alignment
entriesRes = arrayOf(
R.string.alignment_top,
R.string.alignment_center,
R.string.alignment_bottom,
)
entryValues = arrayOf("0", "1", "2")
summary = "%s"
}
}
}
preferenceCategory {
titleRes = R.string.pref_category_timestamps
intListPreference {
bindTo(uiPreferences.relativeTime())
titleRes = R.string.pref_relative_format
val values = arrayOf("0", "2", "7")
entryValues = values
entries = values.map {
when (it) {
"0" -> context.getString(R.string.off)
"2" -> context.getString(R.string.pref_relative_time_short)
else -> context.getString(R.string.pref_relative_time_long)
}
}.toTypedArray()
summary = "%s"
}
listPreference {
bindTo(uiPreferences.dateFormat())
titleRes = R.string.pref_date_format
entryValues = arrayOf("", "MM/dd/yy", "dd/MM/yy", "yyyy-MM-dd", "dd MMM yyyy", "MMM dd, yyyy")
val now = Date().time
entries = entryValues.map { value ->
val formattedDate = UiPreferences.dateFormat(value.toString()).format(now)
if (value == "") {
"${context.getString(R.string.label_default)} ($formattedDate)"
} else {
"$value ($formattedDate)"
}
}.toTypedArray()
summary = "%s"
}
}
}
override fun onSaveViewState(view: View, outState: Bundle) {
themesPreference?.let {
outState.putInt(THEMES_SCROLL_POSITION, it.lastScrollPosition ?: 0)
}
super.onSaveInstanceState(outState)
}
override fun onRestoreViewState(view: View, savedViewState: Bundle) {
super.onRestoreViewState(view, savedViewState)
themesPreference?.lastScrollPosition = savedViewState.getInt(THEMES_SCROLL_POSITION, 0)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
themesPreference = null
}
}
private const val THEMES_SCROLL_POSITION = "themesScrollPosition"

View File

@@ -1,306 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.Activity
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.preference.PreferenceScreen
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hippo.unifile.UniFile
import eu.kanade.domain.backup.service.BackupPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.infoPreference
import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.getParcelableCompat
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
class SettingsBackupController : SettingsController() {
/**
* Flags containing information of what to backup.
*/
private var backupFlags = 0
private val backupPreferences: BackupPreferences by injectLazy()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 500)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.label_backup
preference {
key = "pref_create_backup"
titleRes = R.string.pref_create_backup
summaryRes = R.string.pref_create_backup_summ
onClick {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
if (!BackupCreatorJob.isManualJobRunning(context)) {
val ctrl = CreateBackupDialog()
ctrl.targetController = this@SettingsBackupController
ctrl.showDialog(router)
} else {
context.toast(R.string.backup_in_progress)
}
}
}
preference {
key = "pref_restore_backup"
titleRes = R.string.pref_restore_backup
summaryRes = R.string.pref_restore_backup_summ
onClick {
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
}
if (!BackupRestoreService.isRunning(context)) {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}
val title = resources?.getString(R.string.file_select_backup)
val chooser = Intent.createChooser(intent, title)
startActivityForResult(chooser, CODE_BACKUP_RESTORE)
} else {
context.toast(R.string.restore_in_progress)
}
}
}
preferenceCategory {
titleRes = R.string.pref_backup_service_category
intListPreference {
bindTo(backupPreferences.backupInterval())
titleRes = R.string.pref_backup_interval
entriesRes = arrayOf(
R.string.update_6hour,
R.string.update_12hour,
R.string.update_24hour,
R.string.update_48hour,
R.string.update_weekly,
)
entryValues = arrayOf("6", "12", "24", "48", "168")
summary = "%s"
onChange { newValue ->
val interval = (newValue as String).toInt()
BackupCreatorJob.setupTask(context, interval)
true
}
}
preference {
bindTo(backupPreferences.backupsDirectory())
titleRes = R.string.pref_backup_directory
onClick {
try {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, CODE_BACKUP_DIR)
} catch (e: ActivityNotFoundException) {
activity?.toast(R.string.file_picker_error)
}
}
backupPreferences.backupsDirectory().changes()
.onEach { path ->
val dir = UniFile.fromUri(context, path.toUri())
summary = dir.filePath + "/automatic"
}
.launchIn(viewScope)
}
intListPreference {
bindTo(backupPreferences.numberOfBackups())
titleRes = R.string.pref_backup_slots
entries = arrayOf("2", "3", "4", "5")
entryValues = entries
summary = "%s"
}
}
infoPreference(R.string.backup_info)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.settings_backup, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_backup_help -> activity?.openInBrowser(HELP_URL)
}
return super.onOptionsItemSelected(item)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (data != null && resultCode == Activity.RESULT_OK) {
val activity = activity ?: return
val uri = data.data
if (uri == null) {
activity.toast(R.string.backup_restore_invalid_uri)
return
}
when (requestCode) {
CODE_BACKUP_DIR -> {
// Get UriPermission so it's possible to write files
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
activity.contentResolver.takePersistableUriPermission(uri, flags)
backupPreferences.backupsDirectory().set(uri.toString())
}
CODE_BACKUP_CREATE -> {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
activity.contentResolver.takePersistableUriPermission(uri, flags)
BackupCreatorJob.startNow(activity, uri, backupFlags)
}
CODE_BACKUP_RESTORE -> {
RestoreBackupDialog(uri).showDialog(router)
}
}
}
}
fun createBackup(flags: Int) {
backupFlags = flags
try {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("application/*")
.putExtra(Intent.EXTRA_TITLE, Backup.getBackupFilename())
startActivityForResult(intent, CODE_BACKUP_CREATE)
} catch (e: ActivityNotFoundException) {
activity?.toast(R.string.file_picker_error)
}
}
class CreateBackupDialog(bundle: Bundle? = null) : DialogController(bundle) {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val activity = activity!!
val options = arrayOf(
R.string.manga,
R.string.categories,
R.string.chapters,
R.string.track,
R.string.history,
)
.map { activity.getString(it) }
val selected = options.map { true }.toBooleanArray()
return MaterialAlertDialogBuilder(activity)
.setTitle(R.string.backup_choice)
.setMultiChoiceItems(options.toTypedArray(), selected) { dialog, which, checked ->
if (which == 0) {
(dialog as AlertDialog).listView.setItemChecked(which, true)
} else {
selected[which] = checked
}
}
.setPositiveButton(R.string.action_create) { _, _ ->
var flags = 0
selected.forEachIndexed { i, checked ->
if (checked) {
when (i) {
1 -> flags = flags or BackupConst.BACKUP_CATEGORY
2 -> flags = flags or BackupConst.BACKUP_CHAPTER
3 -> flags = flags or BackupConst.BACKUP_TRACK
4 -> flags = flags or BackupConst.BACKUP_HISTORY
}
}
}
(targetController as? SettingsBackupController)?.createBackup(flags)
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}
class RestoreBackupDialog(bundle: Bundle? = null) : DialogController(bundle) {
constructor(uri: Uri) : this(
bundleOf(KEY_URI to uri),
)
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val activity = activity!!
val uri = args.getParcelableCompat<Uri>(KEY_URI)!!
return try {
val results = BackupFileValidator().validate(activity, uri)
var message = activity.getString(R.string.backup_restore_content_full)
if (results.missingSources.isNotEmpty()) {
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}"
}
if (results.missingTrackers.isNotEmpty()) {
message += "\n\n${activity.getString(R.string.backup_restore_missing_trackers)}\n${results.missingTrackers.joinToString("\n") { "- $it" }}"
}
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.pref_restore_backup)
.setMessage(message)
.setPositiveButton(R.string.action_restore) { _, _ ->
BackupRestoreService.start(activity, uri)
}
.create()
} catch (e: Exception) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.invalid_backup_file)
.setMessage(e.message)
.setPositiveButton(android.R.string.cancel, null)
.create()
}
}
}
}
private const val KEY_URI = "RestoreBackupDialog.uri"
private const val CODE_BACKUP_DIR = 503
private const val CODE_BACKUP_CREATE = 504
private const val CODE_BACKUP_RESTORE = 505
private const val HELP_URL = "https://tachiyomi.org/help/guides/backups/"

View File

@@ -1,80 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceScreen
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.infoPreference
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.requireAuthentication
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
import uy.kohesive.injekt.injectLazy
class SettingsBrowseController : SettingsController() {
private val sourcePreferences: SourcePreferences by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.browse
preferenceCategory {
titleRes = R.string.label_sources
switchPreference {
bindTo(sourcePreferences.duplicatePinnedSources())
titleRes = R.string.pref_duplicate_pinned_sources
summaryRes = R.string.pref_duplicate_pinned_sources_summary
}
}
preferenceCategory {
titleRes = R.string.label_extensions
switchPreference {
bindTo(preferences.automaticExtUpdates())
titleRes = R.string.pref_enable_automatic_extension_updates
onChange { newValue ->
val checked = newValue as Boolean
ExtensionUpdateJob.setupTask(activity!!, checked)
true
}
}
}
preferenceCategory {
titleRes = R.string.action_global_search
switchPreference {
bindTo(sourcePreferences.searchPinnedSourcesOnly())
titleRes = R.string.pref_search_pinned_sources_only
}
}
preferenceCategory {
titleRes = R.string.pref_category_nsfw_content
switchPreference {
bindTo(sourcePreferences.showNsfwSource())
titleRes = R.string.pref_show_nsfw_source
summaryRes = R.string.requires_app_restart
if (context.isAuthenticationSupported() && activity != null) {
requireAuthentication(
activity as? FragmentActivity,
context.getString(R.string.pref_category_nsfw_content),
context.getString(R.string.confirm_lock_change),
)
}
}
infoPreference(R.string.parental_controls_info)
}
}
}

View File

@@ -1,124 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.animation.doOnEnd
import androidx.preference.Preference
import androidx.preference.PreferenceController
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceScreen
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.preference.asHotFlow
import eu.kanade.tachiyomi.util.system.getResourceColor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
abstract class SettingsController : PreferenceController() {
var preferenceKey: String? = null
val preferences: BasePreferences = Injekt.get()
val viewScope: CoroutineScope = MainScope()
private var themedContext: Context? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
val view = super.onCreateView(inflater, container, savedInstanceState)
listView.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return view
}
override fun onAttach(view: View) {
super.onAttach(view)
preferenceKey?.let { prefKey ->
val adapter = listView.adapter
scrollToPreference(prefKey)
listView.post {
if (adapter is PreferenceGroup.PreferencePositionCallback) {
val pos = adapter.getPreferenceAdapterPosition(prefKey)
listView.findViewHolderForAdapterPosition(pos)?.let {
animatePreferenceHighlight(it.itemView)
}
}
// Explicitly clear it to avoid re-scrolling/animating on activity recreations
preferenceKey = null
}
}
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) {
setTitle()
}
setHasOptionsMenu(type.isEnter)
super.onChangeStarted(handler, type)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
viewScope.cancel()
themedContext = null
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val tv = TypedValue()
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
themedContext = ContextThemeWrapper(activity, tv.resourceId)
val screen = preferenceManager.createPreferenceScreen(themedContext!!)
preferenceScreen = screen
setupPreferenceScreen(screen)
}
abstract fun setupPreferenceScreen(screen: PreferenceScreen): PreferenceScreen
private fun animatePreferenceHighlight(view: View) {
val origBackground = view.background
ValueAnimator
.ofObject(ArgbEvaluator(), Color.TRANSPARENT, view.context.getResourceColor(R.attr.colorControlHighlight))
.apply {
duration = 200L
repeatCount = 5
repeatMode = ValueAnimator.REVERSE
addUpdateListener { animator -> view.setBackgroundColor(animator.animatedValue as Int) }
start()
}
.doOnEnd {
// Restore original ripple
view.background = origBackground
}
}
private fun setTitle() {
(activity as? AppCompatActivity)?.supportActionBar?.title = preferenceScreen?.title?.toString()
}
inline fun <T> Preference.visibleIf(preference: eu.kanade.tachiyomi.core.preference.Preference<T>, crossinline block: (T) -> Boolean) {
preference.asHotFlow { isVisible = block(it) }
.launchIn(viewScope)
}
}

View File

@@ -1,315 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import android.app.Activity
import android.app.Dialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.os.Environment
import androidx.core.net.toUri
import androidx.core.text.buildSpannedString
import androidx.preference.PreferenceScreen
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hippo.unifile.UniFile
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.presentation.category.visualName
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.infoPreference
import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.multiSelectListPreference
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
class SettingsDownloadController : SettingsController() {
private val getCategories: GetCategories by injectLazy()
private val downloadPreferences: DownloadPreferences by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_downloads
val categories = runBlocking { getCategories.await() }
preference {
bindTo(downloadPreferences.downloadsDirectory())
titleRes = R.string.pref_download_directory
onClick {
val ctrl = DownloadDirectoriesDialog()
ctrl.targetController = this@SettingsDownloadController
ctrl.showDialog(router)
}
downloadPreferences.downloadsDirectory().changes()
.onEach { path ->
val dir = UniFile.fromUri(context, path.toUri())
summary = dir.filePath ?: path
}
.launchIn(viewScope)
}
switchPreference {
bindTo(downloadPreferences.downloadOnlyOverWifi())
titleRes = R.string.connected_to_wifi
}
switchPreference {
bindTo(downloadPreferences.saveChaptersAsCBZ())
titleRes = R.string.save_chapter_as_cbz
}
switchPreference {
bindTo(downloadPreferences.splitTallImages())
titleRes = R.string.split_tall_images
summaryRes = R.string.split_tall_images_summary
}
preferenceCategory {
titleRes = R.string.pref_category_delete_chapters
switchPreference {
bindTo(downloadPreferences.removeAfterMarkedAsRead())
titleRes = R.string.pref_remove_after_marked_as_read
}
intListPreference {
bindTo(downloadPreferences.removeAfterReadSlots())
titleRes = R.string.pref_remove_after_read
entriesRes = arrayOf(
R.string.disabled,
R.string.last_read_chapter,
R.string.second_to_last,
R.string.third_to_last,
R.string.fourth_to_last,
R.string.fifth_to_last,
)
entryValues = arrayOf("-1", "0", "1", "2", "3", "4")
summary = "%s"
}
switchPreference {
bindTo(downloadPreferences.removeBookmarkedChapters())
titleRes = R.string.pref_remove_bookmarked_chapters
}
multiSelectListPreference {
bindTo(downloadPreferences.removeExcludeCategories())
titleRes = R.string.pref_remove_exclude_categories
entries = categories.map { it.visualName(context) }.toTypedArray()
entryValues = categories.map { it.id.toString() }.toTypedArray()
downloadPreferences.removeExcludeCategories().changes()
.onEach { mutable ->
val selected = mutable
.mapNotNull { id -> categories.find { it.id == id.toLong() } }
.sortedBy { it.order }
summary = if (selected.isEmpty()) {
resources?.getString(R.string.none)
} else {
selected.joinToString { it.visualName(context) }
}
}.launchIn(viewScope)
}
}
preferenceCategory {
titleRes = R.string.pref_download_new
switchPreference {
bindTo(downloadPreferences.downloadNewChapters())
titleRes = R.string.pref_download_new
}
preference {
bindTo(downloadPreferences.downloadNewChapterCategories())
titleRes = R.string.categories
onClick {
DownloadCategoriesDialog().showDialog(router)
}
visibleIf(downloadPreferences.downloadNewChapters()) { it }
fun updateSummary() {
val selectedCategories = downloadPreferences.downloadNewChapterCategories().get()
.mapNotNull { id -> categories.find { it.id == id.toLong() } }
.sortedBy { it.order }
val includedItemsText = if (selectedCategories.isEmpty()) {
context.getString(R.string.all)
} else {
selectedCategories.joinToString { it.visualName(context) }
}
val excludedCategories = downloadPreferences.downloadNewChapterCategoriesExclude().get()
.mapNotNull { id -> categories.find { it.id == id.toLong() } }
.sortedBy { it.order }
val excludedItemsText = if (excludedCategories.isEmpty()) {
context.getString(R.string.none)
} else {
excludedCategories.joinToString { it.visualName(context) }
}
summary = buildSpannedString {
append(context.getString(R.string.include, includedItemsText))
appendLine()
append(context.getString(R.string.exclude, excludedItemsText))
}
}
downloadPreferences.downloadNewChapterCategories().changes()
.onEach { updateSummary() }
.launchIn(viewScope)
downloadPreferences.downloadNewChapterCategoriesExclude().changes()
.onEach { updateSummary() }
.launchIn(viewScope)
}
}
preferenceCategory {
titleRes = R.string.download_ahead
intListPreference {
bindTo(downloadPreferences.autoDownloadWhileReading())
titleRes = R.string.auto_download_while_reading
entries = arrayOf(
context.getString(R.string.disabled),
context.resources.getQuantityString(R.plurals.next_unread_chapters, 2, 2),
context.resources.getQuantityString(R.plurals.next_unread_chapters, 3, 3),
context.resources.getQuantityString(R.plurals.next_unread_chapters, 5, 5),
context.resources.getQuantityString(R.plurals.next_unread_chapters, 10, 10),
)
entryValues = arrayOf("0", "2", "3", "5", "10")
summary = "%s"
}
infoPreference(R.string.download_ahead_info)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
DOWNLOAD_DIR -> if (data != null && resultCode == Activity.RESULT_OK) {
val context = applicationContext ?: return
val uri = data.data
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
if (uri != null) {
@Suppress("NewApi")
context.contentResolver.takePersistableUriPermission(uri, flags)
}
val file = UniFile.fromUri(context, uri)
downloadPreferences.downloadsDirectory().set(file.uri.toString())
}
}
}
fun predefinedDirectorySelected(selectedDir: String) {
val path = File(selectedDir).toUri()
downloadPreferences.downloadsDirectory().set(path.toString())
}
fun customDirectorySelected() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
try {
startActivityForResult(intent, DOWNLOAD_DIR)
} catch (e: ActivityNotFoundException) {
activity?.toast(R.string.file_picker_error)
}
}
class DownloadDirectoriesDialog : DialogController() {
private val downloadPreferences: DownloadPreferences = Injekt.get()
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val activity = activity!!
val currentDir = downloadPreferences.downloadsDirectory().get()
val externalDirs = listOf(getDefaultDownloadDir(), File(activity.getString(R.string.custom_dir))).map(File::toString)
var selectedIndex = externalDirs.indexOfFirst { it in currentDir }
return MaterialAlertDialogBuilder(activity)
.setTitle(R.string.pref_download_directory)
.setSingleChoiceItems(externalDirs.toTypedArray(), selectedIndex) { _, which ->
selectedIndex = which
}
.setPositiveButton(android.R.string.ok) { _, _ ->
val target = targetController as? SettingsDownloadController
if (selectedIndex == externalDirs.lastIndex) {
target?.customDirectorySelected()
} else {
target?.predefinedDirectorySelected(externalDirs[selectedIndex])
}
}
.create()
}
private fun getDefaultDownloadDir(): File {
val defaultDir = Environment.getExternalStorageDirectory().absolutePath +
File.separator + resources?.getString(R.string.app_name) +
File.separator + "downloads"
return File(defaultDir)
}
}
class DownloadCategoriesDialog : DialogController() {
private val downloadPreferences: DownloadPreferences = Injekt.get()
private val getCategories: GetCategories = Injekt.get()
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val categories = runBlocking { getCategories.await() }
val items = categories.map { it.visualName(activity!!) }
var selected = categories
.map {
when (it.id.toString()) {
in downloadPreferences.downloadNewChapterCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
in downloadPreferences.downloadNewChapterCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
}
}
.toIntArray()
return MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.categories)
.setQuadStateMultiChoiceItems(
message = R.string.pref_download_new_categories_details,
items = items,
initialSelected = selected,
) { selections ->
selected = selections
}
.setPositiveButton(android.R.string.ok) { _, _ ->
val included = selected
.mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) index else null }
.filterNotNull()
.map { categories[it].id.toString() }
.toSet()
val excluded = selected
.mapIndexed { index, value -> if (value == QuadStateTextView.State.INVERSED.ordinal) index else null }
.filterNotNull()
.map { categories[it].id.toString() }
.toSet()
downloadPreferences.downloadNewChapterCategories().set(included)
downloadPreferences.downloadNewChapterCategoriesExclude().set(excluded)
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}
}
private const val DOWNLOAD_DIR = 104

View File

@@ -1,92 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceScreen
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.listPreference
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.LocaleHelper
import org.xmlpull.v1.XmlPullParser
import uy.kohesive.injekt.injectLazy
class SettingsGeneralController : SettingsController() {
private val libraryPreferences: LibraryPreferences by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_general
switchPreference {
bindTo(libraryPreferences.showUpdatesNavBadge())
titleRes = R.string.pref_library_update_show_tab_badge
}
switchPreference {
bindTo(preferences.confirmExit())
titleRes = R.string.pref_confirm_exit
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
preference {
key = "pref_manage_notifications"
titleRes = R.string.pref_manage_notifications
onClick {
val intent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
}
startActivity(intent)
}
}
}
listPreference {
key = "app_lang"
isPersistent = false
titleRes = R.string.pref_app_language
val langs = mutableListOf<Pair<String, String>>()
val parser = context.resources.getXml(R.xml.locales_config)
var eventType = parser.eventType
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && parser.name == "locale") {
for (i in 0 until parser.attributeCount) {
if (parser.getAttributeName(i) == "name") {
val langTag = parser.getAttributeValue(i)
val displayName = LocaleHelper.getDisplayName(langTag)
if (displayName.isNotEmpty()) {
langs.add(Pair(langTag, displayName))
}
}
}
}
eventType = parser.next()
}
langs.sortBy { it.second }
langs.add(0, Pair("", context.getString(R.string.label_default)))
entryValues = langs.map { it.first }.toTypedArray()
entries = langs.map { it.second }.toTypedArray()
summary = "%s"
value = AppCompatDelegate.getApplicationLocales().get(0)?.toLanguageTag() ?: ""
onChange { newValue ->
val locale = if ((newValue as String).isEmpty()) {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(newValue)
}
AppCompatDelegate.setApplicationLocales(locale)
true
}
}
}
}

View File

@@ -1,388 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import androidx.core.content.ContextCompat
import androidx.core.text.buildSpannedString
import androidx.preference.PreferenceScreen
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.domain.category.interactor.GetCategories
import eu.kanade.domain.category.interactor.ResetCategoryFlags
import eu.kanade.domain.category.model.Category
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.presentation.category.visualName
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.DEVICE_BATTERY_NOT_LOW
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
import eu.kanade.tachiyomi.data.preference.DEVICE_NETWORK_NOT_METERED
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.databinding.PrefLibraryColumnsBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.category.CategoryController
import eu.kanade.tachiyomi.util.preference.bindTo
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.multiSelectListPreference
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class SettingsLibraryController : SettingsController() {
private val getCategories: GetCategories by injectLazy()
private val trackManager: TrackManager by injectLazy()
private val resetCategoryFlags: ResetCategoryFlags by injectLazy()
private val libraryPreferences: LibraryPreferences by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_library
val allCategories = runBlocking { getCategories.await() }
val userCategories = allCategories.filterNot(Category::isSystemCategory)
preferenceCategory {
titleRes = R.string.pref_category_display
preference {
key = "pref_library_columns"
titleRes = R.string.pref_library_columns
onClick {
LibraryColumnsDialog().showDialog(router)
}
fun getColumnValue(value: Int): String {
return if (value == 0) {
context.getString(R.string.label_default)
} else {
value.toString()
}
}
combine(libraryPreferences.portraitColumns().changes(), libraryPreferences.landscapeColumns().changes()) { portraitCols, landscapeCols -> Pair(portraitCols, landscapeCols) }
.onEach { (portraitCols, landscapeCols) ->
val portrait = getColumnValue(portraitCols)
val landscape = getColumnValue(landscapeCols)
summary = "${context.getString(R.string.portrait)}: $portrait, " +
"${context.getString(R.string.landscape)}: $landscape"
}
.launchIn(viewScope)
}
}
preferenceCategory {
titleRes = R.string.categories
preference {
key = "pref_action_edit_categories"
titleRes = R.string.action_edit_categories
val catCount = userCategories.size
summary = context.resources.getQuantityString(R.plurals.num_categories, catCount, catCount)
onClick {
router.pushController(CategoryController())
}
}
intListPreference {
val defaultCategory = libraryPreferences.defaultCategory()
bindTo(defaultCategory)
titleRes = R.string.default_category
entries = arrayOf(context.getString(R.string.default_category_summary)) +
allCategories.map { it.visualName(context) }.toTypedArray()
entryValues = arrayOf(defaultCategory.defaultValue().toString()) + allCategories.map { it.id.toString() }.toTypedArray()
val selectedCategory = allCategories.find { it.id == defaultCategory.get().toLong() }
summary = selectedCategory?.visualName(context)
?: context.getString(R.string.default_category_summary)
onChange { newValue ->
summary = allCategories.find {
it.id == (newValue as String).toLong()
}?.visualName(context) ?: context.getString(R.string.default_category_summary)
true
}
}
switchPreference {
bindTo(libraryPreferences.categorizedDisplaySettings())
titleRes = R.string.categorized_display_settings
libraryPreferences.categorizedDisplaySettings().changes()
.onEach {
if (it.not()) {
resetCategoryFlags.await()
}
}
.launchIn(viewScope)
}
}
preferenceCategory {
titleRes = R.string.pref_category_library_update
intListPreference {
bindTo(libraryPreferences.libraryUpdateInterval())
titleRes = R.string.pref_library_update_interval
entriesRes = arrayOf(
R.string.update_never,
R.string.update_12hour,
R.string.update_24hour,
R.string.update_48hour,
R.string.update_72hour,
R.string.update_weekly,
)
entryValues = arrayOf("0", "12", "24", "48", "72", "168")
summary = "%s"
onChange { newValue ->
val interval = (newValue as String).toInt()
LibraryUpdateJob.setupTask(context, interval)
true
}
}
multiSelectListPreference {
bindTo(libraryPreferences.libraryUpdateDeviceRestriction())
titleRes = R.string.pref_library_update_restriction
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.network_not_metered, R.string.charging, R.string.battery_not_low)
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_NETWORK_NOT_METERED, DEVICE_CHARGING, DEVICE_BATTERY_NOT_LOW)
visibleIf(libraryPreferences.libraryUpdateInterval()) { it > 0 }
onChange {
// Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
true
}
fun updateSummary() {
val restrictions = libraryPreferences.libraryUpdateDeviceRestriction().get()
.sorted()
.map {
when (it) {
DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi)
DEVICE_NETWORK_NOT_METERED -> context.getString(R.string.network_not_metered)
DEVICE_CHARGING -> context.getString(R.string.charging)
DEVICE_BATTERY_NOT_LOW -> context.getString(R.string.battery_not_low)
else -> it
}
}
val restrictionsText = if (restrictions.isEmpty()) {
context.getString(R.string.none)
} else {
restrictions.joinToString()
}
summary = context.getString(R.string.restrictions, restrictionsText)
}
libraryPreferences.libraryUpdateDeviceRestriction().changes()
.onEach { updateSummary() }
.launchIn(viewScope)
}
multiSelectListPreference {
bindTo(libraryPreferences.libraryUpdateMangaRestriction())
titleRes = R.string.pref_library_update_manga_restriction
entriesRes = arrayOf(R.string.pref_update_only_completely_read, R.string.pref_update_only_started, R.string.pref_update_only_non_completed)
entryValues = arrayOf(MANGA_HAS_UNREAD, MANGA_NON_READ, MANGA_NON_COMPLETED)
fun updateSummary() {
val restrictions = libraryPreferences.libraryUpdateMangaRestriction().get().sorted()
.map {
when (it) {
MANGA_NON_READ -> context.getString(R.string.pref_update_only_started)
MANGA_HAS_UNREAD -> context.getString(R.string.pref_update_only_completely_read)
MANGA_NON_COMPLETED -> context.getString(R.string.pref_update_only_non_completed)
else -> it
}
}
val restrictionsText = if (restrictions.isEmpty()) {
context.getString(R.string.none)
} else {
restrictions.joinToString()
}
summary = restrictionsText
}
libraryPreferences.libraryUpdateMangaRestriction().changes()
.onEach { updateSummary() }
.launchIn(viewScope)
}
preference {
bindTo(libraryPreferences.libraryUpdateCategories())
titleRes = R.string.categories
onClick {
LibraryGlobalUpdateCategoriesDialog().showDialog(router)
}
fun updateSummary() {
val includedCategories = libraryPreferences.libraryUpdateCategories().get()
.mapNotNull { id -> allCategories.find { it.id == id.toLong() } }
.sortedBy { it.order }
val excludedCategories = libraryPreferences.libraryUpdateCategoriesExclude().get()
.mapNotNull { id -> allCategories.find { it.id == id.toLong() } }
.sortedBy { it.order }
val allExcluded = excludedCategories.size == allCategories.size
val includedItemsText = when {
// Some selected, but not all
includedCategories.isNotEmpty() && includedCategories.size != allCategories.size -> includedCategories.joinToString { it.visualName(context) }
// All explicitly selected
includedCategories.size == allCategories.size -> context.getString(R.string.all)
allExcluded -> context.getString(R.string.none)
else -> context.getString(R.string.all)
}
val excludedItemsText = when {
excludedCategories.isEmpty() -> context.getString(R.string.none)
allExcluded -> context.getString(R.string.all)
else -> excludedCategories.joinToString { it.visualName(context) }
}
summary = buildSpannedString {
append(context.getString(R.string.include, includedItemsText))
appendLine()
append(context.getString(R.string.exclude, excludedItemsText))
}
}
libraryPreferences.libraryUpdateCategories().changes()
.onEach { updateSummary() }
.launchIn(viewScope)
libraryPreferences.libraryUpdateCategoriesExclude().changes()
.onEach { updateSummary() }
.launchIn(viewScope)
}
switchPreference {
bindTo(libraryPreferences.autoUpdateMetadata())
titleRes = R.string.pref_library_update_refresh_metadata
summaryRes = R.string.pref_library_update_refresh_metadata_summary
}
if (trackManager.hasLoggedServices()) {
switchPreference {
bindTo(libraryPreferences.autoUpdateTrackers())
titleRes = R.string.pref_library_update_refresh_trackers
summaryRes = R.string.pref_library_update_refresh_trackers_summary
}
}
}
}
class LibraryColumnsDialog : DialogController() {
private val preferences: LibraryPreferences = Injekt.get()
private var portrait = preferences.portraitColumns().get()
private var landscape = preferences.landscapeColumns().get()
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val binding = PrefLibraryColumnsBinding.inflate(LayoutInflater.from(activity!!))
onViewCreated(binding)
return MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.pref_library_columns)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
preferences.portraitColumns().set(portrait)
preferences.landscapeColumns().set(landscape)
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
fun onViewCreated(binding: PrefLibraryColumnsBinding) {
with(binding.portraitColumns) {
displayedValues = arrayOf(context.getString(R.string.label_default)) +
IntRange(1, 10).map(Int::toString)
value = portrait
setOnValueChangedListener { _, _, newValue ->
portrait = newValue
}
}
with(binding.landscapeColumns) {
displayedValues = arrayOf(context.getString(R.string.label_default)) +
IntRange(1, 10).map(Int::toString)
value = landscape
setOnValueChangedListener { _, _, newValue ->
landscape = newValue
}
}
}
}
class LibraryGlobalUpdateCategoriesDialog : DialogController() {
private val preferences: LibraryPreferences = Injekt.get()
private val getCategories: GetCategories = Injekt.get()
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val categories = runBlocking { getCategories.await() }
val items = categories.map { it.visualName(activity!!) }
var selected = categories
.map {
when (it.id.toString()) {
in preferences.libraryUpdateCategories()
.get(),
-> QuadStateTextView.State.CHECKED.ordinal
in preferences.libraryUpdateCategoriesExclude()
.get(),
-> QuadStateTextView.State.INVERSED.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
}
}
.toIntArray()
return MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.categories)
.setQuadStateMultiChoiceItems(
message = R.string.pref_library_update_categories_details,
items = items,
initialSelected = selected,
) { selections ->
selected = selections
}
.setPositiveButton(android.R.string.ok) { _, _ ->
val included = selected
.mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) index else null }
.filterNotNull()
.map { categories[it].id.toString() }
.toSet()
val excluded = selected
.mapIndexed { index, value -> if (value == QuadStateTextView.State.INVERSED.ordinal) index else null }
.filterNotNull()
.map { categories[it].id.toString() }
.toSet()
preferences.libraryUpdateCategories().set(included)
preferences.libraryUpdateCategoriesExclude().set(excluded)
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}
}

View File

@@ -1,325 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import android.os.Build
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferenceValues.TappingInvertMode
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.util.preference.bindTo
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.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import uy.kohesive.injekt.injectLazy
class SettingsReaderController : SettingsController() {
private val readerPreferences: ReaderPreferences by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_reader
intListPreference {
bindTo(readerPreferences.defaultReadingMode())
titleRes = R.string.pref_viewer_type
entriesRes = arrayOf(
R.string.left_to_right_viewer,
R.string.right_to_left_viewer,
R.string.vertical_viewer,
R.string.webtoon_viewer,
R.string.vertical_plus_viewer,
)
entryValues = ReadingModeType.values().drop(1)
.map { value -> "${value.flagValue}" }.toTypedArray()
summary = "%s"
}
intListPreference {
bindTo(readerPreferences.doubleTapAnimSpeed())
titleRes = R.string.pref_double_tap_anim_speed
entries = arrayOf(context.getString(R.string.double_tap_anim_speed_0), context.getString(R.string.double_tap_anim_speed_normal), context.getString(R.string.double_tap_anim_speed_fast))
entryValues = arrayOf("1", "500", "250") // using a value of 0 breaks the image viewer, so min is 1
summary = "%s"
}
switchPreference {
bindTo(readerPreferences.showReadingMode())
titleRes = R.string.pref_show_reading_mode
summaryRes = R.string.pref_show_reading_mode_summary
}
switchPreference {
bindTo(readerPreferences.showNavigationOverlayOnStart())
titleRes = R.string.pref_show_navigation_mode
summaryRes = R.string.pref_show_navigation_mode_summary
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
switchPreference {
bindTo(readerPreferences.trueColor())
titleRes = R.string.pref_true_color
summaryRes = R.string.pref_true_color_summary
}
}
switchPreference {
bindTo(readerPreferences.pageTransitions())
titleRes = R.string.pref_page_transitions
}
preferenceCategory {
titleRes = R.string.pref_category_display
intListPreference {
bindTo(readerPreferences.defaultOrientationType())
titleRes = R.string.pref_rotation_type
entriesRes = arrayOf(
R.string.rotation_free,
R.string.rotation_portrait,
R.string.rotation_reverse_portrait,
R.string.rotation_landscape,
R.string.rotation_force_portrait,
R.string.rotation_force_landscape,
)
entryValues = OrientationType.values().drop(1)
.map { value -> "${value.flagValue}" }.toTypedArray()
summary = "%s"
}
intListPreference {
bindTo(readerPreferences.readerTheme())
titleRes = R.string.pref_reader_theme
entriesRes = arrayOf(R.string.black_background, R.string.gray_background, R.string.white_background, R.string.automatic_background)
entryValues = arrayOf("1", "2", "0", "3")
summary = "%s"
}
switchPreference {
bindTo(readerPreferences.fullscreen())
titleRes = R.string.pref_fullscreen
}
if (activity?.hasDisplayCutout() == true) {
switchPreference {
bindTo(readerPreferences.cutoutShort())
titleRes = R.string.pref_cutout_short
visibleIf(readerPreferences.fullscreen()) { it }
}
}
switchPreference {
bindTo(readerPreferences.keepScreenOn())
titleRes = R.string.pref_keep_screen_on
}
switchPreference {
bindTo(readerPreferences.showPageNumber())
titleRes = R.string.pref_show_page_number
}
}
preferenceCategory {
titleRes = R.string.pref_category_reading
switchPreference {
bindTo(readerPreferences.skipRead())
titleRes = R.string.pref_skip_read_chapters
}
switchPreference {
bindTo(readerPreferences.skipFiltered())
titleRes = R.string.pref_skip_filtered_chapters
}
switchPreference {
bindTo(readerPreferences.alwaysShowChapterTransition())
titleRes = R.string.pref_always_show_chapter_transition
}
}
preferenceCategory {
titleRes = R.string.pager_viewer
intListPreference {
bindTo(readerPreferences.navigationModePager())
titleRes = R.string.pref_viewer_nav
entries = context.resources.getStringArray(R.array.pager_nav).also { values ->
entryValues = values.indices.map { index -> "$index" }.toTypedArray()
}
summary = "%s"
}
listPreference {
bindTo(readerPreferences.pagerNavInverted())
titleRes = R.string.pref_read_with_tapping_inverted
entriesRes = arrayOf(
R.string.tapping_inverted_none,
R.string.tapping_inverted_horizontal,
R.string.tapping_inverted_vertical,
R.string.tapping_inverted_both,
)
entryValues = arrayOf(
TappingInvertMode.NONE.name,
TappingInvertMode.HORIZONTAL.name,
TappingInvertMode.VERTICAL.name,
TappingInvertMode.BOTH.name,
)
summary = "%s"
visibleIf(readerPreferences.navigationModePager()) { it != 5 }
}
switchPreference {
bindTo(readerPreferences.navigateToPan())
titleRes = R.string.pref_navigate_pan
visibleIf(readerPreferences.navigationModePager()) { it != 5 }
}
intListPreference {
bindTo(readerPreferences.imageScaleType())
titleRes = R.string.pref_image_scale_type
entriesRes = arrayOf(
R.string.scale_type_fit_screen,
R.string.scale_type_stretch,
R.string.scale_type_fit_width,
R.string.scale_type_fit_height,
R.string.scale_type_original_size,
R.string.scale_type_smart_fit,
)
entryValues = arrayOf("1", "2", "3", "4", "5", "6")
summary = "%s"
}
switchPreference {
bindTo(readerPreferences.landscapeZoom())
titleRes = R.string.pref_landscape_zoom
visibleIf(readerPreferences.imageScaleType()) { it == 1 }
}
intListPreference {
bindTo(readerPreferences.zoomStart())
titleRes = R.string.pref_zoom_start
entriesRes = arrayOf(
R.string.zoom_start_automatic,
R.string.zoom_start_left,
R.string.zoom_start_right,
R.string.zoom_start_center,
)
entryValues = arrayOf("1", "2", "3", "4")
summary = "%s"
}
switchPreference {
bindTo(readerPreferences.cropBorders())
titleRes = R.string.pref_crop_borders
}
switchPreference {
bindTo(readerPreferences.dualPageSplitPaged())
titleRes = R.string.pref_dual_page_split
}
switchPreference {
bindTo(readerPreferences.dualPageInvertPaged())
titleRes = R.string.pref_dual_page_invert
summaryRes = R.string.pref_dual_page_invert_summary
visibleIf(readerPreferences.dualPageSplitPaged()) { it }
}
}
preferenceCategory {
titleRes = R.string.webtoon_viewer
intListPreference {
bindTo(readerPreferences.navigationModeWebtoon())
titleRes = R.string.pref_viewer_nav
entries = context.resources.getStringArray(R.array.webtoon_nav).also { values ->
entryValues = values.indices.map { index -> "$index" }.toTypedArray()
}
summary = "%s"
}
listPreference {
bindTo(readerPreferences.webtoonNavInverted())
titleRes = R.string.pref_read_with_tapping_inverted
entriesRes = arrayOf(
R.string.tapping_inverted_none,
R.string.tapping_inverted_horizontal,
R.string.tapping_inverted_vertical,
R.string.tapping_inverted_both,
)
entryValues = arrayOf(
TappingInvertMode.NONE.name,
TappingInvertMode.HORIZONTAL.name,
TappingInvertMode.VERTICAL.name,
TappingInvertMode.BOTH.name,
)
summary = "%s"
visibleIf(readerPreferences.navigationModeWebtoon()) { it != 5 }
}
intListPreference {
bindTo(readerPreferences.webtoonSidePadding())
titleRes = R.string.pref_webtoon_side_padding
entriesRes = arrayOf(
R.string.webtoon_side_padding_0,
R.string.webtoon_side_padding_5,
R.string.webtoon_side_padding_10,
R.string.webtoon_side_padding_15,
R.string.webtoon_side_padding_20,
R.string.webtoon_side_padding_25,
)
entryValues = arrayOf("0", "5", "10", "15", "20", "25")
summary = "%s"
}
listPreference {
bindTo(readerPreferences.readerHideThreshold())
titleRes = R.string.pref_hide_threshold
entriesRes = arrayOf(
R.string.pref_highest,
R.string.pref_high,
R.string.pref_low,
R.string.pref_lowest,
)
entryValues = PreferenceValues.ReaderHideThreshold.values()
.map { it.name }
.toTypedArray()
summary = "%s"
}
switchPreference {
bindTo(readerPreferences.cropBordersWebtoon())
titleRes = R.string.pref_crop_borders
}
switchPreference {
bindTo(readerPreferences.dualPageSplitWebtoon())
titleRes = R.string.pref_dual_page_split
}
switchPreference {
bindTo(readerPreferences.dualPageInvertWebtoon())
titleRes = R.string.pref_dual_page_invert
summaryRes = R.string.pref_dual_page_invert_summary
visibleIf(readerPreferences.dualPageSplitWebtoon()) { it }
}
switchPreference {
bindTo(readerPreferences.longStripSplitWebtoon())
titleRes = R.string.pref_long_strip_split
summaryRes = R.string.split_tall_images_summary
}
}
preferenceCategory {
titleRes = R.string.pref_reader_navigation
switchPreference {
bindTo(readerPreferences.readWithVolumeKeys())
titleRes = R.string.pref_read_with_volume_keys
}
switchPreference {
bindTo(readerPreferences.readWithVolumeKeysInverted())
titleRes = R.string.pref_read_with_volume_keys_inverted
visibleIf(readerPreferences.readWithVolumeKeys()) { it }
}
}
preferenceCategory {
titleRes = R.string.pref_reader_actions
switchPreference {
bindTo(readerPreferences.readWithLongTap())
titleRes = R.string.pref_read_with_long_tap
}
switchPreference {
bindTo(readerPreferences.folderPerManga())
titleRes = R.string.pref_create_folder_per_manga
summaryRes = R.string.pref_create_folder_per_manga_summary
}
}
}
}

View File

@@ -1,102 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import androidx.preference.Preference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.entriesRes
import eu.kanade.tachiyomi.util.preference.infoPreference
import eu.kanade.tachiyomi.util.preference.intListPreference
import eu.kanade.tachiyomi.util.preference.listPreference
import eu.kanade.tachiyomi.util.preference.requireAuthentication
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.isAuthenticationSupported
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.startAuthentication
import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.injectLazy
class SettingsSecurityController : SettingsController() {
private val securityPreferences: SecurityPreferences by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_security
if (context.isAuthenticationSupported()) {
switchPreference {
bindTo(securityPreferences.useAuthenticator())
titleRes = R.string.lock_with_biometrics
requireAuthentication(
activity as? FragmentActivity,
context.getString(R.string.lock_with_biometrics),
context.getString(R.string.confirm_lock_change),
)
}
intListPreference {
bindTo(securityPreferences.lockAppAfter())
titleRes = R.string.lock_when_idle
val values = arrayOf("0", "1", "2", "5", "10", "-1")
entries = values.mapNotNull {
when (it) {
"-1" -> context.getString(R.string.lock_never)
"0" -> context.getString(R.string.lock_always)
else -> resources?.getQuantityString(R.plurals.lock_after_mins, it.toInt(), it)
}
}.toTypedArray()
entryValues = values
summary = "%s"
onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
if (value == newValue) return@OnPreferenceChangeListener false
(activity as? FragmentActivity)?.startAuthentication(
activity!!.getString(R.string.lock_when_idle),
activity!!.getString(R.string.confirm_lock_change),
callback = object : AuthenticatorUtil.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
activity: FragmentActivity?,
result: BiometricPrompt.AuthenticationResult,
) {
super.onAuthenticationSucceeded(activity, result)
value = newValue as String
}
override fun onAuthenticationError(
activity: FragmentActivity?,
errorCode: Int,
errString: CharSequence,
) {
super.onAuthenticationError(activity, errorCode, errString)
activity?.toast(errString.toString())
}
},
)
false
}
visibleIf(securityPreferences.useAuthenticator()) { it }
}
}
switchPreference {
bindTo(securityPreferences.hideNotificationContent())
titleRes = R.string.hide_notification_content
}
listPreference {
bindTo(securityPreferences.secureScreen())
titleRes = R.string.secure_screen
summary = "%s"
entriesRes = SecurityPreferences.SecureScreenMode.values().map { it.titleResId }.toTypedArray()
entryValues = SecurityPreferences.SecureScreenMode.values().map { it.name }.toTypedArray()
}
infoPreference(R.string.secure_screen_summary)
}
}

View File

@@ -1,163 +0,0 @@
package eu.kanade.tachiyomi.ui.setting
import android.app.Activity
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.widget.Toast
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceScreen
import eu.kanade.domain.track.service.TrackPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.NoLoginTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeListApi
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.setting.track.TrackLoginDialog
import eu.kanade.tachiyomi.ui.setting.track.TrackLogoutDialog
import eu.kanade.tachiyomi.util.preference.add
import eu.kanade.tachiyomi.util.preference.bindTo
import eu.kanade.tachiyomi.util.preference.iconRes
import eu.kanade.tachiyomi.util.preference.infoPreference
import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.preference.TrackerPreference
import uy.kohesive.injekt.injectLazy
class SettingsTrackingController :
SettingsController(),
TrackLoginDialog.Listener,
TrackLogoutDialog.Listener {
private val trackManager: TrackManager by injectLazy()
private val trackPreferences: TrackPreferences by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_tracking
switchPreference {
bindTo(trackPreferences.autoUpdateTrack())
titleRes = R.string.pref_auto_update_manga_sync
}
preferenceCategory {
titleRes = R.string.services
trackPreference(trackManager.myAnimeList) {
activity?.openInBrowser(MyAnimeListApi.authUrl(), forceDefaultBrowser = true)
}
trackPreference(trackManager.aniList) {
activity?.openInBrowser(AnilistApi.authUrl(), forceDefaultBrowser = true)
}
trackPreference(trackManager.kitsu) {
val dialog = TrackLoginDialog(trackManager.kitsu, R.string.email)
dialog.targetController = this@SettingsTrackingController
dialog.showDialog(router)
}
trackPreference(trackManager.mangaUpdates) {
val dialog = TrackLoginDialog(trackManager.mangaUpdates, R.string.username)
dialog.targetController = this@SettingsTrackingController
dialog.showDialog(router)
}
trackPreference(trackManager.shikimori) {
activity?.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true)
}
trackPreference(trackManager.bangumi) {
activity?.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true)
}
infoPreference(R.string.tracking_info)
}
preferenceCategory {
titleRes = R.string.enhanced_services
trackPreference(trackManager.komga) {
val acceptedSources = trackManager.komga.getAcceptedSources()
val hasValidSourceInstalled = sourceManager.getCatalogueSources()
.any { it::class.qualifiedName in acceptedSources }
if (hasValidSourceInstalled) {
trackManager.komga.loginNoop()
updatePreference(trackManager.komga.id)
} else {
context.toast(R.string.tracker_komga_warning, Toast.LENGTH_LONG)
}
}
infoPreference(R.string.enhanced_tracking_info)
}
}
private inline fun PreferenceGroup.trackPreference(
service: TrackService,
crossinline login: () -> Unit,
): TrackerPreference {
return add(
TrackerPreference(context).apply {
key = TrackPreferences.trackUsername(service.id)
titleRes = service.nameRes()
iconRes = service.getLogo()
iconColor = service.getLogoColor()
onClick {
if (service.isLogged) {
if (service is NoLoginTrackService) {
service.logout()
updatePreference(service.id)
} else {
val dialog = TrackLogoutDialog(service)
dialog.targetController = this@SettingsTrackingController
dialog.showDialog(router)
}
} else {
login()
}
}
},
)
}
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
// Manually refresh OAuth trackers' holders
updatePreference(trackManager.myAnimeList.id)
updatePreference(trackManager.aniList.id)
updatePreference(trackManager.shikimori.id)
updatePreference(trackManager.bangumi.id)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.settings_tracking, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_tracking_help -> activity?.openInBrowser(HELP_URL)
}
return super.onOptionsItemSelected(item)
}
private fun updatePreference(id: Long) {
val pref = findPreference(TrackPreferences.trackUsername(id)) as? TrackerPreference
pref?.notifyChanged()
}
override fun trackLoginDialogClosed(service: TrackService) {
updatePreference(service.id)
}
override fun trackLogoutDialogClosed(service: TrackService) {
updatePreference(service.id)
}
}
private const val HELP_URL = "https://tachiyomi.org/help/guides/tracking/"

View File

@@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.database
import androidx.compose.runtime.Composable
import eu.kanade.presentation.more.settings.database.ClearDatabaseScreen
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
class ClearDatabaseController : FullComposeController<ClearDatabasePresenter>() {
override fun createPresenter(): ClearDatabasePresenter {
return ClearDatabasePresenter()
}
@Composable
override fun ComposeContent() {
ClearDatabaseScreen(
presenter = presenter,
navigateUp = { router.popCurrentController() },
)
}
}

View File

@@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.database
import android.os.Bundle
import eu.kanade.domain.source.interactor.GetSourcesWithNonLibraryManga
import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.more.settings.database.ClearDatabaseState
import eu.kanade.presentation.more.settings.database.ClearDatabaseStateImpl
import eu.kanade.tachiyomi.Database
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.collectLatest
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ClearDatabasePresenter(
private val state: ClearDatabaseStateImpl = ClearDatabaseState() as ClearDatabaseStateImpl,
private val database: Database = Injekt.get(),
private val getSourcesWithNonLibraryManga: GetSourcesWithNonLibraryManga = Injekt.get(),
) : BasePresenter<ClearDatabaseController>(), ClearDatabaseState by state {
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
presenterScope.launchIO {
getSourcesWithNonLibraryManga.subscribe()
.collectLatest { list ->
state.items = list.sortedBy { it.name }
}
}
}
fun removeMangaBySourceId(sourceIds: List<Long>) {
database.mangasQueries.deleteMangasNotInLibraryBySourceIds(sourceIds)
database.historyQueries.removeResettedHistory()
}
fun toggleSelection(source: Source) {
val mutableList = state.selection.toMutableList()
if (mutableList.contains(source.id)) {
mutableList.remove(source.id)
} else {
mutableList.add(source.id)
}
state.selection = mutableList
}
fun clearSelection() {
state.selection = emptyList()
}
fun selectAll() {
state.selection = state.items.map { it.id }
}
fun invertSelection() {
state.selection = state.items.map { it.id }.filterNot { it in state.selection }
}
sealed class Dialog {
data class Delete(val sourceIds: List<Long>) : Dialog()
}
}

View File

@@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.search
import androidx.compose.runtime.Composable
import eu.kanade.presentation.more.settings.SettingsSearchScreen
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController
class SettingsSearchController : FullComposeController<SettingsSearchPresenter>() {
override fun createPresenter() = SettingsSearchPresenter()
@Composable
override fun ComposeContent() {
SettingsSearchScreen(
navigateUp = router::popCurrentController,
presenter = presenter,
onClickResult = { router.pushController(it) },
)
}
}

View File

@@ -1,138 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.search
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceManager
import androidx.preference.forEach
import androidx.preference.get
import eu.kanade.tachiyomi.ui.setting.SettingsAdvancedController
import eu.kanade.tachiyomi.ui.setting.SettingsAppearanceController
import eu.kanade.tachiyomi.ui.setting.SettingsBackupController
import eu.kanade.tachiyomi.ui.setting.SettingsBrowseController
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.ui.setting.SettingsDownloadController
import eu.kanade.tachiyomi.ui.setting.SettingsGeneralController
import eu.kanade.tachiyomi.ui.setting.SettingsLibraryController
import eu.kanade.tachiyomi.ui.setting.SettingsReaderController
import eu.kanade.tachiyomi.ui.setting.SettingsSecurityController
import eu.kanade.tachiyomi.ui.setting.SettingsTrackingController
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.isLTR
import kotlin.reflect.KClass
import kotlin.reflect.full.createInstance
object SettingsSearchHelper {
private var prefSearchResultList: MutableList<SettingsSearchResult> = mutableListOf()
/**
* All subclasses of `SettingsController` should be listed here, in order to have their preferences searchable.
*/
private val settingControllersList: List<KClass<out SettingsController>> = listOf(
SettingsAdvancedController::class,
SettingsAppearanceController::class,
SettingsBackupController::class,
SettingsBrowseController::class,
SettingsDownloadController::class,
SettingsGeneralController::class,
SettingsLibraryController::class,
SettingsReaderController::class,
SettingsSecurityController::class,
SettingsTrackingController::class,
)
/**
* Must be called to populate `prefSearchResultList`
*/
@SuppressLint("RestrictedApi")
fun initPreferenceSearchResults(context: Context) {
val preferenceManager = PreferenceManager(context)
prefSearchResultList.clear()
launchNow {
settingControllersList.forEach { kClass ->
val ctrl = kClass.createInstance()
val settingsPrefScreen = ctrl.setupPreferenceScreen(preferenceManager.createPreferenceScreen(context))
val prefCount = settingsPrefScreen.preferenceCount
for (i in 0 until prefCount) {
val rootPref = settingsPrefScreen[i]
if (rootPref.title == null) continue // no title, not a preference. (note: only info notes appear to not have titles)
getSettingSearchResult(ctrl, rootPref, "${settingsPrefScreen.title}")
}
}
}
}
fun getFilteredResults(query: String): List<SettingsSearchResult> {
return prefSearchResultList.filter {
val inTitle = it.title.contains(query, true)
val inSummary = it.summary.contains(query, true)
val inBreadcrumb = it.breadcrumb.contains(query, true)
return@filter inTitle || inSummary || inBreadcrumb
}
}
/**
* Extracts the data needed from a `Preference` to create a `SettingsSearchResult`, and then adds it to `prefSearchResultList`
* Future enhancement: make bold the text matched by the search query.
*/
private fun getSettingSearchResult(
ctrl: SettingsController,
pref: Preference,
breadcrumbs: String = "",
) {
when {
pref is PreferenceGroup -> {
val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}")
pref.forEach {
getSettingSearchResult(ctrl, it, breadcrumbsStr) // recursion
}
}
pref is PreferenceCategory -> {
val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}")
pref.forEach {
getSettingSearchResult(ctrl, it, breadcrumbsStr) // recursion
}
}
(pref.title != null && pref.isVisible) -> {
// Is an actual preference
val title = pref.title.toString()
// ListPreferences occasionally run into ArrayIndexOutOfBoundsException issues
val summary = try { pref.summary?.toString() ?: "" } catch (e: Throwable) { "" }
val breadcrumbsStr = addLocalizedBreadcrumb(breadcrumbs, "${pref.title}")
prefSearchResultList.add(
SettingsSearchResult(
key = pref.key,
title = title,
summary = summary,
breadcrumb = breadcrumbsStr,
searchController = ctrl,
),
)
}
}
}
private fun addLocalizedBreadcrumb(path: String, node: String): String {
return if (Resources.getSystem().isLTR) {
// This locale reads left to right.
"$path > $node"
} else {
// This locale reads right to left.
"$node < $path"
}
}
data class SettingsSearchResult(
val key: String?,
val title: String,
val summary: String,
val breadcrumb: String,
val searchController: SettingsController,
)
}

View File

@@ -1,33 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.search
import android.os.Bundle
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SettingsSearchPresenter(
private val preferences: BasePreferences = Injekt.get(),
) : BasePresenter<SettingsSearchController>() {
private val _state: MutableStateFlow<List<SettingsSearchHelper.SettingsSearchResult>> =
MutableStateFlow(emptyList())
val state: StateFlow<List<SettingsSearchHelper.SettingsSearchResult>> = _state.asStateFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
SettingsSearchHelper.initPreferenceSearchResults(preferences.context)
}
fun searchSettings(query: String?) {
_state.value = if (!query.isNullOrBlank()) {
SettingsSearchHelper.getFilteredResults(query)
} else {
emptyList()
}
}
}

View File

@@ -1,66 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.os.Bundle
import android.view.View
import androidx.annotation.StringRes
import androidx.core.os.bundleOf
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.preference.LoginDialogPreference
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TrackLoginDialog(
@StringRes usernameLabelRes: Int? = null,
bundle: Bundle? = null,
) : LoginDialogPreference(usernameLabelRes, bundle) {
private val service = Injekt.get<TrackManager>().getService(args.getLong("serviceId"))!!
constructor(service: TrackService, @StringRes usernameLabelRes: Int?) :
this(usernameLabelRes, bundleOf("serviceId" to service.id))
@StringRes
override fun getTitleName(): Int = service.nameRes()
override fun setCredentialsOnView(view: View) {
binding?.username?.setText(service.getUsername())
binding?.password?.setText(service.getPassword())
}
override fun checkLogin() {
if (binding!!.username.text.isNullOrEmpty() || binding!!.password.text.isNullOrEmpty()) {
return
}
binding!!.login.progress = 1
val user = binding!!.username.text.toString()
val pass = binding!!.password.text.toString()
launchIO {
try {
service.login(user, pass)
dialog?.dismiss()
withUIContext { view?.context?.toast(R.string.login_success) }
} catch (e: Throwable) {
service.logout()
binding?.login?.progress = -1
binding?.login?.setText(R.string.unknown_error)
withUIContext { e.message?.let { view?.context?.toast(it) } }
}
}
}
override fun onDialogClosed() {
super.onDialogClosed()
(targetController as? Listener)?.trackLoginDialogClosed(service)
}
interface Listener {
fun trackLoginDialogClosed(service: TrackService)
}
}

View File

@@ -1,37 +0,0 @@
package eu.kanade.tachiyomi.ui.setting.track
import android.app.Dialog
import android.os.Bundle
import androidx.core.os.bundleOf
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TrackLogoutDialog(bundle: Bundle? = null) : DialogController(bundle) {
private val service = Injekt.get<TrackManager>().getService(args.getLong("serviceId"))!!
constructor(service: TrackService) : this(bundleOf("serviceId" to service.id))
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val serviceName = activity!!.getString(service.nameRes())
return MaterialAlertDialogBuilder(activity!!)
.setTitle(activity!!.getString(R.string.logout_title, serviceName))
.setPositiveButton(R.string.logout) { _, _ ->
service.logout()
(targetController as? Listener)?.trackLogoutDialogClosed(service)
activity?.toast(R.string.logout_success)
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
interface Listener {
fun trackLogoutDialogClosed(service: TrackService)
}
}