Fixes and refactor

pull/26/head
Koitharu 5 years ago
parent a242aa6633
commit 8f8d85d172

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" /> <bytecodeTargetLevel target="11" />
</component> </component>
</project> </project>

@ -4,7 +4,7 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="testRunner" value="PLATFORM" /> <option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="1.8" /> <option name="gradleJvm" value="1.8" />
@ -15,7 +15,6 @@
</set> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

@ -2,6 +2,8 @@
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="BooleanLiteralArgument" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="BooleanLiteralArgument" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="KeySetIterationMayUseEntrySet" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" /> <inspection_tool class="TrailingComma" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="ZeroLengthArrayInitialization" enabled="true" level="WARNING" enabled_by_default="true" />
</profile> </profile>
</component> </component>

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

@ -66,7 +66,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation 'androidx.core:core-ktx:1.5.0-alpha05' implementation 'androidx.core:core-ktx:1.5.0-beta01'
implementation 'androidx.activity:activity-ktx:1.2.0-rc01' implementation 'androidx.activity:activity-ktx:1.2.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.3.0-rc01' implementation 'androidx.fragment:fragment-ktx:1.3.0-rc01'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-rc01' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-rc01'
@ -79,8 +79,8 @@ dependencies {
implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01' implementation 'androidx.recyclerview:recyclerview:1.2.0-beta01'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.5.0-beta02' implementation 'androidx.work:work-runtime-ktx:2.5.0-rc01'
implementation 'com.google.android.material:material:1.3.0-beta01' implementation 'com.google.android.material:material:1.3.0-rc01'
//noinspection LifecycleAnnotationProcessorWithJava8 //noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0-rc01' kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0-rc01'
@ -89,7 +89,7 @@ dependencies {
kapt 'androidx.room:room-compiler:2.2.6' kapt 'androidx.room:room-compiler:2.2.6'
implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okio:okio:2.9.0' implementation 'com.squareup.okio:okio:2.10.0'
implementation 'org.jsoup:jsoup:1.13.1' implementation 'org.jsoup:jsoup:1.13.1'
implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0' implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.3.0'
@ -97,7 +97,7 @@ dependencies {
implementation 'org.koin:koin-android:2.2.2' implementation 'org.koin:koin-android:2.2.2'
implementation 'org.koin:koin-androidx-viewmodel:2.2.2' implementation 'org.koin:koin-androidx-viewmodel:2.2.2'
implementation 'io.coil-kt:coil-base:1.1.0' implementation 'io.coil-kt:coil-base:1.1.1'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0' implementation 'com.tomclaw.cache:cache:1.0'

@ -9,7 +9,6 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.koitharu.kotatsu.utils.ext.safe
class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), KoinComponent { class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), KoinComponent {
@ -45,7 +44,7 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), Ko
return request?.url?.toString()?.let(::doRequest) return request?.url?.toString()?.let(::doRequest)
} }
private fun doRequest(url: String): WebResourceResponse? = safe { private fun doRequest(url: String): WebResourceResponse? = runCatching {
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.build() .build()
@ -56,5 +55,5 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), Ko
ct?.charset()?.name() ?: "utf-8", ct?.charset()?.name() ?: "utf-8",
response.body?.byteStream() response.body?.byteStream()
) )
} }.getOrNull()
} }

