Refactor and deprecations fixes

This commit is contained in:
Koitharu
2022-02-26 10:14:40 +02:00
parent 94e9fa35e2
commit def2d5f494
33 changed files with 169 additions and 248 deletions

View File

@@ -5,29 +5,29 @@ import android.net.Uri
import android.os.Bundle
import org.koitharu.kotatsu.core.model.Manga
class MangaIntent(
class MangaIntent private constructor(
val manga: Manga?,
val mangaId: Long,
val uri: Uri?
val uri: Uri?,
) {
constructor(intent: Intent?) : this(
manga = intent?.getParcelableExtra(KEY_MANGA),
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
uri = intent?.data
)
constructor(args: Bundle?) : this(
manga = args?.getParcelable(KEY_MANGA),
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
uri = null
)
companion object {
fun from(intent: Intent?) = MangaIntent(
manga = intent?.getParcelableExtra(KEY_MANGA),
mangaId = intent?.getLongExtra(KEY_ID, ID_NONE) ?: ID_NONE,
uri = intent?.data
)
fun from(args: Bundle?) = MangaIntent(
manga = args?.getParcelable(KEY_MANGA),
mangaId = args?.getLong(KEY_ID, ID_NONE) ?: ID_NONE,
uri = null
)
const val ID_NONE = 0L
const val KEY_MANGA = "manga"
const val KEY_ID = "id"
}
}
}

View File

@@ -1,24 +0,0 @@
package org.koitharu.kotatsu.base.domain
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
object MangaProviderFactory {
fun getSources(settings: AppSettings, includeHidden: Boolean): List<MangaSource> {
val list = MangaSource.values().toList() - MangaSource.LOCAL
val order = settings.sourcesOrder
val sorted = list.sortedBy { x ->
val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
}
return if (includeHidden) {
sorted
} else {
val hidden = settings.hiddenSources
sorted.filterNot { x ->
x.name in hidden
}
}
}
}

View File

@@ -5,7 +5,6 @@ import android.net.Uri
import android.util.Size
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.component.KoinComponent
@@ -14,7 +13,6 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.medianOrNull
import java.io.InputStream
@@ -40,15 +38,14 @@ object MangaUtils : KoinComponent {
}
}
} else {
val client = get<OkHttpClient>()
val request = Request.Builder()
.url(url)
.get()
.header(CommonHeaders.REFERER, page.referer)
.cacheControl(CacheUtils.CONTROL_DISABLED)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.build()
client.newCall(request).await().use {
withContext(Dispatchers.IO) {
get<OkHttpClient>().newCall(request).await().use {
runInterruptible(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
}
@@ -66,10 +63,10 @@ object MangaUtils : KoinComponent {
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeStream(input, null, options)
BitmapFactory.decodeStream(input, null, options)?.recycle()
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
check(imageHeight > 0 && imageWidth > 0)
return Size(imageWidth, imageHeight)
}
}
}

View File

@@ -17,8 +17,7 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
get() = checkNotNull(viewBinding)
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext())
val binding = onInflateView(inflater, null)
val binding = onInflateView(layoutInflater, null)
viewBinding = binding
return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(binding.root)
@@ -43,4 +42,4 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
protected fun bindingOrNull(): B? = viewBinding
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}
}

View File

@@ -35,9 +35,10 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
private var lastInsets: Insets = Insets.NONE
override fun onCreate(savedInstanceState: Bundle?) {
val settings = get<AppSettings>()
when {
get<AppSettings>().isAmoledTheme -> setTheme(R.style.ThemeOverlay_Kotatsu_AMOLED)
get<AppSettings>().isDynamicTheme -> setTheme(R.style.Theme_Kotatsu_Monet)
settings.isAmoledTheme -> setTheme(R.style.ThemeOverlay_Kotatsu_AMOLED)
settings.isDynamicTheme -> setTheme(R.style.Theme_Kotatsu_Monet)
}
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -130,4 +131,4 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
super.onBackPressed()
}
}
}
}

View File

@@ -10,8 +10,7 @@ import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.koitharu.kotatsu.R
abstract class BaseBottomSheet<B : ViewBinding> :
BottomSheetDialogFragment() {
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
private var viewBinding: B? = null
@@ -40,4 +39,4 @@ abstract class BaseBottomSheet<B : ViewBinding> :
}
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
}
}

View File

