Straw phase I: SB segment count chip + clear-history buttons

Detail screen: VideoDetailViewModel now also fetches SponsorBlock segment
count for the user's currently-enabled categories alongside RYD. When >0,
detail screen shows a "⏭ N skip(s)" AssistChip next to the like/dislike
chips. Lets the user see at-a-glance whether SB is going to do anything
on this video before tapping Play.

Settings screen: "History" section at the bottom with two OutlinedButtons —
"Clear watch history" and "Clear searches". Each calls the corresponding
HistoryStore.clear* method. List updates immediately via the StateFlow.

Empty-category set on SB now means "skip the API call" (was already
checking this in PlayerViewModel.resolve, now also in VideoDetailViewModel).
This commit is contained in:
Kayos 2026-05-23 19:54:37 -07:00
parent ce3ba9afa2
commit 6f5e1ed199
3 changed files with 32 additions and 0 deletions

View file

@ -115,6 +115,12 @@ fun VideoDetailScreen(
),
)
}
if (d.sbSegmentCount > 0) {
AssistChip(
onClick = {},
label = { Text("${d.sbSegmentCount} skip${if (d.sbSegmentCount == 1) "" else "s"}") },
)
}
}
Spacer(modifier = Modifier.height(16.dp))

View file

@ -8,9 +8,11 @@ package com.sulkta.straw.feature.detail
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sulkta.straw.data.History
import com.sulkta.straw.data.Settings
import com.sulkta.straw.data.WatchHistoryItem
import com.sulkta.straw.net.RydClient
import com.sulkta.straw.net.RydVotes
import com.sulkta.straw.net.SponsorBlockClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -27,6 +29,7 @@ data class VideoDetail(
val description: String,
val thumbnail: String?,
val ryd: RydVotes? = null,
val sbSegmentCount: Int = 0,
)
data class VideoDetailUiState(
@ -70,6 +73,10 @@ class VideoDetailViewModel : ViewModel() {
val ryd = withContext(Dispatchers.IO) {
runCatching { RydClient.fetch(videoId) }.getOrNull()
}
val sbCats = Settings.get().sbCategories.value.map { it.key }
val sbCount = if (sbCats.isEmpty()) 0 else withContext(Dispatchers.IO) {
runCatching { SponsorBlockClient.fetch(videoId, sbCats).size }.getOrDefault(0)
}
_ui.value = VideoDetailUiState(
loading = false,
detail = VideoDetail(
@ -80,6 +87,7 @@ class VideoDetailViewModel : ViewModel() {
description = info.description?.content ?: "",
thumbnail = thumb,
ryd = ryd,
sbSegmentCount = sbCount,
),
streamInfo = info,
)

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -26,6 +27,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.sulkta.straw.data.History
import com.sulkta.straw.data.SbCategory
import com.sulkta.straw.data.Settings
@ -68,6 +70,22 @@ fun SettingsScreen() {
)
HorizontalDivider()
}
Spacer(modifier = Modifier.height(32.dp))
Text(
"History",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Spacer(modifier = Modifier.height(12.dp))
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedButton(onClick = { History.get().clearWatches() }) {
Text("Clear watch history")
}
OutlinedButton(onClick = { History.get().clearSearches() }) {
Text("Clear searches")
}
}
}
}