From 759bc0c88b647702cda1a87db6a00e51159b728b Mon Sep 17 00:00:00 2001 From: Damontecres Date: Sat, 27 Dec 2025 18:17:32 -0500 Subject: [PATCH] WIP permissions --- .../wholphin/data/model/SeerrPermission.kt | 49 ++++++++++ .../services/SeerrServerRepository.kt | 89 +++++++++---------- .../wholphin/ui/components/PlayButtons.kt | 4 + .../detail/discover/DiscoverMovieDetails.kt | 18 ++++ .../detail/discover/DiscoverMovieViewModel.kt | 5 ++ .../detail/discover/DiscoverSeriesDetails.kt | 18 ++++ .../discover/DiscoverSeriesViewModel.kt | 5 ++ .../discover/ExpandableDiscoverButtons.kt | 7 +- 8 files changed, 144 insertions(+), 51 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt new file mode 100644 index 00000000..1e9f963d --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/SeerrPermission.kt @@ -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) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt index 1c1c9335..871f6b47 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/SeerrServerRepository.kt @@ -18,10 +18,11 @@ import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.util.LoadingState import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import okhttp3.OkHttpClient import timber.log.Timber @@ -37,20 +38,10 @@ class SeerrServerRepository private val serverRepository: ServerRepository, @param:StandardOkHttpClient private val okHttpClient: OkHttpClient, ) { - private val _currentServer = MutableStateFlow(null) - val currentServer: StateFlow = _currentServer - - private val _currentUser = MutableStateFlow(null) - val currentUser: StateFlow = _currentUser - - val current = - currentServer.combine(currentUser) { server, user -> - if (server != null && user != null) { - CurrentSeerr(server, user) - } else { - null - } - } + private val _current = MutableStateFlow(null) + val current: StateFlow = _current + val currentServer: Flow = current.map { it?.server } + val currentUser: Flow = current.map { it?.user } /** * Whether Seerr integration is currently active of not @@ -58,20 +49,17 @@ class SeerrServerRepository suspend fun active(): Boolean = current.firstOrNull() != null && seerrApi.active fun clear() { - _currentServer.update { null } - _currentUser.update { null } + _current.update { null } seerrApi.update("", null) } suspend fun set( server: SeerrServer, user: SeerrUser, + userConfig: SeerrUserConfig, ) { - _currentServer.update { - _currentUser.update { - user - } - server + _current.update { + CurrentSeerr(server, user, userConfig) } } @@ -99,12 +87,8 @@ class SeerrServerRepository seerrServerDao.addUser(user) seerrApi.update(server.url, apiKey) - _currentServer.update { - _currentUser.update { - user - } - server - } + val userConfig = seerrApi.api.usersApi.authMeGet() + set(server, user, userConfig) } } } @@ -124,7 +108,7 @@ class SeerrServerRepository serverRepository.currentUser.value?.let { jellyfinUser -> // TODO Need to update server early so that cookies are saved seerrApi.update(server.url, null) - login(seerrApi.api, authMethod, username, password) + val userConfig = login(seerrApi.api, authMethod, username, password) val user = SeerrUser( @@ -136,12 +120,7 @@ class SeerrServerRepository credential = null, ) seerrServerDao.addUser(user) - _currentServer.update { - _currentUser.update { - user - } - server - } + set(server, user, userConfig) } } } @@ -163,9 +142,12 @@ class SeerrServerRepository } } +typealias SeerrUserConfig = User + data class CurrentSeerr( val server: SeerrServer, val user: SeerrUser, + val config: SeerrUserConfig, ) private suspend fun login( @@ -224,21 +206,30 @@ class UserSwitchListener if (server != null) { Timber.i("Found a seerr user & server") seerrApi.update(server.url, seerrUser.credential) - if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) { - try { - login( - seerrApi.api, - seerrUser.authMethod, - seerrUser.username, - seerrUser.password, - ) - } catch (ex: Exception) { - Timber.w(ex, "Error logging into %s", server.url) - seerrServerRepository.clear() - return@let + val userConfig = + if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) { + try { + login( + seerrApi.api, + seerrUser.authMethod, + seerrUser.username, + seerrUser.password, + ) + } catch (ex: Exception) { + Timber.w(ex, "Error logging into %s", server.url) + 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) + seerrServerRepository.set(server, seerrUser, userConfig) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt index 0f99f8bc..ec615a1d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/PlayButtons.kt @@ -163,10 +163,12 @@ fun ExpandablePlayButton( modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, mirrorIcon: Boolean = false, + enabled: Boolean = true, ) { val isFocused = interactionSource.collectIsFocusedAsState().value Button( onClick = { onClick.invoke(resume) }, + enabled = enabled, modifier = modifier.requiredSizeIn( minWidth = MinButtonSize, @@ -213,10 +215,12 @@ fun ExpandableFaButton( modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, iconColor: Color = Color.Unspecified, + enabled: Boolean = true, ) { val isFocused = interactionSource.collectIsFocusedAsState().value Button( onClick = onClick, + enabled = enabled, modifier = modifier.requiredSizeIn( minWidth = MinButtonSize, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt index 4a099c0e..c1999f04 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieDetails.kt @@ -15,6 +15,7 @@ import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState 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.RemoteTrailer 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.hasPermission 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.ui.Cards import com.github.damontecres.wholphin.ui.cards.DiscoverItemCard @@ -89,6 +93,7 @@ fun DiscoverMovieDetails( val similar by viewModel.similar.observeAsState(listOf()) val recommended by viewModel.similar.observeAsState(listOf()) val loading by viewModel.loading.observeAsState(LoadingState.Loading) + val userConfig by viewModel.userConfig.collectAsState(null) var overviewDialog by remember { mutableStateOf(null) } var moreDialog by remember { mutableStateOf(null) } @@ -117,6 +122,7 @@ fun DiscoverMovieDetails( DiscoverMovieDetailsContent( preferences = preferences, movie = movie, + userConfig = userConfig, rating = rating, people = people, trailers = trailers, @@ -214,6 +220,7 @@ private const val RECOMMENDED_ROW = SIMILAR_ROW + 1 @Composable fun DiscoverMovieDetailsContent( preferences: UserPreferences, + userConfig: SeerrUserConfig?, movie: MovieDetails, rating: DiscoverRating?, people: List, @@ -266,6 +273,15 @@ fun DiscoverMovieDetailsContent( .fillMaxWidth() .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( availability = SeerrAvailability.from(movie.mediaInfo?.status) @@ -282,6 +298,8 @@ fun DiscoverMovieDetailsContent( } } }, + canRequest = userConfig.hasPermission(SeerrPermission.REQUEST), + canCancel = canCancel, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt index c4a33928..3ae97ba7 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverMovieViewModel.kt @@ -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.services.BackdropService 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.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.launchIO @@ -31,6 +32,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient @@ -47,6 +49,7 @@ class DiscoverMovieViewModel private val backdropService: BackdropService, val serverRepository: ServerRepository, val seerrService: SeerrService, + private val seerrServerRepository: SeerrServerRepository, @Assisted val item: DiscoverItem, ) : ViewModel() { @AssistedFactory @@ -63,6 +66,8 @@ class DiscoverMovieViewModel val similar = MutableLiveData>(listOf()) val recommended = MutableLiveData>(listOf()) + val userConfig = seerrServerRepository.current.map { it?.config } + init { init() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt index 3e69102d..ce71fa3a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesDetails.kt @@ -15,6 +15,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState 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.Person 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.hasPermission 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.ui.cards.DiscoverItemCard import com.github.damontecres.wholphin.ui.cards.ItemRow @@ -88,6 +92,7 @@ fun DiscoverSeriesDetails( val people by viewModel.people.observeAsState(listOf()) val similar by viewModel.similar.observeAsState(listOf()) val recommended by viewModel.recommended.observeAsState(listOf()) + val userConfig by viewModel.userConfig.collectAsState(null) var overviewDialog by remember { mutableStateOf(null) } var seasonDialog by remember { mutableStateOf(null) } @@ -109,6 +114,7 @@ fun DiscoverSeriesDetails( DiscoverSeriesDetailsContent( preferences = preferences, series = item, + userConfig = userConfig, rating = rating, seasons = seasons, people = people, @@ -192,6 +198,7 @@ private const val RECOMMENDED_ROW = SIMILAR_ROW + 1 @Composable fun DiscoverSeriesDetailsContent( preferences: UserPreferences, + userConfig: SeerrUserConfig?, series: TvDetails, rating: DiscoverRating?, seasons: List, @@ -248,6 +255,15 @@ fun DiscoverSeriesDetailsContent( .bringIntoViewRequester(bringIntoViewRequester) .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( availability = SeerrAvailability.from(series.mediaInfo?.status) @@ -264,6 +280,8 @@ fun DiscoverSeriesDetailsContent( } } }, + canRequest = userConfig.hasPermission(SeerrPermission.REQUEST), + canCancel = canCancel, modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt index 41f91f62..2f8fb5e0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/DiscoverSeriesViewModel.kt @@ -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.services.BackdropService 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.ui.launchIO import com.github.damontecres.wholphin.ui.nav.Destination @@ -29,6 +30,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.async +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient @@ -45,6 +47,7 @@ class DiscoverSeriesViewModel private val backdropService: BackdropService, val serverRepository: ServerRepository, val seerrService: SeerrService, + private val seerrServerRepository: SeerrServerRepository, @Assisted val item: DiscoverItem, ) : ViewModel() { @AssistedFactory @@ -62,6 +65,8 @@ class DiscoverSeriesViewModel val similar = MutableLiveData>(listOf()) val recommended = MutableLiveData>(listOf()) + val userConfig = seerrServerRepository.current.map { it?.config } + init { init() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/ExpandableDiscoverButtons.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/ExpandableDiscoverButtons.kt index 8da229ca..5b6b44c0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/ExpandableDiscoverButtons.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/discover/ExpandableDiscoverButtons.kt @@ -24,6 +24,8 @@ import kotlin.time.Duration @Composable fun ExpandableDiscoverButtons( + canRequest: Boolean, + canCancel: Boolean, availability: SeerrAvailability, requestOnClick: () -> Unit, cancelOnClick: () -> Unit, @@ -69,11 +71,11 @@ fun ExpandableDiscoverButtons( SeerrAvailability.DELETED -> R.string.fa_video // TODO } - item("first") { ExpandableFaButton( title = text, iconStringRes = icon, + enabled = if (availability == SeerrAvailability.UNKNOWN) canRequest else true, onClick = { when (availability) { SeerrAvailability.UNKNOWN -> { @@ -83,7 +85,7 @@ fun ExpandableDiscoverButtons( SeerrAvailability.PENDING, SeerrAvailability.PROCESSING, -> { - cancelOnClick.invoke() + // TODO? } SeerrAvailability.PARTIALLY_AVAILABLE, @@ -112,6 +114,7 @@ fun ExpandableDiscoverButtons( icon = Icons.Default.Delete, onClick = { cancelOnClick.invoke() }, resume = Duration.ZERO, + enabled = canCancel, modifier = Modifier .onFocusChanged(buttonOnFocusChanged),