diff --git a/buildSrc/src/main/kotlin/ProjectConfig.kt b/buildSrc/src/main/kotlin/ProjectConfig.kt index 72b5ffa36..8580bd278 100644 --- a/buildSrc/src/main/kotlin/ProjectConfig.kt +++ b/buildSrc/src/main/kotlin/ProjectConfig.kt @@ -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" diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt b/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt index 9f7486c11..952f9ab77 100644 --- a/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt +++ b/strawApp/src/main/kotlin/com/sulkta/straw/feature/settings/SettingsScreen.kt @@ -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), diff --git a/strawApp/src/main/kotlin/com/sulkta/straw/util/StorageUsage.kt b/strawApp/src/main/kotlin/com/sulkta/straw/util/StorageUsage.kt new file mode 100644 index 000000000..30e16b1a2 --- /dev/null +++ b/strawApp/src/main/kotlin/com/sulkta/straw/util/StorageUsage.kt @@ -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 `/shared_prefs/.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) + } +}