Refactor and deprecations fixes
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) {
|
||||
|
||||
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +91,4 @@ sealed class Motion {
|
||||
anim.interpolator = DecelerateInterpolator()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)!!
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user