Compare commits
21 Commits
861c21faea
...
d0ed1fb85f
| Author | SHA1 | Date | |
|---|---|---|---|
|
d0ed1fb85f
|
|||
|
9e5664da3a
|
|||
|
35c158d35a
|
|||
|
464f24e9f0
|
|||
|
c8a8203c39
|
|||
|
b414758f32
|
|||
|
1181860e41
|
|||
|
e35521f16f
|
|||
|
|
5fb8ff53f9 | ||
|
|
a66283d035 | ||
|
|
a1ba0b8c21 | ||
|
f3b42b9a42
|
|||
|
|
aa2f2c17fc | ||
|
ebc17b645b
|
|||
|
cc14e1abcf
|
|||
|
b1b474e2e7
|
|||
|
8ca3bece5d
|
|||
|
|
90bd9023d5 | ||
|
|
986627f24d | ||
|
|
cf2b8e2481 | ||
|
b9435de5cd
|
@@ -4,7 +4,7 @@ root = true
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
tab_width = 4
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
/.idea/dictionaries
|
||||
/.idea/modules.xml
|
||||
/.idea/misc.xml
|
||||
/.idea/markdown.xml
|
||||
/.idea/discord.xml
|
||||
/.idea/compiler.xml
|
||||
/.idea/workspace.xml
|
||||
@@ -26,4 +27,4 @@
|
||||
.cxx
|
||||
/.idea/deviceManager.xml
|
||||
/.kotlin/
|
||||
/.idea/AndroidProjectSystem.xml
|
||||
/.idea/AndroidProjectSystem.xml
|
||||
|
||||
74
.idea/codeStyles/Project.xml
generated
74
.idea/codeStyles/Project.xml
generated
@@ -1,9 +1,7 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<option name="OTHER_INDENT_OPTIONS">
|
||||
<value>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</value>
|
||||
<value />
|
||||
</option>
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="LAYOUT_SETTINGS">
|
||||
@@ -22,40 +20,46 @@
|
||||
</value>
|
||||
</option>
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<JavaCodeStyleSettings>
|
||||
<option name="IMPORT_LAYOUT_TABLE">
|
||||
<value>
|
||||
<package name="android" withSubpackages="true" static="true" />
|
||||
<package name="androidx" withSubpackages="true" static="true" />
|
||||
<package name="com" withSubpackages="true" static="true" />
|
||||
<package name="junit" withSubpackages="true" static="true" />
|
||||
<package name="net" withSubpackages="true" static="true" />
|
||||
<package name="org" withSubpackages="true" static="true" />
|
||||
<package name="java" withSubpackages="true" static="true" />
|
||||
<package name="javax" withSubpackages="true" static="true" />
|
||||
<package name="" withSubpackages="true" static="true" />
|
||||
<emptyLine />
|
||||
<package name="android" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="androidx" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="com" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="junit" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="net" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="org" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="java" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="javax" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
<package name="" withSubpackages="true" static="false" />
|
||||
<emptyLine />
|
||||
</value>
|
||||
</option>
|
||||
</JavaCodeStyleSettings>
|
||||
<JetCodeStyleSettings>
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="ALLOW_TRAILING_COMMA_COLLECTION_LITERAL_EXPRESSION" value="true" />
|
||||
<option name="ALLOW_TRAILING_COMMA_VALUE_ARGUMENT_LIST" value="true" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="CMake">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Groovy">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="HTML">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JAVA">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="JSON">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="ObjectiveC">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="Shell Script">
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
@@ -64,7 +68,6 @@
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
@@ -179,9 +182,6 @@
|
||||
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||
<option name="BLOCK_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
|
||||
<indentOptions>
|
||||
<option name="USE_TAB_CHARACTER" value="true" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
@@ -35,7 +35,7 @@
|
||||
* Integration with manga tracking services: Shikimori, AniList, MyAnimeList, Kitsu
|
||||
* Password / fingerprint-protected access to the app
|
||||
* Automatically sync app data with other devices on the same account
|
||||
* Support for older devices running Android 5.0+
|
||||
* Support for older devices running Android 6.0+
|
||||
|
||||
</div>
|
||||
|
||||
@@ -112,6 +112,6 @@ You may copy, distribute and modify the software as long as you track changes/da
|
||||
|
||||
<div align="left">
|
||||
|
||||
The developers of this application do not have any affiliation with the content available in the app. It collects content from sources that are freely available through any web browser.
|
||||
The developers of this application do not have any affiliation with the content available in the app and does not store or distribute any content. This application should be considered a web browser, all content that can be found using this application is freely available on the Internet. All DMCA takedown requests should be sent to the owners of the website where the content is hosted.
|
||||
|
||||
</div>
|
||||
|
||||
@@ -21,8 +21,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 23
|
||||
targetSdk = 36
|
||||
versionCode = 1030
|
||||
versionName = '9.2'
|
||||
versionCode = 1031
|
||||
versionName = '9.3'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
|
||||
@@ -26,7 +26,9 @@ import org.koitharu.kotatsu.backups.data.model.CategoryBackup
|
||||
import org.koitharu.kotatsu.backups.data.model.FavouriteBackup
|
||||
import org.koitharu.kotatsu.backups.data.model.HistoryBackup
|
||||
import org.koitharu.kotatsu.backups.data.model.MangaBackup
|
||||
import org.koitharu.kotatsu.backups.data.model.ScrobblingBackup
|
||||
import org.koitharu.kotatsu.backups.data.model.SourceBackup
|
||||
import org.koitharu.kotatsu.backups.data.model.StatisticBackup
|
||||
import org.koitharu.kotatsu.backups.domain.BackupSection
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
@@ -109,6 +111,18 @@ class BackupRepository @Inject constructor(
|
||||
data = database.getSourcesDao().dumpEnabled().map { SourceBackup(it) },
|
||||
serializer = serializer(),
|
||||
)
|
||||
|
||||
BackupSection.SCROBBLING -> output.writeJsonArray(
|
||||
section = BackupSection.SCROBBLING,
|
||||
data = database.getScrobblingDao().dumpEnabled().map { ScrobblingBackup(it) },
|
||||
serializer = serializer(),
|
||||
)
|
||||
|
||||
BackupSection.STATS -> output.writeJsonArray(
|
||||
section = BackupSection.STATS,
|
||||
data = database.getStatsDao().dumpEnabled().map { StatisticBackup(it) },
|
||||
serializer = serializer(),
|
||||
)
|
||||
}
|
||||
progress?.emit(commonProgress)
|
||||
commonProgress++
|
||||
@@ -163,6 +177,14 @@ class BackupRepository @Inject constructor(
|
||||
getSourcesDao().upsert(it.toEntity())
|
||||
}
|
||||
|
||||
BackupSection.SCROBBLING -> input.readJsonArray<ScrobblingBackup>(serializer()).restoreToDb {
|
||||
getScrobblingDao().upsert(it.toEntity())
|
||||
}
|
||||
|
||||
BackupSection.STATS -> input.readJsonArray<StatisticBackup>(serializer()).restoreToDb {
|
||||
getStatsDao().upsert(it.toEntity())
|
||||
}
|
||||
|
||||
null -> CompositeResult.EMPTY // skip unknown entries
|
||||
}
|
||||
progress?.emit(commonProgress)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.koitharu.kotatsu.backups.data.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koitharu.kotatsu.scrobbling.common.data.ScrobblingEntity
|
||||
|
||||
@Serializable
|
||||
class ScrobblingBackup(
|
||||
@SerialName("scrobbler") val scrobbler: Int,
|
||||
@SerialName("id") val id: Int,
|
||||
@SerialName("manga_id") val mangaId: Long,
|
||||
@SerialName("target_id") val targetId: Long,
|
||||
@SerialName("status") val status: String?,
|
||||
@SerialName("chapter") val chapter: Int,
|
||||
@SerialName("comment") val comment: String?,
|
||||
@SerialName("rating") val rating: Float,
|
||||
) {
|
||||
|
||||
constructor(entity: ScrobblingEntity) : this(
|
||||
scrobbler = entity.scrobbler,
|
||||
id = entity.id,
|
||||
mangaId = entity.mangaId,
|
||||
targetId = entity.targetId,
|
||||
status = entity.status,
|
||||
chapter = entity.chapter,
|
||||
comment = entity.comment,
|
||||
rating = entity.rating,
|
||||
)
|
||||
|
||||
fun toEntity() = ScrobblingEntity(
|
||||
scrobbler = scrobbler,
|
||||
id = id,
|
||||
mangaId = mangaId,
|
||||
targetId = targetId,
|
||||
status = status,
|
||||
chapter = chapter,
|
||||
comment = comment,
|
||||
rating = rating,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.backups.data.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koitharu.kotatsu.stats.data.StatsEntity
|
||||
|
||||
@Serializable
|
||||
class StatisticBackup(
|
||||
@SerialName("manga_id") val mangaId: Long,
|
||||
@SerialName("started_at") val startedAt: Long,
|
||||
@SerialName("duration") val duration: Long,
|
||||
@SerialName("pages") val pages: Int,
|
||||
) {
|
||||
|
||||
constructor(entity: StatsEntity) : this(
|
||||
mangaId = entity.mangaId,
|
||||
startedAt = entity.startedAt,
|
||||
duration = entity.duration,
|
||||
pages = entity.pages,
|
||||
)
|
||||
|
||||
fun toEntity() = StatsEntity(
|
||||
mangaId = mangaId,
|
||||
startedAt = startedAt,
|
||||
duration = duration,
|
||||
pages = pages,
|
||||
)
|
||||
}
|
||||
@@ -15,6 +15,8 @@ enum class BackupSection(
|
||||
SETTINGS_READER_GRID("reader_grid"),
|
||||
BOOKMARKS("bookmarks"),
|
||||
SOURCES("sources"),
|
||||
SCROBBLING("scrobbling"),
|
||||
STATS("statistics"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -23,6 +23,8 @@ data class BackupSectionModel(
|
||||
BackupSection.SETTINGS_READER_GRID -> R.string.reader_actions
|
||||
BackupSection.BOOKMARKS -> R.string.bookmarks
|
||||
BackupSection.SOURCES -> R.string.remote_sources
|
||||
BackupSection.SCROBBLING -> R.string.tracking
|
||||
BackupSection.STATS -> R.string.statistics
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
|
||||
@@ -34,6 +34,9 @@ abstract class MangaDao {
|
||||
@Query("SELECT author FROM manga WHERE author LIKE :query GROUP BY author ORDER BY COUNT(author) DESC LIMIT :limit")
|
||||
abstract suspend fun findAuthors(query: String, limit: Int): List<String>
|
||||
|
||||
@Query("SELECT author FROM manga WHERE manga.source = :source AND author IS NOT NULL AND author != '' GROUP BY author ORDER BY COUNT(author) DESC LIMIT :limit")
|
||||
abstract suspend fun findAuthorsBySource(source: String, limit: Int): List<String>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM manga WHERE (title LIKE :query OR alt_title LIKE :query) AND manga_id IN (SELECT manga_id FROM favourites UNION SELECT manga_id FROM history) LIMIT :limit")
|
||||
abstract suspend fun searchByTitle(query: String, limit: Int): List<MangaWithTags>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.koitharu.kotatsu.core.model
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.serialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
object MangaSourceSerializer : KSerializer<MangaSource> {
|
||||
|
||||
override val descriptor: SerialDescriptor = serialDescriptor<String>()
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: MangaSource
|
||||
) = encoder.encodeString(value.name)
|
||||
|
||||
override fun deserialize(decoder: Decoder): MangaSource = MangaSource(decoder.decodeString())
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.koitharu.kotatsu.core.network.webview
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AndroidRuntimeException
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
@@ -41,7 +42,13 @@ class WebViewExecutor @Inject constructor(
|
||||
private val mutex = Mutex()
|
||||
|
||||
val defaultUserAgent: String? by lazy {
|
||||
WebSettings.getDefaultUserAgent(context)
|
||||
try {
|
||||
WebSettings.getDefaultUserAgent(context)
|
||||
} catch (e: AndroidRuntimeException) {
|
||||
e.printStackTraceDebug()
|
||||
// Probably WebView is not available
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun evaluateJs(baseUrl: String?, script: String): String? = mutex.withLock {
|
||||
|
||||
@@ -409,6 +409,9 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val isReaderBarTransparent: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_BAR_TRANSPARENT, true)
|
||||
|
||||
val isReaderChapterToastEnabled: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_CHAPTER_TOAST, true)
|
||||
|
||||
val isReaderKeepScreenOn: Boolean
|
||||
get() = prefs.getBoolean(KEY_READER_SCREEN_ON, true)
|
||||
|
||||
@@ -747,6 +750,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_SYNC_SETTINGS = "sync_settings"
|
||||
const val KEY_READER_BAR = "reader_bar"
|
||||
const val KEY_READER_BAR_TRANSPARENT = "reader_bar_transparent"
|
||||
const val KEY_READER_CHAPTER_TOAST = "reader_chapter_toast"
|
||||
const val KEY_READER_BACKGROUND = "reader_background"
|
||||
const val KEY_READER_SCREEN_ON = "reader_screen_on"
|
||||
const val KEY_SHORTCUTS = "dynamic_shortcuts"
|
||||
|
||||
@@ -13,10 +13,14 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.ifNullOrEmpty
|
||||
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
|
||||
import org.koitharu.kotatsu.settings.utils.validation.DomainValidator
|
||||
import java.io.File
|
||||
|
||||
class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig {
|
||||
|
||||
private val prefs = context.getSharedPreferences(source.name, Context.MODE_PRIVATE)
|
||||
private val prefs = context.getSharedPreferences(
|
||||
source.name.replace(File.separatorChar, '$'),
|
||||
Context.MODE_PRIVATE,
|
||||
)
|
||||
|
||||
var defaultSortOrder: SortOrder?
|
||||
get() = prefs.getEnumValue(KEY_SORT_ORDER, SortOrder::class.java)
|
||||
|
||||
@@ -2,10 +2,17 @@ package org.koitharu.kotatsu.core.ui.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.annotation.UiContext
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -15,54 +22,103 @@ import com.hannesdorfmann.adapterdelegates4.AdapterDelegatesManager
|
||||
import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.databinding.DialogCheckboxBinding
|
||||
import org.koitharu.kotatsu.databinding.ViewDialogAutocompleteBinding
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
inline fun buildAlertDialog(
|
||||
@UiContext context: Context,
|
||||
isCentered: Boolean = false,
|
||||
block: MaterialAlertDialogBuilder.() -> Unit,
|
||||
@UiContext context: Context,
|
||||
isCentered: Boolean = false,
|
||||
block: MaterialAlertDialogBuilder.() -> Unit,
|
||||
): AlertDialog = MaterialAlertDialogBuilder(
|
||||
context,
|
||||
if (isCentered) materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered else 0,
|
||||
context,
|
||||
if (isCentered) materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered else 0,
|
||||
).apply(block).create()
|
||||
|
||||
fun <B : AlertDialog.Builder> B.setCheckbox(
|
||||
@StringRes textResId: Int,
|
||||
isChecked: Boolean,
|
||||
onCheckedChangeListener: OnCheckedChangeListener
|
||||
@StringRes textResId: Int,
|
||||
isChecked: Boolean,
|
||||
onCheckedChangeListener: OnCheckedChangeListener
|
||||
) = apply {
|
||||
val binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
|
||||
binding.checkbox.setText(textResId)
|
||||
binding.checkbox.isChecked = isChecked
|
||||
binding.checkbox.setOnCheckedChangeListener(onCheckedChangeListener)
|
||||
setView(binding.root)
|
||||
val binding = DialogCheckboxBinding.inflate(LayoutInflater.from(context))
|
||||
binding.checkbox.setText(textResId)
|
||||
binding.checkbox.isChecked = isChecked
|
||||
binding.checkbox.setOnCheckedChangeListener(onCheckedChangeListener)
|
||||
setView(binding.root)
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder, T> B.setRecyclerViewList(
|
||||
list: List<T>,
|
||||
delegate: AdapterDelegate<List<T>>,
|
||||
list: List<T>,
|
||||
delegate: AdapterDelegate<List<T>>,
|
||||
) = apply {
|
||||
val delegatesManager = AdapterDelegatesManager<List<T>>()
|
||||
delegatesManager.addDelegate(delegate)
|
||||
setRecyclerViewList(ListDelegationAdapter(delegatesManager).also { it.items = list })
|
||||
val delegatesManager = AdapterDelegatesManager<List<T>>()
|
||||
delegatesManager.addDelegate(delegate)
|
||||
setRecyclerViewList(ListDelegationAdapter(delegatesManager).also { it.items = list })
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder, T> B.setRecyclerViewList(
|
||||
list: List<T>,
|
||||
vararg delegates: AdapterDelegate<List<T>>,
|
||||
list: List<T>,
|
||||
vararg delegates: AdapterDelegate<List<T>>,
|
||||
) = apply {
|
||||
val delegatesManager = AdapterDelegatesManager<List<T>>()
|
||||
delegates.forEach { delegatesManager.addDelegate(it) }
|
||||
setRecyclerViewList(ListDelegationAdapter(delegatesManager).also { it.items = list })
|
||||
val delegatesManager = AdapterDelegatesManager<List<T>>()
|
||||
delegates.forEach { delegatesManager.addDelegate(it) }
|
||||
setRecyclerViewList(ListDelegationAdapter(delegatesManager).also { it.items = list })
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder> B.setRecyclerViewList(adapter: RecyclerView.Adapter<*>) = apply {
|
||||
val recyclerView = RecyclerView(context)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
recyclerView.updatePadding(
|
||||
top = context.resources.getDimensionPixelOffset(R.dimen.list_spacing),
|
||||
)
|
||||
recyclerView.clipToPadding = false
|
||||
recyclerView.adapter = adapter
|
||||
setView(recyclerView)
|
||||
val recyclerView = RecyclerView(context)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
recyclerView.updatePadding(
|
||||
top = context.resources.getDimensionPixelOffset(R.dimen.list_spacing),
|
||||
)
|
||||
recyclerView.clipToPadding = false
|
||||
recyclerView.adapter = adapter
|
||||
setView(recyclerView)
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder> B.setEditText(
|
||||
inputType: Int,
|
||||
singleLine: Boolean,
|
||||
): EditText {
|
||||
val editText = AppCompatEditText(context)
|
||||
editText.inputType = inputType
|
||||
if (singleLine) {
|
||||
editText.setSingleLine()
|
||||
editText.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
}
|
||||
val layout = FrameLayout(context)
|
||||
val lp = FrameLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
val horizontalMargin = context.resources.getDimensionPixelOffset(R.dimen.screen_padding)
|
||||
lp.setMargins(
|
||||
horizontalMargin,
|
||||
context.resources.getDimensionPixelOffset(R.dimen.margin_small),
|
||||
horizontalMargin,
|
||||
0,
|
||||
)
|
||||
layout.addView(editText, lp)
|
||||
setView(layout)
|
||||
return editText
|
||||
}
|
||||
|
||||
fun <B : AlertDialog.Builder> B.setEditText(
|
||||
entries: List<CharSequence>,
|
||||
inputType: Int,
|
||||
singleLine: Boolean,
|
||||
): EditText {
|
||||
if (entries.isEmpty()) {
|
||||
return setEditText(inputType, singleLine)
|
||||
}
|
||||
val binding = ViewDialogAutocompleteBinding.inflate(LayoutInflater.from(context))
|
||||
binding.autoCompleteTextView.setAdapter(
|
||||
ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, entries),
|
||||
)
|
||||
binding.dropdown.setOnClickListener {
|
||||
binding.autoCompleteTextView.showDropDown()
|
||||
}
|
||||
binding.autoCompleteTextView.inputType = inputType
|
||||
if (singleLine) {
|
||||
binding.autoCompleteTextView.setSingleLine()
|
||||
binding.autoCompleteTextView.imeOptions = EditorInfo.IME_ACTION_DONE
|
||||
}
|
||||
setView(binding.root)
|
||||
return binding.autoCompleteTextView
|
||||
}
|
||||
|
||||
@@ -56,6 +56,11 @@ class ChipsView @JvmOverloads constructor(
|
||||
val data = it.tag
|
||||
onChipCloseClickListener?.onChipCloseClick(chip, data) ?: onChipClickListener?.onChipClick(chip, data)
|
||||
}
|
||||
private val chipOnLongClickListener = OnLongClickListener {
|
||||
val chip = it as Chip
|
||||
val data = it.tag
|
||||
onChipLongClickListener?.onChipLongClick(chip, data) ?: false
|
||||
}
|
||||
private val chipStyle: Int
|
||||
private val iconsVisible: Boolean
|
||||
var onChipClickListener: OnChipClickListener? = null
|
||||
@@ -66,6 +71,8 @@ class ChipsView @JvmOverloads constructor(
|
||||
}
|
||||
var onChipCloseClickListener: OnChipCloseClickListener? = null
|
||||
|
||||
var onChipLongClickListener: OnChipLongClickListener? = null
|
||||
|
||||
init {
|
||||
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChipsView, defStyleAttr, 0)
|
||||
chipStyle = ta.getResourceId(R.styleable.ChipsView_chipStyle, R.style.Widget_Kotatsu_Chip)
|
||||
@@ -145,6 +152,7 @@ class ChipsView @JvmOverloads constructor(
|
||||
setOnCloseIconClickListener(chipOnCloseListener)
|
||||
setEnsureMinTouchTargetSize(false)
|
||||
setOnClickListener(chipOnClickListener)
|
||||
setOnLongClickListener(chipOnLongClickListener)
|
||||
isElegantTextHeight = false
|
||||
}
|
||||
|
||||
@@ -276,4 +284,9 @@ class ChipsView @JvmOverloads constructor(
|
||||
|
||||
fun onChipCloseClick(chip: Chip, data: Any?)
|
||||
}
|
||||
|
||||
fun interface OnChipLongClickListener {
|
||||
|
||||
fun onChipLongClick(chip: Chip, data: Any?): Boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.koitharu.kotatsu.core.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class TouchBlockLayout @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
|
||||
var isTouchEventsAllowed = true
|
||||
|
||||
override fun onInterceptTouchEvent(
|
||||
ev: MotionEvent?
|
||||
): Boolean = if (isTouchEventsAllowed) {
|
||||
super.onInterceptTouchEvent(ev)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -99,10 +99,11 @@ class ChaptersPagesSheet : BaseAdaptiveSheet<SheetChaptersPagesBinding>(),
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat = insets
|
||||
|
||||
override fun onStateChanged(sheet: View, newState: Int) {
|
||||
if (newState == STATE_DRAGGING || newState == STATE_SETTLING) {
|
||||
return
|
||||
}
|
||||
val binding = viewBinding ?: return
|
||||
val binding = viewBinding ?: return
|
||||
binding.layoutTouchBlock.isTouchEventsAllowed = newState != STATE_COLLAPSED
|
||||
if (newState == STATE_DRAGGING || newState == STATE_SETTLING) {
|
||||
return
|
||||
}
|
||||
val isActionModeStarted = actionModeDelegate?.isActionModeStarted == true
|
||||
binding.toolbar.menuView?.isVisible = newState == STATE_EXPANDED && !isActionModeStarted
|
||||
binding.splitButtonRead.isVisible = newState != STATE_EXPANDED && !isActionModeStarted
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.details.ui.pager.chapters
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
@@ -11,6 +12,7 @@ import org.koitharu.kotatsu.core.model.LocalMangaSource
|
||||
import org.koitharu.kotatsu.core.nav.AppRouter
|
||||
import org.koitharu.kotatsu.core.ui.list.BaseListSelectionCallback
|
||||
import org.koitharu.kotatsu.core.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.toCollection
|
||||
import org.koitharu.kotatsu.core.util.ext.toSet
|
||||
import org.koitharu.kotatsu.details.ui.pager.ChaptersPagesViewModel
|
||||
@@ -78,11 +80,20 @@ class ChaptersSelectionCallback(
|
||||
ids.size == manga.chapters?.size -> viewModel.deleteLocal()
|
||||
else -> {
|
||||
LocalChaptersRemoveService.start(recyclerView.context, manga, ids.toSet())
|
||||
Snackbar.make(
|
||||
recyclerView,
|
||||
R.string.chapters_will_removed_background,
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
try {
|
||||
Snackbar.make(
|
||||
recyclerView,
|
||||
R.string.chapters_will_removed_background,
|
||||
Snackbar.LENGTH_LONG,
|
||||
).show()
|
||||
} catch (e: IllegalArgumentException) {
|
||||
e.printStackTraceDebug()
|
||||
Toast.makeText(
|
||||
recyclerView.context,
|
||||
R.string.chapters_will_removed_background,
|
||||
Toast.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
mode?.finish()
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.koitharu.kotatsu.filter.data
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.builtins.SetSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.element
|
||||
import kotlinx.serialization.encoding.CompositeDecoder
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.encoding.decodeStructure
|
||||
import kotlinx.serialization.encoding.encodeStructure
|
||||
import kotlinx.serialization.serializer
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.util.ext.toLocaleOrNull
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
import org.koitharu.kotatsu.parsers.model.Demographic
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import java.util.Locale
|
||||
|
||||
object MangaListFilterSerializer : KSerializer<MangaListFilter> {
|
||||
|
||||
override val descriptor: SerialDescriptor =
|
||||
buildClassSerialDescriptor(MangaListFilter::class.java.name) {
|
||||
element<String?>("query", isOptional = true)
|
||||
element(
|
||||
elementName = "tags",
|
||||
descriptor = SetSerializer(MangaTagSerializer).descriptor,
|
||||
isOptional = true,
|
||||
)
|
||||
element(
|
||||
elementName = "tagsExclude",
|
||||
descriptor = SetSerializer(MangaTagSerializer).descriptor,
|
||||
isOptional = true,
|
||||
)
|
||||
element<String?>("locale", isOptional = true)
|
||||
element<String?>("originalLocale", isOptional = true)
|
||||
element<Set<MangaState>>("states", isOptional = true)
|
||||
element<Set<ContentRating>>("contentRating", isOptional = true)
|
||||
element<Set<ContentType>>("types", isOptional = true)
|
||||
element<Set<Demographic>>("demographics", isOptional = true)
|
||||
element<Int>("year", isOptional = true)
|
||||
element<Int>("yearFrom", isOptional = true)
|
||||
element<Int>("yearTo", isOptional = true)
|
||||
element<String?>("author", isOptional = true)
|
||||
}
|
||||
|
||||
override fun serialize(
|
||||
encoder: Encoder,
|
||||
value: MangaListFilter
|
||||
) = encoder.encodeStructure(descriptor) {
|
||||
encodeNullableSerializableElement(descriptor, 0, String.serializer(), value.query)
|
||||
encodeSerializableElement(descriptor, 1, SetSerializer(MangaTagSerializer), value.tags)
|
||||
encodeSerializableElement(descriptor, 2, SetSerializer(MangaTagSerializer), value.tagsExclude)
|
||||
encodeNullableSerializableElement(descriptor, 3, String.serializer(), value.locale?.toLanguageTag())
|
||||
encodeNullableSerializableElement(descriptor, 4, String.serializer(), value.originalLocale?.toLanguageTag())
|
||||
encodeSerializableElement(descriptor, 5, SetSerializer(serializer()), value.states)
|
||||
encodeSerializableElement(descriptor, 6, SetSerializer(serializer()), value.contentRating)
|
||||
encodeSerializableElement(descriptor, 7, SetSerializer(serializer()), value.types)
|
||||
encodeSerializableElement(descriptor, 8, SetSerializer(serializer()), value.demographics)
|
||||
encodeIntElement(descriptor, 9, value.year)
|
||||
encodeIntElement(descriptor, 10, value.yearFrom)
|
||||
encodeIntElement(descriptor, 11, value.yearTo)
|
||||
encodeNullableSerializableElement(descriptor, 12, String.serializer(), value.author)
|
||||
}
|
||||
|
||||
override fun deserialize(
|
||||
decoder: Decoder
|
||||
): MangaListFilter = decoder.decodeStructure(descriptor) {
|
||||
var query: String? = MangaListFilter.EMPTY.query
|
||||
var tags: Set<MangaTag> = MangaListFilter.EMPTY.tags
|
||||
var tagsExclude: Set<MangaTag> = MangaListFilter.EMPTY.tagsExclude
|
||||
var locale: Locale? = MangaListFilter.EMPTY.locale
|
||||
var originalLocale: Locale? = MangaListFilter.EMPTY.originalLocale
|
||||
var states: Set<MangaState> = MangaListFilter.EMPTY.states
|
||||
var contentRating: Set<ContentRating> = MangaListFilter.EMPTY.contentRating
|
||||
var types: Set<ContentType> = MangaListFilter.EMPTY.types
|
||||
var demographics: Set<Demographic> = MangaListFilter.EMPTY.demographics
|
||||
var year: Int = MangaListFilter.EMPTY.year
|
||||
var yearFrom: Int = MangaListFilter.EMPTY.yearFrom
|
||||
var yearTo: Int = MangaListFilter.EMPTY.yearTo
|
||||
var author: String? = MangaListFilter.EMPTY.author
|
||||
|
||||
while (true) {
|
||||
when (decodeElementIndex(descriptor)) {
|
||||
0 -> query = decodeNullableSerializableElement(descriptor, 0, serializer<String>())
|
||||
1 -> tags = decodeSerializableElement(descriptor, 1, SetSerializer(MangaTagSerializer))
|
||||
2 -> tagsExclude = decodeSerializableElement(descriptor, 2, SetSerializer(MangaTagSerializer))
|
||||
3 -> locale = decodeNullableSerializableElement(descriptor, 3, serializer<String>())?.toLocaleOrNull()
|
||||
4 -> originalLocale =
|
||||
decodeNullableSerializableElement(descriptor, 4, serializer<String>())?.toLocaleOrNull()
|
||||
|
||||
5 -> states = decodeSerializableElement(descriptor, 5, SetSerializer(serializer()))
|
||||
6 -> contentRating = decodeSerializableElement(descriptor, 6, SetSerializer(serializer()))
|
||||
7 -> types = decodeSerializableElement(descriptor, 7, SetSerializer(serializer()))
|
||||
8 -> demographics = decodeSerializableElement(descriptor, 8, SetSerializer(serializer()))
|
||||
9 -> year = decodeIntElement(descriptor, 9)
|
||||
10 -> yearFrom = decodeIntElement(descriptor, 10)
|
||||
11 -> yearTo = decodeIntElement(descriptor, 11)
|
||||
12 -> author = decodeNullableSerializableElement(descriptor, 12, serializer<String>())
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
}
|
||||
}
|
||||
|
||||
MangaListFilter(
|
||||
query = query,
|
||||
tags = tags,
|
||||
tagsExclude = tagsExclude,
|
||||
locale = locale,
|
||||
originalLocale = originalLocale,
|
||||
states = states,
|
||||
contentRating = contentRating,
|
||||
types = types,
|
||||
demographics = demographics,
|
||||
year = year,
|
||||
yearFrom = yearFrom,
|
||||
yearTo = yearTo,
|
||||
author = author,
|
||||
)
|
||||
}
|
||||
|
||||
private object MangaTagSerializer : KSerializer<MangaTag> {
|
||||
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor(MangaTag::class.java.name) {
|
||||
element<String>("title")
|
||||
element<String>("key")
|
||||
element<String>("source")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: MangaTag) = encoder.encodeStructure(descriptor) {
|
||||
encodeStringElement(descriptor, 0, value.title)
|
||||
encodeStringElement(descriptor, 1, value.key)
|
||||
encodeStringElement(descriptor, 2, value.source.name)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): MangaTag = decoder.decodeStructure(descriptor) {
|
||||
var title: String? = null
|
||||
var key: String? = null
|
||||
var source: String? = null
|
||||
|
||||
while (true) {
|
||||
when (decodeElementIndex(descriptor)) {
|
||||
0 -> title = decodeStringElement(descriptor, 0)
|
||||
1 -> key = decodeStringElement(descriptor, 1)
|
||||
2 -> source = decodeStringElement(descriptor, 2)
|
||||
CompositeDecoder.DECODE_DONE -> break
|
||||
}
|
||||
}
|
||||
|
||||
MangaTag(
|
||||
title = title ?: error("Missing 'title' field"),
|
||||
key = key ?: error("Missing 'key' field"),
|
||||
source = MangaSource(source),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.koitharu.kotatsu.filter.data
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonIgnoreUnknownKeys
|
||||
import org.koitharu.kotatsu.core.model.MangaSourceSerializer
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
|
||||
@Serializable
|
||||
@JsonIgnoreUnknownKeys
|
||||
data class PersistableFilter(
|
||||
@SerialName("name")
|
||||
val name: String,
|
||||
@Serializable(with = MangaSourceSerializer::class)
|
||||
@SerialName("source")
|
||||
val source: MangaSource,
|
||||
@Serializable(with = MangaListFilterSerializer::class)
|
||||
@SerialName("filter")
|
||||
val filter: MangaListFilter,
|
||||
) {
|
||||
|
||||
val id: Int
|
||||
get() = name.hashCode()
|
||||
|
||||
companion object {
|
||||
|
||||
const val MAX_TITLE_LENGTH = 18
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.koitharu.kotatsu.filter.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koitharu.kotatsu.core.util.ext.observeChanges
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
class SavedFiltersRepository @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
fun observeAll(source: MangaSource): Flow<List<PersistableFilter>> = getPrefs(source).observeChanges()
|
||||
.onStart { emit(null) }
|
||||
.map {
|
||||
getAll(source)
|
||||
}.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
suspend fun getAll(source: MangaSource): List<PersistableFilter> = withContext(Dispatchers.Default) {
|
||||
val prefs = getPrefs(source)
|
||||
val keys = prefs.all.keys.filter { it.startsWith(FILTER_PREFIX) }
|
||||
keys.mapNotNull { key ->
|
||||
val value = prefs.getString(key, null) ?: return@mapNotNull null
|
||||
try {
|
||||
Json.decodeFromString(value)
|
||||
} catch (e: SerializationException) {
|
||||
e.printStackTraceDebug()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun save(
|
||||
source: MangaSource,
|
||||
name: String,
|
||||
filter: MangaListFilter,
|
||||
): PersistableFilter = withContext(Dispatchers.Default) {
|
||||
val persistableFilter = PersistableFilter(
|
||||
name = name,
|
||||
source = source,
|
||||
filter = filter,
|
||||
)
|
||||
persist(source, persistableFilter)
|
||||
persistableFilter
|
||||
}
|
||||
|
||||
suspend fun rename(source: MangaSource, id: Int, newName: String) = withContext(Dispatchers.Default) {
|
||||
val filter = load(source, id) ?: return@withContext
|
||||
val newFilter = filter.copy(name = newName)
|
||||
val prefs = getPrefs(source)
|
||||
prefs.edit(commit = true) {
|
||||
remove(key(id))
|
||||
putString(key(newFilter.id), Json.encodeToString(newFilter))
|
||||
}
|
||||
newFilter
|
||||
}
|
||||
|
||||
suspend fun delete(source: MangaSource, id: Int) = withContext(Dispatchers.Default) {
|
||||
val prefs = getPrefs(source)
|
||||
prefs.edit(commit = true) {
|
||||
remove(key(id))
|
||||
}
|
||||
}
|
||||
|
||||
private fun persist(source: MangaSource, persistableFilter: PersistableFilter) {
|
||||
val prefs = getPrefs(source)
|
||||
val json = Json.encodeToString(persistableFilter)
|
||||
prefs.edit(commit = true) {
|
||||
putString(key(persistableFilter.id), json)
|
||||
}
|
||||
}
|
||||
|
||||
private fun load(source: MangaSource, id: Int): PersistableFilter? {
|
||||
val prefs = getPrefs(source)
|
||||
val json = prefs.getString(key(id), null) ?: return null
|
||||
return try {
|
||||
Json.decodeFromString<PersistableFilter>(json)
|
||||
} catch (e: SerializationException) {
|
||||
e.printStackTraceDebug()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPrefs(source: MangaSource): SharedPreferences {
|
||||
val key = source.name.replace(File.separatorChar, '$')
|
||||
return context.getSharedPreferences(key, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val FILTER_PREFIX = "__pf_"
|
||||
|
||||
fun key(id: Int) = FILTER_PREFIX + id
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.model.MangaSource
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
@@ -25,6 +26,8 @@ import org.koitharu.kotatsu.core.util.ext.asFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.lifecycleScope
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedByOrdinal
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
||||
import org.koitharu.kotatsu.filter.data.PersistableFilter
|
||||
import org.koitharu.kotatsu.filter.data.SavedFiltersRepository
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.filter.ui.tags.TagTitleComparator
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
@@ -48,469 +51,502 @@ import javax.inject.Inject
|
||||
|
||||
@ViewModelScoped
|
||||
class FilterCoordinator @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
lifecycle: ViewModelLifecycle,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
mangaRepositoryFactory: MangaRepository.Factory,
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
private val savedFiltersRepository: SavedFiltersRepository,
|
||||
lifecycle: ViewModelLifecycle,
|
||||
) {
|
||||
|
||||
private val coroutineScope = lifecycle.lifecycleScope + Dispatchers.Default
|
||||
private val repository = mangaRepositoryFactory.create(MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE]))
|
||||
private val sourceLocale = (repository.source as? MangaParserSource)?.locale
|
||||
private val coroutineScope = lifecycle.lifecycleScope + Dispatchers.Default
|
||||
private val repository = mangaRepositoryFactory.create(MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE]))
|
||||
private val sourceLocale = (repository.source as? MangaParserSource)?.locale
|
||||
|
||||
private val currentListFilter = MutableStateFlow(MangaListFilter.EMPTY)
|
||||
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
|
||||
private val currentListFilter = MutableStateFlow(MangaListFilter.EMPTY)
|
||||
private val currentSortOrder = MutableStateFlow(repository.defaultSortOrder)
|
||||
|
||||
private val availableSortOrders = repository.sortOrders
|
||||
private val filterOptions = suspendLazy { repository.getFilterOptions() }
|
||||
val capabilities = repository.filterCapabilities
|
||||
private val availableSortOrders = repository.sortOrders
|
||||
private val filterOptions = suspendLazy { repository.getFilterOptions() }
|
||||
|
||||
val mangaSource: MangaSource
|
||||
get() = repository.source
|
||||
val capabilities = repository.filterCapabilities
|
||||
|
||||
val isFilterApplied: Boolean
|
||||
get() = currentListFilter.value.isNotEmpty()
|
||||
val mangaSource: MangaSource
|
||||
get() = repository.source
|
||||
|
||||
val query: StateFlow<String?> = currentListFilter.map { it.query }
|
||||
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
|
||||
val isFilterApplied: Boolean
|
||||
get() = currentListFilter.value.isNotEmpty()
|
||||
|
||||
val sortOrder: StateFlow<FilterProperty<SortOrder>> = currentSortOrder.map { selected ->
|
||||
FilterProperty(
|
||||
availableItems = availableSortOrders.sortedByOrdinal(),
|
||||
selectedItem = selected,
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
val query: StateFlow<String?> = currentListFilter.map { it.query }
|
||||
.stateIn(coroutineScope, SharingStarted.Eagerly, null)
|
||||
|
||||
val tags: StateFlow<FilterProperty<MangaTag>> = combine(
|
||||
getTopTags(TAGS_LIMIT),
|
||||
currentListFilter.distinctUntilChangedBy { it.tags },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.addFirstDistinct(selected.tags),
|
||||
selectedItems = selected.tags,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
val sortOrder: StateFlow<FilterProperty<SortOrder>> = currentSortOrder.map { selected ->
|
||||
FilterProperty(
|
||||
availableItems = availableSortOrders.sortedByOrdinal(),
|
||||
selectedItem = selected,
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
|
||||
val tagsExcluded: StateFlow<FilterProperty<MangaTag>> = if (capabilities.isTagsExclusionSupported) {
|
||||
combine(
|
||||
getBottomTags(TAGS_LIMIT),
|
||||
currentListFilter.distinctUntilChangedBy { it.tagsExclude },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.addFirstDistinct(selected.tagsExclude),
|
||||
selectedItems = selected.tagsExclude,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
} else {
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
val tags: StateFlow<FilterProperty<MangaTag>> = combine(
|
||||
getTopTags(TAGS_LIMIT),
|
||||
currentListFilter.distinctUntilChangedBy { it.tags },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.addFirstDistinct(selected.tags),
|
||||
selectedItems = selected.tags,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
|
||||
val states: StateFlow<FilterProperty<MangaState>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.states },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableStates.sortedByOrdinal(),
|
||||
selectedItems = selected.states,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
val tagsExcluded: StateFlow<FilterProperty<MangaTag>> = if (capabilities.isTagsExclusionSupported) {
|
||||
combine(
|
||||
getBottomTags(TAGS_LIMIT),
|
||||
currentListFilter.distinctUntilChangedBy { it.tagsExclude },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.addFirstDistinct(selected.tagsExclude),
|
||||
selectedItems = selected.tagsExclude,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
} else {
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
|
||||
val contentRating: StateFlow<FilterProperty<ContentRating>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.contentRating },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableContentRating.sortedByOrdinal(),
|
||||
selectedItems = selected.contentRating,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
val authors: StateFlow<FilterProperty<String>> = if (capabilities.isAuthorSearchSupported) {
|
||||
combine(
|
||||
flow { emit(searchRepository.getAuthors(repository.source, TAGS_LIMIT)) },
|
||||
currentListFilter.distinctUntilChangedBy { it.author },
|
||||
) { available, selected ->
|
||||
FilterProperty(
|
||||
availableItems = available,
|
||||
selectedItems = setOfNotNull(selected.author),
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
} else {
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
|
||||
val contentTypes: StateFlow<FilterProperty<ContentType>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.types },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableContentTypes.sortedByOrdinal(),
|
||||
selectedItems = selected.types,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
val states: StateFlow<FilterProperty<MangaState>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.states },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableStates.sortedByOrdinal(),
|
||||
selectedItems = selected.states,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
|
||||
val demographics: StateFlow<FilterProperty<Demographic>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.demographics },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableDemographics.sortedByOrdinal(),
|
||||
selectedItems = selected.demographics,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
val contentRating: StateFlow<FilterProperty<ContentRating>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.contentRating },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableContentRating.sortedByOrdinal(),
|
||||
selectedItems = selected.contentRating,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
|
||||
val locale: StateFlow<FilterProperty<Locale?>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.locale },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableLocales.sortedWithSafe(LocaleComparator()).addFirstDistinct(null),
|
||||
selectedItems = setOfNotNull(selected.locale),
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
val contentTypes: StateFlow<FilterProperty<ContentType>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.types },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableContentTypes.sortedByOrdinal(),
|
||||
selectedItems = selected.types,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
|
||||
val originalLocale: StateFlow<FilterProperty<Locale?>> = if (capabilities.isOriginalLocaleSupported) {
|
||||
combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.originalLocale },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableLocales.sortedWithSafe(LocaleComparator()).addFirstDistinct(null),
|
||||
selectedItems = setOfNotNull(selected.originalLocale),
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
} else {
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
val demographics: StateFlow<FilterProperty<Demographic>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.demographics },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableDemographics.sortedByOrdinal(),
|
||||
selectedItems = selected.demographics,
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
|
||||
val year: StateFlow<FilterProperty<Int>> = if (capabilities.isYearSupported) {
|
||||
currentListFilter.distinctUntilChangedBy { it.year }.map { selected ->
|
||||
FilterProperty(
|
||||
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
||||
selectedItems = setOf(selected.year),
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
} else {
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
val locale: StateFlow<FilterProperty<Locale?>> = combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.locale },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableLocales.sortedWithSafe(LocaleComparator()).addFirstDistinct(null),
|
||||
selectedItems = setOfNotNull(selected.locale),
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
|
||||
val yearRange: StateFlow<FilterProperty<Int>> = if (capabilities.isYearRangeSupported) {
|
||||
currentListFilter.distinctUntilChanged { old, new ->
|
||||
old.yearTo == new.yearTo && old.yearFrom == new.yearFrom
|
||||
}.map { selected ->
|
||||
FilterProperty(
|
||||
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
||||
selectedItems = setOf(selected.yearFrom.ifZero { YEAR_MIN }, selected.yearTo.ifZero { MAX_YEAR }),
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
} else {
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
val originalLocale: StateFlow<FilterProperty<Locale?>> = if (capabilities.isOriginalLocaleSupported) {
|
||||
combine(
|
||||
filterOptions.asFlow(),
|
||||
currentListFilter.distinctUntilChangedBy { it.originalLocale },
|
||||
) { available, selected ->
|
||||
available.fold(
|
||||
onSuccess = {
|
||||
FilterProperty(
|
||||
availableItems = it.availableLocales.sortedWithSafe(LocaleComparator()).addFirstDistinct(null),
|
||||
selectedItems = setOfNotNull(selected.originalLocale),
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
FilterProperty.error(it)
|
||||
},
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
} else {
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
currentListFilter.value = MangaListFilter.EMPTY
|
||||
}
|
||||
val year: StateFlow<FilterProperty<Int>> = if (capabilities.isYearSupported) {
|
||||
currentListFilter.distinctUntilChangedBy { it.year }.map { selected ->
|
||||
FilterProperty(
|
||||
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
||||
selectedItems = setOf(selected.year),
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
} else {
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
|
||||
fun snapshot() = Snapshot(
|
||||
sortOrder = currentSortOrder.value,
|
||||
listFilter = currentListFilter.value,
|
||||
)
|
||||
val yearRange: StateFlow<FilterProperty<Int>> = if (capabilities.isYearRangeSupported) {
|
||||
currentListFilter.distinctUntilChanged { old, new ->
|
||||
old.yearTo == new.yearTo && old.yearFrom == new.yearFrom
|
||||
}.map { selected ->
|
||||
FilterProperty(
|
||||
availableItems = listOf(YEAR_MIN, MAX_YEAR),
|
||||
selectedItems = setOf(selected.yearFrom.ifZero { YEAR_MIN }, selected.yearTo.ifZero { MAX_YEAR }),
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.LOADING)
|
||||
} else {
|
||||
MutableStateFlow(FilterProperty.EMPTY)
|
||||
}
|
||||
|
||||
fun observe(): Flow<Snapshot> = combine(currentSortOrder, currentListFilter, ::Snapshot)
|
||||
val savedFilters: StateFlow<FilterProperty<PersistableFilter>> = combine(
|
||||
savedFiltersRepository.observeAll(repository.source),
|
||||
currentListFilter,
|
||||
) { available, applied ->
|
||||
FilterProperty(
|
||||
availableItems = available,
|
||||
selectedItems = setOfNotNull(available.find { it.filter == applied }),
|
||||
)
|
||||
}.stateIn(coroutineScope, SharingStarted.Lazily, FilterProperty.EMPTY)
|
||||
|
||||
fun setSortOrder(newSortOrder: SortOrder) {
|
||||
currentSortOrder.value = newSortOrder
|
||||
repository.defaultSortOrder = newSortOrder
|
||||
}
|
||||
fun reset() {
|
||||
currentListFilter.value = MangaListFilter.EMPTY
|
||||
}
|
||||
|
||||
fun set(value: MangaListFilter) {
|
||||
currentListFilter.value = value
|
||||
}
|
||||
fun snapshot() = Snapshot(
|
||||
sortOrder = currentSortOrder.value,
|
||||
listFilter = currentListFilter.value,
|
||||
)
|
||||
|
||||
fun setAdjusted(value: MangaListFilter) {
|
||||
var newFilter = value
|
||||
if (!newFilter.author.isNullOrEmpty() && !capabilities.isAuthorSearchSupported) {
|
||||
newFilter = newFilter.copy(
|
||||
query = newFilter.author,
|
||||
author = null,
|
||||
)
|
||||
}
|
||||
if (!capabilities.isSearchSupported && !newFilter.query.isNullOrEmpty()) {
|
||||
newFilter = newFilter.copy(
|
||||
query = null,
|
||||
)
|
||||
}
|
||||
if (!newFilter.query.isNullOrEmpty() && !newFilter.hasNonSearchOptions() && !capabilities.isSearchWithFiltersSupported) {
|
||||
newFilter = MangaListFilter(query = newFilter.query)
|
||||
}
|
||||
set(newFilter)
|
||||
}
|
||||
fun observe(): Flow<Snapshot> = combine(currentSortOrder, currentListFilter, ::Snapshot)
|
||||
|
||||
fun setQuery(value: String?) {
|
||||
val newQuery = value?.trim()?.nullIfEmpty()
|
||||
currentListFilter.update { oldValue ->
|
||||
if (capabilities.isSearchWithFiltersSupported || newQuery == null) {
|
||||
oldValue.copy(query = newQuery)
|
||||
} else {
|
||||
MangaListFilter(query = newQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun setSortOrder(newSortOrder: SortOrder) {
|
||||
currentSortOrder.value = newSortOrder
|
||||
repository.defaultSortOrder = newSortOrder
|
||||
}
|
||||
|
||||
fun setLocale(value: Locale?) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
locale = value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun set(value: MangaListFilter) {
|
||||
currentListFilter.value = value
|
||||
}
|
||||
|
||||
fun setAuthor(value: String?) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
author = value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun setAdjusted(value: MangaListFilter) {
|
||||
var newFilter = value
|
||||
if (!newFilter.author.isNullOrEmpty() && !capabilities.isAuthorSearchSupported) {
|
||||
newFilter = newFilter.copy(
|
||||
query = newFilter.author,
|
||||
author = null,
|
||||
)
|
||||
}
|
||||
if (!newFilter.query.isNullOrEmpty() && !newFilter.hasNonSearchOptions() && !capabilities.isSearchWithFiltersSupported) {
|
||||
newFilter = MangaListFilter(query = newFilter.query)
|
||||
}
|
||||
set(newFilter)
|
||||
}
|
||||
|
||||
fun setOriginalLocale(value: Locale?) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
originalLocale = value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun saveCurrentFilter(name: String) = coroutineScope.launch {
|
||||
savedFiltersRepository.save(repository.source, name, currentListFilter.value)
|
||||
}
|
||||
|
||||
fun setYear(value: Int) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
year = value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun renameSavedFilter(id: Int, newName: String) = coroutineScope.launch {
|
||||
savedFiltersRepository.rename(repository.source, id, newName)
|
||||
}
|
||||
|
||||
fun setYearRange(valueFrom: Int, valueTo: Int) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
yearFrom = valueFrom,
|
||||
yearTo = valueTo,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun deleteSavedFilter(id: Int) = coroutineScope.launch {
|
||||
savedFiltersRepository.delete(repository.source, id)
|
||||
}
|
||||
|
||||
fun toggleState(value: MangaState, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
states = if (isSelected) oldValue.states + value else oldValue.states - value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun setQuery(value: String?) {
|
||||
val newQuery = value?.trim()?.nullIfEmpty()
|
||||
currentListFilter.update { oldValue ->
|
||||
if (capabilities.isSearchWithFiltersSupported || newQuery == null) {
|
||||
oldValue.copy(query = newQuery)
|
||||
} else {
|
||||
MangaListFilter(query = newQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleContentRating(value: ContentRating, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
contentRating = if (isSelected) oldValue.contentRating + value else oldValue.contentRating - value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun setLocale(value: Locale?) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
locale = value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleDemographic(value: Demographic, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
demographics = if (isSelected) oldValue.demographics + value else oldValue.demographics - value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun setAuthor(value: String?) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
author = value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleContentType(value: ContentType, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
types = if (isSelected) oldValue.types + value else oldValue.types - value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun setOriginalLocale(value: Locale?) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
originalLocale = value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleTag(value: MangaTag, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
val newTags = if (capabilities.isMultipleTagsSupported) {
|
||||
if (isSelected) oldValue.tags + value else oldValue.tags - value
|
||||
} else {
|
||||
if (isSelected) setOf(value) else emptySet()
|
||||
}
|
||||
oldValue.copy(
|
||||
tags = newTags,
|
||||
tagsExclude = oldValue.tagsExclude - newTags,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun setYear(value: Int) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
year = value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleTagExclude(value: MangaTag, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
val newTagsExclude = if (capabilities.isMultipleTagsSupported) {
|
||||
if (isSelected) oldValue.tagsExclude + value else oldValue.tagsExclude - value
|
||||
} else {
|
||||
if (isSelected) setOf(value) else emptySet()
|
||||
}
|
||||
oldValue.copy(
|
||||
tags = oldValue.tags - newTagsExclude,
|
||||
tagsExclude = newTagsExclude,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun setYearRange(valueFrom: Int, valueTo: Int) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
yearFrom = valueFrom,
|
||||
yearTo = valueTo,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllTags(): Flow<Result<List<MangaTag>>> = filterOptions.asFlow().map {
|
||||
it.map { x -> x.availableTags.sortedWithSafe(TagTitleComparator(sourceLocale)) }
|
||||
}
|
||||
fun toggleState(value: MangaState, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
states = if (isSelected) oldValue.states + value else oldValue.states - value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MangaListFilter.takeQueryIfSupported() = when {
|
||||
capabilities.isSearchWithFiltersSupported -> query
|
||||
query.isNullOrEmpty() -> query
|
||||
hasNonSearchOptions() -> null
|
||||
else -> query
|
||||
}
|
||||
fun toggleContentRating(value: ContentRating, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
contentRating = if (isSelected) oldValue.contentRating + value else oldValue.contentRating - value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTopTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
||||
flow { emit(searchRepository.getTopTags(repository.source, limit)) },
|
||||
filterOptions.asFlow(),
|
||||
) { suggested, options ->
|
||||
val all = options.getOrNull()?.availableTags.orEmpty()
|
||||
val result = ArrayList<MangaTag>(limit)
|
||||
result.addAll(suggested.take(limit))
|
||||
if (result.size < limit) {
|
||||
result.addAll(all.shuffled().take(limit - result.size))
|
||||
}
|
||||
if (result.isNotEmpty()) {
|
||||
Result.success(result)
|
||||
} else {
|
||||
options.map { result }
|
||||
}
|
||||
}.catch {
|
||||
emit(Result.failure(it))
|
||||
}
|
||||
fun toggleDemographic(value: Demographic, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
demographics = if (isSelected) oldValue.demographics + value else oldValue.demographics - value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getBottomTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
||||
flow { emit(searchRepository.getRareTags(repository.source, limit)) },
|
||||
filterOptions.asFlow(),
|
||||
) { suggested, options ->
|
||||
val all = options.getOrNull()?.availableTags.orEmpty()
|
||||
val result = ArrayList<MangaTag>(limit)
|
||||
result.addAll(suggested.take(limit))
|
||||
if (result.size < limit) {
|
||||
result.addAll(all.shuffled().take(limit - result.size))
|
||||
}
|
||||
if (result.isNotEmpty()) {
|
||||
Result.success(result)
|
||||
} else {
|
||||
options.map { result }
|
||||
}
|
||||
}.catch {
|
||||
emit(Result.failure(it))
|
||||
}
|
||||
fun toggleContentType(value: ContentType, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
oldValue.copy(
|
||||
types = if (isSelected) oldValue.types + value else oldValue.types - value,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> List<T>.addFirstDistinct(other: Collection<T>): List<T> {
|
||||
val result = ArrayDeque<T>(this.size + other.size)
|
||||
result.addAll(this)
|
||||
for (item in other) {
|
||||
if (item !in result) {
|
||||
result.addFirst(item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
fun toggleTag(value: MangaTag, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
val newTags = if (capabilities.isMultipleTagsSupported) {
|
||||
if (isSelected) oldValue.tags + value else oldValue.tags - value
|
||||
} else {
|
||||
if (isSelected) setOf(value) else emptySet()
|
||||
}
|
||||
oldValue.copy(
|
||||
tags = newTags,
|
||||
tagsExclude = oldValue.tagsExclude - newTags,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> List<T>.addFirstDistinct(item: T): List<T> {
|
||||
val result = ArrayDeque<T>(this.size + 1)
|
||||
result.addAll(this)
|
||||
if (item !in result) {
|
||||
result.addFirst(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
fun toggleTagExclude(value: MangaTag, isSelected: Boolean) {
|
||||
currentListFilter.update { oldValue ->
|
||||
val newTagsExclude = if (capabilities.isMultipleTagsSupported) {
|
||||
if (isSelected) oldValue.tagsExclude + value else oldValue.tagsExclude - value
|
||||
} else {
|
||||
if (isSelected) setOf(value) else emptySet()
|
||||
}
|
||||
oldValue.copy(
|
||||
tags = oldValue.tags - newTagsExclude,
|
||||
tagsExclude = newTagsExclude,
|
||||
query = oldValue.takeQueryIfSupported(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Snapshot(
|
||||
val sortOrder: SortOrder,
|
||||
val listFilter: MangaListFilter,
|
||||
)
|
||||
fun getAllTags(): Flow<Result<List<MangaTag>>> = filterOptions.asFlow().map {
|
||||
it.map { x -> x.availableTags.sortedWithSafe(TagTitleComparator(sourceLocale)) }
|
||||
}
|
||||
|
||||
interface Owner {
|
||||
private fun MangaListFilter.takeQueryIfSupported() = when {
|
||||
capabilities.isSearchWithFiltersSupported -> query
|
||||
query.isNullOrEmpty() -> query
|
||||
hasNonSearchOptions() -> null
|
||||
else -> query
|
||||
}
|
||||
|
||||
val filterCoordinator: FilterCoordinator
|
||||
}
|
||||
private fun getTopTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
||||
flow { emit(searchRepository.getTopTags(repository.source, limit)) },
|
||||
filterOptions.asFlow(),
|
||||
) { suggested, options ->
|
||||
val all = options.getOrNull()?.availableTags.orEmpty()
|
||||
val result = ArrayList<MangaTag>(limit)
|
||||
result.addAll(suggested.take(limit))
|
||||
if (result.size < limit) {
|
||||
result.addAll(all.shuffled().take(limit - result.size))
|
||||
}
|
||||
if (result.isNotEmpty()) {
|
||||
Result.success(result)
|
||||
} else {
|
||||
options.map { result }
|
||||
}
|
||||
}.catch {
|
||||
emit(Result.failure(it))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun getBottomTags(limit: Int): Flow<Result<List<MangaTag>>> = combine(
|
||||
flow { emit(searchRepository.getRareTags(repository.source, limit)) },
|
||||
filterOptions.asFlow(),
|
||||
) { suggested, options ->
|
||||
val all = options.getOrNull()?.availableTags.orEmpty()
|
||||
val result = ArrayList<MangaTag>(limit)
|
||||
result.addAll(suggested.take(limit))
|
||||
if (result.size < limit) {
|
||||
result.addAll(all.shuffled().take(limit - result.size))
|
||||
}
|
||||
if (result.isNotEmpty()) {
|
||||
Result.success(result)
|
||||
} else {
|
||||
options.map { result }
|
||||
}
|
||||
}.catch {
|
||||
emit(Result.failure(it))
|
||||
}
|
||||
|
||||
private const val TAGS_LIMIT = 12
|
||||
private val MAX_YEAR = Calendar.getInstance()[Calendar.YEAR] + 1
|
||||
private fun <T> List<T>.addFirstDistinct(other: Collection<T>): List<T> {
|
||||
val result = ArrayDeque<T>(this.size + other.size)
|
||||
result.addAll(this)
|
||||
for (item in other) {
|
||||
if (item !in result) {
|
||||
result.addFirst(item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun find(fragment: Fragment): FilterCoordinator? {
|
||||
(fragment.activity as? Owner)?.let {
|
||||
return it.filterCoordinator
|
||||
}
|
||||
var f = fragment
|
||||
while (true) {
|
||||
(f as? Owner)?.let {
|
||||
return it.filterCoordinator
|
||||
}
|
||||
f = f.parentFragment ?: break
|
||||
}
|
||||
return null
|
||||
}
|
||||
private fun <T> List<T>.addFirstDistinct(item: T): List<T> {
|
||||
val result = ArrayDeque<T>(this.size + 1)
|
||||
result.addAll(this)
|
||||
if (item !in result) {
|
||||
result.addFirst(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun require(fragment: Fragment): FilterCoordinator {
|
||||
return find(fragment) ?: throw IllegalStateException("FilterCoordinator cannot be found")
|
||||
}
|
||||
}
|
||||
data class Snapshot(
|
||||
val sortOrder: SortOrder,
|
||||
val listFilter: MangaListFilter,
|
||||
)
|
||||
|
||||
interface Owner {
|
||||
|
||||
val filterCoordinator: FilterCoordinator
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAGS_LIMIT = 12
|
||||
private val MAX_YEAR = Calendar.getInstance()[Calendar.YEAR] + 1
|
||||
|
||||
fun find(fragment: Fragment): FilterCoordinator? {
|
||||
(fragment.activity as? Owner)?.let {
|
||||
return it.filterCoordinator
|
||||
}
|
||||
var f = fragment
|
||||
while (true) {
|
||||
(f as? Owner)?.let {
|
||||
return it.filterCoordinator
|
||||
}
|
||||
f = f.parentFragment ?: break
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun require(fragment: Fragment): FilterCoordinator {
|
||||
return find(fragment) ?: throw IllegalStateException("FilterCoordinator cannot be found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.databinding.FragmentFilterHeaderBinding
|
||||
import org.koitharu.kotatsu.filter.data.PersistableFilter
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||
@@ -28,69 +29,75 @@ import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FilterHeaderFragment : BaseFragment<FragmentFilterHeaderBinding>(), ChipsView.OnChipClickListener,
|
||||
ChipsView.OnChipCloseClickListener {
|
||||
ChipsView.OnChipCloseClickListener {
|
||||
|
||||
@Inject
|
||||
lateinit var filterHeaderProducer: FilterHeaderProducer
|
||||
@Inject
|
||||
lateinit var filterHeaderProducer: FilterHeaderProducer
|
||||
|
||||
private val filter: FilterCoordinator
|
||||
get() = (requireActivity() as FilterCoordinator.Owner).filterCoordinator
|
||||
private val filter: FilterCoordinator
|
||||
get() = (requireActivity() as FilterCoordinator.Owner).filterCoordinator
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding {
|
||||
return FragmentFilterHeaderBinding.inflate(inflater, container, false)
|
||||
}
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFilterHeaderBinding {
|
||||
return FragmentFilterHeaderBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
binding.chipsTags.onChipCloseClickListener = this
|
||||
filterHeaderProducer.observeHeader(filter)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.observe(viewLifecycleOwner, ::onDataChanged)
|
||||
}
|
||||
override fun onViewBindingCreated(binding: FragmentFilterHeaderBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
binding.chipsTags.onChipClickListener = this
|
||||
binding.chipsTags.onChipCloseClickListener = this
|
||||
filterHeaderProducer.observeHeader(filter)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.observe(viewLifecycleOwner, ::onDataChanged)
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat = insets
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat = insets
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is MangaTag -> filter.toggleTag(data, !chip.isChecked)
|
||||
is String -> Unit
|
||||
null -> router.showTagsCatalogSheet(excludeMode = false)
|
||||
}
|
||||
}
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is MangaTag -> filter.toggleTag(data, !chip.isChecked)
|
||||
is PersistableFilter -> if (chip.isChecked) {
|
||||
filter.reset()
|
||||
} else {
|
||||
filter.setAdjusted(data.filter)
|
||||
}
|
||||
|
||||
override fun onChipCloseClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is String -> if (data == filter.snapshot().listFilter.author) {
|
||||
filter.setAuthor(null)
|
||||
} else {
|
||||
filter.setQuery(null)
|
||||
}
|
||||
is String -> Unit
|
||||
null -> router.showTagsCatalogSheet(excludeMode = false)
|
||||
}
|
||||
}
|
||||
|
||||
is ContentRating -> filter.toggleContentRating(data, false)
|
||||
is Demographic -> filter.toggleDemographic(data, false)
|
||||
is ContentType -> filter.toggleContentType(data, false)
|
||||
is MangaState -> filter.toggleState(data, false)
|
||||
is Locale -> filter.setLocale(null)
|
||||
is Int -> filter.setYear(YEAR_UNKNOWN)
|
||||
is IntRange -> filter.setYearRange(YEAR_UNKNOWN, YEAR_UNKNOWN)
|
||||
}
|
||||
}
|
||||
override fun onChipCloseClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is String -> if (data == filter.snapshot().listFilter.author) {
|
||||
filter.setAuthor(null)
|
||||
} else {
|
||||
filter.setQuery(null)
|
||||
}
|
||||
|
||||
private fun onDataChanged(header: FilterHeaderModel) {
|
||||
val binding = viewBinding ?: return
|
||||
val chips = header.chips
|
||||
if (chips.isEmpty()) {
|
||||
binding.chipsTags.setChips(emptyList())
|
||||
binding.root.isVisible = false
|
||||
return
|
||||
}
|
||||
binding.chipsTags.setChips(header.chips)
|
||||
binding.root.isVisible = true
|
||||
if (binding.root.context.isAnimationsEnabled) {
|
||||
binding.scrollView.smoothScrollTo(0, 0)
|
||||
} else {
|
||||
binding.scrollView.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
is ContentRating -> filter.toggleContentRating(data, false)
|
||||
is Demographic -> filter.toggleDemographic(data, false)
|
||||
is ContentType -> filter.toggleContentType(data, false)
|
||||
is MangaState -> filter.toggleState(data, false)
|
||||
is Locale -> filter.setLocale(null)
|
||||
is Int -> filter.setYear(YEAR_UNKNOWN)
|
||||
is IntRange -> filter.setYearRange(YEAR_UNKNOWN, YEAR_UNKNOWN)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDataChanged(header: FilterHeaderModel) {
|
||||
val binding = viewBinding ?: return
|
||||
val chips = header.chips
|
||||
if (chips.isEmpty()) {
|
||||
binding.chipsTags.setChips(emptyList())
|
||||
binding.root.isVisible = false
|
||||
return
|
||||
}
|
||||
binding.chipsTags.setChips(header.chips)
|
||||
binding.root.isVisible = true
|
||||
if (binding.root.context.isAnimationsEnabled) {
|
||||
binding.scrollView.smoothScrollTo(0, 0)
|
||||
} else {
|
||||
binding.scrollView.scrollTo(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.filter.data.PersistableFilter
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterHeaderModel
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||
@@ -17,143 +18,162 @@ import javax.inject.Inject
|
||||
import androidx.appcompat.R as appcompatR
|
||||
|
||||
class FilterHeaderProducer @Inject constructor(
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
) {
|
||||
|
||||
fun observeHeader(filterCoordinator: FilterCoordinator): Flow<FilterHeaderModel> {
|
||||
return combine(filterCoordinator.tags, filterCoordinator.observe()) { tags, snapshot ->
|
||||
val chipList = createChipsList(
|
||||
source = filterCoordinator.mangaSource,
|
||||
capabilities = filterCoordinator.capabilities,
|
||||
tagsProperty = tags,
|
||||
snapshot = snapshot.listFilter,
|
||||
limit = 12,
|
||||
)
|
||||
FilterHeaderModel(
|
||||
chips = chipList,
|
||||
sortOrder = snapshot.sortOrder,
|
||||
isFilterApplied = !snapshot.listFilter.isEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
fun observeHeader(filterCoordinator: FilterCoordinator): Flow<FilterHeaderModel> {
|
||||
return combine(
|
||||
filterCoordinator.savedFilters,
|
||||
filterCoordinator.tags,
|
||||
filterCoordinator.observe(),
|
||||
) { saved, tags, snapshot ->
|
||||
val chipList = createChipsList(
|
||||
source = filterCoordinator.mangaSource,
|
||||
capabilities = filterCoordinator.capabilities,
|
||||
savedFilters = saved,
|
||||
tagsProperty = tags,
|
||||
snapshot = snapshot.listFilter,
|
||||
limit = 12,
|
||||
)
|
||||
FilterHeaderModel(
|
||||
chips = chipList,
|
||||
sortOrder = snapshot.sortOrder,
|
||||
isFilterApplied = !snapshot.listFilter.isEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createChipsList(
|
||||
source: MangaSource,
|
||||
capabilities: MangaListFilterCapabilities,
|
||||
tagsProperty: FilterProperty<MangaTag>,
|
||||
snapshot: MangaListFilter,
|
||||
limit: Int,
|
||||
): List<ChipsView.ChipModel> {
|
||||
val result = ArrayDeque<ChipsView.ChipModel>(limit + 3)
|
||||
if (snapshot.query.isNullOrEmpty() || capabilities.isSearchWithFiltersSupported) {
|
||||
val selectedTags = tagsProperty.selectedItems.toMutableSet()
|
||||
var tags = if (selectedTags.isEmpty()) {
|
||||
searchRepository.getTagsSuggestion("", limit, source)
|
||||
} else {
|
||||
searchRepository.getTagsSuggestion(selectedTags).take(limit)
|
||||
}
|
||||
if (tags.size < limit) {
|
||||
tags = tags + tagsProperty.availableItems.take(limit - tags.size)
|
||||
}
|
||||
if (tags.isEmpty() && selectedTags.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
for (tag in tags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = selectedTags.remove(tag),
|
||||
data = tag,
|
||||
)
|
||||
if (model.isChecked) {
|
||||
result.addFirst(model)
|
||||
} else {
|
||||
result.addLast(model)
|
||||
}
|
||||
}
|
||||
for (tag in selectedTags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = true,
|
||||
data = tag,
|
||||
)
|
||||
result.addFirst(model)
|
||||
}
|
||||
}
|
||||
snapshot.locale?.let {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = it.getDisplayName(it).toTitleCase(it),
|
||||
icon = R.drawable.ic_language,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.types.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.demographics.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.contentRating.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.states.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (!snapshot.query.isNullOrEmpty()) {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = snapshot.query,
|
||||
icon = appcompatR.drawable.abc_ic_search_api_material,
|
||||
isCloseable = true,
|
||||
data = snapshot.query,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (!snapshot.author.isNullOrEmpty()) {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = snapshot.author,
|
||||
icon = R.drawable.ic_user,
|
||||
isCloseable = true,
|
||||
data = snapshot.author,
|
||||
),
|
||||
)
|
||||
}
|
||||
val hasTags = result.any { it.data is MangaTag }
|
||||
if (hasTags) {
|
||||
result.addFirst(moreTagsChip())
|
||||
}
|
||||
return result
|
||||
}
|
||||
private suspend fun createChipsList(
|
||||
source: MangaSource,
|
||||
capabilities: MangaListFilterCapabilities,
|
||||
savedFilters: FilterProperty<PersistableFilter>,
|
||||
tagsProperty: FilterProperty<MangaTag>,
|
||||
snapshot: MangaListFilter,
|
||||
limit: Int,
|
||||
): List<ChipsView.ChipModel> {
|
||||
val result = ArrayDeque<ChipsView.ChipModel>(savedFilters.availableItems.size + limit + 3)
|
||||
if (snapshot.query.isNullOrEmpty() || capabilities.isSearchWithFiltersSupported) {
|
||||
val selectedTags = tagsProperty.selectedItems.toMutableSet()
|
||||
var tags = if (selectedTags.isEmpty()) {
|
||||
searchRepository.getTagsSuggestion("", limit, source)
|
||||
} else {
|
||||
searchRepository.getTagsSuggestion(selectedTags).take(limit)
|
||||
}
|
||||
if (tags.size < limit) {
|
||||
tags = tags + tagsProperty.availableItems.take(limit - tags.size)
|
||||
}
|
||||
if (tags.isEmpty() && selectedTags.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
for (saved in savedFilters.availableItems) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = saved.name,
|
||||
isChecked = saved in savedFilters.selectedItems,
|
||||
data = saved,
|
||||
)
|
||||
if (model.isChecked) {
|
||||
selectedTags.removeAll(saved.filter.tags)
|
||||
result.addFirst(model)
|
||||
} else {
|
||||
result.addLast(model)
|
||||
}
|
||||
}
|
||||
for (tag in tags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = selectedTags.remove(tag),
|
||||
data = tag,
|
||||
)
|
||||
if (model.isChecked) {
|
||||
result.addFirst(model)
|
||||
} else {
|
||||
result.addLast(model)
|
||||
}
|
||||
}
|
||||
for (tag in selectedTags) {
|
||||
val model = ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = true,
|
||||
data = tag,
|
||||
)
|
||||
result.addFirst(model)
|
||||
}
|
||||
}
|
||||
snapshot.locale?.let {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = it.getDisplayName(it).toTitleCase(it),
|
||||
icon = R.drawable.ic_language,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.types.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.demographics.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.contentRating.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
snapshot.states.forEach {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
titleResId = it.titleResId,
|
||||
isCloseable = true,
|
||||
data = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (!snapshot.query.isNullOrEmpty()) {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = snapshot.query,
|
||||
icon = appcompatR.drawable.abc_ic_search_api_material,
|
||||
isCloseable = true,
|
||||
data = snapshot.query,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (!snapshot.author.isNullOrEmpty()) {
|
||||
result.addFirst(
|
||||
ChipsView.ChipModel(
|
||||
title = snapshot.author,
|
||||
icon = R.drawable.ic_user,
|
||||
isCloseable = true,
|
||||
data = snapshot.author,
|
||||
),
|
||||
)
|
||||
}
|
||||
val hasTags = result.any { it.data is MangaTag }
|
||||
if (hasTags) {
|
||||
result.addFirst(moreTagsChip())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun moreTagsChip() = ChipsView.ChipModel(
|
||||
titleResId = R.string.genres,
|
||||
icon = R.drawable.ic_drawer_menu_open,
|
||||
)
|
||||
private fun moreTagsChip() = ChipsView.ChipModel(
|
||||
titleResId = R.string.genres,
|
||||
icon = R.drawable.ic_drawer_menu_open,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
package org.koitharu.kotatsu.filter.ui.sheet
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.InputFilter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
import com.google.android.material.slider.Slider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.model.titleResId
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.dialog.setEditText
|
||||
import org.koitharu.kotatsu.core.ui.model.titleRes
|
||||
import org.koitharu.kotatsu.core.ui.sheet.BaseAdaptiveSheet
|
||||
import org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
import org.koitharu.kotatsu.core.util.AlphanumComparator
|
||||
import org.koitharu.kotatsu.core.util.ext.consume
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.core.util.ext.getDisplayName
|
||||
@@ -26,6 +39,8 @@ import org.koitharu.kotatsu.core.util.ext.parentView
|
||||
import org.koitharu.kotatsu.core.util.ext.setValueRounded
|
||||
import org.koitharu.kotatsu.core.util.ext.setValuesRounded
|
||||
import org.koitharu.kotatsu.databinding.SheetFilterBinding
|
||||
import org.koitharu.kotatsu.filter.data.PersistableFilter
|
||||
import org.koitharu.kotatsu.filter.data.PersistableFilter.Companion.MAX_TITLE_LENGTH
|
||||
import org.koitharu.kotatsu.filter.ui.FilterCoordinator
|
||||
import org.koitharu.kotatsu.filter.ui.model.FilterProperty
|
||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||
@@ -35,322 +50,488 @@ import org.koitharu.kotatsu.parsers.model.MangaState
|
||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.model.YEAR_UNKNOWN
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.toIntUp
|
||||
import java.util.Locale
|
||||
import java.util.TreeSet
|
||||
|
||||
class FilterSheetFragment : BaseAdaptiveSheet<SheetFilterBinding>(),
|
||||
AdapterView.OnItemSelectedListener,
|
||||
ChipsView.OnChipClickListener {
|
||||
AdapterView.OnItemSelectedListener,
|
||||
View.OnClickListener,
|
||||
ChipsView.OnChipClickListener,
|
||||
ChipsView.OnChipLongClickListener,
|
||||
ChipsView.OnChipCloseClickListener {
|
||||
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
||||
return SheetFilterBinding.inflate(inflater, container, false)
|
||||
}
|
||||
override fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): SheetFilterBinding {
|
||||
return SheetFilterBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
if (dialog == null) {
|
||||
binding.layoutBody.updatePadding(top = binding.layoutBody.paddingBottom)
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
}
|
||||
val filter = FilterCoordinator.require(this)
|
||||
filter.sortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
|
||||
filter.locale.observe(viewLifecycleOwner, this::onLocaleChanged)
|
||||
filter.originalLocale.observe(viewLifecycleOwner, this::onOriginalLocaleChanged)
|
||||
filter.tags.observe(viewLifecycleOwner, this::onTagsChanged)
|
||||
filter.tagsExcluded.observe(viewLifecycleOwner, this::onTagsExcludedChanged)
|
||||
filter.states.observe(viewLifecycleOwner, this::onStateChanged)
|
||||
filter.contentTypes.observe(viewLifecycleOwner, this::onContentTypesChanged)
|
||||
filter.contentRating.observe(viewLifecycleOwner, this::onContentRatingChanged)
|
||||
filter.demographics.observe(viewLifecycleOwner, this::onDemographicsChanged)
|
||||
filter.year.observe(viewLifecycleOwner, this::onYearChanged)
|
||||
filter.yearRange.observe(viewLifecycleOwner, this::onYearRangeChanged)
|
||||
override fun onViewBindingCreated(binding: SheetFilterBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
if (dialog == null) {
|
||||
binding.layoutBody.updatePadding(top = binding.layoutBody.paddingBottom)
|
||||
binding.scrollView.scrollIndicators = 0
|
||||
}
|
||||
val filter = FilterCoordinator.require(this)
|
||||
filter.sortOrder.observe(viewLifecycleOwner, this::onSortOrderChanged)
|
||||
filter.locale.observe(viewLifecycleOwner, this::onLocaleChanged)
|
||||
filter.originalLocale.observe(viewLifecycleOwner, this::onOriginalLocaleChanged)
|
||||
filter.tags.observe(viewLifecycleOwner, this::onTagsChanged)
|
||||
filter.tagsExcluded.observe(viewLifecycleOwner, this::onTagsExcludedChanged)
|
||||
filter.authors.observe(viewLifecycleOwner, this::onAuthorsChanged)
|
||||
filter.states.observe(viewLifecycleOwner, this::onStateChanged)
|
||||
filter.contentTypes.observe(viewLifecycleOwner, this::onContentTypesChanged)
|
||||
filter.contentRating.observe(viewLifecycleOwner, this::onContentRatingChanged)
|
||||
filter.demographics.observe(viewLifecycleOwner, this::onDemographicsChanged)
|
||||
filter.year.observe(viewLifecycleOwner, this::onYearChanged)
|
||||
filter.yearRange.observe(viewLifecycleOwner, this::onYearRangeChanged)
|
||||
filter.savedFilters.observe(viewLifecycleOwner, ::onSavedPresetsChanged)
|
||||
|
||||
binding.layoutGenres.setTitle(
|
||||
if (filter.capabilities.isMultipleTagsSupported) {
|
||||
R.string.genres
|
||||
} else {
|
||||
R.string.genre
|
||||
},
|
||||
)
|
||||
binding.spinnerLocale.onItemSelectedListener = this
|
||||
binding.spinnerOriginalLocale.onItemSelectedListener = this
|
||||
binding.spinnerOrder.onItemSelectedListener = this
|
||||
binding.chipsState.onChipClickListener = this
|
||||
binding.chipsTypes.onChipClickListener = this
|
||||
binding.chipsContentRating.onChipClickListener = this
|
||||
binding.chipsDemographics.onChipClickListener = this
|
||||
binding.chipsGenres.onChipClickListener = this
|
||||
binding.chipsGenresExclude.onChipClickListener = this
|
||||
binding.sliderYear.addOnChangeListener(this::onSliderValueChange)
|
||||
binding.sliderYearsRange.addOnChangeListener(this::onRangeSliderValueChange)
|
||||
binding.layoutGenres.setOnMoreButtonClickListener {
|
||||
router.showTagsCatalogSheet(excludeMode = false)
|
||||
}
|
||||
binding.layoutGenresExclude.setOnMoreButtonClickListener {
|
||||
router.showTagsCatalogSheet(excludeMode = true)
|
||||
}
|
||||
}
|
||||
binding.layoutGenres.setTitle(
|
||||
if (filter.capabilities.isMultipleTagsSupported) {
|
||||
R.string.genres
|
||||
} else {
|
||||
R.string.genre
|
||||
},
|
||||
)
|
||||
binding.spinnerLocale.onItemSelectedListener = this
|
||||
binding.spinnerOriginalLocale.onItemSelectedListener = this
|
||||
binding.spinnerOrder.onItemSelectedListener = this
|
||||
binding.chipsSavedFilters.onChipClickListener = this
|
||||
binding.chipsState.onChipClickListener = this
|
||||
binding.chipsTypes.onChipClickListener = this
|
||||
binding.chipsContentRating.onChipClickListener = this
|
||||
binding.chipsDemographics.onChipClickListener = this
|
||||
binding.chipsGenres.onChipClickListener = this
|
||||
binding.chipsGenresExclude.onChipClickListener = this
|
||||
binding.chipsAuthor.onChipClickListener = this
|
||||
binding.chipsSavedFilters.onChipLongClickListener = this
|
||||
binding.chipsSavedFilters.onChipCloseClickListener = this
|
||||
binding.sliderYear.addOnChangeListener(this::onSliderValueChange)
|
||||
binding.sliderYearsRange.addOnChangeListener(this::onRangeSliderValueChange)
|
||||
binding.layoutGenres.setOnMoreButtonClickListener {
|
||||
router.showTagsCatalogSheet(excludeMode = false)
|
||||
}
|
||||
binding.layoutGenresExclude.setOnMoreButtonClickListener {
|
||||
router.showTagsCatalogSheet(excludeMode = true)
|
||||
}
|
||||
combine(
|
||||
filter.observe().map { it.listFilter.isNotEmpty() }.distinctUntilChanged(),
|
||||
filter.savedFilters.map { it.selectedItems.isEmpty() }.distinctUntilChanged(),
|
||||
Boolean::and,
|
||||
).flowOn(Dispatchers.Default)
|
||||
.observe(viewLifecycleOwner) {
|
||||
binding.buttonSave.isEnabled = it
|
||||
}
|
||||
binding.buttonSave.setOnClickListener(this)
|
||||
binding.buttonDone.setOnClickListener(this)
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val typeMask = WindowInsetsCompat.Type.systemBars()
|
||||
viewBinding?.scrollView?.updatePadding(
|
||||
bottom = insets.getInsets(typeMask).bottom,
|
||||
)
|
||||
return insets.consume(v, typeMask, bottom = true)
|
||||
}
|
||||
override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
val typeMask = WindowInsetsCompat.Type.systemBars()
|
||||
viewBinding?.layoutBottom?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = insets.getInsets(typeMask).bottom
|
||||
}
|
||||
return insets.consume(v, typeMask, bottom = true)
|
||||
}
|
||||
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (parent.id) {
|
||||
R.id.spinner_order -> filter.setSortOrder(filter.sortOrder.value.availableItems[position])
|
||||
R.id.spinner_locale -> filter.setLocale(filter.locale.value.availableItems[position])
|
||||
R.id.spinner_original_locale -> filter.setOriginalLocale(filter.originalLocale.value.availableItems[position])
|
||||
}
|
||||
}
|
||||
override fun onClick(v: View) {
|
||||
when (v.id) {
|
||||
R.id.button_done -> dismiss()
|
||||
R.id.button_save -> onSaveFilterClick("")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (parent.id) {
|
||||
R.id.spinner_order -> filter.setSortOrder(filter.sortOrder.value.availableItems[position])
|
||||
R.id.spinner_locale -> filter.setLocale(filter.locale.value.availableItems[position])
|
||||
R.id.spinner_original_locale -> filter.setOriginalLocale(filter.originalLocale.value.availableItems[position])
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSliderValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
if (!fromUser) {
|
||||
return
|
||||
}
|
||||
val intValue = value.toInt()
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (slider.id) {
|
||||
R.id.slider_year -> filter.setYear(
|
||||
if (intValue <= slider.valueFrom.toIntUp()) {
|
||||
YEAR_UNKNOWN
|
||||
} else {
|
||||
intValue
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
|
||||
private fun onRangeSliderValueChange(slider: RangeSlider, value: Float, fromUser: Boolean) {
|
||||
if (!fromUser) {
|
||||
return
|
||||
}
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (slider.id) {
|
||||
R.id.slider_yearsRange -> filter.setYearRange(
|
||||
valueFrom = slider.values.firstOrNull()?.let {
|
||||
if (it <= slider.valueFrom) YEAR_UNKNOWN else it.toInt()
|
||||
} ?: YEAR_UNKNOWN,
|
||||
valueTo = slider.values.lastOrNull()?.let {
|
||||
if (it >= slider.valueTo) YEAR_UNKNOWN else it.toInt()
|
||||
} ?: YEAR_UNKNOWN,
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun onSliderValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
if (!fromUser) {
|
||||
return
|
||||
}
|
||||
val intValue = value.toInt()
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (slider.id) {
|
||||
R.id.slider_year -> filter.setYear(
|
||||
if (intValue <= slider.valueFrom.toIntUp()) {
|
||||
YEAR_UNKNOWN
|
||||
} else {
|
||||
intValue
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (data) {
|
||||
is MangaState -> filter.toggleState(data, !chip.isChecked)
|
||||
is MangaTag -> if (chip.parentView?.id == R.id.chips_genresExclude) {
|
||||
filter.toggleTagExclude(data, !chip.isChecked)
|
||||
} else {
|
||||
filter.toggleTag(data, !chip.isChecked)
|
||||
}
|
||||
private fun onRangeSliderValueChange(slider: RangeSlider, value: Float, fromUser: Boolean) {
|
||||
if (!fromUser) {
|
||||
return
|
||||
}
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (slider.id) {
|
||||
R.id.slider_yearsRange -> filter.setYearRange(
|
||||
valueFrom = slider.values.firstOrNull()?.let {
|
||||
if (it <= slider.valueFrom) YEAR_UNKNOWN else it.toInt()
|
||||
} ?: YEAR_UNKNOWN,
|
||||
valueTo = slider.values.lastOrNull()?.let {
|
||||
if (it >= slider.valueTo) YEAR_UNKNOWN else it.toInt()
|
||||
} ?: YEAR_UNKNOWN,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ContentType -> filter.toggleContentType(data, !chip.isChecked)
|
||||
is ContentRating -> filter.toggleContentRating(data, !chip.isChecked)
|
||||
is Demographic -> filter.toggleDemographic(data, !chip.isChecked)
|
||||
null -> router.showTagsCatalogSheet(excludeMode = chip.parentView?.id == R.id.chips_genresExclude)
|
||||
}
|
||||
}
|
||||
override fun onChipClick(chip: Chip, data: Any?) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
when (data) {
|
||||
is MangaState -> filter.toggleState(data, !chip.isChecked)
|
||||
is MangaTag -> if (chip.parentView?.id == R.id.chips_genresExclude) {
|
||||
filter.toggleTagExclude(data, !chip.isChecked)
|
||||
} else {
|
||||
filter.toggleTag(data, !chip.isChecked)
|
||||
}
|
||||
|
||||
private fun onSortOrderChanged(value: FilterProperty<SortOrder>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutOrder.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.single()
|
||||
b.spinnerOrder.adapter = ArrayAdapter(
|
||||
b.spinnerOrder.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map { b.spinnerOrder.context.getString(it.titleRes) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerOrder.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
is ContentType -> filter.toggleContentType(data, !chip.isChecked)
|
||||
is ContentRating -> filter.toggleContentRating(data, !chip.isChecked)
|
||||
is Demographic -> filter.toggleDemographic(data, !chip.isChecked)
|
||||
is PersistableFilter -> filter.setAdjusted(data.filter)
|
||||
is String -> if (chip.isChecked) {
|
||||
filter.setAuthor(null)
|
||||
} else {
|
||||
filter.setAuthor(data)
|
||||
}
|
||||
null -> router.showTagsCatalogSheet(excludeMode = chip.parentView?.id == R.id.chips_genresExclude)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onLocaleChanged(value: FilterProperty<Locale?>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutLocale.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.singleOrNull()
|
||||
b.spinnerLocale.adapter = ArrayAdapter(
|
||||
b.spinnerLocale.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map { it.getDisplayName(b.spinnerLocale.context) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerLocale.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
override fun onChipLongClick(chip: Chip, data: Any?): Boolean {
|
||||
return when (data) {
|
||||
is PersistableFilter -> {
|
||||
showSavedFilterMenu(chip, data)
|
||||
true
|
||||
}
|
||||
|
||||
private fun onOriginalLocaleChanged(value: FilterProperty<Locale?>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutOriginalLocale.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.singleOrNull()
|
||||
b.spinnerOriginalLocale.adapter = ArrayAdapter(
|
||||
b.spinnerOriginalLocale.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map { it.getDisplayName(b.spinnerOriginalLocale.context) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerOriginalLocale.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTagsChanged(value: FilterProperty<MangaTag>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutGenres.isGone = value.isEmptyAndSuccess()
|
||||
b.layoutGenres.setError(value.error?.getDisplayMessage(resources))
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = tag in value.selectedItems,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
b.chipsGenres.setChips(chips)
|
||||
}
|
||||
override fun onChipCloseClick(chip: Chip, data: Any?) {
|
||||
when (data) {
|
||||
is PersistableFilter -> {
|
||||
showSavedFilterMenu(chip, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onTagsExcludedChanged(value: FilterProperty<MangaTag>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutGenresExclude.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = tag in value.selectedItems,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
b.chipsGenresExclude.setChips(chips)
|
||||
}
|
||||
private fun onSortOrderChanged(value: FilterProperty<SortOrder>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutOrder.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.single()
|
||||
b.spinnerOrder.adapter = ArrayAdapter(
|
||||
b.spinnerOrder.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map { b.spinnerOrder.context.getString(it.titleRes) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerOrder.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStateChanged(value: FilterProperty<MangaState>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutState.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { state ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(state.titleResId),
|
||||
isChecked = state in value.selectedItems,
|
||||
data = state,
|
||||
)
|
||||
}
|
||||
b.chipsState.setChips(chips)
|
||||
}
|
||||
private fun onLocaleChanged(value: FilterProperty<Locale?>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutLocale.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.singleOrNull()
|
||||
b.spinnerLocale.adapter = ArrayAdapter(
|
||||
b.spinnerLocale.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map { it.getDisplayName(b.spinnerLocale.context) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerLocale.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContentTypesChanged(value: FilterProperty<ContentType>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutTypes.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { type ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(type.titleResId),
|
||||
isChecked = type in value.selectedItems,
|
||||
data = type,
|
||||
)
|
||||
}
|
||||
b.chipsTypes.setChips(chips)
|
||||
}
|
||||
private fun onOriginalLocaleChanged(value: FilterProperty<Locale?>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutOriginalLocale.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val selected = value.selectedItems.singleOrNull()
|
||||
b.spinnerOriginalLocale.adapter = ArrayAdapter(
|
||||
b.spinnerOriginalLocale.context,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
android.R.id.text1,
|
||||
value.availableItems.map { it.getDisplayName(b.spinnerOriginalLocale.context) },
|
||||
)
|
||||
val selectedIndex = value.availableItems.indexOf(selected)
|
||||
if (selectedIndex >= 0) {
|
||||
b.spinnerOriginalLocale.setSelection(selectedIndex, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContentRatingChanged(value: FilterProperty<ContentRating>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutContentRating.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { contentRating ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(contentRating.titleResId),
|
||||
isChecked = contentRating in value.selectedItems,
|
||||
data = contentRating,
|
||||
)
|
||||
}
|
||||
b.chipsContentRating.setChips(chips)
|
||||
}
|
||||
private fun onTagsChanged(value: FilterProperty<MangaTag>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutGenres.isGone = value.isEmptyAndSuccess()
|
||||
b.layoutGenres.setError(value.error?.getDisplayMessage(resources))
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = tag in value.selectedItems,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
b.chipsGenres.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onDemographicsChanged(value: FilterProperty<Demographic>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutDemographics.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { demographic ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(demographic.titleResId),
|
||||
isChecked = demographic in value.selectedItems,
|
||||
data = demographic,
|
||||
)
|
||||
}
|
||||
b.chipsDemographics.setChips(chips)
|
||||
}
|
||||
private fun onTagsExcludedChanged(value: FilterProperty<MangaTag>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutGenresExclude.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { tag ->
|
||||
ChipsView.ChipModel(
|
||||
title = tag.title,
|
||||
isChecked = tag in value.selectedItems,
|
||||
data = tag,
|
||||
)
|
||||
}
|
||||
b.chipsGenresExclude.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onYearChanged(value: FilterProperty<Int>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutYear.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val currentValue = value.selectedItems.singleOrNull() ?: YEAR_UNKNOWN
|
||||
b.layoutYear.setValueText(
|
||||
if (currentValue == YEAR_UNKNOWN) {
|
||||
getString(R.string.any)
|
||||
} else {
|
||||
currentValue.toString()
|
||||
},
|
||||
)
|
||||
b.sliderYear.valueFrom = value.availableItems.first().toFloat()
|
||||
b.sliderYear.valueTo = value.availableItems.last().toFloat()
|
||||
b.sliderYear.setValueRounded(currentValue.toFloat())
|
||||
}
|
||||
private fun onAuthorsChanged(value: FilterProperty<String>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutAuthor.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { author ->
|
||||
ChipsView.ChipModel(
|
||||
title = author,
|
||||
isChecked = author in value.selectedItems,
|
||||
data = author,
|
||||
)
|
||||
}
|
||||
b.chipsAuthor.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onYearRangeChanged(value: FilterProperty<Int>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutYearsRange.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
b.sliderYearsRange.valueFrom = value.availableItems.first().toFloat()
|
||||
b.sliderYearsRange.valueTo = value.availableItems.last().toFloat()
|
||||
val currentValueFrom = value.selectedItems.firstOrNull()?.toFloat() ?: b.sliderYearsRange.valueFrom
|
||||
val currentValueTo = value.selectedItems.lastOrNull()?.toFloat() ?: b.sliderYearsRange.valueTo
|
||||
b.layoutYearsRange.setValueText(
|
||||
getString(
|
||||
R.string.memory_usage_pattern,
|
||||
currentValueFrom.toInt().toString(),
|
||||
currentValueTo.toInt().toString(),
|
||||
),
|
||||
)
|
||||
b.sliderYearsRange.setValuesRounded(currentValueFrom, currentValueTo)
|
||||
}
|
||||
private fun onStateChanged(value: FilterProperty<MangaState>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutState.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { state ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(state.titleResId),
|
||||
isChecked = state in value.selectedItems,
|
||||
data = state,
|
||||
)
|
||||
}
|
||||
b.chipsState.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onContentTypesChanged(value: FilterProperty<ContentType>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutTypes.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { type ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(type.titleResId),
|
||||
isChecked = type in value.selectedItems,
|
||||
data = type,
|
||||
)
|
||||
}
|
||||
b.chipsTypes.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onContentRatingChanged(value: FilterProperty<ContentRating>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutContentRating.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { contentRating ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(contentRating.titleResId),
|
||||
isChecked = contentRating in value.selectedItems,
|
||||
data = contentRating,
|
||||
)
|
||||
}
|
||||
b.chipsContentRating.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onDemographicsChanged(value: FilterProperty<Demographic>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutDemographics.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { demographic ->
|
||||
ChipsView.ChipModel(
|
||||
title = getString(demographic.titleResId),
|
||||
isChecked = demographic in value.selectedItems,
|
||||
data = demographic,
|
||||
)
|
||||
}
|
||||
b.chipsDemographics.setChips(chips)
|
||||
}
|
||||
|
||||
private fun onYearChanged(value: FilterProperty<Int>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutYear.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val currentValue = value.selectedItems.singleOrNull() ?: YEAR_UNKNOWN
|
||||
b.layoutYear.setValueText(
|
||||
if (currentValue == YEAR_UNKNOWN) {
|
||||
getString(R.string.any)
|
||||
} else {
|
||||
currentValue.toString()
|
||||
},
|
||||
)
|
||||
b.sliderYear.valueFrom = value.availableItems.first().toFloat()
|
||||
b.sliderYear.valueTo = value.availableItems.last().toFloat()
|
||||
b.sliderYear.setValueRounded(currentValue.toFloat())
|
||||
}
|
||||
|
||||
private fun onYearRangeChanged(value: FilterProperty<Int>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutYearsRange.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
b.sliderYearsRange.valueFrom = value.availableItems.first().toFloat()
|
||||
b.sliderYearsRange.valueTo = value.availableItems.last().toFloat()
|
||||
val currentValueFrom = value.selectedItems.firstOrNull()?.toFloat() ?: b.sliderYearsRange.valueFrom
|
||||
val currentValueTo = value.selectedItems.lastOrNull()?.toFloat() ?: b.sliderYearsRange.valueTo
|
||||
b.layoutYearsRange.setValueText(
|
||||
getString(
|
||||
R.string.memory_usage_pattern,
|
||||
currentValueFrom.toInt().toString(),
|
||||
currentValueTo.toInt().toString(),
|
||||
),
|
||||
)
|
||||
b.sliderYearsRange.setValuesRounded(currentValueFrom, currentValueTo)
|
||||
}
|
||||
|
||||
private fun onSavedPresetsChanged(value: FilterProperty<PersistableFilter>) {
|
||||
val b = viewBinding ?: return
|
||||
b.layoutSavedFilters.isGone = value.isEmpty()
|
||||
if (value.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val chips = value.availableItems.map { f ->
|
||||
ChipsView.ChipModel(
|
||||
title = f.name,
|
||||
isChecked = f in value.selectedItems,
|
||||
data = f,
|
||||
isDropdown = true,
|
||||
)
|
||||
}
|
||||
b.chipsSavedFilters.setChips(chips)
|
||||
}
|
||||
|
||||
private fun showSavedFilterMenu(anchor: View, preset: PersistableFilter) {
|
||||
val menu = PopupMenu(context ?: return, anchor)
|
||||
val filter = FilterCoordinator.require(this)
|
||||
menu.inflate(R.menu.popup_saved_filter)
|
||||
menu.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_delete -> filter.deleteSavedFilter(preset.id)
|
||||
R.id.action_rename -> onRenameFilterClick(preset)
|
||||
}
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
}
|
||||
|
||||
private fun onSaveFilterClick(name: String) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
val existingNames = filter.savedFilters.value.availableItems
|
||||
.mapTo(TreeSet(AlphanumComparator()), PersistableFilter::name)
|
||||
buildAlertDialog(context ?: return) {
|
||||
val input = setEditText(
|
||||
entries = existingNames.toList(),
|
||||
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES,
|
||||
singleLine = true,
|
||||
)
|
||||
input.setHint(R.string.enter_name)
|
||||
input.setText(name)
|
||||
input.filters += InputFilter.LengthFilter(MAX_TITLE_LENGTH)
|
||||
setTitle(R.string.save_filter)
|
||||
setPositiveButton(R.string.save) { _, _ ->
|
||||
val text = input.text?.toString()?.trim()
|
||||
if (text.isNullOrEmpty()) {
|
||||
Toast.makeText(context, R.string.invalid_value_message, Toast.LENGTH_SHORT).show()
|
||||
onSaveFilterClick("")
|
||||
} else if (text in existingNames) {
|
||||
askForFilterOverwrite(filter, text)
|
||||
} else {
|
||||
filter.saveCurrentFilter(text)
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun onRenameFilterClick(preset: PersistableFilter) {
|
||||
val filter = FilterCoordinator.require(this)
|
||||
val existingNames = filter.savedFilters.value.availableItems.mapToSet { it.name }
|
||||
buildAlertDialog(context ?: return) {
|
||||
val input = setEditText(
|
||||
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES,
|
||||
singleLine = true,
|
||||
)
|
||||
input.filters += InputFilter.LengthFilter(MAX_TITLE_LENGTH)
|
||||
input.setHint(R.string.enter_name)
|
||||
input.setText(preset.name)
|
||||
setTitle(R.string.rename)
|
||||
setPositiveButton(R.string.save) { _, _ ->
|
||||
val text = input.text?.toString()?.trim()
|
||||
if (text.isNullOrEmpty() || text in existingNames) {
|
||||
Toast.makeText(context, R.string.invalid_value_message, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
filter.renameSavedFilter(preset.id, text)
|
||||
}
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun askForFilterOverwrite(filter: FilterCoordinator, name: String) {
|
||||
buildAlertDialog(context ?: return) {
|
||||
setTitle(R.string.save_filter)
|
||||
setMessage(getString(R.string.filter_overwrite_confirm, name))
|
||||
setPositiveButton(R.string.overwrite) { _, _ ->
|
||||
filter.saveCurrentFilter(name)
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||
onSaveFilterClick(name)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.koitharu.kotatsu.main.ui
|
||||
|
||||
import android.Manifest
|
||||
import android.app.BackgroundServiceStartNotAllowedException
|
||||
import android.app.ServiceStartNotAllowedException
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
import android.os.Build
|
||||
@@ -58,6 +60,7 @@ import org.koitharu.kotatsu.core.util.ext.consume
|
||||
import org.koitharu.kotatsu.core.util.ext.end
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.start
|
||||
import org.koitharu.kotatsu.databinding.ActivityMainBinding
|
||||
import org.koitharu.kotatsu.details.service.MangaPrefetchService
|
||||
@@ -288,7 +291,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
adjustFabVisibility(isResumeEnabled = isEnabled)
|
||||
}
|
||||
|
||||
private fun onFirstStart() {
|
||||
private fun onFirstStart() = try {
|
||||
lifecycleScope.launch(Dispatchers.Main) { // not a default `Main.immediate` dispatcher
|
||||
withContext(Dispatchers.Default) {
|
||||
LocalStorageCleanupWorker.enqueue(applicationContext)
|
||||
@@ -303,6 +306,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
e.printStackTraceDebug()
|
||||
}
|
||||
|
||||
private fun adjustAppbar(topFragment: Fragment) {
|
||||
|
||||
@@ -488,7 +488,11 @@ class ReaderActivity :
|
||||
uiState.incognito -> getString(R.string.incognito_mode)
|
||||
else -> chapterTitle
|
||||
}
|
||||
if (chapterTitle != previous?.getChapterTitle(resources) && chapterTitle.isNotEmpty()) {
|
||||
if (
|
||||
settings.isReaderChapterToastEnabled &&
|
||||
chapterTitle != previous?.getChapterTitle(resources) &&
|
||||
chapterTitle.isNotEmpty()
|
||||
) {
|
||||
viewBinding.toastView.showTemporary(chapterTitle, TOAST_DURATION)
|
||||
}
|
||||
if (uiState.isSliderAvailable()) {
|
||||
|
||||
@@ -29,120 +29,133 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||
import org.koitharu.kotatsu.search.domain.SearchKind
|
||||
|
||||
@AndroidEntryPoint
|
||||
class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner {
|
||||
class RemoteListFragment : MangaListFragment(), FilterCoordinator.Owner, View.OnClickListener {
|
||||
|
||||
override val viewModel by viewModels<RemoteListViewModel>()
|
||||
override val viewModel by viewModels<RemoteListViewModel>()
|
||||
|
||||
override val filterCoordinator: FilterCoordinator
|
||||
get() = viewModel.filterCoordinator
|
||||
override val filterCoordinator: FilterCoordinator
|
||||
get() = viewModel.filterCoordinator
|
||||
|
||||
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
addMenuProvider(RemoteListMenuProvider())
|
||||
addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel))
|
||||
viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
|
||||
viewModel.onOpenManga.observeEvent(viewLifecycleOwner) { router.openDetails(it) }
|
||||
filterCoordinator.observe().distinctUntilChangedBy { it.listFilter.isEmpty() }
|
||||
.drop(1)
|
||||
.observe(viewLifecycleOwner) {
|
||||
activity?.invalidateMenu()
|
||||
}
|
||||
}
|
||||
override fun onViewBindingCreated(binding: FragmentListBinding, savedInstanceState: Bundle?) {
|
||||
super.onViewBindingCreated(binding, savedInstanceState)
|
||||
addMenuProvider(RemoteListMenuProvider())
|
||||
addMenuProvider(MangaSearchMenuProvider(filterCoordinator, viewModel))
|
||||
viewModel.isRandomLoading.observe(viewLifecycleOwner, MenuInvalidator(requireActivity()))
|
||||
viewModel.onOpenManga.observeEvent(viewLifecycleOwner) { router.openDetails(it) }
|
||||
viewModel.onSourceBroken.observeEvent(viewLifecycleOwner) { showSourceBrokenWarning() }
|
||||
filterCoordinator.observe().distinctUntilChangedBy { it.listFilter.isEmpty() }
|
||||
.drop(1)
|
||||
.observe(viewLifecycleOwner) {
|
||||
activity?.invalidateMenu()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScrolledToEnd() {
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
override fun onScrolledToEnd() {
|
||||
viewModel.loadNextPage()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(
|
||||
controller: ListSelectionController,
|
||||
menuInflater: MenuInflater,
|
||||
menu: Menu
|
||||
): Boolean {
|
||||
menuInflater.inflate(R.menu.mode_remote, menu)
|
||||
return super.onCreateActionMode(controller, menuInflater, menu)
|
||||
}
|
||||
override fun onCreateActionMode(
|
||||
controller: ListSelectionController,
|
||||
menuInflater: MenuInflater,
|
||||
menu: Menu
|
||||
): Boolean {
|
||||
menuInflater.inflate(R.menu.mode_remote, menu)
|
||||
return super.onCreateActionMode(controller, menuInflater, menu)
|
||||
}
|
||||
|
||||
override fun onFilterClick(view: View?) {
|
||||
router.showFilterSheet()
|
||||
}
|
||||
override fun onFilterClick(view: View?) {
|
||||
router.showFilterSheet()
|
||||
}
|
||||
|
||||
override fun onEmptyActionClick() {
|
||||
if (filterCoordinator.isFilterApplied) {
|
||||
filterCoordinator.reset()
|
||||
} else {
|
||||
openInBrowser(null) // should never be called
|
||||
}
|
||||
}
|
||||
override fun onEmptyActionClick() {
|
||||
if (filterCoordinator.isFilterApplied) {
|
||||
filterCoordinator.reset()
|
||||
} else {
|
||||
openInBrowser(null) // should never be called
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFooterButtonClick() {
|
||||
val filter = filterCoordinator.snapshot().listFilter
|
||||
when {
|
||||
!filter.query.isNullOrEmpty() -> router.openSearch(filter.query.orEmpty(), SearchKind.SIMPLE)
|
||||
!filter.author.isNullOrEmpty() -> router.openSearch(filter.author.orEmpty(), SearchKind.AUTHOR)
|
||||
filter.tags.size == 1 -> router.openSearch(filter.tags.singleOrNull()?.title.orEmpty(), SearchKind.TAG)
|
||||
}
|
||||
}
|
||||
override fun onFooterButtonClick() {
|
||||
val filter = filterCoordinator.snapshot().listFilter
|
||||
when {
|
||||
!filter.query.isNullOrEmpty() -> router.openSearch(filter.query.orEmpty(), SearchKind.SIMPLE)
|
||||
!filter.author.isNullOrEmpty() -> router.openSearch(filter.author.orEmpty(), SearchKind.AUTHOR)
|
||||
filter.tags.size == 1 -> router.openSearch(filter.tags.singleOrNull()?.title.orEmpty(), SearchKind.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSecondaryErrorActionClick(error: Throwable) {
|
||||
openInBrowser(error.getCauseUrl())
|
||||
}
|
||||
override fun onSecondaryErrorActionClick(error: Throwable) {
|
||||
openInBrowser(error.getCauseUrl())
|
||||
}
|
||||
|
||||
private fun openInBrowser(url: String?) {
|
||||
if (url?.isHttpUrl() == true) {
|
||||
router.openBrowser(
|
||||
url = url,
|
||||
source = viewModel.source,
|
||||
title = viewModel.source.getTitle(requireContext()),
|
||||
)
|
||||
} else {
|
||||
Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
override fun onClick(v: View?) = Unit // from Snackbar, do nothing
|
||||
|
||||
private inner class RemoteListMenuProvider : MenuProvider {
|
||||
private fun openInBrowser(url: String?) {
|
||||
if (url?.isHttpUrl() == true) {
|
||||
router.openBrowser(
|
||||
url = url,
|
||||
source = viewModel.source,
|
||||
title = viewModel.source.getTitle(requireContext()),
|
||||
)
|
||||
} else {
|
||||
Snackbar.make(requireViewBinding().recyclerView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_list_remote, menu)
|
||||
}
|
||||
private fun showSourceBrokenWarning() {
|
||||
val snackbar = Snackbar.make(
|
||||
viewBinding?.recyclerView ?: return,
|
||||
R.string.source_broken_warning,
|
||||
Snackbar.LENGTH_INDEFINITE,
|
||||
)
|
||||
snackbar.setAction(R.string.got_it, this)
|
||||
snackbar.show()
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_source_settings -> {
|
||||
router.openSourceSettings(viewModel.source)
|
||||
true
|
||||
}
|
||||
private inner class RemoteListMenuProvider : MenuProvider {
|
||||
|
||||
R.id.action_random -> {
|
||||
viewModel.openRandom()
|
||||
true
|
||||
}
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.opt_list_remote, menu)
|
||||
}
|
||||
|
||||
R.id.action_filter -> {
|
||||
onFilterClick(null)
|
||||
true
|
||||
}
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.action_source_settings -> {
|
||||
router.openSourceSettings(viewModel.source)
|
||||
true
|
||||
}
|
||||
|
||||
R.id.action_filter_reset -> {
|
||||
filterCoordinator.reset()
|
||||
true
|
||||
}
|
||||
R.id.action_random -> {
|
||||
viewModel.openRandom()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
R.id.action_filter -> {
|
||||
onFilterClick(null)
|
||||
true
|
||||
}
|
||||
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
super.onPrepareMenu(menu)
|
||||
menu.findItem(R.id.action_random)?.isEnabled = !viewModel.isRandomLoading.value
|
||||
menu.findItem(R.id.action_filter_reset)?.isVisible = filterCoordinator.isFilterApplied
|
||||
}
|
||||
}
|
||||
R.id.action_filter_reset -> {
|
||||
filterCoordinator.reset()
|
||||
true
|
||||
}
|
||||
|
||||
companion object {
|
||||
else -> false
|
||||
}
|
||||
|
||||
const val ARG_SOURCE = "provider"
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
super.onPrepareMenu(menu)
|
||||
menu.findItem(R.id.action_random)?.isEnabled = !viewModel.isRandomLoading.value
|
||||
menu.findItem(R.id.action_filter_reset)?.isVisible = filterCoordinator.isFilterApplied
|
||||
}
|
||||
}
|
||||
|
||||
fun newInstance(source: MangaSource) = RemoteListFragment().withArgs(1) {
|
||||
putString(ARG_SOURCE, source.name)
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
|
||||
const val ARG_SOURCE = "provider"
|
||||
|
||||
fun newInstance(source: MangaSource) = RemoteListFragment().withArgs(1) {
|
||||
putString(ARG_SOURCE, source.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.koitharu.kotatsu.list.ui.model.toErrorState
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageChanges
|
||||
import org.koitharu.kotatsu.local.domain.model.LocalManga
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||
import org.koitharu.kotatsu.parsers.util.sizeOrZero
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -65,6 +66,7 @@ open class RemoteListViewModel @Inject constructor(
|
||||
val source = MangaSource(savedStateHandle[RemoteListFragment.ARG_SOURCE])
|
||||
val isRandomLoading = MutableStateFlow(false)
|
||||
val onOpenManga = MutableEventFlow<Manga>()
|
||||
val onSourceBroken = MutableEventFlow<Unit>()
|
||||
|
||||
protected val repository = mangaRepositoryFactory.create(source)
|
||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||
@@ -117,6 +119,11 @@ open class RemoteListViewModel @Inject constructor(
|
||||
launchJob(Dispatchers.Default) {
|
||||
sourcesRepository.trackUsage(source)
|
||||
}
|
||||
|
||||
if (source is MangaParserSource && source.isBroken) {
|
||||
// Just notify one. Will show reason in future
|
||||
onSourceBroken.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package org.koitharu.kotatsu.scrobbling.common.data
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.isActive
|
||||
|
||||
@Dao
|
||||
abstract class ScrobblingDao {
|
||||
@@ -20,4 +23,20 @@ abstract class ScrobblingDao {
|
||||
|
||||
@Query("DELETE FROM scrobblings WHERE scrobbler = :scrobbler AND manga_id = :mangaId")
|
||||
abstract suspend fun delete(scrobbler: Int, mangaId: Long)
|
||||
|
||||
@Query("SELECT * FROM scrobblings ORDER BY scrobbler LIMIT :limit OFFSET :offset")
|
||||
protected abstract suspend fun findAll(offset: Int, limit: Int): List<ScrobblingEntity>
|
||||
|
||||
fun dumpEnabled(): Flow<ScrobblingEntity> = flow {
|
||||
val window = 10
|
||||
var offset = 0
|
||||
while (currentCoroutineContext().isActive) {
|
||||
val list = findAll(offset, window)
|
||||
if (list.isEmpty()) {
|
||||
break
|
||||
}
|
||||
offset += window
|
||||
list.forEach { emit(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,4 +169,8 @@ class MangaSearchRepository @Inject constructor(
|
||||
null,
|
||||
)?.use { cursor -> cursor.count } ?: 0
|
||||
}
|
||||
|
||||
suspend fun getAuthors(source: MangaSource, limit: Int): List<String> {
|
||||
return db.getMangaDao().findAuthorsBySource(source.name, limit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,16 @@ import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.TwoStatePreference
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.ProgressIndicatorMode
|
||||
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
||||
import org.koitharu.kotatsu.core.prefs.SearchSuggestionType
|
||||
import org.koitharu.kotatsu.core.prefs.TriStateOption
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.util.LocaleComparator
|
||||
@@ -24,8 +29,10 @@ import org.koitharu.kotatsu.core.util.ext.postDelayed
|
||||
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.sortedWithSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.toList
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.names
|
||||
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
|
||||
import org.koitharu.kotatsu.settings.utils.ActivityListPreference
|
||||
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
|
||||
import org.koitharu.kotatsu.settings.utils.PercentSummaryProvider
|
||||
@@ -34,106 +41,145 @@ import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AppearanceSettingsFragment :
|
||||
BasePreferenceFragment(R.string.appearance),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
BasePreferenceFragment(R.string.appearance),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
@Inject
|
||||
lateinit var activityRecreationHandle: ActivityRecreationHandle
|
||||
@Inject
|
||||
lateinit var activityRecreationHandle: ActivityRecreationHandle
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_appearance)
|
||||
findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.summaryProvider = PercentSummaryProvider()
|
||||
findPreference<ListPreference>(AppSettings.KEY_LIST_MODE)?.run {
|
||||
entryValues = ListMode.entries.names()
|
||||
setDefaultValueCompat(ListMode.GRID.name)
|
||||
}
|
||||
findPreference<ListPreference>(AppSettings.KEY_PROGRESS_INDICATORS)?.run {
|
||||
entryValues = ProgressIndicatorMode.entries.names()
|
||||
setDefaultValueCompat(ProgressIndicatorMode.PERCENT_READ.name)
|
||||
}
|
||||
findPreference<ActivityListPreference>(AppSettings.KEY_APP_LOCALE)?.run {
|
||||
initLocalePicker(this)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
activityIntent = Intent(
|
||||
Settings.ACTION_APP_LOCALE_SETTINGS,
|
||||
Uri.fromParts("package", context.packageName, null),
|
||||
)
|
||||
}
|
||||
summaryProvider = Preference.SummaryProvider<ActivityListPreference> {
|
||||
val locale = AppCompatDelegate.getApplicationLocales().get(0)
|
||||
locale?.getDisplayName(locale)?.toTitleCase(locale) ?: getString(R.string.follow_system)
|
||||
}
|
||||
setDefaultValueCompat("")
|
||||
}
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_MANGA_LIST_BADGES)?.run {
|
||||
summaryProvider = MultiSummaryProvider(R.string.none)
|
||||
}
|
||||
bindNavSummary()
|
||||
}
|
||||
@Inject
|
||||
lateinit var appShortcutManager: AppShortcutManager
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_appearance)
|
||||
findPreference<SliderPreference>(AppSettings.KEY_GRID_SIZE)?.summaryProvider = PercentSummaryProvider()
|
||||
findPreference<ListPreference>(AppSettings.KEY_LIST_MODE)?.run {
|
||||
entryValues = ListMode.entries.names()
|
||||
setDefaultValueCompat(ListMode.GRID.name)
|
||||
}
|
||||
findPreference<ListPreference>(AppSettings.KEY_PROGRESS_INDICATORS)?.run {
|
||||
entryValues = ProgressIndicatorMode.entries.names()
|
||||
setDefaultValueCompat(ProgressIndicatorMode.PERCENT_READ.name)
|
||||
}
|
||||
findPreference<ActivityListPreference>(AppSettings.KEY_APP_LOCALE)?.run {
|
||||
initLocalePicker(this)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
activityIntent = Intent(
|
||||
Settings.ACTION_APP_LOCALE_SETTINGS,
|
||||
Uri.fromParts("package", context.packageName, null),
|
||||
)
|
||||
}
|
||||
summaryProvider = Preference.SummaryProvider<ActivityListPreference> {
|
||||
val locale = AppCompatDelegate.getApplicationLocales().get(0)
|
||||
locale?.getDisplayName(locale)?.toTitleCase(locale) ?: getString(R.string.follow_system)
|
||||
}
|
||||
setDefaultValueCompat("")
|
||||
}
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_MANGA_LIST_BADGES)?.run {
|
||||
summaryProvider = MultiSummaryProvider(R.string.none)
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_SHORTCUTS)?.isVisible =
|
||||
appShortcutManager.isDynamicShortcutsAvailable()
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
findPreference<ListPreference>(AppSettings.KEY_SCREENSHOTS_POLICY)?.run {
|
||||
entryValues = ScreenshotsPolicy.entries.names()
|
||||
setDefaultValueCompat(ScreenshotsPolicy.ALLOW.name)
|
||||
}
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_SEARCH_SUGGESTION_TYPES)?.let { pref ->
|
||||
pref.entryValues = SearchSuggestionType.entries.names()
|
||||
pref.entries = SearchSuggestionType.entries.map { pref.context.getString(it.titleResId) }.toTypedArray()
|
||||
pref.summaryProvider = MultiSummaryProvider(R.string.none)
|
||||
pref.values = settings.searchSuggestionTypes.mapToSet { it.name }
|
||||
}
|
||||
bindNavSummary()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_THEME -> {
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
}
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
AppSettings.KEY_COLOR_THEME,
|
||||
AppSettings.KEY_THEME_AMOLED,
|
||||
-> {
|
||||
postRestart()
|
||||
}
|
||||
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_THEME -> {
|
||||
AppCompatDelegate.setDefaultNightMode(settings.theme)
|
||||
}
|
||||
|
||||
AppSettings.KEY_APP_LOCALE -> {
|
||||
AppCompatDelegate.setApplicationLocales(settings.appLocales)
|
||||
}
|
||||
AppSettings.KEY_COLOR_THEME,
|
||||
AppSettings.KEY_THEME_AMOLED,
|
||||
-> {
|
||||
postRestart()
|
||||
}
|
||||
|
||||
AppSettings.KEY_NAV_MAIN -> {
|
||||
bindNavSummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
AppSettings.KEY_APP_LOCALE -> {
|
||||
AppCompatDelegate.setApplicationLocales(settings.appLocales)
|
||||
}
|
||||
|
||||
private fun postRestart() {
|
||||
viewLifecycleOwner.lifecycle.postDelayed(400) {
|
||||
activityRecreationHandle.recreateAll()
|
||||
}
|
||||
}
|
||||
AppSettings.KEY_NAV_MAIN -> {
|
||||
bindNavSummary()
|
||||
}
|
||||
|
||||
private fun initLocalePicker(preference: ListPreference) {
|
||||
val locales = preference.context.getLocalesConfig()
|
||||
.toList()
|
||||
.sortedWithSafe(LocaleComparator())
|
||||
preference.entries = Array(locales.size + 1) { i ->
|
||||
if (i == 0) {
|
||||
getString(R.string.follow_system)
|
||||
} else {
|
||||
val lc = locales[i - 1]
|
||||
lc.getDisplayName(lc).toTitleCase(lc)
|
||||
}
|
||||
}
|
||||
preference.entryValues = Array(locales.size + 1) { i ->
|
||||
if (i == 0) {
|
||||
""
|
||||
} else {
|
||||
locales[i - 1].toLanguageTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
AppSettings.KEY_APP_PASSWORD -> {
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindNavSummary() {
|
||||
val pref = findPreference<Preference>(AppSettings.KEY_NAV_MAIN) ?: return
|
||||
pref.summary = settings.mainNavItems.joinToString {
|
||||
getString(it.title)
|
||||
}
|
||||
}
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_PROTECT_APP -> {
|
||||
val pref = (preference as? TwoStatePreference ?: return false)
|
||||
if (pref.isChecked) {
|
||||
pref.isChecked = false
|
||||
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
|
||||
} else {
|
||||
settings.appPassword = null
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postRestart() {
|
||||
viewLifecycleOwner.lifecycle.postDelayed(400) {
|
||||
activityRecreationHandle.recreateAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initLocalePicker(preference: ListPreference) {
|
||||
val locales = preference.context.getLocalesConfig()
|
||||
.toList()
|
||||
.sortedWithSafe(LocaleComparator())
|
||||
preference.entries = Array(locales.size + 1) { i ->
|
||||
if (i == 0) {
|
||||
getString(R.string.follow_system)
|
||||
} else {
|
||||
val lc = locales[i - 1]
|
||||
lc.getDisplayName(lc).toTitleCase(lc)
|
||||
}
|
||||
}
|
||||
preference.entryValues = Array(locales.size + 1) { i ->
|
||||
if (i == 0) {
|
||||
""
|
||||
} else {
|
||||
locales[i - 1].toLanguageTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindNavSummary() {
|
||||
val pref = findPreference<Preference>(AppSettings.KEY_NAV_MAIN) ?: return
|
||||
pref.summary = settings.mainNavItems.joinToString {
|
||||
getString(it.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.parsers.util.names
|
||||
import java.net.Proxy
|
||||
|
||||
class NetworkSettingsFragment :
|
||||
BasePreferenceFragment(R.string.network),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_network)
|
||||
findPreference<ListPreference>(AppSettings.KEY_DOH)?.run {
|
||||
entryValues = DoHProvider.entries.names()
|
||||
setDefaultValueCompat(DoHProvider.NONE.name)
|
||||
}
|
||||
bindProxySummary()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_SSL_BYPASS -> {
|
||||
Snackbar.make(listView, R.string.settings_apply_restart_required, Snackbar.LENGTH_INDEFINITE).show()
|
||||
}
|
||||
|
||||
AppSettings.KEY_PROXY_TYPE,
|
||||
AppSettings.KEY_PROXY_ADDRESS,
|
||||
AppSettings.KEY_PROXY_PORT -> {
|
||||
bindProxySummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindProxySummary() {
|
||||
findPreference<Preference>(AppSettings.KEY_PROXY)?.run {
|
||||
val type = settings.proxyType
|
||||
val address = settings.proxyAddress
|
||||
val port = settings.proxyPort
|
||||
summary = when {
|
||||
type == Proxy.Type.DIRECT -> context.getString(R.string.disabled)
|
||||
address.isNullOrEmpty() || port == 0 -> context.getString(R.string.invalid_proxy_configuration)
|
||||
else -> "$address:$port"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@ class RootSettingsFragment : BasePreferenceFragment(0) {
|
||||
addPreferencesFromResource(R.xml.pref_root_debug)
|
||||
bindPreferenceSummary("appearance", R.string.theme, R.string.list_mode, R.string.language)
|
||||
bindPreferenceSummary("reader", R.string.read_mode, R.string.scale_mode, R.string.switch_pages)
|
||||
bindPreferenceSummary("network", R.string.proxy, R.string.dns_over_https, R.string.prefetch_content)
|
||||
bindPreferenceSummary("userdata", R.string.protect_application, R.string.backup_restore, R.string.data_deletion)
|
||||
bindPreferenceSummary("network", R.string.storage_usage, R.string.proxy, R.string.prefetch_content)
|
||||
bindPreferenceSummary("userdata", R.string.create_or_restore_backup, R.string.periodic_backups)
|
||||
bindPreferenceSummary("downloads", R.string.manga_save_location, R.string.downloads_wifi_only)
|
||||
bindPreferenceSummary("tracker", R.string.track_sources, R.string.notifications_settings)
|
||||
bindPreferenceSummary("services", R.string.suggestions, R.string.sync, R.string.tracking)
|
||||
|
||||
@@ -39,7 +39,7 @@ import org.koitharu.kotatsu.settings.sources.SourceSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.manage.SourcesManageFragment
|
||||
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.userdata.BackupsSettingsFragment
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsActivity :
|
||||
@@ -146,7 +146,7 @@ class SettingsActivity :
|
||||
val fragment = when (intent?.action) {
|
||||
AppRouter.ACTION_READER -> ReaderSettingsFragment()
|
||||
AppRouter.ACTION_SUGGESTIONS -> SuggestionsSettingsFragment()
|
||||
AppRouter.ACTION_HISTORY -> UserDataSettingsFragment()
|
||||
AppRouter.ACTION_HISTORY -> BackupsSettingsFragment()
|
||||
AppRouter.ACTION_TRACKER -> TrackerSettingsFragment()
|
||||
AppRouter.ACTION_PERIODIC_BACKUP -> PeriodicalBackupSettingsFragment()
|
||||
AppRouter.ACTION_SOURCES -> SourcesSettingsFragment()
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.network.DoHProvider
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.parsers.util.names
|
||||
import org.koitharu.kotatsu.settings.userdata.storage.StorageUsagePreference
|
||||
import java.net.Proxy
|
||||
|
||||
class StorageAndNetworkSettingsFragment :
|
||||
BasePreferenceFragment(R.string.storage_and_network),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val viewModel by viewModels<StorageAndNetworkSettingsViewModel>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_network_storage)
|
||||
findPreference<ListPreference>(AppSettings.KEY_DOH)?.run {
|
||||
entryValues = DoHProvider.entries.names()
|
||||
setDefaultValueCompat(DoHProvider.NONE.name)
|
||||
}
|
||||
bindProxySummary()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
||||
settings.subscribe(this)
|
||||
findPreference<StorageUsagePreference>(AppSettings.KEY_STORAGE_USAGE)?.let { pref ->
|
||||
viewModel.storageUsage.observe(viewLifecycleOwner, pref)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(prefs: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_SSL_BYPASS -> {
|
||||
Snackbar.make(listView, R.string.settings_apply_restart_required, Snackbar.LENGTH_INDEFINITE).show()
|
||||
}
|
||||
|
||||
AppSettings.KEY_PROXY_TYPE,
|
||||
AppSettings.KEY_PROXY_ADDRESS,
|
||||
AppSettings.KEY_PROXY_PORT -> {
|
||||
bindProxySummary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindProxySummary() {
|
||||
findPreference<Preference>(AppSettings.KEY_PROXY)?.run {
|
||||
val type = settings.proxyType
|
||||
val address = settings.proxyAddress
|
||||
val port = settings.proxyPort
|
||||
summary = when {
|
||||
type == Proxy.Type.DIRECT -> context.getString(R.string.disabled)
|
||||
address.isNullOrEmpty() || port == 0 -> context.getString(R.string.invalid_proxy_configuration)
|
||||
else -> "$address:$port"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.settings.userdata.storage.StorageUsage
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class StorageAndNetworkSettingsViewModel @Inject constructor(
|
||||
private val storageManager: LocalStorageManager,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val storageUsage: StateFlow<StorageUsage?> = flow {
|
||||
emit(loadStorageUsage())
|
||||
}.withErrorHandling()
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.WhileSubscribed(1000), null)
|
||||
|
||||
private suspend fun loadStorageUsage(): StorageUsage {
|
||||
val pagesCacheSize = storageManager.computeCacheSize(CacheDir.PAGES)
|
||||
val otherCacheSize = storageManager.computeCacheSize() - pagesCacheSize
|
||||
val storageSize = storageManager.computeStorageSize()
|
||||
val availableSpace = storageManager.computeAvailableSize()
|
||||
val totalBytes = pagesCacheSize + otherCacheSize + storageSize + availableSpace
|
||||
return StorageUsage(
|
||||
savedManga = StorageUsage.Item(
|
||||
bytes = storageSize,
|
||||
percent = (storageSize.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
pagesCache = StorageUsage.Item(
|
||||
bytes = pagesCacheSize,
|
||||
percent = (pagesCacheSize.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
otherCache = StorageUsage.Item(
|
||||
bytes = otherCacheSize,
|
||||
percent = (otherCacheSize.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
available = StorageUsage.Item(
|
||||
bytes = availableSpace,
|
||||
percent = (availableSpace.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ class AppUpdateActivity : BaseActivity<ActivityAppUpdateBinding>(), View.OnClick
|
||||
viewModel.installIntent.value?.let { intent ->
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
} catch (e: Exception) {
|
||||
onError(e)
|
||||
}
|
||||
return
|
||||
|
||||
@@ -11,6 +11,6 @@ data class SettingsItem(
|
||||
) : ListModel {
|
||||
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
return other is SettingsItem && other.key == key
|
||||
return other is SettingsItem && other.key == key && other.fragmentClass == fragmentClass
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,106 +13,118 @@ import org.koitharu.kotatsu.backups.ui.periodical.PeriodicalBackupSettingsFragme
|
||||
import org.koitharu.kotatsu.core.LocalizedAppContext
|
||||
import org.koitharu.kotatsu.settings.AppearanceSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.DownloadsSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.NetworkSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.ProxySettingsFragment
|
||||
import org.koitharu.kotatsu.settings.ReaderSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.ServicesSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.StorageAndNetworkSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.SuggestionsSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.about.AboutSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.discord.DiscordSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.userdata.storage.StorageManageSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.userdata.BackupsSettingsFragment
|
||||
import org.koitharu.kotatsu.settings.userdata.storage.DataCleanupSettingsFragment
|
||||
import javax.inject.Inject
|
||||
|
||||
@Reusable
|
||||
@SuppressLint("RestrictedApi")
|
||||
class SettingsSearchHelper @Inject constructor(
|
||||
@LocalizedAppContext private val context: Context,
|
||||
@LocalizedAppContext private val context: Context,
|
||||
) {
|
||||
|
||||
fun inflatePreferences(): List<SettingsItem> {
|
||||
val preferenceManager = PreferenceManager(context)
|
||||
val result = ArrayList<SettingsItem>()
|
||||
preferenceManager.inflateTo(result, R.xml.pref_appearance, emptyList(), AppearanceSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_sources, emptyList(), SourcesSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_reader, emptyList(), ReaderSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_network, emptyList(), NetworkSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_user_data, emptyList(), UserDataSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_storage,
|
||||
listOf(context.getString(R.string.data_and_privacy)),
|
||||
StorageManageSettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_downloads, emptyList(), DownloadsSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_tracker, emptyList(), TrackerSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_services, emptyList(), ServicesSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_about, emptyList(), AboutSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_backup_periodic,
|
||||
listOf(context.getString(R.string.data_and_privacy)),
|
||||
PeriodicalBackupSettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_proxy,
|
||||
listOf(context.getString(R.string.proxy)),
|
||||
ProxySettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_suggestions,
|
||||
listOf(context.getString(R.string.suggestions)),
|
||||
SuggestionsSettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_sources,
|
||||
listOf(context.getString(R.string.remote_sources)),
|
||||
SourcesSettingsFragment::class.java,
|
||||
)
|
||||
return result
|
||||
}
|
||||
fun inflatePreferences(): List<SettingsItem> {
|
||||
val preferenceManager = PreferenceManager(context)
|
||||
val result = ArrayList<SettingsItem>()
|
||||
preferenceManager.inflateTo(result, R.xml.pref_appearance, emptyList(), AppearanceSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_sources, emptyList(), SourcesSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_reader, emptyList(), ReaderSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_network_storage,
|
||||
emptyList(),
|
||||
StorageAndNetworkSettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_backups, emptyList(), BackupsSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_data_cleanup,
|
||||
listOf(context.getString(R.string.storage_and_network)),
|
||||
DataCleanupSettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_downloads, emptyList(), DownloadsSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_tracker, emptyList(), TrackerSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_services, emptyList(), ServicesSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(result, R.xml.pref_about, emptyList(), AboutSettingsFragment::class.java)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_backup_periodic,
|
||||
listOf(context.getString(R.string.backup_restore)),
|
||||
PeriodicalBackupSettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_proxy,
|
||||
listOf(context.getString(R.string.storage_and_network)),
|
||||
ProxySettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_suggestions,
|
||||
listOf(context.getString(R.string.services)),
|
||||
SuggestionsSettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_discord,
|
||||
listOf(context.getString(R.string.services)),
|
||||
DiscordSettingsFragment::class.java,
|
||||
)
|
||||
preferenceManager.inflateTo(
|
||||
result,
|
||||
R.xml.pref_sources,
|
||||
listOf(),
|
||||
SourcesSettingsFragment::class.java,
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun PreferenceManager.inflateTo(
|
||||
result: MutableList<SettingsItem>,
|
||||
@XmlRes resId: Int,
|
||||
breadcrumbs: List<String>,
|
||||
fragmentClass: Class<out PreferenceFragmentCompat>
|
||||
) {
|
||||
val screen = inflateFromResource(context, resId, null)
|
||||
val screenTitle = screen.title?.toString()
|
||||
screen.inflateTo(
|
||||
result = result,
|
||||
breadcrumbs = if (screenTitle.isNullOrEmpty()) breadcrumbs else breadcrumbs + screenTitle,
|
||||
fragmentClass = fragmentClass,
|
||||
)
|
||||
}
|
||||
private fun PreferenceManager.inflateTo(
|
||||
result: MutableList<SettingsItem>,
|
||||
@XmlRes resId: Int,
|
||||
breadcrumbs: List<String>,
|
||||
fragmentClass: Class<out PreferenceFragmentCompat>
|
||||
) {
|
||||
val screen = inflateFromResource(context, resId, null)
|
||||
val screenTitle = screen.title?.toString()
|
||||
screen.inflateTo(
|
||||
result = result,
|
||||
breadcrumbs = if (screenTitle.isNullOrEmpty()) breadcrumbs else breadcrumbs + screenTitle,
|
||||
fragmentClass = fragmentClass,
|
||||
)
|
||||
}
|
||||
|
||||
private fun PreferenceScreen.inflateTo(
|
||||
result: MutableList<SettingsItem>,
|
||||
breadcrumbs: List<String>,
|
||||
fragmentClass: Class<out PreferenceFragmentCompat>
|
||||
): Unit = repeat(preferenceCount) { i ->
|
||||
val pref = this[i]
|
||||
if (pref is PreferenceScreen) {
|
||||
val screenTitle = pref.title?.toString()
|
||||
pref.inflateTo(
|
||||
result = result,
|
||||
breadcrumbs = if (screenTitle.isNullOrEmpty()) breadcrumbs else breadcrumbs + screenTitle,
|
||||
fragmentClass = fragmentClass,
|
||||
)
|
||||
} else {
|
||||
result.add(
|
||||
SettingsItem(
|
||||
key = pref.key ?: return@repeat,
|
||||
title = pref.title ?: return@repeat,
|
||||
breadcrumbs = breadcrumbs,
|
||||
fragmentClass = fragmentClass,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun PreferenceScreen.inflateTo(
|
||||
result: MutableList<SettingsItem>,
|
||||
breadcrumbs: List<String>,
|
||||
fragmentClass: Class<out PreferenceFragmentCompat>
|
||||
): Unit = repeat(preferenceCount) { i ->
|
||||
val pref = this[i]
|
||||
if (pref is PreferenceScreen) {
|
||||
val screenTitle = pref.title?.toString()
|
||||
pref.inflateTo(
|
||||
result = result,
|
||||
breadcrumbs = if (screenTitle.isNullOrEmpty()) breadcrumbs else breadcrumbs + screenTitle,
|
||||
fragmentClass = fragmentClass,
|
||||
)
|
||||
} else {
|
||||
result.add(
|
||||
SettingsItem(
|
||||
key = pref.key ?: return@repeat,
|
||||
title = pref.title ?: return@repeat,
|
||||
breadcrumbs = breadcrumbs,
|
||||
fragmentClass = fragmentClass,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.TriStateOption
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
@@ -31,6 +32,10 @@ class SourcesSettingsFragment : BasePreferenceFragment(R.string.remote_sources),
|
||||
entries = SourcesSortOrder.entries.map { context.getString(it.titleResId) }.toTypedArray()
|
||||
setDefaultValueCompat(SourcesSortOrder.MANUAL.name)
|
||||
}
|
||||
findPreference<ListPreference>(AppSettings.KEY_INCOGNITO_NSFW)?.run {
|
||||
entryValues = TriStateOption.entries.names()
|
||||
setDefaultValueCompat(TriStateOption.ASK.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package org.koitharu.kotatsu.settings.userdata
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.backups.domain.BackupUtils
|
||||
import org.koitharu.kotatsu.backups.ui.backup.BackupService
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BackupsSettingsFragment : BasePreferenceFragment(R.string.backup_restore),
|
||||
ActivityResultCallback<Uri?> {
|
||||
|
||||
private val viewModel: BackupsSettingsViewModel by viewModels()
|
||||
|
||||
private val backupSelectCall = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument(),
|
||||
this,
|
||||
)
|
||||
|
||||
private val backupCreateCall = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/zip"),
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
if (!BackupService.start(requireContext(), uri)) {
|
||||
Snackbar.make(
|
||||
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_backups)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
bindPeriodicalBackupSummary()
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_BACKUP -> {
|
||||
if (!backupCreateCall.tryLaunch(BackupUtils.generateFileName(preference.context))) {
|
||||
Snackbar.make(
|
||||
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_RESTORE -> {
|
||||
if (!backupSelectCall.tryLaunch(arrayOf("*/*"))) {
|
||||
Snackbar.make(
|
||||
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: Uri?) {
|
||||
if (result != null) {
|
||||
router.showBackupRestoreDialog(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindPeriodicalBackupSummary() {
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_ENABLED) ?: return
|
||||
val entries = resources.getStringArray(R.array.backup_frequency)
|
||||
val entryValues = resources.getStringArray(R.array.values_backup_frequency)
|
||||
viewModel.periodicalBackupFrequency.observe(viewLifecycleOwner) { freq ->
|
||||
preference.summary = if (freq == 0L) {
|
||||
getString(R.string.disabled)
|
||||
} else {
|
||||
val index = entryValues.indexOf(freq.toString())
|
||||
entries.getOrNull(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package org.koitharu.kotatsu.settings.userdata
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BackupsSettingsViewModel @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val periodicalBackupFrequency = settings.observeAsFlow(
|
||||
key = AppSettings.KEY_BACKUP_PERIODICAL_ENABLED,
|
||||
valueProducer = { isPeriodicalBackupEnabled },
|
||||
).flatMapLatest { isEnabled ->
|
||||
if (isEnabled) {
|
||||
settings.observeAsFlow(
|
||||
key = AppSettings.KEY_BACKUP_PERIODICAL_FREQUENCY,
|
||||
valueProducer = { periodicalBackupFrequency },
|
||||
)
|
||||
} else {
|
||||
flowOf(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.userdata
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.MultiSelectListPreference
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.TwoStatePreference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.backups.domain.BackupUtils
|
||||
import org.koitharu.kotatsu.backups.ui.backup.BackupService
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.nav.router
|
||||
import org.koitharu.kotatsu.core.os.AppShortcutManager
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ScreenshotsPolicy
|
||||
import org.koitharu.kotatsu.core.prefs.SearchSuggestionType
|
||||
import org.koitharu.kotatsu.core.prefs.TriStateOption
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.core.util.ext.setDefaultValueCompat
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.names
|
||||
import org.koitharu.kotatsu.settings.protect.ProtectSetupActivity
|
||||
import org.koitharu.kotatsu.settings.utils.MultiSummaryProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class UserDataSettingsFragment : BasePreferenceFragment(R.string.data_and_privacy),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
ActivityResultCallback<Uri?> {
|
||||
|
||||
@Inject
|
||||
lateinit var appShortcutManager: AppShortcutManager
|
||||
|
||||
private val viewModel: UserDataSettingsViewModel by viewModels()
|
||||
|
||||
private val backupSelectCall = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocument(),
|
||||
this,
|
||||
)
|
||||
|
||||
private val backupCreateCall = registerForActivityResult(
|
||||
ActivityResultContracts.CreateDocument("application/zip"),
|
||||
) { uri ->
|
||||
if (uri != null) {
|
||||
if (!BackupService.start(requireContext(), uri)) {
|
||||
Snackbar.make(
|
||||
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_user_data)
|
||||
findPreference<Preference>(AppSettings.KEY_SHORTCUTS)?.isVisible =
|
||||
appShortcutManager.isDynamicShortcutsAvailable()
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
findPreference<ListPreference>(AppSettings.KEY_SCREENSHOTS_POLICY)?.run {
|
||||
entryValues = ScreenshotsPolicy.entries.names()
|
||||
setDefaultValueCompat(ScreenshotsPolicy.ALLOW.name)
|
||||
}
|
||||
findPreference<ListPreference>(AppSettings.KEY_INCOGNITO_NSFW)?.run {
|
||||
entryValues = TriStateOption.entries.names()
|
||||
setDefaultValueCompat(TriStateOption.ASK.name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
bindPeriodicalBackupSummary()
|
||||
findPreference<MultiSelectListPreference>(AppSettings.KEY_SEARCH_SUGGESTION_TYPES)?.let { pref ->
|
||||
pref.entryValues = SearchSuggestionType.entries.names()
|
||||
pref.entries = SearchSuggestionType.entries.map { pref.context.getString(it.titleResId) }.toTypedArray()
|
||||
pref.summaryProvider = MultiSummaryProvider(R.string.none)
|
||||
pref.values = settings.searchSuggestionTypes.mapToSet { it.name }
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_STORAGE_USAGE)?.let { pref ->
|
||||
viewModel.storageUsage.observe(viewLifecycleOwner) { size ->
|
||||
pref.summary = if (size < 0L) {
|
||||
pref.context.getString(R.string.computing_)
|
||||
} else {
|
||||
FileSize.BYTES.format(pref.context, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
||||
settings.subscribe(this)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
settings.unsubscribe(this)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
return when (preference.key) {
|
||||
AppSettings.KEY_BACKUP -> {
|
||||
if (!backupCreateCall.tryLaunch(BackupUtils.generateFileName(preference.context))) {
|
||||
Snackbar.make(
|
||||
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_RESTORE -> {
|
||||
if (!backupSelectCall.tryLaunch(arrayOf("*/*"))) {
|
||||
Snackbar.make(
|
||||
listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT,
|
||||
).show()
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_PROTECT_APP -> {
|
||||
val pref = (preference as? TwoStatePreference ?: return false)
|
||||
if (pref.isChecked) {
|
||||
pref.isChecked = false
|
||||
startActivity(Intent(preference.context, ProtectSetupActivity::class.java))
|
||||
} else {
|
||||
settings.appPassword = null
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
AppSettings.KEY_APP_PASSWORD -> {
|
||||
findPreference<TwoStatePreference>(AppSettings.KEY_PROTECT_APP)
|
||||
?.isChecked = !settings.appPassword.isNullOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(result: Uri?) {
|
||||
if (result != null) {
|
||||
router.showBackupRestoreDialog(result)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindPeriodicalBackupSummary() {
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_ENABLED) ?: return
|
||||
val entries = resources.getStringArray(R.array.backup_frequency)
|
||||
val entryValues = resources.getStringArray(R.array.values_backup_frequency)
|
||||
viewModel.periodicalBackupFrequency.observe(viewLifecycleOwner) { freq ->
|
||||
preference.summary = if (freq == 0L) {
|
||||
getString(R.string.disabled)
|
||||
} else {
|
||||
val index = entryValues.indexOf(freq.toString())
|
||||
entries.getOrNull(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.userdata
|
||||
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class UserDataSettingsViewModel @Inject constructor(
|
||||
private val storageManager: LocalStorageManager,
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val storageUsage = MutableStateFlow(-1L)
|
||||
|
||||
val periodicalBackupFrequency = settings.observeAsFlow(
|
||||
key = AppSettings.KEY_BACKUP_PERIODICAL_ENABLED,
|
||||
valueProducer = { isPeriodicalBackupEnabled },
|
||||
).flatMapLatest { isEnabled ->
|
||||
if (isEnabled) {
|
||||
settings.observeAsFlow(
|
||||
key = AppSettings.KEY_BACKUP_PERIODICAL_FREQUENCY,
|
||||
valueProducer = { periodicalBackupFrequency },
|
||||
)
|
||||
} else {
|
||||
flowOf(0)
|
||||
}
|
||||
}
|
||||
|
||||
private var storageUsageJob: Job? = null
|
||||
|
||||
init {
|
||||
loadStorageUsage()
|
||||
}
|
||||
|
||||
private fun loadStorageUsage(): Job {
|
||||
val prevJob = storageUsageJob
|
||||
return launchJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
val totalBytes = storageManager.computeCacheSize() + storageManager.computeStorageSize()
|
||||
storageUsage.value = totalBytes
|
||||
}.also {
|
||||
storageUsageJob = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package org.koitharu.kotatsu.settings.userdata.storage
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.ui.dialog.buildAlertDialog
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DataCleanupSettingsFragment : BasePreferenceFragment(R.string.data_removal) {
|
||||
|
||||
private val viewModel by viewModels<DataCleanupSettingsViewModel>()
|
||||
private val loadingPrefs = HashSet<String>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_data_cleanup)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.PAGES]))
|
||||
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.THUMBS]))
|
||||
findPreference<Preference>(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindBytesSizeSummary(viewModel.httpCacheSize)
|
||||
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
|
||||
viewModel.searchHistoryCount.observe(viewLifecycleOwner) {
|
||||
pref.summary = if (it < 0) {
|
||||
view.context.getString(R.string.loading_)
|
||||
} else {
|
||||
pref.context.resources.getQuantityStringSafe(R.plurals.items, it, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_UPDATES_FEED_CLEAR)?.let { pref ->
|
||||
viewModel.feedItemsCount.observe(viewLifecycleOwner) {
|
||||
pref.summary = if (it < 0) {
|
||||
view.context.getString(R.string.loading_)
|
||||
} else {
|
||||
pref.context.resources.getQuantityStringSafe(R.plurals.items, it, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_WEBVIEW_CLEAR)?.isVisible = viewModel.isBrowserDataCleanupEnabled
|
||||
|
||||
viewModel.loadingKeys.observe(viewLifecycleOwner) { keys ->
|
||||
loadingPrefs.addAll(keys)
|
||||
loadingPrefs.forEach { prefKey ->
|
||||
findPreference<Preference>(prefKey)?.isEnabled = prefKey !in keys
|
||||
}
|
||||
}
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView))
|
||||
viewModel.onChaptersCleanedUp.observeEvent(viewLifecycleOwner, ::onChaptersCleanedUp)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean = when (preference.key) {
|
||||
AppSettings.KEY_COOKIES_CLEAR -> {
|
||||
clearCookies()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
|
||||
clearSearchHistory()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_PAGES_CACHE_CLEAR -> {
|
||||
viewModel.clearCache(preference.key, CacheDir.PAGES)
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
||||
viewModel.clearCache(preference.key, CacheDir.THUMBS, CacheDir.FAVICONS)
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_HTTP_CACHE_CLEAR -> {
|
||||
viewModel.clearHttpCache()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_CHAPTERS_CLEAR -> {
|
||||
cleanupChapters()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_WEBVIEW_CLEAR -> {
|
||||
viewModel.clearBrowserData()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_CLEAR_MANGA_DATA -> {
|
||||
viewModel.clearMangaData()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
||||
viewModel.clearUpdatesFeed()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
private fun onChaptersCleanedUp(result: Pair<Int, Long>) {
|
||||
val c = context ?: return
|
||||
val text = if (result.first == 0 && result.second == 0L) {
|
||||
c.getString(R.string.no_chapters_deleted)
|
||||
} else {
|
||||
c.getString(
|
||||
R.string.chapters_deleted_pattern,
|
||||
c.resources.getQuantityStringSafe(R.plurals.chapters, result.first, result.first),
|
||||
FileSize.BYTES.format(c, result.second),
|
||||
)
|
||||
}
|
||||
Snackbar.make(listView, text, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun Preference.bindBytesSizeSummary(stateFlow: StateFlow<Long>) {
|
||||
stateFlow.observe(viewLifecycleOwner) { size ->
|
||||
summary = if (size < 0) {
|
||||
context.getString(R.string.computing_)
|
||||
} else {
|
||||
FileSize.BYTES.format(context, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearSearchHistory() {
|
||||
buildAlertDialog(context ?: return) {
|
||||
setTitle(R.string.clear_search_history)
|
||||
setMessage(R.string.text_clear_search_history_prompt)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearSearchHistory()
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun clearCookies() {
|
||||
buildAlertDialog(context ?: return) {
|
||||
setTitle(R.string.clear_cookies)
|
||||
setMessage(R.string.text_clear_cookies_prompt)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearCookies()
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun cleanupChapters() {
|
||||
buildAlertDialog(context ?: return) {
|
||||
setTitle(R.string.delete_read_chapters)
|
||||
setMessage(R.string.delete_read_chapters_prompt)
|
||||
setNegativeButton(android.R.string.cancel, null)
|
||||
setPositiveButton(R.string.delete) { _, _ ->
|
||||
viewModel.cleanupChapters()
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package org.koitharu.kotatsu.settings.userdata.storage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.webkit.WebStorage
|
||||
import androidx.webkit.WebStorageCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import coil3.ImageLoader
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.Cache
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.local.domain.DeleteReadChaptersUseCase
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import java.util.EnumMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@HiltViewModel
|
||||
class DataCleanupSettingsViewModel @Inject constructor(
|
||||
private val storageManager: LocalStorageManager,
|
||||
private val httpCache: Cache,
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val deleteReadChaptersUseCase: DeleteReadChaptersUseCase,
|
||||
private val mangaDataRepositoryProvider: Provider<MangaDataRepository>,
|
||||
private val coil: ImageLoader,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val loadingKeys = MutableStateFlow(emptySet<String>())
|
||||
|
||||
val searchHistoryCount = MutableStateFlow(-1)
|
||||
val feedItemsCount = MutableStateFlow(-1)
|
||||
val httpCacheSize = MutableStateFlow(-1L)
|
||||
val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java)
|
||||
|
||||
val onChaptersCleanedUp = MutableEventFlow<Pair<Int, Long>>()
|
||||
|
||||
val isBrowserDataCleanupEnabled: Boolean
|
||||
get() = WebViewFeature.isFeatureSupported(WebViewFeature.DELETE_BROWSING_DATA)
|
||||
|
||||
init {
|
||||
CacheDir.entries.forEach {
|
||||
cacheSizes[it] = MutableStateFlow(-1L)
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
searchHistoryCount.value = searchRepository.getSearchHistoryCount()
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
feedItemsCount.value = trackingRepository.getLogsCount()
|
||||
}
|
||||
CacheDir.entries.forEach { cache ->
|
||||
launchJob(Dispatchers.Default) {
|
||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||
}
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
httpCacheSize.value = runInterruptible { httpCache.size() }
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCache(key: String, vararg caches: CacheDir) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + key }
|
||||
for (cache in caches) {
|
||||
storageManager.clearCache(cache)
|
||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||
if (cache == CacheDir.THUMBS) {
|
||||
coil.memoryCache?.clear()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loadingKeys.update { it - key }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearHttpCache() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_HTTP_CACHE_CLEAR }
|
||||
val size = runInterruptible(Dispatchers.IO) {
|
||||
httpCache.evictAll()
|
||||
httpCache.size()
|
||||
}
|
||||
httpCacheSize.value = size
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_HTTP_CACHE_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSearchHistory() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
searchRepository.clearSearchHistory()
|
||||
searchHistoryCount.value = searchRepository.getSearchHistoryCount()
|
||||
onActionDone.call(ReversibleAction(R.string.search_history_cleared, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCookies() {
|
||||
launchJob {
|
||||
cookieJar.clear()
|
||||
onActionDone.call(ReversibleAction(R.string.cookies_cleared, null))
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RequiresFeature")
|
||||
fun clearBrowserData() {
|
||||
launchJob {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_WEBVIEW_CLEAR }
|
||||
val storage = WebStorage.getInstance()
|
||||
suspendCoroutine { cont ->
|
||||
WebStorageCompat.deleteBrowsingData(storage) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
}
|
||||
onActionDone.call(ReversibleAction(R.string.updates_feed_cleared, null))
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_WEBVIEW_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearUpdatesFeed() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_UPDATES_FEED_CLEAR }
|
||||
trackingRepository.clearLogs()
|
||||
feedItemsCount.value = trackingRepository.getLogsCount()
|
||||
onActionDone.call(ReversibleAction(R.string.updates_feed_cleared, null))
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_UPDATES_FEED_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMangaData() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_CLEAR_MANGA_DATA }
|
||||
trackingRepository.gc()
|
||||
val repository = mangaDataRepositoryProvider.get()
|
||||
repository.cleanupLocalManga()
|
||||
repository.cleanupDatabase()
|
||||
onActionDone.call(ReversibleAction(R.string.updates_feed_cleared, null))
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_CLEAR_MANGA_DATA }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanupChapters() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_CHAPTERS_CLEAR }
|
||||
val oldSize = storageManager.computeStorageSize()
|
||||
val chaptersCount = deleteReadChaptersUseCase.invoke()
|
||||
val newSize = storageManager.computeStorageSize()
|
||||
onChaptersCleanedUp.call(chaptersCount to oldSize - newSize)
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_CHAPTERS_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.userdata.storage
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.preference.Preference
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.SnackbarErrorObserver
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.core.util.FileSize
|
||||
import org.koitharu.kotatsu.core.util.ext.getQuantityStringSafe
|
||||
import org.koitharu.kotatsu.core.util.ext.observe
|
||||
import org.koitharu.kotatsu.core.util.ext.observeEvent
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
|
||||
@AndroidEntryPoint
|
||||
class StorageManageSettingsFragment : BasePreferenceFragment(R.string.storage_usage) {
|
||||
|
||||
private val viewModel by viewModels<StorageManageSettingsViewModel>()
|
||||
private val loadingPrefs = HashSet<String>()
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_storage)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.PAGES]))
|
||||
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.bindBytesSizeSummary(checkNotNull(viewModel.cacheSizes[CacheDir.THUMBS]))
|
||||
findPreference<Preference>(AppSettings.KEY_HTTP_CACHE_CLEAR)?.bindBytesSizeSummary(viewModel.httpCacheSize)
|
||||
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
|
||||
viewModel.searchHistoryCount.observe(viewLifecycleOwner) {
|
||||
pref.summary = if (it < 0) {
|
||||
view.context.getString(R.string.loading_)
|
||||
} else {
|
||||
pref.context.resources.getQuantityStringSafe(R.plurals.items, it, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_UPDATES_FEED_CLEAR)?.let { pref ->
|
||||
viewModel.feedItemsCount.observe(viewLifecycleOwner) {
|
||||
pref.summary = if (it < 0) {
|
||||
view.context.getString(R.string.loading_)
|
||||
} else {
|
||||
pref.context.resources.getQuantityStringSafe(R.plurals.items, it, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
findPreference<StorageUsagePreference>(AppSettings.KEY_STORAGE_USAGE)?.let { pref ->
|
||||
viewModel.storageUsage.observe(viewLifecycleOwner, pref)
|
||||
}
|
||||
findPreference<Preference>(AppSettings.KEY_WEBVIEW_CLEAR)?.isVisible = viewModel.isBrowserDataCleanupEnabled
|
||||
|
||||
viewModel.loadingKeys.observe(viewLifecycleOwner) { keys ->
|
||||
loadingPrefs.addAll(keys)
|
||||
loadingPrefs.forEach { prefKey ->
|
||||
findPreference<Preference>(prefKey)?.isEnabled = prefKey !in keys
|
||||
}
|
||||
}
|
||||
viewModel.onError.observeEvent(viewLifecycleOwner, SnackbarErrorObserver(listView, this))
|
||||
viewModel.onActionDone.observeEvent(viewLifecycleOwner, ReversibleActionObserver(listView))
|
||||
viewModel.onChaptersCleanedUp.observeEvent(viewLifecycleOwner, ::onChaptersCleanedUp)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean = when (preference.key) {
|
||||
AppSettings.KEY_COOKIES_CLEAR -> {
|
||||
clearCookies()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
|
||||
clearSearchHistory()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_PAGES_CACHE_CLEAR -> {
|
||||
viewModel.clearCache(preference.key, CacheDir.PAGES)
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
||||
viewModel.clearCache(preference.key, CacheDir.THUMBS, CacheDir.FAVICONS)
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_HTTP_CACHE_CLEAR -> {
|
||||
viewModel.clearHttpCache()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_CHAPTERS_CLEAR -> {
|
||||
cleanupChapters()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_WEBVIEW_CLEAR -> {
|
||||
viewModel.clearBrowserData()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_CLEAR_MANGA_DATA -> {
|
||||
viewModel.clearMangaData()
|
||||
true
|
||||
}
|
||||
|
||||
AppSettings.KEY_UPDATES_FEED_CLEAR -> {
|
||||
viewModel.clearUpdatesFeed()
|
||||
true
|
||||
}
|
||||
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
private fun onChaptersCleanedUp(result: Pair<Int, Long>) {
|
||||
val c = context ?: return
|
||||
val text = if (result.first == 0 && result.second == 0L) {
|
||||
c.getString(R.string.no_chapters_deleted)
|
||||
} else {
|
||||
c.getString(
|
||||
R.string.chapters_deleted_pattern,
|
||||
c.resources.getQuantityStringSafe(R.plurals.chapters, result.first, result.first),
|
||||
FileSize.BYTES.format(c, result.second),
|
||||
)
|
||||
}
|
||||
Snackbar.make(listView, text, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun Preference.bindBytesSizeSummary(stateFlow: StateFlow<Long>) {
|
||||
stateFlow.observe(viewLifecycleOwner) { size ->
|
||||
summary = if (size < 0) {
|
||||
context.getString(R.string.computing_)
|
||||
} else {
|
||||
FileSize.BYTES.format(context, size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearSearchHistory() {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.clear_search_history)
|
||||
.setMessage(R.string.text_clear_search_history_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearSearchHistory()
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun clearCookies() {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.clear_cookies)
|
||||
.setMessage(R.string.text_clear_cookies_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.clearCookies()
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun cleanupChapters() {
|
||||
MaterialAlertDialogBuilder(context ?: return)
|
||||
.setTitle(R.string.delete_read_chapters)
|
||||
.setMessage(R.string.delete_read_chapters_prompt)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.delete) { _, _ ->
|
||||
viewModel.cleanupChapters()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.userdata.storage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.webkit.WebStorage
|
||||
import androidx.webkit.WebStorageCompat
|
||||
import androidx.webkit.WebViewFeature
|
||||
import coil3.ImageLoader
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okhttp3.Cache
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
|
||||
import org.koitharu.kotatsu.core.parser.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
|
||||
import org.koitharu.kotatsu.core.util.ext.call
|
||||
import org.koitharu.kotatsu.core.util.ext.firstNotNull
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.local.domain.DeleteReadChaptersUseCase
|
||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||
import java.util.EnumMap
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@HiltViewModel
|
||||
class StorageManageSettingsViewModel @Inject constructor(
|
||||
private val storageManager: LocalStorageManager,
|
||||
private val httpCache: Cache,
|
||||
private val searchRepository: MangaSearchRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val cookieJar: MutableCookieJar,
|
||||
private val deleteReadChaptersUseCase: DeleteReadChaptersUseCase,
|
||||
private val mangaDataRepositoryProvider: Provider<MangaDataRepository>,
|
||||
private val coil: ImageLoader,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
||||
val loadingKeys = MutableStateFlow(emptySet<String>())
|
||||
|
||||
val searchHistoryCount = MutableStateFlow(-1)
|
||||
val feedItemsCount = MutableStateFlow(-1)
|
||||
val httpCacheSize = MutableStateFlow(-1L)
|
||||
val cacheSizes = EnumMap<CacheDir, MutableStateFlow<Long>>(CacheDir::class.java)
|
||||
val storageUsage = MutableStateFlow<StorageUsage?>(null)
|
||||
|
||||
val onChaptersCleanedUp = MutableEventFlow<Pair<Int, Long>>()
|
||||
|
||||
val isBrowserDataCleanupEnabled: Boolean
|
||||
get() = WebViewFeature.isFeatureSupported(WebViewFeature.DELETE_BROWSING_DATA)
|
||||
|
||||
private var storageUsageJob: Job? = null
|
||||
|
||||
init {
|
||||
CacheDir.entries.forEach {
|
||||
cacheSizes[it] = MutableStateFlow(-1L)
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
searchHistoryCount.value = searchRepository.getSearchHistoryCount()
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
feedItemsCount.value = trackingRepository.getLogsCount()
|
||||
}
|
||||
CacheDir.entries.forEach { cache ->
|
||||
launchJob(Dispatchers.Default) {
|
||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||
}
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
httpCacheSize.value = runInterruptible { httpCache.size() }
|
||||
}
|
||||
loadStorageUsage()
|
||||
}
|
||||
|
||||
fun clearCache(key: String, vararg caches: CacheDir) {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + key }
|
||||
for (cache in caches) {
|
||||
storageManager.clearCache(cache)
|
||||
checkNotNull(cacheSizes[cache]).value = storageManager.computeCacheSize(cache)
|
||||
if (cache == CacheDir.THUMBS) {
|
||||
coil.memoryCache?.clear()
|
||||
}
|
||||
}
|
||||
loadStorageUsage()
|
||||
} finally {
|
||||
loadingKeys.update { it - key }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearHttpCache() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_HTTP_CACHE_CLEAR }
|
||||
val size = runInterruptible(Dispatchers.IO) {
|
||||
httpCache.evictAll()
|
||||
httpCache.size()
|
||||
}
|
||||
httpCacheSize.value = size
|
||||
loadStorageUsage()
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_HTTP_CACHE_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSearchHistory() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
searchRepository.clearSearchHistory()
|
||||
searchHistoryCount.value = searchRepository.getSearchHistoryCount()
|
||||
onActionDone.call(ReversibleAction(R.string.search_history_cleared, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCookies() {
|
||||
launchJob {
|
||||
cookieJar.clear()
|
||||
onActionDone.call(ReversibleAction(R.string.cookies_cleared, null))
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RequiresFeature")
|
||||
fun clearBrowserData() {
|
||||
launchJob {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_WEBVIEW_CLEAR }
|
||||
val storage = WebStorage.getInstance()
|
||||
suspendCoroutine { cont ->
|
||||
WebStorageCompat.deleteBrowsingData(storage) {
|
||||
cont.resume(Unit)
|
||||
}
|
||||
}
|
||||
onActionDone.call(ReversibleAction(R.string.updates_feed_cleared, null))
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_WEBVIEW_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearUpdatesFeed() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_UPDATES_FEED_CLEAR }
|
||||
trackingRepository.clearLogs()
|
||||
feedItemsCount.value = trackingRepository.getLogsCount()
|
||||
onActionDone.call(ReversibleAction(R.string.updates_feed_cleared, null))
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_UPDATES_FEED_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMangaData() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_CLEAR_MANGA_DATA }
|
||||
trackingRepository.gc()
|
||||
val repository = mangaDataRepositoryProvider.get()
|
||||
repository.cleanupLocalManga()
|
||||
repository.cleanupDatabase()
|
||||
onActionDone.call(ReversibleAction(R.string.updates_feed_cleared, null))
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_CLEAR_MANGA_DATA }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanupChapters() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
try {
|
||||
loadingKeys.update { it + AppSettings.KEY_CHAPTERS_CLEAR }
|
||||
val oldSize = storageUsage.firstNotNull().savedManga.bytes
|
||||
val chaptersCount = deleteReadChaptersUseCase.invoke()
|
||||
loadStorageUsage().join()
|
||||
val newSize = storageUsage.firstNotNull().savedManga.bytes
|
||||
onChaptersCleanedUp.call(chaptersCount to oldSize - newSize)
|
||||
} finally {
|
||||
loadingKeys.update { it - AppSettings.KEY_CHAPTERS_CLEAR }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadStorageUsage(): Job {
|
||||
val prevJob = storageUsageJob
|
||||
return launchJob(Dispatchers.Default) {
|
||||
prevJob?.cancelAndJoin()
|
||||
val pagesCacheSize = storageManager.computeCacheSize(CacheDir.PAGES)
|
||||
val otherCacheSize = storageManager.computeCacheSize() - pagesCacheSize
|
||||
val storageSize = storageManager.computeStorageSize()
|
||||
val availableSpace = storageManager.computeAvailableSize()
|
||||
val totalBytes = pagesCacheSize + otherCacheSize + storageSize + availableSpace
|
||||
storageUsage.value = StorageUsage(
|
||||
savedManga = StorageUsage.Item(
|
||||
bytes = storageSize,
|
||||
percent = (storageSize.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
pagesCache = StorageUsage.Item(
|
||||
bytes = pagesCacheSize,
|
||||
percent = (pagesCacheSize.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
otherCache = StorageUsage.Item(
|
||||
bytes = otherCacheSize,
|
||||
percent = (otherCacheSize.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
available = StorageUsage.Item(
|
||||
bytes = availableSpace,
|
||||
percent = (availableSpace.toDouble() / totalBytes).toFloat(),
|
||||
),
|
||||
)
|
||||
}.also {
|
||||
storageUsageJob = it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,12 @@ import androidx.room.RawQuery
|
||||
import androidx.room.Upsert
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import androidx.sqlite.db.SupportSQLiteQuery
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.isActive
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import kotlin.collections.forEach
|
||||
|
||||
@Dao
|
||||
abstract class StatsDao {
|
||||
@@ -61,4 +65,19 @@ abstract class StatsDao {
|
||||
protected abstract suspend fun getDurationStatsImpl(
|
||||
query: SupportSQLiteQuery
|
||||
): Map<@MapColumn("manga") MangaEntity, @MapColumn("d") Long>
|
||||
|
||||
@Query("SELECT * FROM stats ORDER BY started_at LIMIT :limit OFFSET :offset")
|
||||
protected abstract suspend fun findAll(offset: Int, limit: Int): List<StatsEntity>
|
||||
fun dumpEnabled(): Flow<StatsEntity> = flow {
|
||||
val window = 10
|
||||
var offset = 0
|
||||
while (currentCoroutineContext().isActive) {
|
||||
val list = findAll(offset, window)
|
||||
if (list.isEmpty()) {
|
||||
break
|
||||
}
|
||||
offset += window
|
||||
list.forEach { emit(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
app/src/main/res/drawable/ic_usage.xml
Normal file
12
app/src/main/res/drawable/ic_usage.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M13,2.05V5.08C16.39,5.57 19,8.47 19,12C19,12.9 18.82,13.75 18.5,14.54L21.12,16.07C21.68,14.83 22,13.45 22,12C22,6.82 18.05,2.55 13,2.05M12,19A7,7 0 0,1 5,12C5,8.47 7.61,5.57 11,5.08V2.05C5.94,2.55 2,6.81 2,12A10,10 0 0,0 12,22C15.3,22 18.23,20.39 20.05,17.91L17.45,16.38C16.17,18 14.21,19 12,19Z" />
|
||||
</vector>
|
||||
@@ -42,7 +42,7 @@
|
||||
android:importantForAutofill="no"
|
||||
android:minHeight="48dp" />
|
||||
|
||||
<ImageView
|
||||
<ImageButton
|
||||
android:id="@+id/dropdown"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
|
||||
@@ -1,82 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
|
||||
android:id="@+id/headerBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/chapters" />
|
||||
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
|
||||
android:id="@+id/headerBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/chapters" />
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
style="?tabSecondaryStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:background="@null"
|
||||
app:tabGravity="start"
|
||||
app:tabIndicator="@drawable/bg_tab_pill"
|
||||
app:tabIndicatorAnimationMode="fade"
|
||||
app:tabIndicatorColor="?colorSurfaceDim"
|
||||
app:tabIndicatorFullWidth="true"
|
||||
app:tabIndicatorGravity="stretch"
|
||||
app:tabInlineLabel="true"
|
||||
app:tabMinWidth="0dp"
|
||||
app:tabMode="scrollable"
|
||||
app:tabUnboundedRipple="true" />
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
style="?tabSecondaryStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:background="@null"
|
||||
app:tabGravity="start"
|
||||
app:tabIndicator="@drawable/bg_tab_pill"
|
||||
app:tabIndicatorAnimationMode="fade"
|
||||
app:tabIndicatorColor="?colorSurfaceDim"
|
||||
app:tabIndicatorFullWidth="true"
|
||||
app:tabIndicatorGravity="stretch"
|
||||
app:tabInlineLabel="true"
|
||||
app:tabMinWidth="0dp"
|
||||
app:tabMode="scrollable"
|
||||
app:tabUnboundedRipple="true" />
|
||||
|
||||
<com.google.android.material.button.MaterialSplitButton
|
||||
android:id="@+id/split_button_read"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp">
|
||||
<com.google.android.material.button.MaterialSplitButton
|
||||
android:id="@+id/split_button_read"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_read"
|
||||
style="?materialSplitButtonLeadingFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/read_button_min_width"
|
||||
android:text="@string/read" />
|
||||
<Button
|
||||
android:id="@+id/button_read"
|
||||
style="?materialSplitButtonLeadingFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="@dimen/read_button_min_width"
|
||||
android:text="@string/read" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_read_menu"
|
||||
style="?materialSplitButtonIconFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/show_menu"
|
||||
app:icon="?expandCollapseIndicator"
|
||||
app:toggleCheckedStateOnClick="false" />
|
||||
<Button
|
||||
android:id="@+id/button_read_menu"
|
||||
style="?materialSplitButtonIconFilledStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/show_menu"
|
||||
app:icon="?expandCollapseIndicator"
|
||||
app:toggleCheckedStateOnClick="false" />
|
||||
|
||||
</com.google.android.material.button.MaterialSplitButton>
|
||||
</com.google.android.material.button.MaterialSplitButton>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
<org.koitharu.kotatsu.core.ui.widgets.TouchBlockLayout
|
||||
android:id="@+id/layout_touchBlock"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</org.koitharu.kotatsu.core.ui.widgets.TouchBlockLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,257 +1,329 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
|
||||
android:id="@+id/headerBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/filter" />
|
||||
<org.koitharu.kotatsu.core.ui.sheet.AdaptiveSheetHeaderBar
|
||||
android:id="@+id/headerBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/filter" />
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:scrollIndicators="top"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/scrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:clipToPadding="false"
|
||||
android:scrollIndicators="top|bottom"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/margin_small"
|
||||
android:paddingBottom="@dimen/margin_normal">
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingHorizontal="@dimen/margin_small"
|
||||
android:paddingBottom="@dimen/margin_normal">
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/sort_order">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:title="@string/sort_order">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_order"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_order"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp" />
|
||||
<Spinner
|
||||
android:id="@+id/spinner_order"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/language">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_saved_filters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/saved_filters">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_locale"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_saved_filters"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:popupBackground="@drawable/m3_spinner_popup_background" />
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/language">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_locale"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_original_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/original_language">
|
||||
<Spinner
|
||||
android:id="@+id/spinner_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:popupBackground="@drawable/m3_spinner_popup_background" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_original_locale"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_original_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:popupBackground="@drawable/m3_spinner_popup_background" />
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_original_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/original_language">
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_original_locale"
|
||||
style="?materialCardViewOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small">
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_genres"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:showMoreButton="true"
|
||||
app:title="@string/genres">
|
||||
<Spinner
|
||||
android:id="@+id/spinner_original_locale"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/spinner_height"
|
||||
android:minHeight="?listPreferredItemHeightSmall"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:popupBackground="@drawable/m3_spinner_popup_background" />
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genres"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_genresExclude"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:showMoreButton="true"
|
||||
app:title="@string/genres_exclude">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_genres"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:showMoreButton="true"
|
||||
app:title="@string/genres">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genresExclude"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genres"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_types"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/type">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_genresExclude"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:showMoreButton="true"
|
||||
app:title="@string/genres_exclude">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_types"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_genresExclude"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/state">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_author"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:showMoreButton="false"
|
||||
app:title="@string/author">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_author"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_contentRating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/content_rating">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_types"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/type">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_contentRating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_types"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_demographics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/demographics">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/state">
|
||||
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_demographics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_year"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/year">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_contentRating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/content_rating">
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_year"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stepSize="1"
|
||||
app:labelBehavior="gone"
|
||||
app:tickVisible="true"
|
||||
tools:value="2020"
|
||||
tools:valueFrom="1900"
|
||||
tools:valueTo="2090" />
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_contentRating"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_yearsRange"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/years">
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_demographics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/demographics">
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/slider_yearsRange"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:stepSize="1"
|
||||
app:labelBehavior="gone"
|
||||
app:tickVisible="true"
|
||||
tools:valueFrom="1900"
|
||||
tools:valueTo="2090" />
|
||||
<org.koitharu.kotatsu.core.ui.widgets.ChipsView
|
||||
android:id="@+id/chips_demographics"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="@dimen/margin_small"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:chipStyle="@style/Widget.Kotatsu.Chip.Filter" />
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_year"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/year">
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/slider_year"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stepSize="1"
|
||||
app:labelBehavior="gone"
|
||||
app:tickVisible="true"
|
||||
tools:value="2020"
|
||||
tools:valueFrom="1900"
|
||||
tools:valueTo="2090" />
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
<org.koitharu.kotatsu.filter.ui.FilterFieldLayout
|
||||
android:id="@+id/layout_yearsRange"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
app:title="@string/years">
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/slider_yearsRange"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_normal"
|
||||
android:stepSize="1"
|
||||
app:labelBehavior="gone"
|
||||
app:tickVisible="true"
|
||||
tools:valueFrom="1900"
|
||||
tools:valueTo="2090" />
|
||||
|
||||
</org.koitharu.kotatsu.filter.ui.FilterFieldLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<com.google.android.material.dockedtoolbar.DockedToolbarLayout
|
||||
android:id="@+id/docked_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="false">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/m3_comp_toolbar_docked_container_height"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_save"
|
||||
style="?materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/margin_small"
|
||||
android:layout_weight="1"
|
||||
android:enabled="false"
|
||||
android:text="@string/save"
|
||||
tools:enabled="true" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_done"
|
||||
style="?materialButtonTonalStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/margin_small"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/done" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.dockedtoolbar.DockedToolbarLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
33
app/src/main/res/layout/view_dialog_autocomplete.xml
Normal file
33
app/src/main/res/layout/view_dialog_autocomplete.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/screen_padding"
|
||||
android:paddingTop="@dimen/margin_small">
|
||||
|
||||
<AutoCompleteTextView
|
||||
android:id="@+id/autoCompleteTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@id/dropdown"
|
||||
tools:ignore="LabelFor" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/dropdown"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_gravity="center_vertical|end"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:paddingBottom="2dp"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_expand_more"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</RelativeLayout>
|
||||
12
app/src/main/res/menu/popup_saved_filter.xml
Normal file
12
app/src/main/res/menu/popup_saved_filter.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_rename"
|
||||
android:title="@string/rename" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/delete" />
|
||||
</menu>
|
||||
@@ -667,4 +667,8 @@
|
||||
<string name="error_not_image">無効な形式です: 画像が期待されましたが %s が入力されました</string>
|
||||
<string name="start_download">ダウンロード開始</string>
|
||||
<string name="save_manga_confirm">選択したマンガを保存しますか?通信量とディスク容量を消費する可能性があります</string>
|
||||
<string name="save_manga">マンガを保存</string>
|
||||
<string name="genre">ジャンル</string>
|
||||
<string name="download_added">ダウンロードに追加</string>
|
||||
<string name="chapter_selection_hint">チャプターリストの項目を長押しすると、ダウンロードするチャプターを選択できます。</string>
|
||||
</resources>
|
||||
|
||||
@@ -866,4 +866,5 @@
|
||||
<string name="pull_bottom_no_next">Sonraki bölüm yok</string>
|
||||
<string name="enable_pull_gesture_title">Çekme hareketini etkinleştir</string>
|
||||
<string name="enable_pull_gesture_summary">Webtoon modunda bölüm değiştirmek için çekme hareketi kullan</string>
|
||||
<string name="two_page_scroll_sensitivity">İki Sayfa Kaydırma Hassaslığı</string>
|
||||
</resources>
|
||||
|
||||
@@ -866,4 +866,5 @@
|
||||
<string name="pull_bottom_no_next">Немає наступного розділу</string>
|
||||
<string name="enable_pull_gesture_title">Увімкнути жест потягування</string>
|
||||
<string name="enable_pull_gesture_summary">Використовуйте жест потягування для перемикання розділів у вебтуні</string>
|
||||
<string name="two_page_scroll_sensitivity">Чутливість прокручування на дві сторінки</string>
|
||||
</resources>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,122 +1,149 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/appearance">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/appearance">
|
||||
|
||||
<org.koitharu.kotatsu.settings.utils.ThemeChooserPreference
|
||||
android:key="color_theme"
|
||||
android:title="@string/color_theme" />
|
||||
<org.koitharu.kotatsu.settings.utils.ThemeChooserPreference
|
||||
android:key="color_theme"
|
||||
android:title="@string/color_theme" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="-1"
|
||||
android:entries="@array/themes"
|
||||
android:entryValues="@array/values_theme"
|
||||
android:key="theme"
|
||||
android:title="@string/theme"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<ListPreference
|
||||
android:defaultValue="-1"
|
||||
android:entries="@array/themes"
|
||||
android:entryValues="@array/values_theme"
|
||||
android:key="theme"
|
||||
android:title="@string/theme"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="amoled_theme"
|
||||
android:summary="@string/black_dark_theme_summary"
|
||||
android:title="@string/black_dark_theme" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="amoled_theme"
|
||||
android:summary="@string/black_dark_theme_summary"
|
||||
android:title="@string/black_dark_theme" />
|
||||
|
||||
<org.koitharu.kotatsu.settings.utils.ActivityListPreference
|
||||
android:key="app_locale"
|
||||
android:title="@string/language" />
|
||||
<org.koitharu.kotatsu.settings.utils.ActivityListPreference
|
||||
android:key="app_locale"
|
||||
android:title="@string/language" />
|
||||
|
||||
<PreferenceCategory android:title="@string/manga_list">
|
||||
<PreferenceCategory android:title="@string/manga_list">
|
||||
|
||||
<ListPreference
|
||||
android:entries="@array/list_modes"
|
||||
android:key="list_mode_2"
|
||||
android:title="@string/list_mode"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<ListPreference
|
||||
android:entries="@array/list_modes"
|
||||
android:key="list_mode_2"
|
||||
android:title="@string/list_mode"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<org.koitharu.kotatsu.settings.utils.SliderPreference
|
||||
android:key="grid_size"
|
||||
android:stepSize="5"
|
||||
android:title="@string/grid_size"
|
||||
android:valueFrom="50"
|
||||
android:valueTo="150"
|
||||
app:defaultValue="100" />
|
||||
<org.koitharu.kotatsu.settings.utils.SliderPreference
|
||||
android:key="grid_size"
|
||||
android:stepSize="5"
|
||||
android:title="@string/grid_size"
|
||||
android:valueFrom="50"
|
||||
android:valueTo="150"
|
||||
app:defaultValue="100" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="quick_filter"
|
||||
android:summary="@string/show_quick_filters_summary"
|
||||
android:title="@string/show_quick_filters" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="quick_filter"
|
||||
android:summary="@string/show_quick_filters_summary"
|
||||
android:title="@string/show_quick_filters" />
|
||||
|
||||
<ListPreference
|
||||
android:entries="@array/progress_indicators"
|
||||
android:key="progress_indicators"
|
||||
android:title="@string/show_reading_indicators"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<ListPreference
|
||||
android:entries="@array/progress_indicators"
|
||||
android:key="progress_indicators"
|
||||
android:title="@string/show_reading_indicators"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<MultiSelectListPreference
|
||||
android:defaultValue="@array/values_list_badges"
|
||||
android:entries="@array/list_badges"
|
||||
android:entryValues="@array/values_list_badges"
|
||||
android:key="manga_list_badges"
|
||||
android:title="@string/badges_in_lists" />
|
||||
<MultiSelectListPreference
|
||||
android:defaultValue="@array/values_list_badges"
|
||||
android:entries="@array/list_badges"
|
||||
android:entryValues="@array/values_list_badges"
|
||||
android:key="manga_list_badges"
|
||||
android:title="@string/badges_in_lists" />
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/details">
|
||||
<PreferenceCategory android:title="@string/details">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="description_collapse"
|
||||
android:title="@string/collapse_long_description" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="description_collapse"
|
||||
android:title="@string/collapse_long_description" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="pages_tab"
|
||||
android:summary="@string/show_pages_thumbs_summary"
|
||||
android:title="@string/show_pages_thumbs" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="pages_tab"
|
||||
android:summary="@string/show_pages_thumbs_summary"
|
||||
android:title="@string/show_pages_thumbs" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="-1"
|
||||
android:dependency="pages_tab"
|
||||
android:entries="@array/details_tabs"
|
||||
android:entryValues="@array/details_tabs_values"
|
||||
android:key="details_tab"
|
||||
android:title="@string/default_tab"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<ListPreference
|
||||
android:defaultValue="-1"
|
||||
android:dependency="pages_tab"
|
||||
android:entries="@array/details_tabs"
|
||||
android:entryValues="@array/details_tabs_values"
|
||||
android:key="details_tab"
|
||||
android:title="@string/default_tab"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/main_screen">
|
||||
<PreferenceCategory android:title="@string/main_screen">
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.nav.NavConfigFragment"
|
||||
android:key="nav_main"
|
||||
android:title="@string/main_screen_sections" />
|
||||
<MultiSelectListPreference
|
||||
android:key="search_suggest_types"
|
||||
android:title="@string/search_suggestions" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="main_fab"
|
||||
android:summary="@string/main_screen_fab_summary"
|
||||
android:title="@string/main_screen_fab" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.nav.NavConfigFragment"
|
||||
android:key="nav_main"
|
||||
android:title="@string/main_screen_sections" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="nav_labels"
|
||||
android:title="@string/show_labels_in_navbar" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="main_fab"
|
||||
android:summary="@string/main_screen_fab_summary"
|
||||
android:title="@string/main_screen_fab" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="nav_pinned"
|
||||
android:summary="@string/pin_navigation_ui_summary"
|
||||
android:title="@string/pin_navigation_ui" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="nav_labels"
|
||||
android:title="@string/show_labels_in_navbar" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="exit_confirm"
|
||||
android:summary="@string/exit_confirmation_summary"
|
||||
android:title="@string/exit_confirmation" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="nav_pinned"
|
||||
android:summary="@string/pin_navigation_ui_summary"
|
||||
android:title="@string/pin_navigation_ui" />
|
||||
|
||||
</PreferenceCategory>
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="exit_confirm"
|
||||
android:summary="@string/exit_confirmation_summary"
|
||||
android:title="@string/exit_confirmation" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="dynamic_shortcuts"
|
||||
android:summary="@string/history_shortcuts_summary"
|
||||
android:title="@string/history_shortcuts" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/privacy">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="protect_app"
|
||||
android:persistent="false"
|
||||
android:summary="@string/protect_application_summary"
|
||||
android:title="@string/protect_application" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="allow"
|
||||
android:entries="@array/screenshots_policy"
|
||||
android:key="screenshots_policy"
|
||||
android:title="@string/screenshots_policy"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
25
app/src/main/res/xml/pref_backups.xml
Normal file
25
app/src/main/res/xml/pref_backups.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/backup_restore">
|
||||
|
||||
<Preference
|
||||
android:key="backup"
|
||||
android:persistent="false"
|
||||
android:summary="@string/backup_information"
|
||||
android:title="@string/create_backup" />
|
||||
|
||||
<Preference
|
||||
android:key="restore"
|
||||
android:persistent="false"
|
||||
android:summary="@string/restore_summary"
|
||||
android:title="@string/restore_backup" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.backups.ui.periodical.PeriodicalBackupSettingsFragment"
|
||||
android:key="backup_periodic"
|
||||
android:persistent="false"
|
||||
android:title="@string/periodic_backups" />
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -2,9 +2,7 @@
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/storage_usage">
|
||||
|
||||
<org.koitharu.kotatsu.settings.userdata.storage.StorageUsagePreference android:key="storage_usage" />
|
||||
android:title="@string/data_removal">
|
||||
|
||||
<Preference
|
||||
android:key="search_history_clear"
|
||||
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:title="@string/network">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/network_policy"
|
||||
android:entryValues="@array/values_network_policy"
|
||||
android:key="prefetch_content"
|
||||
android:title="@string/prefetch_content"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
tools:isPreferenceVisible="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="2"
|
||||
android:entries="@array/network_policy"
|
||||
android:entryValues="@array/values_network_policy"
|
||||
android:key="pages_preload"
|
||||
android:title="@string/preload_pages"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.ProxySettingsFragment"
|
||||
android:key="proxy"
|
||||
android:title="@string/proxy"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<ListPreference
|
||||
android:entries="@array/doh_providers"
|
||||
android:key="doh"
|
||||
android:title="@string/dns_over_https"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="adblock"
|
||||
android:summary="@string/adblock_summary"
|
||||
android:title="@string/adblock" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="-1"
|
||||
android:entries="@array/image_proxies"
|
||||
android:entryValues="@array/values_image_proxies"
|
||||
android:key="images_proxy_2"
|
||||
android:title="@string/images_proxy_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="mirror_switching"
|
||||
android:summary="@string/mirror_switching_summary"
|
||||
android:title="@string/mirror_switching" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="ssl_bypass"
|
||||
android:summary="@string/ignore_ssl_errors_summary"
|
||||
android:title="@string/ignore_ssl_errors" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="no_offline"
|
||||
android:summary="@string/disable_connectivity_check_summary"
|
||||
android:title="@string/disable_connectivity_check" />
|
||||
|
||||
</PreferenceScreen>
|
||||
73
app/src/main/res/xml/pref_network_storage.xml
Normal file
73
app/src/main/res/xml/pref_network_storage.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:title="@string/network">
|
||||
|
||||
<PreferenceCategory android:title="@string/storage_usage">
|
||||
|
||||
<org.koitharu.kotatsu.settings.userdata.storage.StorageUsagePreference android:key="storage_usage" />
|
||||
|
||||
<Preference
|
||||
android:fragment="org.koitharu.kotatsu.settings.userdata.storage.DataCleanupSettingsFragment"
|
||||
android:persistent="false"
|
||||
android:title="@string/data_removal" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/network_policy"
|
||||
android:entryValues="@array/values_network_policy"
|
||||
android:key="prefetch_content"
|
||||
android:title="@string/prefetch_content"
|
||||
app:allowDividerAbove="true"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
tools:isPreferenceVisible="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="2"
|
||||
android:entries="@array/network_policy"
|
||||
android:entryValues="@array/values_network_policy"
|
||||
android:key="pages_preload"
|
||||
android:title="@string/preload_pages"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.ProxySettingsFragment"
|
||||
android:key="proxy"
|
||||
android:title="@string/proxy"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<ListPreference
|
||||
android:entries="@array/doh_providers"
|
||||
android:key="doh"
|
||||
android:title="@string/dns_over_https"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="-1"
|
||||
android:entries="@array/image_proxies"
|
||||
android:entryValues="@array/values_image_proxies"
|
||||
android:key="images_proxy_2"
|
||||
android:title="@string/images_proxy_title"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="ssl_bypass"
|
||||
android:summary="@string/ignore_ssl_errors_summary"
|
||||
android:title="@string/ignore_ssl_errors" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="no_offline"
|
||||
android:summary="@string/disable_connectivity_check_summary"
|
||||
android:title="@string/disable_connectivity_check" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="adblock"
|
||||
android:summary="@string/adblock_summary"
|
||||
android:title="@string/adblock" />
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -144,6 +144,12 @@
|
||||
android:key="reader_bar_transparent"
|
||||
android:title="@string/reader_info_bar_transparent" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="reader_chapter_toast"
|
||||
android:summary="@string/reader_chapter_toast_summary"
|
||||
android:title="@string/reader_chapter_toast" />
|
||||
|
||||
<ListPreference
|
||||
android:entries="@array/reader_backgrounds"
|
||||
android:key="reader_background"
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.AppearanceSettingsFragment"
|
||||
android:icon="@drawable/ic_appearance"
|
||||
android:key="appearance"
|
||||
android:title="@string/appearance" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.AppearanceSettingsFragment"
|
||||
android:icon="@drawable/ic_appearance"
|
||||
android:key="appearance"
|
||||
android:title="@string/appearance" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment"
|
||||
android:icon="@drawable/ic_manga_source"
|
||||
android:key="remote_sources"
|
||||
android:title="@string/remote_sources" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.sources.SourcesSettingsFragment"
|
||||
android:icon="@drawable/ic_manga_source"
|
||||
android:key="remote_sources"
|
||||
android:title="@string/remote_sources" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.ReaderSettingsFragment"
|
||||
android:icon="@drawable/ic_book_page"
|
||||
android:key="reader"
|
||||
android:title="@string/reader_settings" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.ReaderSettingsFragment"
|
||||
android:icon="@drawable/ic_book_page"
|
||||
android:key="reader"
|
||||
android:title="@string/reader_settings" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.NetworkSettingsFragment"
|
||||
android:icon="@drawable/ic_web"
|
||||
android:key="network"
|
||||
android:title="@string/network" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.StorageAndNetworkSettingsFragment"
|
||||
android:icon="@drawable/ic_usage"
|
||||
android:key="network"
|
||||
android:title="@string/storage_and_network" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.userdata.UserDataSettingsFragment"
|
||||
android:icon="@drawable/ic_data_privacy"
|
||||
android:key="userdata"
|
||||
android:title="@string/data_and_privacy" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.DownloadsSettingsFragment"
|
||||
android:icon="@drawable/ic_download"
|
||||
android:key="downloads"
|
||||
android:title="@string/downloads" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.DownloadsSettingsFragment"
|
||||
android:icon="@drawable/ic_download"
|
||||
android:key="downloads"
|
||||
android:title="@string/downloads" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment"
|
||||
android:icon="@drawable/ic_feed"
|
||||
android:key="tracker"
|
||||
android:title="@string/check_for_new_chapters" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.tracker.TrackerSettingsFragment"
|
||||
android:icon="@drawable/ic_feed"
|
||||
android:key="tracker"
|
||||
android:title="@string/check_for_new_chapters" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.ServicesSettingsFragment"
|
||||
android:icon="@drawable/ic_services"
|
||||
android:key="services"
|
||||
android:title="@string/services" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.ServicesSettingsFragment"
|
||||
android:icon="@drawable/ic_services"
|
||||
android:key="services"
|
||||
android:title="@string/services" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.userdata.BackupsSettingsFragment"
|
||||
android:icon="@drawable/ic_backup_restore"
|
||||
android:key="userdata"
|
||||
android:title="@string/backup_restore" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.about.AboutSettingsFragment"
|
||||
android:icon="@drawable/ic_info_outline"
|
||||
android:key="about"
|
||||
android:title="@string/about" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.about.AboutSettingsFragment"
|
||||
android:icon="@drawable/ic_info_outline"
|
||||
android:key="about"
|
||||
android:title="@string/about" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
@@ -1,54 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/remote_sources">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/remote_sources">
|
||||
|
||||
<ListPreference
|
||||
android:key="sources_sort_order"
|
||||
android:title="@string/sort_order"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
<ListPreference
|
||||
android:key="sources_sort_order"
|
||||
android:title="@string/sort_order"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="sources_grid"
|
||||
android:title="@string/show_in_grid_view" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="sources_grid"
|
||||
android:title="@string/show_in_grid_view" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.sources.manage.SourcesManageFragment"
|
||||
android:key="remote_sources"
|
||||
android:persistent="false"
|
||||
android:title="@string/manage_sources" />
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.sources.manage.SourcesManageFragment"
|
||||
android:key="remote_sources"
|
||||
android:persistent="false"
|
||||
android:title="@string/manage_sources" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="sources_enabled_all"
|
||||
android:summary="@string/enable_all_sources_summary"
|
||||
android:title="@string/enable_all_sources"
|
||||
app:allowDividerAbove="true" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="sources_enabled_all"
|
||||
android:summary="@string/enable_all_sources_summary"
|
||||
android:title="@string/enable_all_sources"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<Preference
|
||||
android:key="sources_catalog"
|
||||
android:persistent="false"
|
||||
android:title="@string/sources_catalog" />
|
||||
<Preference
|
||||
android:key="sources_catalog"
|
||||
android:persistent="false"
|
||||
android:title="@string/sources_catalog" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="no_nsfw"
|
||||
android:summary="@string/disable_nsfw_summary"
|
||||
android:title="@string/disable_nsfw" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="no_nsfw"
|
||||
android:summary="@string/disable_nsfw_summary"
|
||||
android:title="@string/disable_nsfw" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="tags_warnings"
|
||||
android:summary="@string/tags_warnings_summary"
|
||||
android:title="@string/tags_warnings" />
|
||||
<ListPreference
|
||||
android:entries="@array/incognito_nsfw_options"
|
||||
android:key="incognito_nsfw"
|
||||
android:title="@string/incognito_for_nsfw"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="handle_links"
|
||||
android:persistent="false"
|
||||
android:summary="@string/handle_links_summary"
|
||||
android:title="@string/handle_links"
|
||||
app:allowDividerAbove="true" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="tags_warnings"
|
||||
android:summary="@string/tags_warnings_summary"
|
||||
android:title="@string/tags_warnings" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="mirror_switching"
|
||||
android:summary="@string/mirror_switching_summary"
|
||||
android:title="@string/mirror_switching"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="handle_links"
|
||||
android:persistent="false"
|
||||
android:summary="@string/handle_links_summary"
|
||||
android:title="@string/handle_links" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:title="@string/data_and_privacy">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:key="protect_app"
|
||||
android:persistent="false"
|
||||
android:summary="@string/protect_application_summary"
|
||||
android:title="@string/protect_application" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="allow"
|
||||
android:entries="@array/screenshots_policy"
|
||||
android:key="screenshots_policy"
|
||||
android:title="@string/screenshots_policy"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:entries="@array/incognito_nsfw_options"
|
||||
android:key="incognito_nsfw"
|
||||
android:title="@string/incognito_for_nsfw"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="dynamic_shortcuts"
|
||||
android:summary="@string/history_shortcuts_summary"
|
||||
android:title="@string/history_shortcuts" />
|
||||
|
||||
<MultiSelectListPreference
|
||||
android:key="search_suggest_types"
|
||||
android:title="@string/search_suggestions" />
|
||||
|
||||
<PreferenceCategory android:title="@string/backup_restore">
|
||||
|
||||
<Preference
|
||||
android:key="backup"
|
||||
android:persistent="false"
|
||||
android:summary="@string/backup_information"
|
||||
android:title="@string/create_backup" />
|
||||
|
||||
<Preference
|
||||
android:key="restore"
|
||||
android:persistent="false"
|
||||
android:summary="@string/restore_summary"
|
||||
android:title="@string/restore_backup" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.backups.ui.periodical.PeriodicalBackupSettingsFragment"
|
||||
android:key="backup_periodic"
|
||||
android:persistent="false"
|
||||
android:title="@string/periodic_backups" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.settings.userdata.storage.StorageManageSettingsFragment"
|
||||
android:key="storage_usage"
|
||||
android:title="@string/storage_usage"
|
||||
app:allowDividerAbove="true" />
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -35,7 +35,7 @@ material = "1.14.0-alpha05"
|
||||
moshi = "1.15.2"
|
||||
okhttp = "5.2.1"
|
||||
okio = "3.16.1"
|
||||
parsers = "8908031eee"
|
||||
parsers = "df1cab3f9d"
|
||||
preference = "1.2.1"
|
||||
recyclerview = "1.4.0"
|
||||
room = "2.7.2"
|
||||
|
||||
Reference in New Issue
Block a user