@@ -11,8 +11,8 @@ import androidx.preference.PreferenceFragmentCompat
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.core.prefs.AppSettings
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), OnApplyWindowInsetsListener {
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : PreferenceFragmentCompat(),
OnApplyWindowInsetsListener {
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
@@ -36,4 +36,4 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
)
return insets
}
}
}

View File

@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.core.network
import okhttp3.CacheControl
object CommonHeaders {
const val REFERER = "Referer"
@@ -7,4 +9,7 @@ object CommonHeaders {
const val ACCEPT = "Accept"
const val CONTENT_DISPOSITION = "Content-Disposition"
const val COOKIE = "Cookie"
}
val CACHE_CONTROL_DISABLED: CacheControl
get() = CacheControl.Builder().noStore().build()
}

View File

@@ -2,27 +2,24 @@ package org.koitharu.kotatsu.core.network
import okhttp3.CookieJar
import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.utils.DownloadManagerHelper
import java.util.concurrent.TimeUnit
val networkModule
get() = module {
single { AndroidCookieJar() } bind CookieJar::class
single(named(CacheUtils.QUALIFIER_HTTP)) { CacheUtils.createHttpCache(androidContext()) }
single {
OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
readTimeout(60, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
cookieJar(get())
cache(get(named(CacheUtils.QUALIFIER_HTTP)))
cache(get<LocalStorageManager>().createHttpCache())
addInterceptor(UserAgentInterceptor())
addInterceptor(CloudFlareInterceptor())
if (BuildConfig.DEBUG) {
@@ -32,4 +29,4 @@ val networkModule
}
factory { DownloadManagerHelper(get(), get()) }
single { MangaLoaderContext(get(), get()) }
}
}

View File

@@ -64,10 +64,10 @@ abstract class RemoteMangaRepository(
protected fun generateUid(url: String): Long {
var h = 1125899906842597L
source.name.forEach { c ->
h = 31 * h + c.toLong()
h = 31 * h + c.code
}
url.forEach { c ->
h = 31 * h + c.toLong()
h = 31 * h + c.code
}
return h
}
@@ -75,7 +75,7 @@ abstract class RemoteMangaRepository(
protected fun generateUid(id: Long): Long {
var h = 1125899906842597L
source.name.forEach { c ->
h = 31 * h + c.toLong()
h = 31 * h + c.code
}
h = 31 * h + id
return h
@@ -84,4 +84,4 @@ abstract class RemoteMangaRepository(
protected fun parseFailed(message: String? = null): Nothing {
throw ParseException(message)
}
}
}

View File

@@ -13,6 +13,7 @@ import com.google.android.material.color.DynamicColors
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.callbackFlow
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.ZoomMode
import org.koitharu.kotatsu.utils.ext.toUriOrNull
import java.io.File
@@ -122,22 +123,17 @@ class AppSettings(context: Context) {
val isPagesNumbersEnabled: Boolean
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
fun getFallbackStorageDir(): File? {
return prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
var mangaStorageDir: File?
get() = prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
File(it)
}?.takeIf { it.exists() }
}
@Deprecated("Use LocalStorageManager instead")
fun setStorageDir(file: File?) {
prefs.edit {
if (file == null) {
set(value) = prefs.edit {
if (value == null) {
remove(KEY_LOCAL_STORAGE)
} else {
putString(KEY_LOCAL_STORAGE, file.path)
putString(KEY_LOCAL_STORAGE, value.path)
}
}
}
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
when (format) {
@@ -145,7 +141,21 @@ class AppSettings(context: Context) {
else -> SimpleDateFormat(format, Locale.getDefault())
}
@Deprecated("Use observe()")
fun getMangaSources(includeHidden: Boolean): List<MangaSource> {
val list = MangaSource.values().toMutableList()
list.remove(MangaSource.LOCAL)
val order = sourcesOrder
list.sortBy { x ->
val e = order.indexOf(x.ordinal)
if (e == -1) order.size + x.ordinal else e
}
if (!includeHidden) {
val hidden = hiddenSources
list.removeAll { x -> x.name in hidden }
}
return list
}
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
prefs.registerOnSharedPreferenceChangeListener(listener)
}

View File

@@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) {
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
}
}
}

View File

@@ -47,7 +47,7 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
TabLayoutMediator.TabConfigurationStrategy {
private val viewModel by viewModel<DetailsViewModel> {
parametersOf(MangaIntent.from(intent))
parametersOf(MangaIntent(intent))
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -280,4 +280,4 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
.putExtra(MangaIntent.KEY_ID, mangaId)
}
}
}
}