@ -4,7 +4,9 @@ import okhttp3.CookieJar
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.network.cookies.ClearableCookieJar
import org.koitharu.kotatsu.core.network.cookies.PersistentCookieJar import org.koitharu.kotatsu.core.network.cookies.PersistentCookieJar
import org.koitharu.kotatsu.core.network.cookies.cache.SetCookieCache import org.koitharu.kotatsu.core.network.cookies.cache.SetCookieCache
import org.koitharu.kotatsu.core.network.cookies.persistence.SharedPrefsCookiePersistor import org.koitharu.kotatsu.core.network.cookies.persistence.SharedPrefsCookiePersistor
@ -18,7 +20,7 @@ val networkModule
SetCookieCache(), SetCookieCache(),
SharedPrefsCookiePersistor(androidContext()) SharedPrefsCookiePersistor(androidContext())
) )
} } bind ClearableCookieJar::class
single(named(CacheUtils.QUALIFIER_HTTP)) { CacheUtils.createHttpCache(androidContext()) } single(named(CacheUtils.QUALIFIER_HTTP)) { CacheUtils.createHttpCache(androidContext()) }
single { single {
OkHttpClient.Builder().apply { OkHttpClient.Builder().apply {

@ -2,16 +2,22 @@ package org.koitharu.kotatsu.core.network
import android.os.Build import android.os.Build
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import java.util.* import java.util.*
class UserAgentInterceptor : Interceptor { class UserAgentInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain) = chain.proceed( override fun intercept(chain: Interceptor.Chain): Response {
chain.request().newBuilder() val request = chain.request()
return chain.proceed(
if (request.header(HEADER_USER_AGENT) == null) {
request.newBuilder()
.header(HEADER_USER_AGENT, userAgent) .header(HEADER_USER_AGENT, userAgent)
.build() .build()
} else request
) )
}
companion object { companion object {

@ -35,7 +35,7 @@ class PersistentCookieJar(
cache.addAll(persistor.loadAll()) cache.addAll(persistor.loadAll())
} }
cache.addAll(cookies) cache.addAll(cookies)
persistor.saveAll(filterPersistentCookies(cookies)) persistor.saveAll(cookies.filter { it.persistent })
} }
@Synchronized @Synchronized
@ -48,13 +48,16 @@ class PersistentCookieJar(
val it = cache.iterator() val it = cache.iterator()
while (it.hasNext()) { while (it.hasNext()) {
val currentCookie = it.next() val currentCookie = it.next()
if (isCookieExpired(currentCookie)) { when {
currentCookie.isExpired() -> {
cookiesToRemove.add(currentCookie) cookiesToRemove.add(currentCookie)
it.remove() it.remove()
} else if (currentCookie.matches(url)) { }
currentCookie.matches(url) -> {
validCookies.add(currentCookie) validCookies.add(currentCookie)
} }
} }
}
persistor.removeAll(cookiesToRemove) persistor.removeAll(cookiesToRemove)
return validCookies return validCookies
} }
@ -73,18 +76,8 @@ class PersistentCookieJar(
private companion object { private companion object {
fun filterPersistentCookies(cookies: List<Cookie>): List<Cookie> { fun Cookie.isExpired(): Boolean {
val persistentCookies: MutableList<Cookie> = ArrayList() return expiresAt < System.currentTimeMillis()
for (cookie in cookies) {
if (cookie.persistent) {
persistentCookies.add(cookie)
}
}
return persistentCookies
}
fun isCookieExpired(cookie: Cookie): Boolean {
return cookie.expiresAt < System.currentTimeMillis()
} }
} }
} }

@ -19,7 +19,7 @@ val parserModule
factory<MangaRepository>(named(MangaSource.HENCHAN)) { HenChanRepository(get()) } factory<MangaRepository>(named(MangaSource.HENCHAN)) { HenChanRepository(get()) }
factory<MangaRepository>(named(MangaSource.YAOICHAN)) { YaoiChanRepository(get()) } factory<MangaRepository>(named(MangaSource.YAOICHAN)) { YaoiChanRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGATOWN)) { MangaTownRepository(get()) } factory<MangaRepository>(named(MangaSource.MANGATOWN)) { MangaTownRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGALIB)) { MangaLibRepository(get()) } single<MangaRepository>(named(MangaSource.MANGALIB)) { MangaLibRepository(get()) }
factory<MangaRepository>(named(MangaSource.NUDEMOON)) { NudeMoonRepository(get()) } factory<MangaRepository>(named(MangaSource.NUDEMOON)) { NudeMoonRepository(get()) }
factory<MangaRepository>(named(MangaSource.MANGAREAD)) { MangareadRepository(get()) } factory<MangaRepository>(named(MangaSource.MANGAREAD)) { MangareadRepository(get()) }
} }

