mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
WIP permissions
This commit is contained in:
parent
bb55dc744a
commit
759bc0c88b
8 changed files with 144 additions and 51 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue