WIP permissions

This commit is contained in:
Damontecres 2025-12-27 18:17:32 -05:00
parent bb55dc744a
commit 759bc0c88b
No known key found for this signature in database
8 changed files with 144 additions and 51 deletions

View file

@ -0,0 +1,49 @@
package com.github.damontecres.wholphin.data.model
import com.github.damontecres.wholphin.services.SeerrUserConfig
enum class SeerrPermission(
private val flag: Int,
) {
// Source: https://github.com/seerr-team/seerr/blob/develop/server/lib/permissions.ts
NONE(0),
ADMIN(2),
MANAGE_SETTINGS(4),
MANAGE_USERS(8),
MANAGE_REQUESTS(16),
REQUEST(32),
VOTE(64),
AUTO_APPROVE(128),
AUTO_APPROVE_MOVIE(256),
AUTO_APPROVE_TV(512),
REQUEST_4K(1024),
REQUEST_4K_MOVIE(2048),
REQUEST_4K_TV(4096),
REQUEST_ADVANCED(8192),
REQUEST_VIEW(16384),
AUTO_APPROVE_4K(32768),
AUTO_APPROVE_4K_MOVIE(65536),
AUTO_APPROVE_4K_TV(131072),
REQUEST_MOVIE(262144),
REQUEST_TV(524288),
MANAGE_ISSUES(1048576),
VIEW_ISSUES(2097152),
CREATE_ISSUES(4194304),
AUTO_REQUEST(8388608),
AUTO_REQUEST_MOVIE(16777216),
AUTO_REQUEST_TV(33554432),
RECENT_VIEW(67108864),
WATCHLIST_VIEW(134217728),
MANAGE_BLACKLIST(268435456),
VIEW_BLACKLIST(1073741824),
;
internal fun hasPermission(permissions: Int) = flag.and(permissions) == flag
}
/**
* Check whether the user has the given permissions (or is an admin)
*/
fun SeerrUserConfig?.hasPermission(permission: SeerrPermission): Boolean {
return permission.hasPermission(this?.permissions ?: return false) || SeerrPermission.ADMIN.hasPermission(permissions)
}

View file