View File

@@ -15,9 +15,7 @@ import androidx.core.view.updatePadding
import coil.ImageLoader
import coil.request.ImageRequest
import coil.util.CoilUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
import org.koitharu.kotatsu.R
@@ -33,7 +31,7 @@ import org.koitharu.kotatsu.image.ui.ImageActivity
import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.reader.ui.ReaderState
import org.koitharu.kotatsu.search.ui.SearchActivity
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.*
class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickListener,
@@ -114,10 +112,8 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
val file = manga.url.toUri().toFileOrNull()
if (file != null) {
viewLifecycleScope.launch {
val size = withContext(Dispatchers.IO) {
file.length()
}
textViewSize.text = FileSizeUtils.formatBytes(requireContext(), size)
val size = file.computeSize()
textViewSize.text = FileSize.BYTES.format(requireContext(), size)
}
sizeContainer.isVisible = true
} else {
@@ -270,4 +266,4 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
.lifecycle(viewLifecycleOwner)
.enqueueWith(coil)
}
}
}

View File

@@ -21,7 +21,6 @@ import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.MangaZip
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.waitForNetwork
@@ -134,7 +133,7 @@ class DownloadManager(
val request = Request.Builder()
.url(url)
.header(CommonHeaders.REFERER, referer)
.cacheControl(CacheUtils.CONTROL_DISABLED)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.get()
.build()
val call = okHttp.newCall(request)
@@ -234,4 +233,4 @@ class DownloadManager(
private const val DOWNLOAD_ERROR_DELAY = 500L
private const val TEMP_PAGE_FILE = "page.tmp"
}
}
}

View File

@@ -1,7 +1,7 @@
package org.koitharu.kotatsu.local.data
enum class Cache(val dir: String) {
enum class CacheDir(val dir: String) {
THUMBS("image_cache"),
PAGES("pages");
}
}

View File

