SettingsLibraryController.kt 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. package eu.kanade.tachiyomi.ui.setting
  2. import android.app.Dialog
  3. import android.os.Bundle
  4. import android.view.LayoutInflater
  5. import androidx.core.content.ContextCompat
  6. import androidx.core.text.buildSpannedString
  7. import androidx.preference.Preference
  8. import androidx.preference.PreferenceScreen
  9. import com.google.android.material.dialog.MaterialAlertDialogBuilder
  10. import eu.kanade.tachiyomi.R
  11. import eu.kanade.tachiyomi.data.database.DatabaseHelper
  12. import eu.kanade.tachiyomi.data.database.models.Category
  13. import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
  14. import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
  15. import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
  16. import eu.kanade.tachiyomi.data.preference.MANGA_FULLY_READ
  17. import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
  18. import eu.kanade.tachiyomi.data.preference.PreferencesHelper
  19. import eu.kanade.tachiyomi.data.track.TrackManager
  20. import eu.kanade.tachiyomi.databinding.PrefLibraryColumnsBinding
  21. import eu.kanade.tachiyomi.ui.base.controller.DialogController
  22. import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
  23. import eu.kanade.tachiyomi.ui.category.CategoryController
  24. import eu.kanade.tachiyomi.util.preference.defaultValue
  25. import eu.kanade.tachiyomi.util.preference.entriesRes
  26. import eu.kanade.tachiyomi.util.preference.intListPreference
  27. import eu.kanade.tachiyomi.util.preference.multiSelectListPreference
  28. import eu.kanade.tachiyomi.util.preference.onChange
  29. import eu.kanade.tachiyomi.util.preference.onClick
  30. import eu.kanade.tachiyomi.util.preference.preference
  31. import eu.kanade.tachiyomi.util.preference.preferenceCategory
  32. import eu.kanade.tachiyomi.util.preference.summaryRes
  33. import eu.kanade.tachiyomi.util.preference.switchPreference
  34. import eu.kanade.tachiyomi.util.preference.titleRes
  35. import eu.kanade.tachiyomi.util.system.isTablet
  36. import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
  37. import eu.kanade.tachiyomi.widget.materialdialogs.setQuadStateMultiChoiceItems
  38. import kotlinx.coroutines.flow.combine
  39. import kotlinx.coroutines.flow.launchIn
  40. import kotlinx.coroutines.flow.onEach
  41. import uy.kohesive.injekt.Injekt
  42. import uy.kohesive.injekt.api.get
  43. import uy.kohesive.injekt.injectLazy
  44. import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
  45. class SettingsLibraryController : SettingsController() {
  46. private val db: DatabaseHelper = Injekt.get()
  47. private val trackManager: TrackManager by injectLazy()
  48. override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
  49. titleRes = R.string.pref_category_library
  50. val dbCategories = db.getCategories().executeAsBlocking()
  51. val categories = listOf(Category.createDefault(context)) + dbCategories
  52. preferenceCategory {
  53. titleRes = R.string.pref_category_display
  54. preference {
  55. key = "pref_library_columns"
  56. titleRes = R.string.pref_library_columns
  57. onClick {
  58. LibraryColumnsDialog().showDialog(router)
  59. }
  60. fun getColumnValue(value: Int): String {
  61. return if (value == 0) {
  62. context.getString(R.string.label_default)
  63. } else {
  64. value.toString()
  65. }
  66. }
  67. preferences.portraitColumns().asFlow().combine(preferences.landscapeColumns().asFlow()) { portraitCols, landscapeCols -> Pair(portraitCols, landscapeCols) }
  68. .onEach { (portraitCols, landscapeCols) ->
  69. val portrait = getColumnValue(portraitCols)
  70. val landscape = getColumnValue(landscapeCols)
  71. summary = "${context.getString(R.string.portrait)}: $portrait, " +
  72. "${context.getString(R.string.landscape)}: $landscape"
  73. }
  74. .launchIn(viewScope)
  75. }
  76. if (!context.isTablet()) {
  77. switchPreference {
  78. key = Keys.jumpToChapters
  79. titleRes = R.string.pref_jump_to_chapters
  80. defaultValue = false
  81. }
  82. }
  83. }
  84. preferenceCategory {
  85. titleRes = R.string.categories
  86. preference {
  87. key = "pref_action_edit_categories"
  88. titleRes = R.string.action_edit_categories
  89. val catCount = dbCategories.size
  90. summary = context.resources.getQuantityString(R.plurals.num_categories, catCount, catCount)
  91. onClick {
  92. router.pushController(CategoryController().withFadeTransaction())
  93. }
  94. }
  95. intListPreference {
  96. key = Keys.defaultCategory
  97. titleRes = R.string.default_category
  98. entries = arrayOf(context.getString(R.string.default_category_summary)) +
  99. categories.map { it.name }.toTypedArray()
  100. entryValues = arrayOf("-1") + categories.map { it.id.toString() }.toTypedArray()
  101. defaultValue = "-1"
  102. val selectedCategory = categories.find { it.id == preferences.defaultCategory() }
  103. summary = selectedCategory?.name
  104. ?: context.getString(R.string.default_category_summary)
  105. onChange { newValue ->
  106. summary = categories.find {
  107. it.id == (newValue as String).toInt()
  108. }?.name ?: context.getString(R.string.default_category_summary)
  109. true
  110. }
  111. }
  112. switchPreference {
  113. key = Keys.categorizedDisplay
  114. titleRes = R.string.categorized_display_settings
  115. defaultValue = false
  116. }
  117. }
  118. preferenceCategory {
  119. titleRes = R.string.pref_category_library_update
  120. intListPreference {
  121. key = Keys.libraryUpdateInterval
  122. titleRes = R.string.pref_library_update_interval
  123. entriesRes = arrayOf(
  124. R.string.update_never,
  125. R.string.update_12hour,
  126. R.string.update_24hour,
  127. R.string.update_48hour,
  128. R.string.update_72hour,
  129. R.string.update_weekly
  130. )
  131. entryValues = arrayOf("0", "12", "24", "48", "72", "168")
  132. defaultValue = "24"
  133. summary = "%s"
  134. onChange { newValue ->
  135. val interval = (newValue as String).toInt()
  136. LibraryUpdateJob.setupTask(context, interval)
  137. true
  138. }
  139. }
  140. multiSelectListPreference {
  141. key = Keys.libraryUpdateDeviceRestriction
  142. titleRes = R.string.pref_library_update_restriction
  143. entriesRes = arrayOf(R.string.connected_to_wifi, R.string.charging)
  144. entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_CHARGING)
  145. defaultValue = preferences.libraryUpdateDeviceRestriction().defaultValue
  146. visibleIfGlobalUpdateEnabled()
  147. onChange {
  148. // Post to event looper to allow the preference to be updated.
  149. ContextCompat.getMainExecutor(context).execute { LibraryUpdateJob.setupTask(context) }
  150. true
  151. }
  152. fun updateSummary() {
  153. val restrictions = preferences.libraryUpdateDeviceRestriction().get()
  154. .sorted()
  155. .map {
  156. when (it) {
  157. DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi)
  158. DEVICE_CHARGING -> context.getString(R.string.charging)
  159. else -> it
  160. }
  161. }
  162. val restrictionsText = if (restrictions.isEmpty()) {
  163. context.getString(R.string.none)
  164. } else {
  165. restrictions.joinToString()
  166. }
  167. summary = context.getString(R.string.restrictions, restrictionsText)
  168. }
  169. preferences.libraryUpdateDeviceRestriction().asFlow()
  170. .onEach { updateSummary() }
  171. .launchIn(viewScope)
  172. }
  173. multiSelectListPreference {
  174. key = Keys.libraryUpdateMangaRestriction
  175. titleRes = R.string.pref_library_update_manga_restriction
  176. entriesRes = arrayOf(R.string.pref_update_only_completely_read, R.string.pref_update_only_non_completed)
  177. entryValues = arrayOf(MANGA_FULLY_READ, MANGA_ONGOING)
  178. defaultValue = preferences.libraryUpdateMangaRestriction().defaultValue
  179. fun updateSummary() {
  180. val restrictions = preferences.libraryUpdateMangaRestriction().get()
  181. .sorted()
  182. .map {
  183. when (it) {
  184. MANGA_ONGOING -> context.getString(R.string.pref_update_only_non_completed)
  185. MANGA_FULLY_READ -> context.getString(R.string.pref_update_only_completely_read)
  186. else -> it
  187. }
  188. }
  189. val restrictionsText = if (restrictions.isEmpty()) {
  190. context.getString(R.string.none)
  191. } else {
  192. restrictions.joinToString()
  193. }
  194. summary = context.getString(R.string.only_update_restrictions, restrictionsText)
  195. }
  196. preferences.libraryUpdateMangaRestriction().asFlow()
  197. .onEach { updateSummary() }
  198. .launchIn(viewScope)
  199. }
  200. preference {
  201. key = Keys.libraryUpdateCategories
  202. titleRes = R.string.categories
  203. onClick {
  204. LibraryGlobalUpdateCategoriesDialog().showDialog(router)
  205. }
  206. fun updateSummary() {
  207. val dbCategories = db.getCategories().executeAsBlocking()
  208. val allCategories = listOf(Category.createDefault(activity!!)) + dbCategories
  209. val includedCategories = preferences.libraryUpdateCategories().get()
  210. .mapNotNull { id -> categories.find { it.id == id.toInt() } }
  211. .sortedBy { it.order }
  212. val excludedCategories = preferences.libraryUpdateCategoriesExclude().get()
  213. .mapNotNull { id -> categories.find { it.id == id.toInt() } }
  214. .sortedBy { it.order }
  215. val includedItemsText = if (includedCategories.isEmpty()) {
  216. if (excludedCategories.size == allCategories.size) context.getString(R.string.none)
  217. else context.getString(R.string.all)
  218. } else {
  219. includedCategories.joinToString { it.name }
  220. }
  221. val excludedItemsText = if (excludedCategories.isEmpty()) {
  222. context.getString(R.string.none)
  223. } else {
  224. if (excludedCategories.size == allCategories.size) context.getString(R.string.all)
  225. else excludedCategories.joinToString { it.name }
  226. }
  227. summary = buildSpannedString {
  228. append(context.getString(R.string.include, includedItemsText))
  229. appendLine()
  230. append(context.getString(R.string.exclude, excludedItemsText))
  231. }
  232. }
  233. preferences.libraryUpdateCategories().asFlow()
  234. .onEach { updateSummary() }
  235. .launchIn(viewScope)
  236. preferences.libraryUpdateCategoriesExclude().asFlow()
  237. .onEach { updateSummary() }
  238. .launchIn(viewScope)
  239. }
  240. switchPreference {
  241. key = Keys.autoUpdateMetadata
  242. titleRes = R.string.pref_library_update_refresh_metadata
  243. summaryRes = R.string.pref_library_update_refresh_metadata_summary
  244. defaultValue = false
  245. }
  246. if (trackManager.hasLoggedServices()) {
  247. switchPreference {
  248. key = Keys.autoUpdateTrackers
  249. titleRes = R.string.pref_library_update_refresh_trackers
  250. summaryRes = R.string.pref_library_update_refresh_trackers_summary
  251. defaultValue = false
  252. }
  253. }
  254. }
  255. }
  256. private inline fun Preference.visibleIfGlobalUpdateEnabled() {
  257. visibleIf(preferences.libraryUpdateInterval()) { it > 0 }
  258. }
  259. class LibraryColumnsDialog : DialogController() {
  260. private val preferences: PreferencesHelper = Injekt.get()
  261. private var portrait = preferences.portraitColumns().get()
  262. private var landscape = preferences.landscapeColumns().get()
  263. override fun onCreateDialog(savedViewState: Bundle?): Dialog {
  264. val binding = PrefLibraryColumnsBinding.inflate(LayoutInflater.from(activity!!))
  265. onViewCreated(binding)
  266. return MaterialAlertDialogBuilder(activity!!)
  267. .setTitle(R.string.pref_library_columns)
  268. .setView(binding.root)
  269. .setPositiveButton(android.R.string.ok) { _, _ ->
  270. preferences.portraitColumns().set(portrait)
  271. preferences.landscapeColumns().set(landscape)
  272. }
  273. .setNegativeButton(android.R.string.cancel, null)
  274. .create()
  275. }
  276. fun onViewCreated(binding: PrefLibraryColumnsBinding) {
  277. with(binding.portraitColumns) {
  278. displayedValues = arrayOf(context.getString(R.string.label_default)) +
  279. IntRange(1, 10).map(Int::toString)
  280. value = portrait
  281. setOnValueChangedListener { _, _, newValue ->
  282. portrait = newValue
  283. }
  284. }
  285. with(binding.landscapeColumns) {
  286. displayedValues = arrayOf(context.getString(R.string.label_default)) +
  287. IntRange(1, 10).map(Int::toString)
  288. value = landscape
  289. setOnValueChangedListener { _, _, newValue ->
  290. landscape = newValue
  291. }
  292. }
  293. }
  294. }
  295. class LibraryGlobalUpdateCategoriesDialog : DialogController() {
  296. private val preferences: PreferencesHelper = Injekt.get()
  297. private val db: DatabaseHelper = Injekt.get()
  298. override fun onCreateDialog(savedViewState: Bundle?): Dialog {
  299. val dbCategories = db.getCategories().executeAsBlocking()
  300. val categories = listOf(Category.createDefault(activity!!)) + dbCategories
  301. val items = categories.map { it.name }
  302. var selected = categories
  303. .map {
  304. when (it.id.toString()) {
  305. in preferences.libraryUpdateCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
  306. in preferences.libraryUpdateCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
  307. else -> QuadStateTextView.State.UNCHECKED.ordinal
  308. }
  309. }
  310. .toIntArray()
  311. return MaterialAlertDialogBuilder(activity!!)
  312. .setTitle(R.string.categories)
  313. .setQuadStateMultiChoiceItems(
  314. message = R.string.pref_library_update_categories_details,
  315. items = items,
  316. initialSelected = selected
  317. ) { selections ->
  318. selected = selections
  319. }
  320. .setPositiveButton(android.R.string.ok) { _, _ ->
  321. val included = selected
  322. .mapIndexed { index, value -> if (value == QuadStateTextView.State.CHECKED.ordinal) index else null }
  323. .filterNotNull()
  324. .map { categories[it].id.toString() }
  325. .toSet()
  326. val excluded = selected
  327. .mapIndexed { index, value -> if (value == QuadStateTextView.State.INVERSED.ordinal) index else null }
  328. .filterNotNull()
  329. .map { categories[it].id.toString() }
  330. .toSet()
  331. preferences.libraryUpdateCategories().set(included)
  332. preferences.libraryUpdateCategoriesExclude().set(excluded)
  333. }
  334. .setNegativeButton(android.R.string.cancel, null)
  335. .create()
  336. }
  337. }
  338. }