Add storage usage to Tools screen
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package org.koitharu.kotatsu.base.ui.widgets
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Outline
|
||||
import android.graphics.Paint
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import org.koitharu.kotatsu.parsers.util.replaceWith
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
import kotlin.random.Random
|
||||
|
||||
class SegmentedBarView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val segmentsData = ArrayList<Segment>()
|
||||
private val minSegmentSize = context.resources.resolveDp(3f)
|
||||
|
||||
var segments: List<Segment>
|
||||
get() = segmentsData
|
||||
set(value) {
|
||||
segmentsData.replaceWith(value)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
init {
|
||||
paint.style = Paint.Style.FILL
|
||||
outlineProvider = OutlineProvider()
|
||||
clipToOutline = true
|
||||
|
||||
if (isInEditMode) {
|
||||
segments = List(Random.nextInt(3, 5)) {
|
||||
Segment(
|
||||
percent = Random.nextFloat(),
|
||||
color = ColorUtils.HSLToColor(floatArrayOf(Random.nextInt(0, 360).toFloat(), 0.5f, 0.5f)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
var x = 0f
|
||||
val w = width.toFloat()
|
||||
for (segment in segmentsData) {
|
||||
paint.color = segment.color
|
||||
val segmentWidth = (w * segment.percent).coerceAtLeast(minSegmentSize)
|
||||
canvas.drawRect(x, 0f, x + segmentWidth, height.toFloat(), paint)
|
||||
x += segmentWidth
|
||||
}
|
||||
}
|
||||
|
||||
class Segment(
|
||||
@FloatRange(from = 0.0, to = 1.0) val percent: Float,
|
||||
@ColorInt val color: Int,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Segment
|
||||
|
||||
if (percent != other.percent) return false
|
||||
if (color != other.color) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = percent.hashCode()
|
||||
result = 31 * result + color
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private class OutlineProvider : ViewOutlineProvider() {
|
||||
override fun getOutline(view: View, outline: Outline) {
|
||||
outline.setRoundRect(0, 0, view.width, view.height, view.height / 2f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,15 @@ import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.StatFs
|
||||
import androidx.annotation.WorkerThread
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Cache
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
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
|
||||
@@ -37,6 +38,18 @@ class LocalStorageManager(
|
||||
getCacheDirs(cache.dir).sumOf { it.computeSize() }
|
||||
}
|
||||
|
||||
suspend fun computeCacheSize() = withContext(Dispatchers.IO) {
|
||||
getCacheDirs().sumOf { it.computeSize() }
|
||||
}
|
||||
|
||||
suspend fun computeStorageSize() = withContext(Dispatchers.IO) {
|
||||
getAvailableStorageDirs().sumOf { it.computeSize() }
|
||||
}
|
||||
|
||||
suspend fun computeAvailableSize() = runInterruptible(Dispatchers.IO) {
|
||||
getAvailableStorageDirs().mapToSet { it.freeSpace }.sum()
|
||||
}
|
||||
|
||||
suspend fun clearCache(cache: CacheDir) = runInterruptible(Dispatchers.IO) {
|
||||
getCacheDirs(cache.dir).forEach { it.deleteRecursively() }
|
||||
}
|
||||
@@ -93,6 +106,14 @@ class LocalStorageManager(
|
||||
return result
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun getCacheDirs(): MutableSet<File> {
|
||||
val result = LinkedHashSet<File>()
|
||||
result += context.cacheDir
|
||||
context.externalCacheDirs.filterNotNullTo(result)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun calculateDiskCacheSize(cacheDirectory: File): Long {
|
||||
return try {
|
||||
val cacheDir = StatFs(cacheDirectory.absolutePath)
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel
|
||||
import org.koitharu.kotatsu.settings.onboard.OnboardViewModel
|
||||
import org.koitharu.kotatsu.settings.protect.ProtectSetupViewModel
|
||||
import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel
|
||||
import org.koitharu.kotatsu.settings.tools.ToolsViewModel
|
||||
|
||||
val settingsModule
|
||||
get() = module {
|
||||
@@ -29,4 +30,5 @@ val settingsModule
|
||||
viewModel { OnboardViewModel(get()) }
|
||||
viewModel { SourcesSettingsViewModel(get()) }
|
||||
viewModel { NewSourcesViewModel(get()) }
|
||||
viewModel { ToolsViewModel(get()) }
|
||||
}
|
||||
@@ -1,21 +1,35 @@
|
||||
package org.koitharu.kotatsu.settings.tools
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import android.transition.TransitionManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseFragment
|
||||
import org.koitharu.kotatsu.base.ui.widgets.SegmentedBarView
|
||||
import org.koitharu.kotatsu.databinding.FragmentToolsBinding
|
||||
import org.koitharu.kotatsu.download.ui.DownloadsActivity
|
||||
import org.koitharu.kotatsu.settings.AppUpdateChecker
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.tools.model.StorageUsage
|
||||
import org.koitharu.kotatsu.utils.FileSize
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class ToolsFragment : BaseFragment<FragmentToolsBinding>(), View.OnClickListener {
|
||||
|
||||
private var updateChecker: AppUpdateChecker? = null
|
||||
private val viewModel by viewModel<ToolsViewModel>()
|
||||
|
||||
override fun onInflateView(inflater: LayoutInflater, container: ViewGroup?): FragmentToolsBinding {
|
||||
return FragmentToolsBinding.inflate(inflater, container, false)
|
||||
@@ -27,6 +41,8 @@ class ToolsFragment : BaseFragment<FragmentToolsBinding>(), View.OnClickListener
|
||||
binding.buttonDownloads.setOnClickListener(this)
|
||||
binding.cardUpdate.root.setOnClickListener(this)
|
||||
binding.cardUpdate.buttonDownload.setOnClickListener(this)
|
||||
|
||||
viewModel.storageUsage.observe(viewLifecycleOwner, ::onStorageUsageChanged)
|
||||
}
|
||||
|
||||
override fun onClick(v: View) {
|
||||
@@ -44,6 +60,50 @@ class ToolsFragment : BaseFragment<FragmentToolsBinding>(), View.OnClickListener
|
||||
)
|
||||
}
|
||||
|
||||
private fun onStorageUsageChanged(usage: StorageUsage) {
|
||||
val storageSegment = SegmentedBarView.Segment(usage.savedManga.percent, segmentColor(1))
|
||||
val pagesSegment = SegmentedBarView.Segment(usage.pagesCache.percent, segmentColor(2))
|
||||
val otherSegment = SegmentedBarView.Segment(usage.otherCache.percent, segmentColor(3))
|
||||
|
||||
with(binding.layoutStorage) {
|
||||
bar.segments = listOf(storageSegment, pagesSegment, otherSegment)
|
||||
val pattern = getString(R.string.memory_usage_pattern)
|
||||
labelStorage.text = pattern.format(
|
||||
FileSize.BYTES.format(root.context, usage.savedManga.bytes),
|
||||
getString(R.string.saved_manga)
|
||||
)
|
||||
labelPagesCache.text = pattern.format(
|
||||
FileSize.BYTES.format(root.context, usage.pagesCache.bytes),
|
||||
getString(R.string.pages_cache)
|
||||
)
|
||||
labelOtherCache.text = pattern.format(
|
||||
FileSize.BYTES.format(root.context, usage.otherCache.bytes),
|
||||
getString(R.string.other_cache)
|
||||
)
|
||||
labelAvailable.text = pattern.format(
|
||||
FileSize.BYTES.format(root.context, usage.available.bytes),
|
||||
getString(R.string.available)
|
||||
)
|
||||
TextViewCompat.setCompoundDrawableTintList(labelStorage, ColorStateList.valueOf(storageSegment.color))
|
||||
TextViewCompat.setCompoundDrawableTintList(labelPagesCache, ColorStateList.valueOf(pagesSegment.color))
|
||||
TextViewCompat.setCompoundDrawableTintList(labelOtherCache, ColorStateList.valueOf(otherSegment.color))
|
||||
if (!labelStorage.isVisible) {
|
||||
TransitionManager.beginDelayedTransition(root)
|
||||
}
|
||||
labelStorage.isVisible = true
|
||||
labelPagesCache.isVisible = true
|
||||
labelOtherCache.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
private fun segmentColor(i: Int): Int {
|
||||
val hue = (93.6f * i) % 360
|
||||
val color = ColorUtils.HSLToColor(floatArrayOf(hue, 0.5f, 0.5f))
|
||||
val backgroundColor = requireContext().getThemeColor(materialR.attr.colorSecondaryContainer)
|
||||
return MaterialColors.harmonize(color, backgroundColor)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newInstance() = ToolsFragment()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.koitharu.kotatsu.settings.tools
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.local.data.CacheDir
|
||||
import org.koitharu.kotatsu.local.data.LocalStorageManager
|
||||
import org.koitharu.kotatsu.settings.tools.model.StorageUsage
|
||||
|
||||
class ToolsViewModel(
|
||||
private val storageManager: LocalStorageManager,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val storageUsage: LiveData<StorageUsage> = liveData(
|
||||
context = viewModelScope.coroutineContext + Dispatchers.Default,
|
||||
) {
|
||||
emit(collectStorageUsage())
|
||||
}
|
||||
|
||||
private suspend fun collectStorageUsage(): StorageUsage {
|
||||
val pagesCacheSize = storageManager.computeCacheSize(CacheDir.PAGES)
|
||||
val otherCacheSize = storageManager.computeCacheSize() - pagesCacheSize
|
||||
val storageSize = storageManager.computeStorageSize()
|
||||
val availableSpace = storageManager.computeAvailableSize()
|
||||
val totalBytes = pagesCacheSize + otherCacheSize + storageSize + availableSpace
|
||||
return StorageUsage(
|
||||
savedManga = StorageUsage.Item(
|
||||
bytes = storageSize,
|
||||
percent = (storageSize.toDouble() / totalBytes).toFloat(),
|
||||
), pagesCache = StorageUsage.Item(
|
||||
bytes = pagesCacheSize,
|
||||
percent = (pagesCacheSize.toDouble() / totalBytes).toFloat(),
|
||||
), otherCache = StorageUsage.Item(
|
||||
bytes = otherCacheSize,
|
||||
percent = (otherCacheSize.toDouble() / totalBytes).toFloat(),
|
||||
), available = StorageUsage.Item(
|
||||
bytes = availableSpace,
|
||||
percent = (availableSpace.toDouble() / totalBytes).toFloat(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.koitharu.kotatsu.settings.tools.model
|
||||
|
||||
class StorageUsage(
|
||||
val savedManga: Item,
|
||||
val pagesCache: Item,
|
||||
val otherCache: Item,
|
||||
val available: Item,
|
||||
) {
|
||||
|
||||
class Item(
|
||||
val bytes: Long,
|
||||
val percent: Float,
|
||||
)
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.utils.ext.getThemeColor
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
class FaviconFallbackDrawable(
|
||||
@@ -16,7 +14,7 @@ class FaviconFallbackDrawable(
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val letter = name.take(1).uppercase()
|
||||
private val color = MaterialColors.harmonize(colorOfString(name), context.getThemeColor(android.R.attr.colorPrimary))
|
||||
private val color = MaterialColors.harmonizeWithPrimary(context, colorOfString(name))
|
||||
private val textBounds = Rect()
|
||||
private val tempRect = Rect()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user