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()
|
||||
|
||||
|
||||
@@ -5,4 +5,8 @@
|
||||
|
||||
<solid android:color="?colorSurface" />
|
||||
|
||||
<size
|
||||
android:width="12dp"
|
||||
android:height="12dp" />
|
||||
|
||||
</shape>
|
||||
@@ -16,6 +16,17 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_normal" />
|
||||
|
||||
<include
|
||||
android:id="@+id/layout_storage"
|
||||
layout="@layout/layout_memory_usage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="@dimen/margin_small" />
|
||||
|
||||
<org.koitharu.kotatsu.base.ui.widgets.ListItemTextView
|
||||
android:id="@+id/button_downloads"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
69
app/src/main/res/layout/layout_memory_usage.xml
Normal file
69
app/src/main/res/layout/layout_memory_usage.xml
Normal file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/screen_padding">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/storage_usage"
|
||||
android:textAppearance="?textAppearanceTitleMedium" />
|
||||
|
||||
<org.koitharu.kotatsu.base.ui.widgets.SegmentedBarView
|
||||
android:id="@+id/bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
android:layout_marginVertical="@dimen/margin_normal"
|
||||
android:background="?colorSecondaryContainer" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_storage"
|
||||
style="@style/Widget.Kotatsu.TextView.Indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/saved_manga"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/bg_circle"
|
||||
tools:drawableTint="?colorPrimary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_pages_cache"
|
||||
style="@style/Widget.Kotatsu.TextView.Indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:text="@string/pages_cache"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/bg_circle"
|
||||
tools:drawableTint="?colorSecondary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_other_cache"
|
||||
style="@style/Widget.Kotatsu.TextView.Indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:text="@string/other_cache"
|
||||
android:visibility="gone"
|
||||
app:drawableStartCompat="@drawable/bg_circle"
|
||||
tools:drawableTint="?colorTertiary"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label_available"
|
||||
style="@style/Widget.Kotatsu.TextView.Indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/margin_small"
|
||||
android:text="@string/computing_"
|
||||
app:drawableStartCompat="@drawable/bg_circle"
|
||||
app:drawableTint="?colorSecondaryContainer" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -340,4 +340,10 @@
|
||||
<string name="confirm_exit">Press "Back" again to exit</string>
|
||||
<string name="exit_confirmation_summary">Press "Back" twice to exit the app</string>
|
||||
<string name="exit_confirmation">Exit confirmation</string>
|
||||
<string name="saved_manga">Saved manga</string>
|
||||
<string name="pages_cache">Pages cache</string>
|
||||
<string name="other_cache">Other cache</string>
|
||||
<string name="storage_usage">Storage usage</string>
|
||||
<string name="available">Available</string>
|
||||
<string name="memory_usage_pattern">%s - %s</string>
|
||||
</resources>
|
||||
@@ -150,6 +150,12 @@
|
||||
<item name="android:textAppearance">?textAppearanceButton</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Kotatsu.TextView.Indicator" parent="Widget.MaterialComponents.TextView">
|
||||
<item name="android:drawablePadding">12dp</item>
|
||||
<item name="android:gravity">center_vertical</item>
|
||||
<item name="android:textAppearance">?textAppearanceLabelMedium</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay.Kotatsu.MainToolbar" parent="">
|
||||
<item name="colorControlHighlight">@color/selector_overlay</item>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user