SettingsLibraryController.kt 17 KB

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