@@ -1,20 +1,41 @@
package org.koitharu.kotatsu.local.data
import android.content.Context
import android.os.StatFs
import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import okhttp3.Cache
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.ext.computeSize
import org.koitharu.kotatsu.utils.ext.getStorageName
import java.io.File
private const val DIR_NAME = "manga"
private const val CACHE_DISK_PERCENTAGE = 0.02
private const val CACHE_SIZE_MIN: Long = 10 * 1024 * 1024 // 10MB
private const val CACHE_SIZE_MAX: Long = 250 * 1024 * 1024 // 250MB
class LocalStorageManager(
private val context: Context,
private val settings: AppSettings,
) {
fun createHttpCache(): Cache {
val directory = File(context.externalCacheDir ?: context.cacheDir, "http")
directory.mkdirs()
val maxSize = calculateDiskCacheSize(directory)
return Cache(directory, maxSize)
}
suspend fun computeCacheSize(cache: CacheDir) = runInterruptible(Dispatchers.IO) {
getCacheDirs(cache.dir).sumOf { it.computeSize() }
}
suspend fun clearCache(cache: CacheDir) = runInterruptible(Dispatchers.IO) {
getCacheDirs(cache.dir).forEach { it.deleteRecursively() }
}
suspend fun getReadableDirs(): List<File> = runInterruptible(Dispatchers.IO) {
getConfiguredStorageDirs()
.filter { it.isReadable() }
@@ -26,7 +47,7 @@ class LocalStorageManager(
}
suspend fun getDefaultWriteableDir(): File? = runInterruptible(Dispatchers.IO) {
val preferredDir = settings.getFallbackStorageDir()?.takeIf { it.isWriteable() }
val preferredDir = settings.mangaStorageDir?.takeIf { it.isWriteable() }
preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWriteable() }
}
@@ -35,7 +56,7 @@ class LocalStorageManager(
@WorkerThread
private fun getConfiguredStorageDirs(): MutableSet<File> {
val set = getAvailableStorageDirs()
settings.getFallbackStorageDir()?.let {
settings.mangaStorageDir?.let {
set.add(it)
}
return set
@@ -57,6 +78,24 @@ class LocalStorageManager(
}
}
@WorkerThread
private fun getCacheDirs(subDir: String): MutableSet<File> {
val result = LinkedHashSet<File>()
result += File(context.cacheDir, subDir)
result += context.getExternalFilesDirs(subDir)
return result
}
private fun calculateDiskCacheSize(cacheDirectory: File): Long {
return try {
val cacheDir = StatFs(cacheDirectory.absolutePath)
val size = CACHE_DISK_PERCENTAGE * cacheDir.blockCountLong * cacheDir.blockSizeLong
return size.toLong().coerceIn(CACHE_SIZE_MIN, CACHE_SIZE_MAX)
} catch (_: Exception) {
CACHE_SIZE_MIN
}
}
private fun File.isReadable() = runCatching {
canRead()
}.getOrDefault(false)
@@ -64,4 +103,4 @@ class LocalStorageManager(
private fun File.isWriteable() = runCatching {
canWrite()
}.getOrDefault(false)
}
}

View File

@@ -2,35 +2,25 @@ package org.koitharu.kotatsu.local.data
import android.content.Context
import com.tomclaw.cache.DiskLruCache
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.subdir
import org.koitharu.kotatsu.utils.ext.takeIfReadable
import java.io.File
import java.io.InputStream
import java.io.OutputStream
class PagesCache(context: Context) {
private val cacheDir = context.externalCacheDir ?: context.cacheDir
private val lruCache = DiskLruCache.create(
cacheDir.subdir(Cache.PAGES.dir),
FileSizeUtils.mbToBytes(200)
cacheDir.subdir(CacheDir.PAGES.dir),
FileSize.MEGABYTES.convert(200, FileSize.BYTES),
)
operator fun get(url: String): File? {
return lruCache.get(url)?.takeIfReadable()
}
@Deprecated("Useless lambda")
fun put(url: String, writer: (OutputStream) -> Unit): File {
val file = File(cacheDir, url.longHashCode().toString())
file.outputStream().use(writer)
val res = lruCache.put(url, file)
file.delete()
return res
}
fun put(url: String, inputStream: InputStream): File {
val file = File(cacheDir, url.longHashCode().toString())
file.outputStream().use { out ->
@@ -40,4 +30,4 @@ class PagesCache(context: Context) {
file.delete()
return res
}
}
}

View File

@@ -5,7 +5,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.model.Manga
@@ -25,7 +24,7 @@ class MainViewModel(
val remoteSources = settings.observe()
.filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN }
.onStart { emit("") }
.map { MangaProviderFactory.getSources(settings, includeHidden = false) }
.map { settings.getMangaSources(includeHidden = false) }
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
fun openLastReader() {
@@ -35,4 +34,4 @@ class MainViewModel(
onOpenReader.call(manga)
}
}
}
}

View File

@@ -15,7 +15,6 @@ import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.core.network.CommonHeaders
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
import java.io.File
@@ -70,7 +69,7 @@ class PageLoader(
.get()
.header(CommonHeaders.REFERER, page.referer)
.header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
.cacheControl(CacheUtils.CONTROL_DISABLED)
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
.build()
okHttp.newCall(request).await().use { response ->
check(response.isSuccessful) {
@@ -103,4 +102,4 @@ class PageLoader(
}
private companion object Lock
}
}

View File

@@ -56,7 +56,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
ActivityResultCallback<Boolean>, ReaderControlDelegate.OnInteractionListener {
private val viewModel by viewModel<ReaderViewModel> {
parametersOf(MangaIntent.from(intent), intent?.getParcelableExtra<ReaderState>(EXTRA_STATE))
parametersOf(MangaIntent(intent), intent?.getParcelableExtra<ReaderState>(EXTRA_STATE))
}
private lateinit var touchHelper: GridTouchHelper
@@ -371,4 +371,4 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
.putExtra(EXTRA_STATE, state)
}
}
}
}

View File

@@ -160,7 +160,7 @@ class ReaderViewModel(
val downloadId = downloadManagerHelper.downloadPage(page, pageUrl)
val uri = downloadManagerHelper.awaitDownload(downloadId)
onPageSaved.postCall(uri)
} catch (e: CancellationException) {
} catch (_: CancellationException) {
} catch (e: Exception) {
onPageSaved.postCall(null)
}
@@ -267,4 +267,4 @@ class ReaderViewModel(
}
}
}
}

View File

