Refactor and deprecations fixes
This commit is contained in:
@@ -5,29 +5,29 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
|
||||||
class MangaIntent(
|
class MangaIntent private constructor(
|
||||||
val manga: Manga?,
|
val manga: Manga?,
|
||||||
val mangaId: Long,
|
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 {
|
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 ID_NONE = 0L
|
||||||
|
|
||||||
const val KEY_MANGA = "manga"
|
const val KEY_MANGA = "manga"
|
||||||
const val KEY_ID = "id"
|
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 android.util.Size
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.koin.core.component.KoinComponent
|
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.model.MangaPage
|
||||||
import org.koitharu.kotatsu.core.network.CommonHeaders
|
import org.koitharu.kotatsu.core.network.CommonHeaders
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
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.await
|
||||||
import org.koitharu.kotatsu.utils.ext.medianOrNull
|
import org.koitharu.kotatsu.utils.ext.medianOrNull
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -40,15 +38,14 @@ object MangaUtils : KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val client = get<OkHttpClient>()
|
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.get()
|
.get()
|
||||||
.header(CommonHeaders.REFERER, page.referer)
|
.header(CommonHeaders.REFERER, page.referer)
|
||||||
.cacheControl(CacheUtils.CONTROL_DISABLED)
|
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
||||||
.build()
|
.build()
|
||||||
client.newCall(request).await().use {
|
get<OkHttpClient>().newCall(request).await().use {
|
||||||
withContext(Dispatchers.IO) {
|
runInterruptible(Dispatchers.IO) {
|
||||||
getBitmapSize(it.body?.byteStream())
|
getBitmapSize(it.body?.byteStream())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,10 +63,10 @@ object MangaUtils : KoinComponent {
|
|||||||
val options = BitmapFactory.Options().apply {
|
val options = BitmapFactory.Options().apply {
|
||||||
inJustDecodeBounds = true
|
inJustDecodeBounds = true
|
||||||
}
|
}
|
||||||
BitmapFactory.decodeStream(input, null, options)
|
BitmapFactory.decodeStream(input, null, options)?.recycle()
|
||||||
val imageHeight: Int = options.outHeight
|
val imageHeight: Int = options.outHeight
|
||||||
val imageWidth: Int = options.outWidth
|
val imageWidth: Int = options.outWidth
|
||||||
check(imageHeight > 0 && imageWidth > 0)
|
check(imageHeight > 0 && imageWidth > 0)
|
||||||
return Size(imageWidth, imageHeight)
|
return Size(imageWidth, imageHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
|
|||||||
get() = checkNotNull(viewBinding)
|
get() = checkNotNull(viewBinding)
|
||||||
|
|
||||||
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val inflater = activity?.layoutInflater ?: LayoutInflater.from(requireContext())
|
val binding = onInflateView(layoutInflater, null)
|
||||||
val binding = onInflateView(inflater, null)
|
|
||||||
viewBinding = binding
|
viewBinding = binding
|
||||||
return MaterialAlertDialogBuilder(requireContext(), theme)
|
return MaterialAlertDialogBuilder(requireContext(), theme)
|
||||||
.setView(binding.root)
|
.setView(binding.root)
|
||||||
@@ -43,4 +42,4 @@ abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
|
|||||||
protected fun bindingOrNull(): B? = viewBinding
|
protected fun bindingOrNull(): B? = viewBinding
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
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
|
private var lastInsets: Insets = Insets.NONE
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
val settings = get<AppSettings>()
|
||||||
when {
|
when {
|
||||||
get<AppSettings>().isAmoledTheme -> setTheme(R.style.ThemeOverlay_Kotatsu_AMOLED)
|
settings.isAmoledTheme -> setTheme(R.style.ThemeOverlay_Kotatsu_AMOLED)
|
||||||
get<AppSettings>().isDynamicTheme -> setTheme(R.style.Theme_Kotatsu_Monet)
|
settings.isDynamicTheme -> setTheme(R.style.Theme_Kotatsu_Monet)
|
||||||
}
|
}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
@@ -130,4 +131,4 @@ abstract class BaseActivity<B : ViewBinding> : AppCompatActivity(), OnApplyWindo
|
|||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import androidx.viewbinding.ViewBinding
|
|||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
|
|
||||||
abstract class BaseBottomSheet<B : ViewBinding> :
|
abstract class BaseBottomSheet<B : ViewBinding> : BottomSheetDialogFragment() {
|
||||||
BottomSheetDialogFragment() {
|
|
||||||
|
|
||||||
private var viewBinding: B? = null
|
private var viewBinding: B? = null
|
||||||
|
|
||||||
@@ -40,4 +39,4 @@ abstract class BaseBottomSheet<B : ViewBinding> :
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): B
|
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.koin.android.ext.android.inject
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
|
||||||
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : PreferenceFragmentCompat(),
|
||||||
PreferenceFragmentCompat(), OnApplyWindowInsetsListener {
|
OnApplyWindowInsetsListener {
|
||||||
|
|
||||||
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
|
protected val settings by inject<AppSettings>(mode = LazyThreadSafetyMode.NONE)
|
||||||
|
|
||||||
@@ -36,4 +36,4 @@ abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
|
|||||||
)
|
)
|
||||||
return insets
|
return insets
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.core.network
|
package org.koitharu.kotatsu.core.network
|
||||||
|
|
||||||
|
import okhttp3.CacheControl
|
||||||
|
|
||||||
object CommonHeaders {
|
object CommonHeaders {
|
||||||
|
|
||||||
const val REFERER = "Referer"
|
const val REFERER = "Referer"
|
||||||
@@ -7,4 +9,7 @@ object CommonHeaders {
|
|||||||
const val ACCEPT = "Accept"
|
const val ACCEPT = "Accept"
|
||||||
const val CONTENT_DISPOSITION = "Content-Disposition"
|
const val CONTENT_DISPOSITION = "Content-Disposition"
|
||||||
const val COOKIE = "Cookie"
|
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.CookieJar
|
||||||
import okhttp3.OkHttpClient
|
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.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koitharu.kotatsu.BuildConfig
|
import org.koitharu.kotatsu.BuildConfig
|
||||||
import org.koitharu.kotatsu.base.domain.MangaLoaderContext
|
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 org.koitharu.kotatsu.utils.DownloadManagerHelper
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
val networkModule
|
val networkModule
|
||||||
get() = module {
|
get() = module {
|
||||||
single { AndroidCookieJar() } bind CookieJar::class
|
single { AndroidCookieJar() } bind CookieJar::class
|
||||||
single(named(CacheUtils.QUALIFIER_HTTP)) { CacheUtils.createHttpCache(androidContext()) }
|
|
||||||
single {
|
single {
|
||||||
OkHttpClient.Builder().apply {
|
OkHttpClient.Builder().apply {
|
||||||
connectTimeout(20, TimeUnit.SECONDS)
|
connectTimeout(20, TimeUnit.SECONDS)
|
||||||
readTimeout(60, TimeUnit.SECONDS)
|
readTimeout(60, TimeUnit.SECONDS)
|
||||||
writeTimeout(20, TimeUnit.SECONDS)
|
writeTimeout(20, TimeUnit.SECONDS)
|
||||||
cookieJar(get())
|
cookieJar(get())
|
||||||
cache(get(named(CacheUtils.QUALIFIER_HTTP)))
|
cache(get<LocalStorageManager>().createHttpCache())
|
||||||
addInterceptor(UserAgentInterceptor())
|
addInterceptor(UserAgentInterceptor())
|
||||||
addInterceptor(CloudFlareInterceptor())
|
addInterceptor(CloudFlareInterceptor())
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
@@ -32,4 +29,4 @@ val networkModule
|
|||||||
}
|
}
|
||||||
factory { DownloadManagerHelper(get(), get()) }
|
factory { DownloadManagerHelper(get(), get()) }
|
||||||
single { MangaLoaderContext(get(), get()) }
|
single { MangaLoaderContext(get(), get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ abstract class RemoteMangaRepository(
|
|||||||
protected fun generateUid(url: String): Long {
|
protected fun generateUid(url: String): Long {
|
||||||
var h = 1125899906842597L
|
var h = 1125899906842597L
|
||||||
source.name.forEach { c ->
|
source.name.forEach { c ->
|
||||||
h = 31 * h + c.toLong()
|
h = 31 * h + c.code
|
||||||
}
|
}
|
||||||
url.forEach { c ->
|
url.forEach { c ->
|
||||||
h = 31 * h + c.toLong()
|
h = 31 * h + c.code
|
||||||
}
|
}
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ abstract class RemoteMangaRepository(
|
|||||||
protected fun generateUid(id: Long): Long {
|
protected fun generateUid(id: Long): Long {
|
||||||
var h = 1125899906842597L
|
var h = 1125899906842597L
|
||||||
source.name.forEach { c ->
|
source.name.forEach { c ->
|
||||||
h = 31 * h + c.toLong()
|
h = 31 * h + c.code
|
||||||
}
|
}
|
||||||
h = 31 * h + id
|
h = 31 * h + id
|
||||||
return h
|
return h
|
||||||
@@ -84,4 +84,4 @@ abstract class RemoteMangaRepository(
|
|||||||
protected fun parseFailed(message: String? = null): Nothing {
|
protected fun parseFailed(message: String? = null): Nothing {
|
||||||
throw ParseException(message)
|
throw ParseException(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.google.android.material.color.DynamicColors
|
|||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.channels.trySendBlocking
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
import org.koitharu.kotatsu.core.model.ZoomMode
|
import org.koitharu.kotatsu.core.model.ZoomMode
|
||||||
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
import org.koitharu.kotatsu.utils.ext.toUriOrNull
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -122,22 +123,17 @@ class AppSettings(context: Context) {
|
|||||||
val isPagesNumbersEnabled: Boolean
|
val isPagesNumbersEnabled: Boolean
|
||||||
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
get() = prefs.getBoolean(KEY_PAGES_NUMBERS, false)
|
||||||
|
|
||||||
fun getFallbackStorageDir(): File? {
|
var mangaStorageDir: File?
|
||||||
return prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
|
get() = prefs.getString(KEY_LOCAL_STORAGE, null)?.let {
|
||||||
File(it)
|
File(it)
|
||||||
}?.takeIf { it.exists() }
|
}?.takeIf { it.exists() }
|
||||||
}
|
set(value) = prefs.edit {
|
||||||
|
if (value == null) {
|
||||||
@Deprecated("Use LocalStorageManager instead")
|
|
||||||
fun setStorageDir(file: File?) {
|
|
||||||
prefs.edit {
|
|
||||||
if (file == null) {
|
|
||||||
remove(KEY_LOCAL_STORAGE)
|
remove(KEY_LOCAL_STORAGE)
|
||||||
} else {
|
} else {
|
||||||
putString(KEY_LOCAL_STORAGE, file.path)
|
putString(KEY_LOCAL_STORAGE, value.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
|
fun getDateFormat(format: String = prefs.getString(KEY_DATE_FORMAT, "").orEmpty()): DateFormat =
|
||||||
when (format) {
|
when (format) {
|
||||||
@@ -145,7 +141,21 @@ class AppSettings(context: Context) {
|
|||||||
else -> SimpleDateFormat(format, Locale.getDefault())
|
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) {
|
fun subscribe(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ enum class ReaderMode(val id: Int) {
|
|||||||
|
|
||||||
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
|
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
|
|||||||
TabLayoutMediator.TabConfigurationStrategy {
|
TabLayoutMediator.TabConfigurationStrategy {
|
||||||
|
|
||||||
private val viewModel by viewModel<DetailsViewModel> {
|
private val viewModel by viewModel<DetailsViewModel> {
|
||||||
parametersOf(MangaIntent.from(intent))
|
parametersOf(MangaIntent(intent))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -280,4 +280,4 @@ class DetailsActivity : BaseActivity<ActivityDetailsBinding>(),
|
|||||||
.putExtra(MangaIntent.KEY_ID, mangaId)
|
.putExtra(MangaIntent.KEY_ID, mangaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ import androidx.core.view.updatePadding
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.util.CoilUtils
|
import coil.util.CoilUtils
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
|
||||||
import org.koitharu.kotatsu.R
|
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.ReaderActivity
|
||||||
import org.koitharu.kotatsu.reader.ui.ReaderState
|
import org.koitharu.kotatsu.reader.ui.ReaderState
|
||||||
import org.koitharu.kotatsu.search.ui.SearchActivity
|
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.*
|
import org.koitharu.kotatsu.utils.ext.*
|
||||||
|
|
||||||
class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickListener,
|
class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickListener,
|
||||||
@@ -114,10 +112,8 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
|||||||
val file = manga.url.toUri().toFileOrNull()
|
val file = manga.url.toUri().toFileOrNull()
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
viewLifecycleScope.launch {
|
viewLifecycleScope.launch {
|
||||||
val size = withContext(Dispatchers.IO) {
|
val size = file.computeSize()
|
||||||
file.length()
|
textViewSize.text = FileSize.BYTES.format(requireContext(), size)
|
||||||
}
|
|
||||||
textViewSize.text = FileSizeUtils.formatBytes(requireContext(), size)
|
|
||||||
}
|
}
|
||||||
sizeContainer.isVisible = true
|
sizeContainer.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
@@ -270,4 +266,4 @@ class DetailsFragment : BaseFragment<FragmentDetailsBinding>(), View.OnClickList
|
|||||||
.lifecycle(viewLifecycleOwner)
|
.lifecycle(viewLifecycleOwner)
|
||||||
.enqueueWith(coil)
|
.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.MangaZip
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
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.await
|
||||||
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
import org.koitharu.kotatsu.utils.ext.deleteAwait
|
||||||
import org.koitharu.kotatsu.utils.ext.waitForNetwork
|
import org.koitharu.kotatsu.utils.ext.waitForNetwork
|
||||||
@@ -134,7 +133,7 @@ class DownloadManager(
|
|||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
.header(CommonHeaders.REFERER, referer)
|
.header(CommonHeaders.REFERER, referer)
|
||||||
.cacheControl(CacheUtils.CONTROL_DISABLED)
|
.cacheControl(CommonHeaders.CACHE_CONTROL_DISABLED)
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
val call = okHttp.newCall(request)
|
val call = okHttp.newCall(request)
|
||||||
@@ -234,4 +233,4 @@ class DownloadManager(
|
|||||||
private const val DOWNLOAD_ERROR_DELAY = 500L
|
private const val DOWNLOAD_ERROR_DELAY = 500L
|
||||||
private const val TEMP_PAGE_FILE = "page.tmp"
|
private const val TEMP_PAGE_FILE = "page.tmp"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.local.data
|
package org.koitharu.kotatsu.local.data
|
||||||
|
|
||||||
enum class Cache(val dir: String) {
|
enum class CacheDir(val dir: String) {
|
||||||
|
|
||||||
THUMBS("image_cache"),
|
THUMBS("image_cache"),
|
||||||
PAGES("pages");
|
PAGES("pages");
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,41 @@
|
|||||||
package org.koitharu.kotatsu.local.data
|
package org.koitharu.kotatsu.local.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.StatFs
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import okhttp3.Cache
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.utils.ext.computeSize
|
||||||
import org.koitharu.kotatsu.utils.ext.getStorageName
|
import org.koitharu.kotatsu.utils.ext.getStorageName
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private const val DIR_NAME = "manga"
|
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(
|
class LocalStorageManager(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val settings: AppSettings,
|
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) {
|
suspend fun getReadableDirs(): List<File> = runInterruptible(Dispatchers.IO) {
|
||||||
getConfiguredStorageDirs()
|
getConfiguredStorageDirs()
|
||||||
.filter { it.isReadable() }
|
.filter { it.isReadable() }
|
||||||
@@ -26,7 +47,7 @@ class LocalStorageManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDefaultWriteableDir(): File? = runInterruptible(Dispatchers.IO) {
|
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() }
|
preferredDir ?: getFallbackStorageDir()?.takeIf { it.isWriteable() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +56,7 @@ class LocalStorageManager(
|
|||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getConfiguredStorageDirs(): MutableSet<File> {
|
private fun getConfiguredStorageDirs(): MutableSet<File> {
|
||||||
val set = getAvailableStorageDirs()
|
val set = getAvailableStorageDirs()
|
||||||
settings.getFallbackStorageDir()?.let {
|
settings.mangaStorageDir?.let {
|
||||||
set.add(it)
|
set.add(it)
|
||||||
}
|
}
|
||||||
return set
|
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 {
|
private fun File.isReadable() = runCatching {
|
||||||
canRead()
|
canRead()
|
||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
@@ -64,4 +103,4 @@ class LocalStorageManager(
|
|||||||
private fun File.isWriteable() = runCatching {
|
private fun File.isWriteable() = runCatching {
|
||||||
canWrite()
|
canWrite()
|
||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,25 @@ package org.koitharu.kotatsu.local.data
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.tomclaw.cache.DiskLruCache
|
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.longHashCode
|
||||||
import org.koitharu.kotatsu.utils.ext.subdir
|
import org.koitharu.kotatsu.utils.ext.subdir
|
||||||
import org.koitharu.kotatsu.utils.ext.takeIfReadable
|
import org.koitharu.kotatsu.utils.ext.takeIfReadable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
|
||||||
|
|
||||||
class PagesCache(context: Context) {
|
class PagesCache(context: Context) {
|
||||||
|
|
||||||
private val cacheDir = context.externalCacheDir ?: context.cacheDir
|
private val cacheDir = context.externalCacheDir ?: context.cacheDir
|
||||||
private val lruCache = DiskLruCache.create(
|
private val lruCache = DiskLruCache.create(
|
||||||
cacheDir.subdir(Cache.PAGES.dir),
|
cacheDir.subdir(CacheDir.PAGES.dir),
|
||||||
FileSizeUtils.mbToBytes(200)
|
FileSize.MEGABYTES.convert(200, FileSize.BYTES),
|
||||||
)
|
)
|
||||||
|
|
||||||
operator fun get(url: String): File? {
|
operator fun get(url: String): File? {
|
||||||
return lruCache.get(url)?.takeIfReadable()
|
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 {
|
fun put(url: String, inputStream: InputStream): File {
|
||||||
val file = File(cacheDir, url.longHashCode().toString())
|
val file = File(cacheDir, url.longHashCode().toString())
|
||||||
file.outputStream().use { out ->
|
file.outputStream().use { out ->
|
||||||
@@ -40,4 +30,4 @@ class PagesCache(context: Context) {
|
|||||||
file.delete()
|
file.delete()
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
@@ -25,7 +24,7 @@ class MainViewModel(
|
|||||||
val remoteSources = settings.observe()
|
val remoteSources = settings.observe()
|
||||||
.filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN }
|
.filter { it == AppSettings.KEY_SOURCES_ORDER || it == AppSettings.KEY_SOURCES_HIDDEN }
|
||||||
.onStart { emit("") }
|
.onStart { emit("") }
|
||||||
.map { MangaProviderFactory.getSources(settings, includeHidden = false) }
|
.map { settings.getMangaSources(includeHidden = false) }
|
||||||
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default)
|
||||||
|
|
||||||
fun openLastReader() {
|
fun openLastReader() {
|
||||||
@@ -35,4 +34,4 @@ class MainViewModel(
|
|||||||
onOpenReader.call(manga)
|
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.network.CommonHeaders
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
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.await
|
||||||
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
|
import org.koitharu.kotatsu.utils.ext.mangaRepositoryOf
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -70,7 +69,7 @@ class PageLoader(
|
|||||||
.get()
|
.get()
|
||||||
.header(CommonHeaders.REFERER, page.referer)
|
.header(CommonHeaders.REFERER, page.referer)
|
||||||
.header(CommonHeaders.ACCEPT, "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
|
.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()
|
.build()
|
||||||
okHttp.newCall(request).await().use { response ->
|
okHttp.newCall(request).await().use { response ->
|
||||||
check(response.isSuccessful) {
|
check(response.isSuccessful) {
|
||||||
@@ -103,4 +102,4 @@ class PageLoader(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private companion object Lock
|
private companion object Lock
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
|||||||
ActivityResultCallback<Boolean>, ReaderControlDelegate.OnInteractionListener {
|
ActivityResultCallback<Boolean>, ReaderControlDelegate.OnInteractionListener {
|
||||||
|
|
||||||
private val viewModel by viewModel<ReaderViewModel> {
|
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
|
private lateinit var touchHelper: GridTouchHelper
|
||||||
@@ -371,4 +371,4 @@ class ReaderActivity : BaseFullscreenActivity<ActivityReaderBinding>(),
|
|||||||
.putExtra(EXTRA_STATE, state)
|
.putExtra(EXTRA_STATE, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ class ReaderViewModel(
|
|||||||
val downloadId = downloadManagerHelper.downloadPage(page, pageUrl)
|
val downloadId = downloadManagerHelper.downloadPage(page, pageUrl)
|
||||||
val uri = downloadManagerHelper.awaitDownload(downloadId)
|
val uri = downloadManagerHelper.awaitDownload(downloadId)
|
||||||
onPageSaved.postCall(uri)
|
onPageSaved.postCall(uri)
|
||||||
} catch (e: CancellationException) {
|
} catch (_: CancellationException) {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
onPageSaved.postCall(null)
|
onPageSaved.postCall(null)
|
||||||
}
|
}
|
||||||
@@ -267,4 +267,4 @@ class ReaderViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import kotlinx.coroutines.currentCoroutineContext
|
|||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
|
|
||||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
import org.koitharu.kotatsu.core.model.MangaSource
|
import org.koitharu.kotatsu.core.model.MangaSource
|
||||||
@@ -27,7 +26,7 @@ class MangaSearchRepository(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
|
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
|
||||||
MangaProviderFactory.getSources(settings, includeHidden = false).asFlow()
|
settings.getMangaSources(includeHidden = false).asFlow()
|
||||||
.flatMapMerge(concurrency) { source ->
|
.flatMapMerge(concurrency) { source ->
|
||||||
runCatching {
|
runCatching {
|
||||||
MangaRepository(source).getList2(
|
MangaRepository(source).getList2(
|
||||||
@@ -128,4 +127,4 @@ class MangaSearchRepository(
|
|||||||
return false
|
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.GithubRepository
|
||||||
import org.koitharu.kotatsu.core.github.VersionId
|
import org.koitharu.kotatsu.core.github.VersionId
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
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 org.koitharu.kotatsu.utils.ext.byte2HexFormatted
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
@@ -85,7 +85,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
|||||||
append(
|
append(
|
||||||
activity.getString(
|
activity.getString(
|
||||||
R.string.size_s,
|
R.string.size_s,
|
||||||
FileSizeUtils.formatBytes(activity, version.apkSize)
|
FileSize.BYTES.format(activity, version.apkSize),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
appendLine()
|
appendLine()
|
||||||
@@ -144,4 +144,4 @@ class AppUpdateChecker(private val activity: ComponentActivity) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,18 @@ import android.view.View
|
|||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.android.ext.android.get
|
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.AndroidCookieJar
|
import org.koitharu.kotatsu.core.network.AndroidCookieJar
|
||||||
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.CacheDir
|
||||||
|
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||||
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
import org.koitharu.kotatsu.search.domain.MangaSearchRepository
|
||||||
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
|
||||||
import org.koitharu.kotatsu.utils.CacheUtils
|
import org.koitharu.kotatsu.utils.FileSize
|
||||||
import org.koitharu.kotatsu.utils.FileSizeUtils
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
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 trackerRepo by inject<TrackingRepository>(mode = LazyThreadSafetyMode.NONE)
|
||||||
private val searchRepository by inject<MangaSearchRepository>(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?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
addPreferencesFromResource(R.xml.pref_history)
|
addPreferencesFromResource(R.xml.pref_history)
|
||||||
@@ -35,18 +34,14 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.let { pref ->
|
findPreference<Preference>(AppSettings.KEY_PAGES_CACHE_CLEAR)?.let { pref ->
|
||||||
viewLifecycleScope.launchWhenResumed {
|
viewLifecycleScope.launchWhenResumed {
|
||||||
val size = withContext(Dispatchers.IO) {
|
val size = storageManager.computeCacheSize(CacheDir.PAGES)
|
||||||
CacheUtils.computeCacheSize(pref.context, Cache.PAGES.dir)
|
pref.summary = FileSize.BYTES.format(pref.context, size)
|
||||||
}
|
|
||||||
pref.summary = FileSizeUtils.formatBytes(pref.context, size)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.let { pref ->
|
findPreference<Preference>(AppSettings.KEY_THUMBS_CACHE_CLEAR)?.let { pref ->
|
||||||
viewLifecycleScope.launchWhenResumed {
|
viewLifecycleScope.launchWhenResumed {
|
||||||
val size = withContext(Dispatchers.IO) {
|
val size = storageManager.computeCacheSize(CacheDir.THUMBS)
|
||||||
CacheUtils.computeCacheSize(pref.context, Cache.THUMBS.dir)
|
pref.summary = FileSize.BYTES.format(pref.context, size)
|
||||||
}
|
|
||||||
pref.summary = FileSizeUtils.formatBytes(pref.context, size)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
findPreference<Preference>(AppSettings.KEY_SEARCH_HISTORY_CLEAR)?.let { pref ->
|
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 {
|
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||||
return when (preference.key) {
|
return when (preference.key) {
|
||||||
AppSettings.KEY_PAGES_CACHE_CLEAR -> {
|
AppSettings.KEY_PAGES_CACHE_CLEAR -> {
|
||||||
clearCache(preference, Cache.PAGES)
|
clearCache(preference, CacheDir.PAGES)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
|
||||||
clearCache(preference, Cache.THUMBS)
|
clearCache(preference, CacheDir.THUMBS)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
AppSettings.KEY_COOKIES_CLEAR -> {
|
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
|
val ctx = preference.context.applicationContext
|
||||||
viewLifecycleScope.launch {
|
viewLifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
preference.isEnabled = false
|
preference.isEnabled = false
|
||||||
val size = withContext(Dispatchers.IO) {
|
storageManager.clearCache(cache)
|
||||||
CacheUtils.clearCache(ctx, cache.dir)
|
val size = storageManager.computeCacheSize(cache)
|
||||||
CacheUtils.computeCacheSize(ctx, cache.dir)
|
preference.summary = FileSize.BYTES.format(ctx, size)
|
||||||
}
|
|
||||||
preference.summary = FileSizeUtils.formatBytes(ctx, size)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
preference.summary = e.getDisplayMessage(ctx.resources)
|
preference.summary = e.getDisplayMessage(ctx.resources)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -154,4 +147,4 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
|
|||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onStorageSelected(file: File) {
|
override fun onStorageSelected(file: File) {
|
||||||
settings.setStorageDir(file)
|
settings.mangaStorageDir = file
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Preference.bindStorageName() {
|
private fun Preference.bindStorageName() {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.settings.sources
|
|||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.domain.MangaProviderFactory
|
|
||||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
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
|
||||||
@@ -70,7 +69,7 @@ class SourcesSettingsViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun buildList() {
|
private fun buildList() {
|
||||||
val sources = MangaProviderFactory.getSources(settings, includeHidden = true)
|
val sources = settings.getMangaSources(includeHidden = true)
|
||||||
val hiddenSources = settings.hiddenSources
|
val hiddenSources = settings.hiddenSources
|
||||||
val query = searchQuery
|
val query = searchQuery
|
||||||
if (!query.isNullOrEmpty()) {
|
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.log10
|
||||||
import kotlin.math.pow
|
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 format(context: Context, amount: Long): String {
|
||||||
|
val bytes = amount * multiplier
|
||||||
fun formatBytes(context: Context, bytes: Long): String {
|
|
||||||
val units = context.getString(R.string.text_file_sizes).split('|')
|
val units = context.getString(R.string.text_file_sizes).split('|')
|
||||||
if (bytes <= 0) {
|
if (bytes <= 0) {
|
||||||
return "0 ${units.first()}"
|
return "0 ${units.first()}"
|
||||||
@@ -23,10 +23,13 @@ object FileSizeUtils {
|
|||||||
append(
|
append(
|
||||||
DecimalFormat("#,##0.#").format(
|
DecimalFormat("#,##0.#").format(
|
||||||
bytes / 1024.0.pow(digitGroups.toDouble())
|
bytes / 1024.0.pow(digitGroups.toDouble())
|
||||||
).toString()
|
)
|
||||||
)
|
)
|
||||||
append(' ')
|
val unit = units.getOrNull(digitGroups)
|
||||||
append(units.getOrNull(digitGroups).orEmpty())
|
if (unit != null) {
|
||||||
|
append(' ')
|
||||||
|
append(unit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,15 +5,17 @@ import android.os.Build
|
|||||||
|
|
||||||
object PendingIntentCompat {
|
object PendingIntentCompat {
|
||||||
|
|
||||||
|
@JvmField
|
||||||
val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
val FLAG_IMMUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmField
|
||||||
val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
val FLAG_MUTABLE = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
PendingIntent.FLAG_MUTABLE
|
PendingIntent.FLAG_MUTABLE
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,4 +91,4 @@ sealed class Motion {
|
|||||||
anim.interpolator = DecelerateInterpolator()
|
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