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:
Sulkta 2026-05-23 19:54:37 -07:00
parent 15b81303e2
commit b2d63bb353
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)) 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.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.sulkta.straw.data.History import com.sulkta.straw.data.History
import com.sulkta.straw.data.Settings
import com.sulkta.straw.data.WatchHistoryItem import com.sulkta.straw.data.WatchHistoryItem
import com.sulkta.straw.net.RydClient import com.sulkta.straw.net.RydClient
import com.sulkta.straw.net.RydVotes import com.sulkta.straw.net.RydVotes
import com.sulkta.straw.net.SponsorBlockClient
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -27,6 +29,7 @@ data class VideoDetail(
val description: String, val description: String,
val thumbnail: String?, val thumbnail: String?,
val ryd: RydVotes? = null, val ryd: RydVotes? = null,
val sbSegmentCount: Int = 0,
) )
data class VideoDetailUiState( data class VideoDetailUiState(
@ -70,6 +73,10 @@ class VideoDetailViewModel : ViewModel() {
val ryd = withContext(Dispatchers.IO) { val ryd = withContext(Dispatchers.IO) {
runCatching { RydClient.fetch(videoId) }.getOrNull() 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( _ui.value = VideoDetailUiState(
loading = false, loading = false,
detail = VideoDetail( detail = VideoDetail(
@ -80,6 +87,7 @@ class VideoDetailViewModel : ViewModel() {
description = info.description?.content ?: "", description = info.description?.content ?: "",
thumbnail = thumb, thumbnail = thumb,
ryd = ryd, ryd = ryd,
sbSegmentCount = sbCount,
), ),
streamInfo = info, streamInfo = info,
) )

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -26,6 +27,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.sulkta.straw.data.History
import com.sulkta.straw.data.SbCategory import com.sulkta.straw.data.SbCategory
import com.sulkta.straw.data.Settings import com.sulkta.straw.data.Settings
@ -68,6 +70,22 @@ fun SettingsScreen() {
) )
HorizontalDivider() 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")
}
}
} }
} }