@@ -9,7 +9,6 @@ import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
@@ -27,7 +26,7 @@ class MangaSearchRepository(
) {
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
MangaProviderFactory.getSources(settings, includeHidden = false).asFlow()
settings.getMangaSources(includeHidden = false).asFlow()
.flatMapMerge(concurrency) { source ->
runCatching {
MangaRepository(source).getList2(
@@ -128,4 +127,4 @@ class MangaSearchRepository(
return false
}
}
}
}

View File

@@ -20,7 +20,7 @@ import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.github.GithubRepository
import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.byte2HexFormatted
import java.io.ByteArrayInputStream
import java.io.InputStream
@@ -85,7 +85,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
append(
activity.getString(
R.string.size_s,
FileSizeUtils.formatBytes(activity, version.apkSize)
FileSize.BYTES.format(activity, version.apkSize),
)
)
appendLine()
@@ -144,4 +144,4 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
}
}
}
}
}

View File

@@ -5,20 +5,18 @@ import android.view.View
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.android.ext.android.get
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
import org.koitharu.kotatsu.core.network.AndroidCookieJar
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.Cache
import org.koitharu.kotatsu.local.data.CacheDir
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@@ -26,6 +24,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
private val trackerRepo by inject<TrackingRepository>(mode = LazyThreadSafetyMode.NONE)
private val searchRepository by inject<MangaSearchRepository>(mode = LazyThreadSafetyMode.NONE)
private val storageManager by inject<LocalStorageManager>(mode = LazyThreadSafetyMode.NONE)
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_history)
@@ -35,18 +34,14 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
super.onViewCreated(view, savedInstanceState)
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.let { pref ->
viewLifecycleScope.launchWhenResumed {
val size = withContext(Dispatchers.IO) {
CacheUtils.computeCacheSize(pref.context, Cache.PAGES.dir)
}
pref.summary = FileSizeUtils.formatBytes(pref.context, size)
val size = storageManager.computeCacheSize(CacheDir.PAGES)
pref.summary = FileSize.BYTES.format(pref.context, size)
}
}
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.let { pref ->
viewLifecycleScope.launchWhenResumed {
val size = withContext(Dispatchers.IO) {
CacheUtils.computeCacheSize(pref.context, Cache.THUMBS.dir)
}
pref.summary = FileSizeUtils.formatBytes(pref.context, size)
val size = storageManager.computeCacheSize(CacheDir.THUMBS)
pref.summary = FileSize.BYTES.format(pref.context, size)
}
}
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
@@ -68,11 +63,11 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
AppSettings.KEY_PAGES_CACHE_CLEAR -> {
clearCache(preference, Cache.PAGES)
clearCache(preference, CacheDir.PAGES)
true
}
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
clearCache(preference, Cache.THUMBS)
clearCache(preference, CacheDir.THUMBS)
true
}
AppSettings.KEY_COOKIES_CLEAR -> {
@@ -100,16 +95,14 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
}
private fun clearCache(preference: Preference, cache: Cache) {
private fun clearCache(preference: Preference, cache: CacheDir) {
val ctx = preference.context.applicationContext
viewLifecycleScope.launch {
try {
preference.isEnabled = false
val size = withContext(Dispatchers.IO) {
CacheUtils.clearCache(ctx, cache.dir)
CacheUtils.computeCacheSize(ctx, cache.dir)
}
preference.summary = FileSizeUtils.formatBytes(ctx, size)
storageManager.clearCache(cache)
val size = storageManager.computeCacheSize(cache)
preference.summary = FileSize.BYTES.format(ctx, size)
} catch (e: Exception) {
preference.summary = e.getDisplayMessage(ctx.resources)
} finally {
@@ -154,4 +147,4 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
}
}.show()
}
}
}

View File

@@ -162,7 +162,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
}
override fun onStorageSelected(file: File) {
settings.setStorageDir(file)
settings.mangaStorageDir = file
}
private fun Preference.bindStorageName() {

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings.sources
import androidx.core.os.LocaleListCompat
import androidx.lifecycle.MutableLiveData
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -70,7 +69,7 @@ class SourcesSettingsViewModel(
}
private fun buildList() {
val sources = MangaProviderFactory.getSources(settings, includeHidden = true)
val sources = settings.getMangaSources(includeHidden = true)
val hiddenSources = settings.hiddenSources
val query = searchQuery
if (!query.isNullOrEmpty()) {
@@ -155,4 +154,4 @@ class SourcesSettingsViewModel(
}
}
}
}
}