@ -18,10 +18,11 @@ import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.scopes.ActivityScoped import dagger.hilt.android.scopes.ActivityScoped
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import timber.log.Timber import timber.log.Timber
@ -37,20 +38,10 @@ class SeerrServerRepository
private val serverRepository: ServerRepository, private val serverRepository: ServerRepository,
@param:StandardOkHttpClient private val okHttpClient: OkHttpClient, @param:StandardOkHttpClient private val okHttpClient: OkHttpClient,
) { ) {
private val _currentServer = MutableStateFlow<SeerrServer?>(null) private val _current = MutableStateFlow<CurrentSeerr?>(null)
val currentServer: StateFlow<SeerrServer?> = _currentServer val current: StateFlow<CurrentSeerr?> = _current
val currentServer: Flow<SeerrServer?> = current.map { it?.server }
private val _currentUser = MutableStateFlow<SeerrUser?>(null) val currentUser: Flow<SeerrUser?> = current.map { it?.user }
val currentUser: StateFlow<SeerrUser?> = _currentUser
val current =
currentServer.combine(currentUser) { server, user ->
if (server != null && user != null) {
CurrentSeerr(server, user)
} else {
null
}
}
/** /**
* Whether Seerr integration is currently active of not * Whether Seerr integration is currently active of not
@ -58,20 +49,17 @@ class SeerrServerRepository
suspend fun active(): Boolean = current.firstOrNull() != null && seerrApi.active suspend fun active(): Boolean = current.firstOrNull() != null && seerrApi.active
fun clear() { fun clear() {
_currentServer.update { null } _current.update { null }
_currentUser.update { null }
seerrApi.update("", null) seerrApi.update("", null)
} }
suspend fun set( suspend fun set(
server: SeerrServer, server: SeerrServer,
user: SeerrUser, user: SeerrUser,
userConfig: SeerrUserConfig,
) { ) {
_currentServer.update { _current.update {
_currentUser.update { CurrentSeerr(server, user, userConfig)
user
}
server
} }
} }
@ -99,12 +87,8 @@ class SeerrServerRepository
seerrServerDao.addUser(user) seerrServerDao.addUser(user)
seerrApi.update(server.url, apiKey) seerrApi.update(server.url, apiKey)
_currentServer.update { val userConfig = seerrApi.api.usersApi.authMeGet()
_currentUser.update { set(server, user, userConfig)
user
}
server
}
} }
} }
} }
@ -124,7 +108,7 @@ class SeerrServerRepository
serverRepository.currentUser.value?.let { jellyfinUser -> serverRepository.currentUser.value?.let { jellyfinUser ->
// TODO Need to update server early so that cookies are saved // TODO Need to update server early so that cookies are saved
seerrApi.update(server.url, null) seerrApi.update(server.url, null)
login(seerrApi.api, authMethod, username, password) val userConfig = login(seerrApi.api, authMethod, username, password)
val user = val user =
SeerrUser( SeerrUser(
@ -136,12 +120,7 @@ class SeerrServerRepository
credential = null, credential = null,
) )
seerrServerDao.addUser(user) seerrServerDao.addUser(user)
_currentServer.update { set(server, user, userConfig)
_currentUser.update {
user
}
server
}
} }
} }
} }
@ -163,9 +142,12 @@ class SeerrServerRepository
} }
} }
typealias SeerrUserConfig = User
data class CurrentSeerr( data class CurrentSeerr(
val server: SeerrServer, val server: SeerrServer,
val user: SeerrUser, val user: SeerrUser,
val config: SeerrUserConfig,
) )
private suspend fun login( private suspend fun login(
@ -224,21 +206,30 @@ class UserSwitchListener
if (server != null) { if (server != null) {
Timber.i("Found a seerr user & server") Timber.i("Found a seerr user & server")
seerrApi.update(server.url, seerrUser.credential) seerrApi.update(server.url, seerrUser.credential)
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) { val userConfig =
try { if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
login( try {
seerrApi.api, login(
seerrUser.authMethod, seerrApi.api,
seerrUser.username, seerrUser.authMethod,
seerrUser.password, seerrUser.username,
) seerrUser.password,
} catch (ex: Exception) { )
Timber.w(ex, "Error logging into %s", server.url) } catch (ex: Exception) {
seerrServerRepository.clear() Timber.w(ex, "Error logging into %s", server.url)
return@let seerrServerRepository.clear()
return@let
}
} else {
try {
seerrApi.api.usersApi.authMeGet()
} catch (ex: Exception) {
Timber.w(ex, "Error logging into %s", server.url)
seerrServerRepository.clear()
return@let
}
} }
} seerrServerRepository.set(server, seerrUser, userConfig)
seerrServerRepository.set(server, seerrUser)
} }
} }
} }

View file

@ -163,10 +163,12 @@ fun ExpandablePlayButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
mirrorIcon: Boolean = false, mirrorIcon: Boolean = false,
enabled: Boolean = true,
) { ) {
val isFocused = interactionSource.collectIsFocusedAsState().value val isFocused = interactionSource.collectIsFocusedAsState().value
Button( Button(
onClick = { onClick.invoke(resume) }, onClick = { onClick.invoke(resume) },
enabled = enabled,
modifier = modifier =
modifier.requiredSizeIn( modifier.requiredSizeIn(
minWidth = MinButtonSize, minWidth = MinButtonSize,
@ -213,10 +215,12 @@ fun ExpandableFaButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
iconColor: Color = Color.Unspecified, iconColor: Color = Color.Unspecified,
enabled: Boolean = true,
) { ) {
val isFocused = interactionSource.collectIsFocusedAsState().value val isFocused = interactionSource.collectIsFocusedAsState().value
Button( Button(
onClick = onClick, onClick = onClick,
enabled = enabled,
modifier = modifier =
modifier.requiredSizeIn( modifier.requiredSizeIn(
minWidth = MinButtonSize, minWidth = MinButtonSize,

View file

@ -15,6 +15,7 @@ import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -42,8 +43,11 @@ import com.github.damontecres.wholphin.data.model.LocalTrailer
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.RemoteTrailer import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.SeerrAvailability import com.github.damontecres.wholphin.data.model.SeerrAvailability
import com.github.damontecres.wholphin.data.model.SeerrPermission
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.data.model.hasPermission
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.SeerrUserConfig
import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.services.TrailerService
import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.Cards
import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
@ -89,6 +93,7 @@ fun DiscoverMovieDetails(
val similar by viewModel.similar.observeAsState(listOf()) val similar by viewModel.similar.observeAsState(listOf())
val recommended by viewModel.similar.observeAsState(listOf()) val recommended by viewModel.similar.observeAsState(listOf())
val loading by viewModel.loading.observeAsState(LoadingState.Loading) val loading by viewModel.loading.observeAsState(LoadingState.Loading)
val userConfig by viewModel.userConfig.collectAsState(null)
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(null) } var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
@ -117,6 +122,7 @@ fun DiscoverMovieDetails(
DiscoverMovieDetailsContent( DiscoverMovieDetailsContent(
preferences = preferences, preferences = preferences,
movie = movie, movie = movie,
userConfig = userConfig,
rating = rating, rating = rating,
people = people, people = people,
trailers = trailers, trailers = trailers,
@ -214,6 +220,7 @@ private const val RECOMMENDED_ROW = SIMILAR_ROW + 1
@Composable @Composable
fun DiscoverMovieDetailsContent( fun DiscoverMovieDetailsContent(
preferences: UserPreferences, preferences: UserPreferences,
userConfig: SeerrUserConfig?,
movie: MovieDetails, movie: MovieDetails,
rating: DiscoverRating?, rating: DiscoverRating?,
people: List<Person>, people: List<Person>,
@ -266,6 +273,15 @@ fun DiscoverMovieDetailsContent(
.fillMaxWidth() .fillMaxWidth()
.padding(top = 32.dp, bottom = 16.dp), .padding(top = 32.dp, bottom = 16.dp),
) )
val canCancel =
remember(movie, userConfig) {
(
// User requested this
userConfig.hasPermission(SeerrPermission.REQUEST) &&
movie.mediaInfo?.requests?.any { it.requestedBy?.id == userConfig?.id } == true
) ||
userConfig.hasPermission(SeerrPermission.MANAGE_REQUESTS)
}
ExpandableDiscoverButtons( ExpandableDiscoverButtons(
availability = availability =
SeerrAvailability.from(movie.mediaInfo?.status) SeerrAvailability.from(movie.mediaInfo?.status)
@ -282,6 +298,8 @@ fun DiscoverMovieDetailsContent(
} }
} }
}, },
canRequest = userConfig.hasPermission(SeerrPermission.REQUEST),
canCancel = canCancel,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.data.model.RemoteTrailer
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SeerrServerRepository
import com.github.damontecres.wholphin.services.SeerrService import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
@ -31,6 +32,7 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -47,6 +49,7 @@ class DiscoverMovieViewModel
private val backdropService: BackdropService, private val backdropService: BackdropService,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val seerrService: SeerrService, val seerrService: SeerrService,
private val seerrServerRepository: SeerrServerRepository,
@Assisted val item: DiscoverItem, @Assisted val item: DiscoverItem,
) : ViewModel() { ) : ViewModel() {
@AssistedFactory @AssistedFactory
@ -63,6 +66,8 @@ class DiscoverMovieViewModel
val similar = MutableLiveData<List<DiscoverItem>>(listOf()) val similar = MutableLiveData<List<DiscoverItem>>(listOf())
val recommended = MutableLiveData<List<DiscoverItem>>(listOf()) val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
val userConfig = seerrServerRepository.current.map { it?.config }
init { init {
init() init()
} }

View file

@ -15,6 +15,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -41,8 +42,11 @@ import com.github.damontecres.wholphin.data.model.DiscoverItem
import com.github.damontecres.wholphin.data.model.DiscoverRating import com.github.damontecres.wholphin.data.model.DiscoverRating
import com.github.damontecres.wholphin.data.model.Person import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.SeerrAvailability import com.github.damontecres.wholphin.data.model.SeerrAvailability
import com.github.damontecres.wholphin.data.model.SeerrPermission
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.data.model.hasPermission
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.SeerrUserConfig
import com.github.damontecres.wholphin.services.TrailerService import com.github.damontecres.wholphin.services.TrailerService
import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard
import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.cards.ItemRow
@ -88,6 +92,7 @@ fun DiscoverSeriesDetails(
val people by viewModel.people.observeAsState(listOf()) val people by viewModel.people.observeAsState(listOf())
val similar by viewModel.similar.observeAsState(listOf()) val similar by viewModel.similar.observeAsState(listOf())
val recommended by viewModel.recommended.observeAsState(listOf()) val recommended by viewModel.recommended.observeAsState(listOf())
val userConfig by viewModel.userConfig.collectAsState(null)
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) } var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
var seasonDialog by remember { mutableStateOf<DialogParams?>(null) } var seasonDialog by remember { mutableStateOf<DialogParams?>(null) }
@ -109,6 +114,7 @@ fun DiscoverSeriesDetails(
DiscoverSeriesDetailsContent( DiscoverSeriesDetailsContent(
preferences = preferences, preferences = preferences,
series = item, series = item,
userConfig = userConfig,
rating = rating, rating = rating,
seasons = seasons, seasons = seasons,
people = people, people = people,
@ -192,6 +198,7 @@ private const val RECOMMENDED_ROW = SIMILAR_ROW + 1
@Composable @Composable
fun DiscoverSeriesDetailsContent( fun DiscoverSeriesDetailsContent(
preferences: UserPreferences, preferences: UserPreferences,
userConfig: SeerrUserConfig?,
series: TvDetails, series: TvDetails,
rating: DiscoverRating?, rating: DiscoverRating?,
seasons: List<Season>, seasons: List<Season>,
@ -248,6 +255,15 @@ fun DiscoverSeriesDetailsContent(
.bringIntoViewRequester(bringIntoViewRequester) .bringIntoViewRequester(bringIntoViewRequester)
.padding(top = 32.dp, bottom = 16.dp), .padding(top = 32.dp, bottom = 16.dp),
) )
val canCancel =
remember(series, userConfig) {
(
// User requested this
userConfig.hasPermission(SeerrPermission.REQUEST) &&
series.mediaInfo?.requests?.any { it.requestedBy?.id == userConfig?.id } == true
) ||
userConfig.hasPermission(SeerrPermission.MANAGE_REQUESTS)
}
ExpandableDiscoverButtons( ExpandableDiscoverButtons(
availability = availability =
SeerrAvailability.from(series.mediaInfo?.status) SeerrAvailability.from(series.mediaInfo?.status)
@ -264,6 +280,8 @@ fun DiscoverSeriesDetailsContent(
} }
} }
}, },
canRequest = userConfig.hasPermission(SeerrPermission.REQUEST),
canCancel = canCancel,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.data.model.Person
import com.github.damontecres.wholphin.data.model.Trailer import com.github.damontecres.wholphin.data.model.Trailer
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SeerrServerRepository
import com.github.damontecres.wholphin.services.SeerrService import com.github.damontecres.wholphin.services.SeerrService
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
@ -29,6 +30,7 @@ import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -45,6 +47,7 @@ class DiscoverSeriesViewModel
private val backdropService: BackdropService, private val backdropService: BackdropService,
val serverRepository: ServerRepository, val serverRepository: ServerRepository,
val seerrService: SeerrService, val seerrService: SeerrService,
private val seerrServerRepository: SeerrServerRepository,
@Assisted val item: DiscoverItem, @Assisted val item: DiscoverItem,
) : ViewModel() { ) : ViewModel() {
@AssistedFactory @AssistedFactory
@ -62,6 +65,8 @@ class DiscoverSeriesViewModel
val similar = MutableLiveData<List<DiscoverItem>>(listOf()) val similar = MutableLiveData<List<DiscoverItem>>(listOf())
val recommended = MutableLiveData<List<DiscoverItem>>(listOf()) val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
val userConfig = seerrServerRepository.current.map { it?.config }
init { init {
init() init()
} }

View file

@ -24,6 +24,8 @@ import kotlin.time.Duration
@Composable @Composable
fun ExpandableDiscoverButtons( fun ExpandableDiscoverButtons(
canRequest: Boolean,
canCancel: Boolean,
availability: SeerrAvailability, availability: SeerrAvailability,
requestOnClick: () -> Unit, requestOnClick: () -> Unit,
cancelOnClick: () -> Unit, cancelOnClick: () -> Unit,
@ -69,11 +71,11 @@ fun ExpandableDiscoverButtons(
SeerrAvailability.DELETED -> R.string.fa_video // TODO SeerrAvailability.DELETED -> R.string.fa_video // TODO
} }
item("first") { item("first") {
ExpandableFaButton( ExpandableFaButton(
title = text, title = text,
iconStringRes = icon, iconStringRes = icon,
enabled = if (availability == SeerrAvailability.UNKNOWN) canRequest else true,
onClick = { onClick = {
when (availability) { when (availability) {
SeerrAvailability.UNKNOWN -> { SeerrAvailability.UNKNOWN -> {
@ -83,7 +85,7 @@ fun ExpandableDiscoverButtons(
SeerrAvailability.PENDING, SeerrAvailability.PENDING,
SeerrAvailability.PROCESSING, SeerrAvailability.PROCESSING,
-> { -> {
cancelOnClick.invoke() // TODO?
} }
SeerrAvailability.PARTIALLY_AVAILABLE, SeerrAvailability.PARTIALLY_AVAILABLE,
@ -112,6 +114,7 @@ fun ExpandableDiscoverButtons(
icon = Icons.Default.Delete, icon = Icons.Default.Delete,
onClick = { cancelOnClick.invoke() }, onClick = { cancelOnClick.invoke() },
resume = Duration.ZERO, resume = Duration.ZERO,
enabled = canCancel,
modifier = modifier =
Modifier Modifier
.onFocusChanged(buttonOnFocusChanged), .onFocusChanged(buttonOnFocusChanged),