@ -56,7 +56,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
).firstOrNull()?.text(), ).firstOrNull()?.text(),
coverUrl = row.selectFirst("div.manga_images")?.selectFirst("img") coverUrl = row.selectFirst("div.manga_images")?.selectFirst("img")
?.attr("src")?.withDomain(domain).orEmpty(), ?.attr("src")?.withDomain(domain).orEmpty(),
tags = safe { tags = runCatching {
row.selectFirst("div.genre")?.select("a")?.mapToSet { row.selectFirst("div.genre")?.select("a")?.mapToSet {
MangaTag( MangaTag(
title = it.text(), title = it.text(),
@ -64,7 +64,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
source = source source = source
) )
} }
}.orEmpty(), }.getOrNull().orEmpty(),
source = source source = source
) )
} }

@ -1,7 +1,7 @@
package org.koitharu.kotatsu.core.parser.site package org.koitharu.kotatsu.core.parser.site
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import androidx.core.net.toUri import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koitharu.kotatsu.base.domain.MangaLoaderContext import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
@ -53,7 +53,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
}.parseHtml() }.parseHtml()
val root = doc.body().getElementById("mangaBox") val root = doc.body().getElementById("mangaBox")
?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root") ?.selectFirst("div.tiles.row") ?: throw ParseException("Cannot find root")
val baseHost = root.baseUri().toUri().host val baseHost = root.baseUri().toHttpUrl().host
return root.select("div.tile").mapNotNull { node -> return root.select("div.tile").mapNotNull { node ->
val imgDiv = node.selectFirst("div.img") ?: return@mapNotNull null val imgDiv = node.selectFirst("div.img") ?: return@mapNotNull null
val descDiv = node.selectFirst("div.desc") ?: return@mapNotNull null val descDiv = node.selectFirst("div.desc") ?: return@mapNotNull null
@ -61,7 +61,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
return@mapNotNull null //skip author return@mapNotNull null //skip author
} }
val href = imgDiv.selectFirst("a").attr("href")?.inContextOf(node) val href = imgDiv.selectFirst("a").attr("href")?.inContextOf(node)
if (href == null || href.toUri().host != baseHost) { if (href == null || href.toHttpUrl().host != baseHost) {
return@mapNotNull null // skip external links return@mapNotNull null // skip external links
} }
val title = descDiv.selectFirst("h3")?.selectFirst("a")?.text() val title = descDiv.selectFirst("h3")?.selectFirst("a")?.text()
@ -73,15 +73,15 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
title = title, title = title,
altTitle = descDiv.selectFirst("h4")?.text(), altTitle = descDiv.selectFirst("h4")?.text(),
coverUrl = imgDiv.selectFirst("img.lazy")?.attr("data-original").orEmpty(), coverUrl = imgDiv.selectFirst("img.lazy")?.attr("data-original").orEmpty(),
rating = safe { rating = runCatching {
node.selectFirst("div.rating") node.selectFirst("div.rating")
?.attr("title") ?.attr("title")
?.substringBefore(' ') ?.substringBefore(' ')
?.toFloatOrNull() ?.toFloatOrNull()
?.div(10f) ?.div(10f)
} ?: Manga.NO_RATING, }.getOrNull() ?: Manga.NO_RATING,
author = tileInfo?.selectFirst("a.person-link")?.text(), author = tileInfo?.selectFirst("a.person-link")?.text(),
tags = safe { tags = runCatching {
tileInfo?.select("a.element-link") tileInfo?.select("a.element-link")
?.mapToSet { ?.mapToSet {
MangaTag( MangaTag(
@ -90,7 +90,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
source = source source = source
) )
} }
}.orEmpty(), }.getOrNull().orEmpty(),
state = when { state = when {
node.selectFirst("div.tags") node.selectFirst("div.tags")
?.selectFirst("span.mangaCompleted") != null -> MangaState.FINISHED ?.selectFirst("span.mangaCompleted") != null -> MangaState.FINISHED

@ -80,14 +80,14 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
val title = root.selectFirst("div.media-header__wrap")?.children() val title = root.selectFirst("div.media-header__wrap")?.children()
val info = root.selectFirst("div.media-content") val info = root.selectFirst("div.media-content")
val chaptersDoc = loaderContext.httpGet(manga.url + "?section=chapters").parseHtml() val chaptersDoc = loaderContext.httpGet(manga.url + "?section=chapters").parseHtml()
val scripts = chaptersDoc.body().select("script") val scripts = chaptersDoc.select("script")
var chapters: ArrayList<MangaChapter>? = null var chapters: ArrayList<MangaChapter>? = null
scripts@ for (script in scripts) { scripts@ for (script in scripts) {
val raw = script.html().lines() val raw = script.html().lines()
for (line in raw) { for (line in raw) {
if (line.startsWith("window.__CHAPTERS_DATA__")) { if (line.startsWith("window.__DATA__")) {
val json = JSONObject(line.substringAfter('=').substringBeforeLast(';')) val json = JSONObject(line.substringAfter('=').substringBeforeLast(';'))
val list = json.getJSONArray("list") val list = json.getJSONObject("chapters").getJSONArray("list")
val total = list.length() val total = list.length()
chapters = ArrayList(total) chapters = ArrayList(total)
for (i in 0 until total) { for (i in 0 until total) {
@ -99,7 +99,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
append("/c") append("/c")
append(item.getString("chapter_number")) append(item.getString("chapter_number"))
append('/') append('/')
append(item.getJSONArray("teams").getJSONObject(0).optString("slug")) append(item.optString("chapter_string"))
} }
var name = item.getString("chapter_name") var name = item.getString("chapter_name")
if (name.isNullOrBlank() || name == "null") { if (name.isNullOrBlank() || name == "null") {

@ -149,6 +149,7 @@ class AppSettings private constructor(private val prefs: SharedPreferences) :
const val KEY_SOURCES_HIDDEN = "sources_hidden" const val KEY_SOURCES_HIDDEN = "sources_hidden"
const val KEY_TRAFFIC_WARNING = "traffic_warning" const val KEY_TRAFFIC_WARNING = "traffic_warning"
const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear" const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear"
const val KEY_COOKIES_CLEAR = "cookies_clear"
const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear" const val KEY_THUMBS_CACHE_CLEAR = "thumbs_cache_clear"
const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear" const val KEY_SEARCH_HISTORY_CLEAR = "search_history_clear"
const val KEY_UPDATES_FEED_CLEAR = "updates_feed_clear" const val KEY_UPDATES_FEED_CLEAR = "updates_feed_clear"

@ -17,7 +17,6 @@ import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.safe
import java.io.IOException import java.io.IOException
class DetailsViewModel( class DetailsViewModel(
@ -93,7 +92,7 @@ class DetailsViewModel(
launchLoadingJob(Dispatchers.Default) { launchLoadingJob(Dispatchers.Default) {
val original = localMangaRepository.getRemoteManga(manga) val original = localMangaRepository.getRemoteManga(manga)
localMangaRepository.delete(manga) || throw IOException("Unable to delete file") localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
safe { runCatching {
historyRepository.deleteOrSwap(manga, original) historyRepository.deleteOrSwap(manga, original)
} }
onMangaRemoved.postCall(manga) onMangaRemoved.postCall(manga)

@ -92,13 +92,13 @@ class DownloadService : BaseService() {
var output: MangaZip? = null var output: MangaZip? = null
try { try {
val repo = manga.source.repository val repo = manga.source.repository
val cover = safe { val cover = runCatching {
imageLoader.execute( imageLoader.execute(
ImageRequest.Builder(this@DownloadService) ImageRequest.Builder(this@DownloadService)
.data(manga.coverUrl) .data(manga.coverUrl)
.build() .build()
).drawable ).drawable
} }.getOrNull()
notification.setLargeIcon(cover) notification.setLargeIcon(cover)
notification.update() notification.update()
val data = if (manga.chapters == null) repo.getDetails(manga) else manga val data = if (manga.chapters == null) repo.getDetails(manga) else manga

@ -9,7 +9,6 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.utils.ext.getStringOrNull import org.koitharu.kotatsu.utils.ext.getStringOrNull
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.utils.ext.safe
class MangaIndex(source: String?) { class MangaIndex(source: String?) {
@ -40,7 +39,7 @@ class MangaIndex(source: String?) {
json.put("app_version", BuildConfig.VERSION_CODE) json.put("app_version", BuildConfig.VERSION_CODE)
} }
fun getMangaInfo(): Manga? = if (json.length() == 0) null else safe { fun getMangaInfo(): Manga? = if (json.length() == 0) null else runCatching {
val source = MangaSource.valueOf(json.getString("source")) val source = MangaSource.valueOf(json.getString("source"))
Manga( Manga(
id = json.getLong("id"), id = json.getLong("id"),
@ -60,7 +59,7 @@ class MangaIndex(source: String?) {
}, },
chapters = getChapters(json.getJSONObject("chapters"), source) chapters = getChapters(json.getJSONObject("chapters"), source)
) )
} }.getOrNull()
fun getCoverEntry(): String? = json.optString("cover_entry") fun getCoverEntry(): String? = json.optString("cover_entry")

@ -15,7 +15,6 @@ import org.koitharu.kotatsu.local.data.MangaZip
import org.koitharu.kotatsu.utils.AlphanumComparator import org.koitharu.kotatsu.utils.AlphanumComparator
import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.readText
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub import org.koitharu.kotatsu.utils.ext.sub
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -37,7 +36,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
} }
val files = getAvailableStorageDirs(context) val files = getAvailableStorageDirs(context)
.flatMap { x -> x.listFiles(filenameFilter)?.toList().orEmpty() } .flatMap { x -> x.listFiles(filenameFilter)?.toList().orEmpty() }
return files.mapNotNull { x -> safe { getFromFile(x) } } return files.mapNotNull { x -> runCatching { getFromFile(x) }.getOrNull() }
} }
override suspend fun getDetails(manga: Manga) = if (manga.chapters == null) { override suspend fun getDetails(manga: Manga) = if (manga.chapters == null) {
@ -128,9 +127,9 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
} }
fun getRemoteManga(localManga: Manga): Manga? { fun getRemoteManga(localManga: Manga): Manga? {
val file = safe { val file = runCatching {
Uri.parse(localManga.url).toFile() Uri.parse(localManga.url).toFile()
} ?: return null }.getOrNull() ?: return null
val zip = ZipFile(file) val zip = ZipFile(file)
val entry = zip.getEntry(MangaZip.INDEX_ENTRY) val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return null val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return null

@ -22,7 +22,6 @@ import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.MediaStoreCompat import org.koitharu.kotatsu.utils.MediaStoreCompat
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub import org.koitharu.kotatsu.utils.ext.sub
import java.io.IOException import java.io.IOException
@ -49,7 +48,10 @@ class LocalListViewModel(
list.isEmpty() -> listOf(EmptyState(R.string.text_local_holder)) list.isEmpty() -> listOf(EmptyState(R.string.text_local_holder))
else -> list.toUi(mode) else -> list.toUi(mode)
} }
}.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState)) }.asLiveDataDistinct(
viewModelScope.coroutineContext + Dispatchers.Default,
listOf(LoadingState)
)
init { init {
onRefresh() onRefresh()
@ -94,7 +96,7 @@ class LocalListViewModel(
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val original = repository.getRemoteManga(manga) val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file") repository.delete(manga) || throw IOException("Unable to delete file")
safe { runCatching {
historyRepository.deleteOrSwap(manga, original) historyRepository.deleteOrSwap(manga, original)
} }
} }

@ -1,4 +1,4 @@
package org.koitharu.kotatsu.reader.ui package org.koitharu.kotatsu.reader.domain
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory

@ -5,7 +5,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
abstract class BasePageHolder<B : ViewBinding>( abstract class BasePageHolder<B : ViewBinding>(
protected val binding: B, protected val binding: B,

@ -8,7 +8,7 @@ import androidx.viewbinding.ViewBinding
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.base.ui.BaseFragment import org.koitharu.kotatsu.base.ui.BaseFragment
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.ReaderState import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.reader.ui.ReaderViewModel import org.koitharu.kotatsu.reader.ui.ReaderViewModel

@ -6,7 +6,7 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.utils.ext.resetTransformations import org.koitharu.kotatsu.utils.ext.resetTransformations
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine

@ -9,7 +9,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.utils.ext.launchAfter import org.koitharu.kotatsu.utils.ext.launchAfter
import org.koitharu.kotatsu.utils.ext.launchInstead import org.koitharu.kotatsu.utils.ext.launchInstead
import java.io.File import java.io.File

@ -6,7 +6,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder import org.koitharu.kotatsu.reader.ui.pager.standard.PageHolder
class ReversedPageHolder( class ReversedPageHolder(

@ -5,7 +5,7 @@ import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class ReversedPagesAdapter( class ReversedPagesAdapter(

@ -12,7 +12,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage

@ -5,7 +5,7 @@ import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageBinding import org.koitharu.kotatsu.databinding.ItemPageBinding
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class PagesAdapter( class PagesAdapter(

@ -5,7 +5,7 @@ import android.view.ViewGroup
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter import org.koitharu.kotatsu.reader.ui.pager.BaseReaderAdapter
class WebtoonAdapter( class WebtoonAdapter(

@ -11,7 +11,7 @@ import org.koitharu.kotatsu.core.exceptions.resolve.ResolvableException
import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding import org.koitharu.kotatsu.databinding.ItemPageWebtoonBinding
import org.koitharu.kotatsu.reader.ui.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder import org.koitharu.kotatsu.reader.ui.pager.BasePageHolder
import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import org.koitharu.kotatsu.reader.ui.pager.ReaderPage
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage

@ -7,7 +7,6 @@ import android.view.MenuItem
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity import org.koitharu.kotatsu.search.ui.global.GlobalSearchActivity
import org.koitharu.kotatsu.utils.ext.safe
import java.io.Closeable import java.io.Closeable
object SearchHelper { object SearchHelper {
@ -43,10 +42,10 @@ object SearchHelper {
override fun onSuggestionSelect(position: Int) = false override fun onSuggestionSelect(position: Int) = false
override fun onSuggestionClick(position: Int): Boolean { override fun onSuggestionClick(position: Int): Boolean {
val query = safe { val query = runCatching {
val c = view.suggestionsAdapter.getItem(position) as? Cursor val c = view.suggestionsAdapter.getItem(position) as? Cursor
c?.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY)) c?.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_QUERY))
} ?: return false }.getOrNull() ?: return false
view.setQuery(query, true) view.setQuery(query, true)
return true return true
} }

@ -7,9 +7,11 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.network.cookies.ClearableCookieJar
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.Cache import org.koitharu.kotatsu.local.data.Cache
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
@ -71,6 +73,20 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
clearCache(preference, Cache.THUMBS) clearCache(preference, Cache.THUMBS)
true true
} }
AppSettings.KEY_COOKIES_CLEAR -> {
viewLifecycleScope.launch {
val cookieJar = get<ClearableCookieJar>()
withContext(Dispatchers.IO) {
cookieJar.clear()
}
Snackbar.make(
listView ?: return@launch,
R.string.cookies_cleared,
Snackbar.LENGTH_SHORT
).show()
}
true
}
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> { AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
viewLifecycleScope.launch { viewLifecycleScope.launch {
MangaSuggestionsProvider.clearHistory(preference.context) MangaSuggestionsProvider.clearHistory(preference.context)

@ -9,18 +9,18 @@ import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.mapToSet import org.koitharu.kotatsu.utils.ext.mapToSet
import org.koitharu.kotatsu.utils.ext.safe
class SourcesAdapter( class SourcesAdapter(
private val settings: AppSettings, private val settings: AppSettings,
private val onItemClickListener: OnListItemClickListener<MangaSource>, private val onItemClickListener: OnListItemClickListener<MangaSource>,
) : RecyclerView.Adapter<SourceViewHolder>() { ) : RecyclerView.Adapter<SourceViewHolder>() {
private val dataSet = MangaProviderFactory.getSources(settings, includeHidden = true).toMutableList() private val dataSet =
MangaProviderFactory.getSources(settings, includeHidden = true).toMutableList()
private val hiddenItems = settings.hiddenSources.mapNotNull { private val hiddenItems = settings.hiddenSources.mapNotNull {
safe { runCatching {
MangaSource.valueOf(it) MangaSource.valueOf(it)
} }.getOrNull()
}.toMutableSet() }.toMutableSet()
override fun onCreateViewHolder( override fun onCreateViewHolder(

@ -21,7 +21,6 @@ import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.toBitmapOrNull
import org.koitharu.kotatsu.utils.ext.toUriOrNull import org.koitharu.kotatsu.utils.ext.toUriOrNull
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -37,23 +36,23 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
private val repository by inject<TrackingRepository>() private val repository by inject<TrackingRepository>()
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
override suspend fun doWork(): Result = withContext(Dispatchers.Default) { override suspend fun doWork(): Result {
val trackSources = settings.trackSources val trackSources = settings.trackSources
if (trackSources.isEmpty()) { if (trackSources.isEmpty()) {
return@withContext Result.success() return Result.success()
} }
val tracks = repository.getAllTracks( val tracks = repository.getAllTracks(
useFavourites = AppSettings.TRACK_FAVOURITES in trackSources, useFavourites = AppSettings.TRACK_FAVOURITES in trackSources,
useHistory = AppSettings.TRACK_HISTORY in trackSources useHistory = AppSettings.TRACK_HISTORY in trackSources
) )
if (tracks.isEmpty()) { if (tracks.isEmpty()) {
return@withContext Result.success() return Result.success()
} }
var success = 0 var success = 0
for (track in tracks) { for (track in tracks) {
val details = safe { val details = runCatching {
track.manga.source.repository.getDetails(track.manga) track.manga.source.repository.getDetails(track.manga)
} }.getOrNull()
val chapters = details?.chapters ?: continue val chapters = details?.chapters ?: continue
when { when {
track.knownChaptersCount == -1 -> { //first check track.knownChaptersCount == -1 -> { //first check
@ -125,7 +124,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
success++ success++
} }
repository.cleanup() repository.cleanup()
if (success == 0) { return if (success == 0) {
Result.retry() Result.retry()
} else { } else {
Result.success() Result.success()

@ -3,7 +3,6 @@ package org.koitharu.kotatsu.utils.ext
import android.content.res.Resources import android.content.res.Resources
import android.util.Log import android.util.Log
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
@ -12,15 +11,6 @@ import org.koitharu.kotatsu.core.exceptions.WrongPasswordException
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
inline fun <T, R> T.safe(action: T.() -> R?) = try {
this.action()
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
null
}
suspend inline fun <T, R> T.retryUntilSuccess(maxAttempts: Int, action: T.() -> R): R { suspend inline fun <T, R> T.retryUntilSuccess(maxAttempts: Int, action: T.() -> R): R {
var attempts = maxAttempts var attempts = maxAttempts
while (true) { while (true) {

@ -35,11 +35,11 @@ inline fun File.findParent(predicate: (File) -> Boolean): File? {
return current return current
} }
fun File.getStorageName(context: Context): String = safe { fun File.getStorageName(context: Context): String = runCatching {
val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager val manager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
manager.getStorageVolume(this)?.getDescription(context)?.let { manager.getStorageVolume(this)?.getDescription(context)?.let {
return@safe it return@runCatching it
} }
} }
when { when {
@ -47,6 +47,6 @@ fun File.getStorageName(context: Context): String = safe {
Environment.isExternalStorageRemovable(this) -> context.getString(R.string.external_storage) Environment.isExternalStorageRemovable(this) -> context.getString(R.string.external_storage)
else -> null else -> null
} }
} ?: context.getString(R.string.other_storage) }.getOrNull() ?: context.getString(R.string.other_storage)
fun Uri.toFileOrNull() = if (scheme == "file") path?.let(::File) else null fun Uri.toFileOrNull() = if (scheme == "file") path?.let(::File) else null

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_list_mode"
android:orderInCategory="20"
android:title="@string/list_mode"
app:showAsAction="never" />
</menu>

@ -190,4 +190,6 @@
<string name="silent">Без звука</string> <string name="silent">Без звука</string>
<string name="captcha_required">Необходимо пройти CAPTCHA</string> <string name="captcha_required">Необходимо пройти CAPTCHA</string>
<string name="resolve">Resolve</string> <string name="resolve">Resolve</string>
<string name="clear_cookies">Очистить куки</string>
<string name="cookies_cleared">Все куки удалены</string>
</resources> </resources>

@ -192,4 +192,6 @@
<string name="silent">Silent</string> <string name="silent">Silent</string>
<string name="captcha_required">CAPTCHA is required</string> <string name="captcha_required">CAPTCHA is required</string>
<string name="resolve">Resolve</string> <string name="resolve">Resolve</string>
<string name="clear_cookies">Clear cookies</string>
<string name="cookies_cleared">All cookies was removed</string>
</resources> </resources>

@ -8,13 +8,6 @@
<item name="android:paddingBottom">10dp</item> <item name="android:paddingBottom">10dp</item>
</style> </style>
<style name="AppToggleButton.Vertical" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="android:checkable">true</item>
<item name="android:gravity">center_horizontal</item>
<item name="iconPadding">6dp</item>
<item name="iconGravity">top</item>
</style>
<style name="AppPopupTheme" parent="ThemeOverlay.MaterialComponents.Light" /> <style name="AppPopupTheme" parent="ThemeOverlay.MaterialComponents.Light" />
<style name="AppToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar"> <style name="AppToolbarTheme" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar">
@ -32,8 +25,4 @@
<item name="background">@color/grey</item> <item name="background">@color/grey</item>
</style> </style>
<style name="AppBadge" parent="Widget.MaterialComponents.Badge">
<item name="backgroundColor">?attr/colorAccent</item>
</style>
</resources> </resources>

@ -16,8 +16,8 @@
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<PreferenceCategory <PreferenceCategory
app:iconSpaceReserved="false" android:title="@string/cache"
android:title="@string/cache"> app:iconSpaceReserved="false">
<Preference <Preference
android:key="thumbs_cache_clear" android:key="thumbs_cache_clear"
@ -33,4 +33,10 @@
</PreferenceCategory> </PreferenceCategory>
<Preference
android:key="cookies_clear"
android:persistent="false"
android:title="@string/clear_cookies"
app:iconSpaceReserved="false" />
</PreferenceScreen> </PreferenceScreen>
Loading…
Cancel
Save