View File

@@ -1,55 +0,0 @@
package org.koitharu.kotatsu.utils
import android.content.Context
import android.os.StatFs
import androidx.annotation.WorkerThread
import okhttp3.Cache
import okhttp3.CacheControl
import org.koitharu.kotatsu.utils.ext.computeSize
import org.koitharu.kotatsu.utils.ext.sub
import java.io.File
object CacheUtils {
const val QUALIFIER_HTTP = "cache_http"
val CONTROL_DISABLED = CacheControl.Builder()
.noStore()
.build()
fun getCacheDirs(context: Context) = (context.externalCacheDirs + context.cacheDir)
.filterNotNull()
.distinctBy { it.absolutePath }
@WorkerThread
fun computeCacheSize(context: Context, name: String) = getCacheDirs(context)
.map { File(it, name) }
.sumOf { x -> x.computeSize() }
@WorkerThread
fun clearCache(context: Context, name: String) = getCacheDirs(context)
.map { File(it, name) }
.forEach { it.deleteRecursively() }
// FIXME need async implementation
fun createHttpCache(context: Context): Cache {
val directory = (context.externalCacheDir ?: context.cacheDir).sub("http")
directory.mkdirs()
val maxSize = calculateDiskCacheSize(directory) // TODO blocking call
return Cache(directory, maxSize)
}
private fun calculateDiskCacheSize(cacheDirectory: File): Long {
return try {
val cacheDir = StatFs(cacheDirectory.absolutePath)
val size = DISK_CACHE_PERCENTAGE * cacheDir.blockCountLong * cacheDir.blockSizeLong
return size.toLong().coerceIn(MIN_DISK_CACHE_SIZE, MAX_DISK_CACHE_SIZE)
} catch (_: Exception) {
MIN_DISK_CACHE_SIZE
}
}
private const val DISK_CACHE_PERCENTAGE = 0.02
private const val MIN_DISK_CACHE_SIZE: Long = 10 * 1024 * 1024 // 10MB
private const val MAX_DISK_CACHE_SIZE: Long = 250 * 1024 * 1024 // 250MB
}

View File

@@ -6,14 +6,14 @@ import java.text.DecimalFormat
import kotlin.math.log10
import kotlin.math.pow
enum class FileSize(private val multiplier: Int) {
object FileSizeUtils {
BYTES(1), KILOBYTES(1024), MEGABYTES(1024 * 1024);
fun mbToBytes(mb: Int) = 1024L * 1024L * mb
fun convert(amount: Long, target: FileSize): Long = amount * multiplier / target.multiplier
fun kbToBytes(kb: Int) = 1024L * kb
fun formatBytes(context: Context, bytes: Long): String {
fun format(context: Context, amount: Long): String {
val bytes = amount * multiplier
val units = context.getString(R.string.text_file_sizes).split('|')
if (bytes <= 0) {
return "0 ${units.first()}"
@@ -23,10 +23,13 @@ object FileSizeUtils {
append(
DecimalFormat("#,##0.#").format(
bytes / 1024.0.pow(digitGroups.toDouble())
).toString()
)
)
append(' ')
append(units.getOrNull(digitGroups).orEmpty())
val unit = units.getOrNull(digitGroups)
if (unit != null) {
append(' ')
append(unit)
}
}
}
}
}

View File

@@ -5,15 +5,17 @@ import android.os.Build
object PendingIntentCompat {
@JvmField
val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
} else {
0
}
@JvmField
val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.FLAG_MUTABLE
} else {
0
}
}
}

View File

@@ -91,4 +91,4 @@ sealed class Motion {
anim.interpolator = DecelerateInterpolator()
}
}
}
}

View File

@@ -1,14 +0,0 @@
package org.koitharu.kotatsu.utils.delegates
import android.os.Parcelable
import androidx.fragment.app.Fragment
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class ParcelableArgumentDelegate<T : Parcelable>(private val name: String) :
ReadOnlyProperty<Fragment, T> {
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return thisRef.requireArguments().getParcelable(name)!!
}
}

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.utils.delegates
import androidx.fragment.app.Fragment
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class StringArgumentDelegate(private val name: String) : ReadOnlyProperty<Fragment, String?> {
override fun getValue(thisRef: Fragment, property: KProperty<*>): String? {
return thisRef.arguments?.getString(name)
}
}