Compare commits
21 Commits
861c21faea
...
d0ed1fb85f
| Author | SHA1 | Date |
|---|---|---|
|
|
d0ed1fb85f | 7 months ago |
|
|
9e5664da3a | 7 months ago |
|
|
35c158d35a | 7 months ago |
|
|
464f24e9f0 | 7 months ago |
|
|
c8a8203c39 | 7 months ago |
|
|
b414758f32 | 7 months ago |
|
|
1181860e41 | 7 months ago |
|
|
e35521f16f | 7 months ago |
|
|
5fb8ff53f9 | 7 months ago |
|
|
a66283d035 | 7 months ago |
|
|
a1ba0b8c21 | 7 months ago |
|
|
f3b42b9a42 | 7 months ago |
|
|
aa2f2c17fc | 7 months ago |
|
|
ebc17b645b | 7 months ago |
|
|
cc14e1abcf | 7 months ago |
|
|
b1b474e2e7 | 7 months ago |
|
|
8ca3bece5d | 7 months ago |
|
|
90bd9023d5 | 7 months ago |
|
|
986627f24d | 7 months ago |
|
|
cf2b8e2481 | 7 months ago |
|
|
b9435de5cd | 7 months ago |
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -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())
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
@ -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>
|
|
||||||
@ -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>
|
||||||
@ -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>
|
|
||||||
Loading…
Reference in New Issue