vc=60: storage usage readouts in cache settings

Each store + Coil image cache shows its actual on-disk byte count
next to its cap chip-row. Closes the loop on vc=59's cache controls
— users can see what each cap is doing instead of guessing.

- StorageUsage.sharedPrefBytes — reads dataDir/shared_prefs/X.xml
  length directly. Cheap, advisory; not authoritative on
  Android's internal SP layout but close enough to be useful.
- StorageUsage.coilDiskCacheBytes — pulls
  SingletonImageLoader.get().diskCache?.size, returns 0 if Coil
  hasn't lazily initialized yet.
- StorageUsage.format — KB/MB/GB renderer with 0 -> '—'.

Usage snapshot is captured once per Settings entry via remember{}
so File.length() doesn't refire on every recomposition.
This commit is contained in:
Kayos 2026-05-26 11:59:19 -07:00
parent aead95f1bc
commit 26c9483b94
3 changed files with 119 additions and 6 deletions

View file

@ -55,6 +55,6 @@ const val NEWPIPE_APPLICATION_ID_NEW = "net.newpipe.app"
// vc=19 / 0.1.0-AE — rust pipeline cutover. Extraction via
// strawcore-core (Sulkta-Coop/strawcore) via the UniFFI wrapper; no
// NewPipeExtractor in the runtime path.
const val STRAW_VERSION_CODE = 59
const val STRAW_VERSION_NAME = "0.1.0-BS"
const val STRAW_VERSION_CODE = 60
const val STRAW_VERSION_NAME = "0.1.0-BT"
const val STRAW_APPLICATION_ID = "com.sulkta.straw"

View file

@ -601,10 +601,23 @@ fun SettingsScreen() {
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(8.dp))
// Sample on-disk usage once per Settings entry — File.length() is
// cheap but we don't need it to recompose on every state change.
// remember keeps the same snapshot for the entire session.
val usage = remember {
object {
val history = com.sulkta.straw.util.StorageUsage.sharedPrefBytes(context, "straw_history")
val resume = com.sulkta.straw.util.StorageUsage.sharedPrefBytes(context, "straw_resume_positions")
val search = com.sulkta.straw.util.StorageUsage.sharedPrefBytes(context, "straw_search_cache")
val feed = com.sulkta.straw.util.StorageUsage.sharedPrefBytes(context, "straw_feed_cache")
val coil = com.sulkta.straw.util.StorageUsage.coilDiskCacheBytes(context)
}
}
CacheCapRow(
label = "Watch history",
label = "Watch + search history",
selected = store.historyWatchesCap.collectAsState().value,
onPick = { store.setHistoryWatchesCap(it) },
usageBytes = usage.history,
)
CacheCapRow(
label = "Search history",
@ -615,12 +628,46 @@ fun SettingsScreen() {
label = "Resume positions",
selected = store.resumePositionsCap.collectAsState().value,
onPick = { store.setResumePositionsCap(it) },
usageBytes = usage.resume,
)
CacheCapRow(
label = "Search results cache",
selected = store.searchCacheCap.collectAsState().value,
onPick = { store.setSearchCacheCap(it) },
usageBytes = usage.search,
)
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
"Subs feed cache",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.weight(1f),
)
Text(
text = "Used: ${com.sulkta.straw.util.StorageUsage.format(usage.feed)}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
"Image cache (thumbnails)",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.weight(1f),
)
Text(
text = "Used: ${com.sulkta.straw.util.StorageUsage.format(usage.coil)}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
"Cache TTL",
@ -799,17 +846,32 @@ private fun CategoryRow(
/**
* Compact chip-group row for picking a CacheCap. Label on the left,
* 5 chips on the right. Used four times in the Cache section so the
* shape is consolidated here.
* 5 chips on the right, optional "Used: X KB" suffix to the right
* of the label so the user can see what each cap is doing.
*/
@Composable
private fun CacheCapRow(
label: String,
selected: CacheCap,
onPick: (CacheCap) -> Unit,
usageBytes: Long = 0L,
) {
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
Text(label, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
label,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.weight(1f),
)
if (usageBytes > 0L) {
Text(
text = "Used: ${com.sulkta.straw.util.StorageUsage.format(usageBytes)}",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),

View file

@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2026 Sulkta-Coop
* SPDX-License-Identifier: GPL-3.0-or-later
*
* On-disk usage helper for the Settings Storage section. Reads the
* actual .xml file size for each SharedPreferences-backed store + the
* Coil disk-cache size, so the user can see what's eating space rather
* than guessing from cap settings.
*
* All values are best-effort: a missing file (store never written)
* returns 0; permission/IO errors return 0 and log silently. The
* displayed numbers are advisory, not authoritative.
*/
package com.sulkta.straw.util
import android.content.Context
import coil3.SingletonImageLoader
import java.io.File
object StorageUsage {
/**
* Bytes-on-disk for a SharedPreferences file. The Android framework
* writes `<dataDir>/shared_prefs/<prefsName>.xml`. dataDir is
* `context.applicationInfo.dataDir` (the parent of filesDir,
* approximately).
*/
fun sharedPrefBytes(context: Context, prefsName: String): Long {
val dataDir = context.applicationInfo.dataDir ?: return 0L
val f = File(dataDir, "shared_prefs/$prefsName.xml")
return if (f.exists()) f.length() else 0L
}
/**
* Coil's disk cache total. Returns 0 if Coil hasn't lazily
* initialized a disk cache yet (no images loaded this session).
*/
fun coilDiskCacheBytes(context: Context): Long = runCatching {
SingletonImageLoader.get(context).diskCache?.size ?: 0L
}.getOrDefault(0L)
/** Human-friendly rendering: "4.2 KB" / "13 MB" / "—" for 0. */
fun format(bytes: Long): String {
if (bytes <= 0L) return ""
val kb = bytes / 1024.0
if (kb < 1024.0) return "%.1f KB".format(kb)
val mb = kb / 1024.0
if (mb < 1024.0) return "%.1f MB".format(mb)
return "%.2f GB".format(mb / 1024.0)
}
}