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 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<SeerrServer?>(null)
val currentServer: StateFlow<SeerrServer?> = _currentServer
private val _currentUser = MutableStateFlow<SeerrUser?>(null)
val currentUser: StateFlow<SeerrUser?> = _currentUser
val current =
currentServer.combine(currentUser) { server, user ->
if (server != null && user != null) {
CurrentSeerr(server, user)
} else {
null
}
}
private val _current = MutableStateFlow<CurrentSeerr?>(null)
val current: StateFlow<CurrentSeerr?> = _current
val currentServer: Flow<SeerrServer?> = current.map { it?.server }
val currentUser: Flow<SeerrUser?> = 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,6 +206,7 @@ class UserSwitchListener
if (server != null) {
Timber.i("Found a seerr user & server")
seerrApi.update(server.url, seerrUser.credential)
val userConfig =
if (seerrUser.authMethod != SeerrAuthMethod.API_KEY) {
try {
login(
@ -237,8 +220,16 @@ class UserSwitchListener
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)
}
}
}

View file

@ -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,

View file

@ -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<ItemDetailsDialogInfo?>(null) }
var moreDialog by remember { mutableStateOf<DialogParams?>(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<Person>,
@ -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()

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.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<List<DiscoverItem>>(listOf())
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
val userConfig = seerrServerRepository.current.map { it?.config }
init {
init()
}

View file

@ -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<ItemDetailsDialogInfo?>(null) }
var seasonDialog by remember { mutableStateOf<DialogParams?>(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<Season>,
@ -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()

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.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<List<DiscoverItem>>(listOf())
val recommended = MutableLiveData<List<DiscoverItem>>(listOf())
val userConfig = seerrServerRepository.current.map { it?.config }
init {
init()
}

View file

@ -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),