mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge branch 'main' into fea/music
This commit is contained in:
commit
11587c5fcf
27 changed files with 614 additions and 139 deletions
|
|
@ -47,7 +47,7 @@ interface SeerrServerDao {
|
|||
suspend fun deleteUser(
|
||||
serverId: Int,
|
||||
jellyfinUserRowId: Int,
|
||||
)
|
||||
): Int
|
||||
|
||||
suspend fun deleteUser(user: SeerrUser) = deleteUser(user.serverId, user.jellyfinUserRowId)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
|
@ -40,11 +41,16 @@ class MediaManagementService
|
|||
val deletedItemFlow: SharedFlow<DeletedItem> = _deletedItemFlow
|
||||
|
||||
suspend fun canDelete(item: BaseItem): Boolean {
|
||||
val appPreferences = userPreferencesService.getCurrent().appPreferences
|
||||
return canDelete(item, appPreferences)
|
||||
}
|
||||
|
||||
fun canDelete(
|
||||
item: BaseItem,
|
||||
appPreferences: AppPreferences,
|
||||
): Boolean {
|
||||
Timber.v("canDelete %s: %s", item.id, item.canDelete)
|
||||
val enabled =
|
||||
userPreferencesService
|
||||
.getCurrent()
|
||||
.appPreferences.interfacePreferences.enableMediaManagement
|
||||
val enabled = appPreferences.interfacePreferences.enableMediaManagement
|
||||
return enabled &&
|
||||
item.canDelete &&
|
||||
if (item.type == BaseItemKind.RECORDING) {
|
||||
|
|
|
|||
|
|
@ -158,33 +158,58 @@ class RefreshRateService
|
|||
if (refreshRateSwitch) {
|
||||
displayModes
|
||||
.filterNot { it.physicalHeight < streamHeight || it.physicalWidth < streamWidth }
|
||||
.filter {
|
||||
it.refreshRateRounded % streamRate == 0 || // Exact multiple
|
||||
it.refreshRateRounded == (streamRate * 2.5).roundToInt() // eg 24 & 60fps
|
||||
}
|
||||
.filter { frameRateMatches(it.refreshRateRounded, streamRate) }
|
||||
} else {
|
||||
displayModes
|
||||
.filterNot { it.physicalHeight < streamHeight || it.physicalWidth < streamWidth }
|
||||
}
|
||||
// Timber.v("display modes candidates: %s", candidates.joinToString("\n"))
|
||||
return if (!resolutionSwitch) {
|
||||
candidates.maxByOrNull { it.physicalWidth * it.physicalHeight }
|
||||
candidates
|
||||
.filter { it.refreshRateRounded == streamRate }
|
||||
.maxByOrNull { it.physicalWidth * it.physicalHeight }
|
||||
?: candidates.maxByOrNull { it.physicalWidth * it.physicalHeight }
|
||||
} else {
|
||||
// Exact resolution & frame rate
|
||||
candidates.firstOrNull {
|
||||
it.physicalWidth == streamWidth &&
|
||||
it.physicalHeight == streamHeight &&
|
||||
it.refreshRateRounded == streamRate
|
||||
}
|
||||
?: candidates.firstOrNull {
|
||||
it.physicalWidth == streamWidth &&
|
||||
// Next highest resolution, exact frame rate
|
||||
?: candidates.lastOrNull {
|
||||
it.physicalWidth >= streamWidth &&
|
||||
it.physicalHeight >= streamHeight &&
|
||||
it.refreshRateRounded == streamRate
|
||||
}
|
||||
// Exact resolution, acceptable frame rate
|
||||
?: candidates.lastOrNull {
|
||||
it.physicalWidth == streamWidth &&
|
||||
it.physicalHeight == streamHeight &&
|
||||
frameRateMatches(it.refreshRateRounded, streamRate)
|
||||
}
|
||||
// Next highest resolution, acceptable frame rate
|
||||
?: candidates.lastOrNull {
|
||||
it.physicalWidth >= streamWidth &&
|
||||
it.physicalHeight >= streamHeight &&
|
||||
frameRateMatches(it.refreshRateRounded, streamRate)
|
||||
}
|
||||
// Fall back to largest resolution, exact frame rate
|
||||
?: candidates
|
||||
.filter { it.refreshRateRounded == streamRate }
|
||||
.maxByOrNull { it.physicalWidth * it.physicalHeight }
|
||||
// Finally, just the highest resolution
|
||||
?: candidates.maxByOrNull { it.physicalWidth * it.physicalHeight }
|
||||
}
|
||||
}
|
||||
|
||||
fun frameRateMatches(
|
||||
refreshRateRounded: Int,
|
||||
streamRate: Int,
|
||||
): Boolean {
|
||||
return refreshRateRounded % streamRate == 0 || // Exact multiple
|
||||
refreshRateRounded == (streamRate * 2.5).roundToInt() // eg 24 & 60fps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.api.seerr.model.PublicSettings
|
|||
import com.github.damontecres.wholphin.api.seerr.model.User
|
||||
import com.github.damontecres.wholphin.data.SeerrServerDao
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||
import com.github.damontecres.wholphin.data.model.SeerrPermission
|
||||
import com.github.damontecres.wholphin.data.model.SeerrServer
|
||||
|
|
@ -24,8 +25,10 @@ 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.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import okhttp3.OkHttpClient
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
|
@ -162,7 +165,7 @@ class SeerrServerRepository
|
|||
apiKey,
|
||||
okHttpClient
|
||||
.newBuilder()
|
||||
.connectTimeout(5.seconds)
|
||||
.connectTimeout(2.seconds)
|
||||
.readTimeout(6.seconds)
|
||||
.build(),
|
||||
)
|
||||
|
|
@ -170,10 +173,11 @@ class SeerrServerRepository
|
|||
return LoadingState.Success
|
||||
}
|
||||
|
||||
suspend fun removeServer() {
|
||||
val current = (_connection.value as? SeerrConnectionStatus.Success)?.current ?: return
|
||||
seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId)
|
||||
suspend fun removeServerForCurrentUser(): Boolean {
|
||||
val current = current.firstOrNull() ?: return false
|
||||
val rows = seerrServerDao.deleteUser(current.server.id, current.user.jellyfinUserRowId)
|
||||
clear()
|
||||
return rows > 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -266,6 +270,14 @@ class UserSwitchListener
|
|||
seerrServerRepository.clear()
|
||||
homeSettingsService.currentSettings.update { HomePageResolvedSettings.EMPTY }
|
||||
if (user != null) {
|
||||
switchUser(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun switchUser(user: JellyfinUser) =
|
||||
supervisorScope {
|
||||
// Check for home settings
|
||||
launchIO {
|
||||
homeSettingsService.loadCurrentSettings(user.id)
|
||||
|
|
@ -293,7 +305,11 @@ class UserSwitchListener
|
|||
} else {
|
||||
seerrApi.api.usersApi.authMeGet()
|
||||
}
|
||||
seerrServerRepository.set(server, seerrUser, userConfig)
|
||||
seerrServerRepository.set(
|
||||
server,
|
||||
seerrUser,
|
||||
userConfig,
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(
|
||||
ex,
|
||||
|
|
@ -307,6 +323,3 @@ class UserSwitchListener
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
|||
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
||||
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
|
|
@ -65,6 +66,7 @@ import com.github.damontecres.wholphin.services.MediaReportService
|
|||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.services.deleteItem
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
|
|
@ -77,6 +79,7 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
|||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
||||
|
|
@ -84,6 +87,7 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
|||
import com.github.damontecres.wholphin.ui.playback.scale
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.ui.util.FilterUtils
|
||||
|
|
@ -100,6 +104,9 @@ import dagger.assisted.AssistedInject
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
|
|
@ -202,18 +209,34 @@ class CollectionFolderViewModel
|
|||
loading.setValueOnMain(DataLoadingState.Error(ex))
|
||||
}
|
||||
}
|
||||
viewModelScope.launchDefault {
|
||||
mediaManagementService.deletedItemFlow.collect { deletedItem ->
|
||||
mediaManagementService.deletedItemFlow
|
||||
.onEach { deletedItem ->
|
||||
refreshAfterDelete(position, deletedItem.item)
|
||||
}.catch { ex ->
|
||||
Timber.e(ex, "Error refreshing after deleted item")
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
private suspend fun refreshAfterDelete(
|
||||
position: Int,
|
||||
deletedItem: BaseItem,
|
||||
) {
|
||||
try {
|
||||
val pager =
|
||||
((loading.value as? DataLoadingState.Success)?.data as? ApiRequestPager<*>)
|
||||
position.let {
|
||||
Timber.v("Item deleted: position=%s, id=%s", it, deletedItem.item.id)
|
||||
Timber.v("Item deleted: position=%s, id=%s", it, itemId)
|
||||
val item = pager?.get(it)
|
||||
if (item?.id == deletedItem.item.id) {
|
||||
pager.refreshPagesAfter(position)
|
||||
}
|
||||
// Exact item deleted (eg a movie) or deleted item was within the series
|
||||
if (item?.id == deletedItem.id ||
|
||||
equalsNotNull(item?.data?.id, deletedItem.data.seriesId)
|
||||
) {
|
||||
pager?.refreshPagesAfter(position)
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error refreshing after deleted item %s", itemId)
|
||||
showToast(context, "Error refreshing after item deleted")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -491,6 +514,22 @@ class CollectionFolderViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteItem(
|
||||
index: Int,
|
||||
item: BaseItem,
|
||||
) {
|
||||
deleteItem(context, mediaManagementService, item) {
|
||||
viewModelScope.launchDefault {
|
||||
refreshAfterDelete(index, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun canDelete(
|
||||
item: BaseItem,
|
||||
appPreferences: AppPreferences,
|
||||
): Boolean = mediaManagementService.canDelete(item, appPreferences)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -578,6 +617,7 @@ fun CollectionFolderGrid(
|
|||
|
||||
var moreDialog by remember { mutableStateOf<Optional<PositionItem>>(Optional.absent()) }
|
||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||
var showDeleteDialog by remember { mutableStateOf<PositionItem?>(null) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
|
||||
when (val state = loading) {
|
||||
|
|
@ -713,6 +753,7 @@ fun CollectionFolderGrid(
|
|||
playbackPosition = item.playbackPosition,
|
||||
watched = item.played,
|
||||
favorite = item.favorite,
|
||||
canDelete = viewModel.canDelete(item, preferences.appPreferences),
|
||||
actions =
|
||||
MoreDialogActions(
|
||||
navigateTo = { viewModel.navigateTo(it) },
|
||||
|
|
@ -727,6 +768,9 @@ fun CollectionFolderGrid(
|
|||
showPlaylistDialog.makePresent(it)
|
||||
},
|
||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||
onClickDelete = {
|
||||
showDeleteDialog = PositionItem(position, item)
|
||||
},
|
||||
),
|
||||
),
|
||||
onDismissRequest = { moreDialog.makeAbsent() },
|
||||
|
|
@ -751,6 +795,16 @@ fun CollectionFolderGrid(
|
|||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
showDeleteDialog?.let { (position, item) ->
|
||||
ConfirmDeleteDialog(
|
||||
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
|
||||
onCancel = { showDeleteDialog = null },
|
||||
onConfirm = {
|
||||
viewModel.deleteItem(position, item)
|
||||
showDeleteDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
|
|
|
|||
|
|
@ -72,12 +72,14 @@ fun ExpandablePlayButtons(
|
|||
resumePosition: Duration,
|
||||
watched: Boolean,
|
||||
favorite: Boolean,
|
||||
canDelete: Boolean,
|
||||
trailers: List<Trailer>?,
|
||||
playOnClick: (position: Duration) -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
trailerOnClick: (Trailer) -> Unit,
|
||||
deleteOnClick: () -> Unit,
|
||||
buttonOnFocusChanged: (FocusState) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -158,6 +160,16 @@ fun ExpandablePlayButtons(
|
|||
)
|
||||
}
|
||||
}
|
||||
if (canDelete) {
|
||||
item("delete") {
|
||||
DeleteButton(
|
||||
onClick = deleteOnClick,
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged(buttonOnFocusChanged),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// More button
|
||||
item("more") {
|
||||
|
|
@ -424,6 +436,8 @@ private fun ExpandablePlayButtonsPreview() {
|
|||
buttonOnFocusChanged = {},
|
||||
trailers = listOf(),
|
||||
trailerOnClick = {},
|
||||
canDelete = true,
|
||||
deleteOnClick = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,11 +20,14 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.MediaManagementService
|
||||
import com.github.damontecres.wholphin.services.MediaReportService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.deleteItem
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
|
|
@ -32,6 +35,7 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
|||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
|
|
@ -52,6 +56,7 @@ abstract class RecommendedViewModel(
|
|||
val favoriteWatchManager: FavoriteWatchManager,
|
||||
val mediaReportService: MediaReportService,
|
||||
private val backdropService: BackdropService,
|
||||
private val mediaManagementService: MediaManagementService,
|
||||
) : ViewModel() {
|
||||
abstract fun init()
|
||||
|
||||
|
|
@ -119,6 +124,25 @@ abstract class RecommendedViewModel(
|
|||
}
|
||||
update(title, row)
|
||||
}
|
||||
|
||||
fun deleteItem(
|
||||
position: RowColumn,
|
||||
item: BaseItem,
|
||||
) {
|
||||
deleteItem(context, mediaManagementService, item) {
|
||||
viewModelScope.launchDefault {
|
||||
val row = rows.value.getOrNull(position.row)
|
||||
if (row is HomeRowLoadingState.Success) {
|
||||
(row.items as? ApiRequestPager<*>)?.refreshPagesAfter(position.column)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun canDelete(
|
||||
item: BaseItem,
|
||||
appPreferences: AppPreferences,
|
||||
): Boolean = mediaManagementService.canDelete(item, appPreferences)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -132,6 +156,7 @@ fun RecommendedContent(
|
|||
val context = LocalContext.current
|
||||
var moreDialog by remember { mutableStateOf<Optional<RowColumnItem>>(Optional.absent()) }
|
||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||
var showDeleteDialog by remember { mutableStateOf<RowColumnItem?>(null) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
|
||||
OneTimeLaunchedEffect {
|
||||
|
|
@ -198,6 +223,7 @@ fun RecommendedContent(
|
|||
playbackPosition = item.playbackPosition,
|
||||
watched = item.played,
|
||||
favorite = item.favorite,
|
||||
canDelete = viewModel.canDelete(item, preferences.appPreferences),
|
||||
actions =
|
||||
MoreDialogActions(
|
||||
navigateTo = { viewModel.navigationManager.navigateTo(it) },
|
||||
|
|
@ -212,6 +238,7 @@ fun RecommendedContent(
|
|||
showPlaylistDialog.makePresent(it)
|
||||
},
|
||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||
onClickDelete = { showDeleteDialog = RowColumnItem(position, item) },
|
||||
),
|
||||
),
|
||||
onDismissRequest = { moreDialog.makeAbsent() },
|
||||
|
|
@ -236,9 +263,19 @@ fun RecommendedContent(
|
|||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
showDeleteDialog?.let { (position, item) ->
|
||||
ConfirmDeleteDialog(
|
||||
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
|
||||
onCancel = { showDeleteDialog = null },
|
||||
onConfirm = {
|
||||
viewModel.deleteItem(position, item)
|
||||
showDeleteDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class RowColumnItem(
|
||||
data class RowColumnItem(
|
||||
val position: RowColumn,
|
||||
val item: BaseItem,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.MediaManagementService
|
||||
import com.github.damontecres.wholphin.services.MediaReportService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.SuggestionService
|
||||
|
|
@ -63,12 +64,14 @@ class RecommendedMovieViewModel
|
|||
favoriteWatchManager: FavoriteWatchManager,
|
||||
mediaReportService: MediaReportService,
|
||||
backdropService: BackdropService,
|
||||
mediaManagementService: MediaManagementService,
|
||||
) : RecommendedViewModel(
|
||||
context,
|
||||
navigationManager,
|
||||
favoriteWatchManager,
|
||||
mediaReportService,
|
||||
backdropService,
|
||||
mediaManagementService,
|
||||
) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
|||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.LatestNextUpService
|
||||
import com.github.damontecres.wholphin.services.MediaManagementService
|
||||
import com.github.damontecres.wholphin.services.MediaReportService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.SuggestionService
|
||||
|
|
@ -67,12 +68,14 @@ class RecommendedTvShowViewModel
|
|||
favoriteWatchManager: FavoriteWatchManager,
|
||||
mediaReportService: MediaReportService,
|
||||
backdropService: BackdropService,
|
||||
mediaManagementService: MediaManagementService,
|
||||
) : RecommendedViewModel(
|
||||
context,
|
||||
navigationManager,
|
||||
favoriteWatchManager,
|
||||
mediaReportService,
|
||||
backdropService,
|
||||
mediaManagementService,
|
||||
) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ data class MoreDialogActions(
|
|||
val onClickFavorite: (UUID, Boolean) -> Unit,
|
||||
val onClickAddPlaylist: (UUID) -> Unit,
|
||||
val onSendMediaInfo: (UUID) -> Unit,
|
||||
val onClickDelete: (BaseItem) -> Unit = {},
|
||||
val onClickDelete: (BaseItem) -> Unit,
|
||||
)
|
||||
|
||||
enum class ClearChosenStreams {
|
||||
|
|
@ -63,7 +63,7 @@ fun buildMoreDialogItems(
|
|||
watched: Boolean,
|
||||
favorite: Boolean,
|
||||
canClearChosenStreams: Boolean,
|
||||
canDelete: Boolean = false,
|
||||
canDelete: Boolean,
|
||||
actions: MoreDialogActions,
|
||||
onChooseVersion: () -> Unit,
|
||||
onChooseTracks: (MediaStreamType) -> Unit,
|
||||
|
|
@ -236,7 +236,7 @@ fun buildMoreDialogItemsForHome(
|
|||
playbackPosition: Duration,
|
||||
watched: Boolean,
|
||||
favorite: Boolean,
|
||||
canDelete: Boolean = false,
|
||||
canDelete: Boolean,
|
||||
actions: MoreDialogActions,
|
||||
): List<DialogItem> =
|
||||
buildList {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
|||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -102,15 +101,6 @@ fun DiscoverMovieDetails(
|
|||
val requestStr = stringResource(R.string.request)
|
||||
val request4kStr = stringResource(R.string.request_4k)
|
||||
|
||||
val moreActions =
|
||||
MoreDialogActions(
|
||||
navigateTo = viewModel::navigateTo,
|
||||
onClickWatch = { itemId, watched -> },
|
||||
onClickFavorite = { itemId, favorite -> },
|
||||
onClickAddPlaylist = { itemId -> },
|
||||
onSendMediaInfo = {},
|
||||
)
|
||||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> {
|
||||
ErrorMessage(state, modifier)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import com.github.damontecres.wholphin.data.ChosenStreams
|
|||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
|
|
@ -82,6 +83,7 @@ fun EpisodeDetails(
|
|||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var chooseVersion by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var showPlaylistDialog by remember { mutableStateOf<Optional<UUID>>(Optional.absent()) }
|
||||
var showDeleteDialog by remember { mutableStateOf<BaseItem?>(null) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
|
||||
val moreActions =
|
||||
|
|
@ -98,6 +100,7 @@ fun EpisodeDetails(
|
|||
showPlaylistDialog.makePresent(itemId)
|
||||
},
|
||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||
onClickDelete = { showDeleteDialog = it },
|
||||
)
|
||||
|
||||
when (val state = loading) {
|
||||
|
|
@ -215,6 +218,7 @@ fun EpisodeDetails(
|
|||
onClearChosenStreams = {
|
||||
viewModel.clearChosenStreams(chosenStreams)
|
||||
},
|
||||
canDelete = viewModel.canDelete,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
@ -224,6 +228,8 @@ fun EpisodeDetails(
|
|||
favoriteOnClick = {
|
||||
viewModel.setFavorite(ep.id, !ep.favorite)
|
||||
},
|
||||
canDelete = viewModel.canDelete,
|
||||
deleteOnClick = { showDeleteDialog = ep },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -276,6 +282,16 @@ fun EpisodeDetails(
|
|||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
showDeleteDialog?.let { item ->
|
||||
ConfirmDeleteDialog(
|
||||
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
|
||||
onCancel = { showDeleteDialog = null },
|
||||
onConfirm = {
|
||||
viewModel.deleteItem(item)
|
||||
showDeleteDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val HEADER_ROW = 0
|
||||
|
|
@ -290,6 +306,8 @@ fun EpisodeDetailsContent(
|
|||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
canDelete: Boolean,
|
||||
deleteOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -347,6 +365,8 @@ fun EpisodeDetailsContent(
|
|||
},
|
||||
trailers = null,
|
||||
trailerOnClick = {},
|
||||
canDelete = canDelete,
|
||||
deleteOnClick = deleteOnClick,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ import com.github.damontecres.wholphin.data.model.ItemPlayback
|
|||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.MediaManagementService
|
||||
import com.github.damontecres.wholphin.services.MediaReportService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||
import com.github.damontecres.wholphin.services.ThemeSongPlayer
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.services.deleteItem
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
|
|
@ -54,6 +56,7 @@ class EpisodeViewModel
|
|||
private val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val backdropService: BackdropService,
|
||||
private val mediaManagementService: MediaManagementService,
|
||||
@Assisted val itemId: UUID,
|
||||
) : ViewModel() {
|
||||
@AssistedFactory
|
||||
|
|
@ -65,6 +68,9 @@ class EpisodeViewModel
|
|||
val item = MutableLiveData<BaseItem?>(null)
|
||||
val chosenStreams = MutableLiveData<ChosenStreams?>(null)
|
||||
|
||||
var canDelete: Boolean = false
|
||||
private set
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
|
@ -95,6 +101,7 @@ class EpisodeViewModel
|
|||
) {
|
||||
val prefs = userPreferencesService.getCurrent()
|
||||
val item = fetchAndSetItem().await()
|
||||
canDelete = mediaManagementService.canDelete(item)
|
||||
val result = itemPlaybackRepository.getSelectedTracks(item.id, item, prefs)
|
||||
withContext(Dispatchers.Main) {
|
||||
this@EpisodeViewModel.item.value = item
|
||||
|
|
@ -199,4 +206,10 @@ class EpisodeViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteItem(item: BaseItem) {
|
||||
deleteItem(context, mediaManagementService, item) {
|
||||
navigationManager.goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,6 +315,8 @@ fun MovieDetails(
|
|||
onClickDiscover = { index, item ->
|
||||
viewModel.navigateTo(item.destination)
|
||||
},
|
||||
canDelete = viewModel.canDelete,
|
||||
deleteOnClick = { showDeleteDialog = movie },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -410,6 +412,8 @@ fun MovieDetailsContent(
|
|||
onLongClickSimilar: (Int, BaseItem) -> Unit,
|
||||
onClickExtra: (Int, ExtrasItem) -> Unit,
|
||||
onClickDiscover: (Int, DiscoverItem) -> Unit,
|
||||
canDelete: Boolean,
|
||||
deleteOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -472,6 +476,8 @@ fun MovieDetailsContent(
|
|||
position = TRAILER_ROW
|
||||
trailerOnClick.invoke(it)
|
||||
},
|
||||
canDelete = canDelete,
|
||||
deleteOnClick = deleteOnClick,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ fun FocusedEpisodeFooter(
|
|||
moreOnClick: () -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
canDelete: Boolean,
|
||||
deleteOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
buttonOnFocusChanged: (FocusState) -> Unit = {},
|
||||
) {
|
||||
|
|
@ -47,6 +49,8 @@ fun FocusedEpisodeFooter(
|
|||
buttonOnFocusChanged = buttonOnFocusChanged,
|
||||
trailers = null,
|
||||
trailerOnClick = {},
|
||||
canDelete = canDelete,
|
||||
deleteOnClick = deleteOnClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -584,6 +584,7 @@ fun SeriesDetailsContent(
|
|||
watched = item.played,
|
||||
favorite = item.favorite,
|
||||
actions = moreActions,
|
||||
canDelete = false,
|
||||
)
|
||||
moreDialog =
|
||||
DialogParams(
|
||||
|
|
|
|||
|
|
@ -383,6 +383,8 @@ fun SeriesOverview(
|
|||
),
|
||||
)
|
||||
},
|
||||
canDelete = { viewModel.canDelete(it, preferences.appPreferences) },
|
||||
deleteOnClick = { showDeleteDialog = it },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ fun SeriesOverviewContent(
|
|||
moreOnClick: () -> Unit,
|
||||
overviewOnClick: () -> Unit,
|
||||
personOnClick: (Person) -> Unit,
|
||||
canDelete: (BaseItem) -> Boolean,
|
||||
deleteOnClick: (BaseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -294,6 +296,8 @@ fun SeriesOverviewContent(
|
|||
}
|
||||
}
|
||||
},
|
||||
canDelete = canDelete.invoke(ep),
|
||||
deleteOnClick = { deleteOnClick.invoke(ep) },
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = 4.dp)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import com.github.damontecres.wholphin.data.model.DiscoverItem
|
|||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||
import com.github.damontecres.wholphin.data.model.Person
|
||||
import com.github.damontecres.wholphin.data.model.Trailer
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.ExtrasService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
|
|
@ -56,6 +57,9 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -229,6 +233,20 @@ class SeriesViewModel
|
|||
discovered.update { results }
|
||||
}
|
||||
}
|
||||
mediaManagementService.deletedItemFlow
|
||||
.onEach { deletedItem ->
|
||||
if (deletedItem.item.data.seriesId == seriesId) {
|
||||
Timber.d(
|
||||
"Item %s deleted from series %s",
|
||||
deletedItem.item.id,
|
||||
seriesId,
|
||||
)
|
||||
val seasons = getSeasons(item, seasonEpisodeIds?.seasonNumber).await()
|
||||
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
||||
}
|
||||
}.catch { ex ->
|
||||
Timber.e(ex, "Error refreshing after deleted item")
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -614,6 +632,11 @@ class SeriesViewModel
|
|||
}
|
||||
|
||||
suspend fun canDelete(item: BaseItem): Boolean = mediaManagementService.canDelete(item)
|
||||
|
||||
fun canDelete(
|
||||
item: BaseItem,
|
||||
appPreferences: AppPreferences,
|
||||
): Boolean = mediaManagementService.canDelete(item, appPreferences)
|
||||
}
|
||||
|
||||
sealed interface EpisodeList {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ import com.github.damontecres.wholphin.ui.cards.BannerCardWithTitle
|
|||
import com.github.damontecres.wholphin.ui.cards.GenreCard
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||
import com.github.damontecres.wholphin.ui.components.ConfirmDeleteDialog
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.EpisodeName
|
||||
|
|
@ -62,6 +63,7 @@ import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
|||
import com.github.damontecres.wholphin.ui.components.FocusableItemRow
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||
import com.github.damontecres.wholphin.ui.components.RowColumnItem
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||
|
|
@ -116,6 +118,7 @@ fun HomePage(
|
|||
LoadingState.Success -> {
|
||||
var dialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
|
||||
var showDeleteDialog by remember { mutableStateOf<RowColumnItem?>(null) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
var position by rememberPosition()
|
||||
HomePageContent(
|
||||
|
|
@ -136,6 +139,7 @@ fun HomePage(
|
|||
playbackPosition = item.playbackPosition,
|
||||
watched = item.played,
|
||||
favorite = item.favorite,
|
||||
canDelete = viewModel.canDelete(item, preferences.appPreferences),
|
||||
actions =
|
||||
MoreDialogActions(
|
||||
navigateTo = viewModel.navigationManager::navigateTo,
|
||||
|
|
@ -150,6 +154,9 @@ fun HomePage(
|
|||
showPlaylistDialog = itemId
|
||||
},
|
||||
onSendMediaInfo = viewModel.mediaReportService::sendReportFor,
|
||||
onClickDelete = {
|
||||
showDeleteDialog = RowColumnItem(position, item)
|
||||
},
|
||||
),
|
||||
)
|
||||
dialog =
|
||||
|
|
@ -190,6 +197,16 @@ fun HomePage(
|
|||
elevation = 3.dp,
|
||||
)
|
||||
}
|
||||
showDeleteDialog?.let { (position, item) ->
|
||||
ConfirmDeleteDialog(
|
||||
itemTitle = listOfNotNull(item.title, item.subtitle).joinToString(" - "),
|
||||
onCancel = { showDeleteDialog = null },
|
||||
onConfirm = {
|
||||
viewModel.deleteItem(position, item)
|
||||
showDeleteDialog = null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,21 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.data.model.HomeRowConfig
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.DatePlayedService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.HomePageResolvedSettings
|
||||
import com.github.damontecres.wholphin.services.HomeSettingsService
|
||||
import com.github.damontecres.wholphin.services.MediaManagementService
|
||||
import com.github.damontecres.wholphin.services.MediaReportService
|
||||
import com.github.damontecres.wholphin.services.NavDrawerService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.services.deleteItem
|
||||
import com.github.damontecres.wholphin.services.tvAccess
|
||||
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
|
|
@ -52,6 +57,7 @@ class HomeViewModel
|
|||
private val datePlayedService: DatePlayedService,
|
||||
private val backdropService: BackdropService,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val mediaManagementService: MediaManagementService,
|
||||
) : ViewModel() {
|
||||
private val _state = MutableStateFlow(HomeState.EMPTY)
|
||||
val state: StateFlow<HomeState> = _state
|
||||
|
|
@ -189,6 +195,36 @@ class HomeViewModel
|
|||
backdropService.submit(item)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteItem(
|
||||
position: RowColumn,
|
||||
item: BaseItem,
|
||||
) {
|
||||
deleteItem(context, mediaManagementService, item) {
|
||||
viewModelScope.launchDefault {
|
||||
val row = state.value.homeRows.getOrNull(position.row)
|
||||
if (row is HomeRowLoadingState.Success) {
|
||||
_state.update {
|
||||
val newRow =
|
||||
row.items.toMutableList().apply {
|
||||
removeAt(position.column)
|
||||
}
|
||||
it.copy(
|
||||
homeRows =
|
||||
it.homeRows.toMutableList().apply {
|
||||
set(position.row, row.copy(items = newRow))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun canDelete(
|
||||
item: BaseItem,
|
||||
appPreferences: AppPreferences,
|
||||
): Boolean = mediaManagementService.canDelete(item, appPreferences)
|
||||
}
|
||||
|
||||
data class HomeState(
|
||||
|
|
|
|||
|
|
@ -554,6 +554,7 @@ fun PreferencesContent(
|
|||
currentUsername = currentUser?.name,
|
||||
status = status,
|
||||
onSubmit = seerrVm::submitServer,
|
||||
onResetStatus = seerrVm::resetStatus,
|
||||
onDismissRequest = { seerrDialogMode = SeerrDialogMode.None },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import androidx.compose.foundation.focusGroup
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
|
@ -66,15 +65,19 @@ fun AddSeerrServerApiKey(
|
|||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
val labelWidth = 90.dp
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.url),
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.width(labelWidth)
|
||||
.padding(end = 8.dp),
|
||||
)
|
||||
EditTextBox(
|
||||
value = url,
|
||||
|
|
@ -105,7 +108,10 @@ fun AddSeerrServerApiKey(
|
|||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.api_key),
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.width(labelWidth)
|
||||
.padding(end = 8.dp),
|
||||
)
|
||||
EditTextBox(
|
||||
value = apiKey,
|
||||
|
|
@ -173,7 +179,7 @@ fun AddSeerrServerUsername(
|
|||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
val labelWidth = 90.dp
|
||||
Row(
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package com.github.damontecres.wholphin.ui.setup.seerr
|
||||
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.SeerrAuthMethod
|
||||
import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||
|
|
@ -20,6 +24,7 @@ fun AddSeerServerDialog(
|
|||
currentUsername: String?,
|
||||
status: LoadingState,
|
||||
onSubmit: (url: String, username: String, passwordOrApiKey: String, method: SeerrAuthMethod) -> Unit,
|
||||
onResetStatus: () -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
var authMethod by remember { mutableStateOf<SeerrAuthMethod?>(null) }
|
||||
|
|
@ -34,6 +39,7 @@ fun AddSeerServerDialog(
|
|||
-> {
|
||||
BasicDialog(
|
||||
onDismissRequest = { authMethod = null },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
AddSeerrServerUsername(
|
||||
onSubmit = { url, username, password ->
|
||||
|
|
@ -41,6 +47,7 @@ fun AddSeerServerDialog(
|
|||
},
|
||||
username = currentUsername ?: "",
|
||||
status = status,
|
||||
modifier = Modifier.widthIn(min = 320.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +61,7 @@ fun AddSeerServerDialog(
|
|||
onSubmit.invoke(url, "", apiKey, SeerrAuthMethod.API_KEY)
|
||||
},
|
||||
status = status,
|
||||
modifier = Modifier.widthIn(min = 320.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -61,7 +69,10 @@ fun AddSeerServerDialog(
|
|||
null -> {
|
||||
ChooseSeerrLoginType(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onChoose = { authMethod = it },
|
||||
onChoose = {
|
||||
onResetStatus.invoke()
|
||||
authMethod = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ class SwitchSeerrViewModel
|
|||
serverConnectionStatus.update { LoadingState.Error("Invalid URL", ex) }
|
||||
return@launchIO
|
||||
}
|
||||
var result: LoadingState = LoadingState.Error("No url")
|
||||
Timber.v("Urls to try: %s", urls)
|
||||
val results = mutableMapOf<HttpUrl, LoadingState>()
|
||||
for (url in urls) {
|
||||
Timber.d("Trying %s", url)
|
||||
result =
|
||||
try {
|
||||
seerrServerRepository.testConnection(
|
||||
authMethod = authMethod,
|
||||
|
|
@ -61,20 +61,24 @@ class SwitchSeerrViewModel
|
|||
username = username.takeIf { authMethod != SeerrAuthMethod.API_KEY },
|
||||
passwordOrApiKey = passwordOrApiKey,
|
||||
)
|
||||
results[url] = LoadingState.Success
|
||||
break
|
||||
} catch (ex: ClientException) {
|
||||
Timber.w(ex, "ClientException logging in")
|
||||
Timber.w(ex, "ClientException logging in %s", url)
|
||||
if (ex.statusCode == 401 || ex.statusCode == 403) {
|
||||
showToast(context, "Invalid credentials")
|
||||
result = LoadingState.Error("Invalid credentials", ex)
|
||||
break
|
||||
results[url] = LoadingState.Error("Invalid credentials", ex)
|
||||
} else {
|
||||
LoadingState.Error("Could not connect with URL")
|
||||
results[url] = LoadingState.Error("Could not connect with URL")
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.w(ex, "Exception logging in")
|
||||
LoadingState.Error(ex)
|
||||
Timber.w(ex, "ClientException logging in %s", url)
|
||||
results[url] = LoadingState.Error(ex)
|
||||
}
|
||||
if (result is LoadingState.Success) {
|
||||
}
|
||||
val result = results.filter { (url, state) -> state is LoadingState.Success }
|
||||
if (result.isNotEmpty()) {
|
||||
val url = result.keys.first()
|
||||
when (authMethod) {
|
||||
SeerrAuthMethod.LOCAL,
|
||||
SeerrAuthMethod.JELLYFIN,
|
||||
|
|
@ -94,19 +98,25 @@ class SwitchSeerrViewModel
|
|||
)
|
||||
}
|
||||
}
|
||||
break
|
||||
} else {
|
||||
val message =
|
||||
results
|
||||
.map { (url, state) ->
|
||||
val s = state as? LoadingState.Error
|
||||
"$url - ${s?.localizedMessage}"
|
||||
}.joinToString("\n")
|
||||
showToast(context, "Could not connect")
|
||||
serverConnectionStatus.update { LoadingState.Error(message) }
|
||||
}
|
||||
}
|
||||
if (result is LoadingState.Error) {
|
||||
showToast(context, "Error: ${result.message}")
|
||||
}
|
||||
serverConnectionStatus.update { result }
|
||||
}
|
||||
}
|
||||
|
||||
fun removeServer() {
|
||||
viewModelScope.launchIO {
|
||||
seerrServerRepository.removeServer()
|
||||
val result = seerrServerRepository.removeServerForCurrentUser()
|
||||
if (!result) {
|
||||
showToast(context, "Could not remove server")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,25 @@ class TestDisplayModeChoice {
|
|||
val UHD_30 = DisplayMode(4, 3840, 2160, 30f)
|
||||
val UHD_24 = DisplayMode(5, 3840, 2160, 24f)
|
||||
|
||||
val ALL_MODES = listOf(UHD_24, UHD_30, UHD_60, HD_24, HD_30, HD_60)
|
||||
val HD720_60 = DisplayMode(6, 1280, 720, 60f)
|
||||
val HD720_30 = DisplayMode(7, 1280, 720, 30f)
|
||||
val HD720_24 = DisplayMode(8, 1280, 720, 24f)
|
||||
|
||||
val ALL_MODES =
|
||||
listOf(
|
||||
UHD_24,
|
||||
UHD_30,
|
||||
UHD_60,
|
||||
HD_24,
|
||||
HD_30,
|
||||
HD_60,
|
||||
HD720_60,
|
||||
HD720_30,
|
||||
HD720_24,
|
||||
).sortedWith(
|
||||
compareByDescending<DisplayMode>({ it.physicalWidth * it.physicalHeight })
|
||||
.thenBy { it.refreshRateRounded },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -32,7 +50,7 @@ class TestDisplayModeChoice {
|
|||
refreshRateSwitch = true,
|
||||
resolutionSwitch = false,
|
||||
)
|
||||
Assert.assertEquals(3, result?.modeId)
|
||||
Assert.assertEquals(UHD_60.modeId, result?.modeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -49,7 +67,7 @@ class TestDisplayModeChoice {
|
|||
refreshRateSwitch = true,
|
||||
resolutionSwitch = true,
|
||||
)
|
||||
Assert.assertEquals(0, result?.modeId)
|
||||
Assert.assertEquals(HD_60.modeId, result?.modeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -66,7 +84,7 @@ class TestDisplayModeChoice {
|
|||
refreshRateSwitch = true,
|
||||
resolutionSwitch = false,
|
||||
)
|
||||
Assert.assertEquals(4, result?.modeId)
|
||||
Assert.assertEquals(UHD_30.modeId, result?.modeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -83,14 +101,14 @@ class TestDisplayModeChoice {
|
|||
refreshRateSwitch = false,
|
||||
resolutionSwitch = true,
|
||||
)
|
||||
Assert.assertEquals(1, result?.modeId)
|
||||
Assert.assertEquals(HD_30.modeId, result?.modeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFraction() {
|
||||
val streamWidth = 1920
|
||||
val streamHeight = 1080
|
||||
val streamRealFrameRate = 30f
|
||||
val streamRealFrameRate = 29.970f
|
||||
|
||||
val displayModes =
|
||||
listOf(
|
||||
|
|
@ -104,7 +122,7 @@ class TestDisplayModeChoice {
|
|||
displayModes = displayModes,
|
||||
streamWidth = streamWidth,
|
||||
streamHeight = streamHeight,
|
||||
targetFrameRate = 29.970f,
|
||||
targetFrameRate = streamRealFrameRate,
|
||||
refreshRateSwitch = true,
|
||||
resolutionSwitch = false,
|
||||
)
|
||||
|
|
@ -119,6 +137,136 @@ class TestDisplayModeChoice {
|
|||
refreshRateSwitch = true,
|
||||
resolutionSwitch = false,
|
||||
)
|
||||
Assert.assertEquals(1, result2?.modeId)
|
||||
Assert.assertEquals(HD_30.modeId, result2?.modeId)
|
||||
}
|
||||
|
||||
private fun test(
|
||||
expected: DisplayMode,
|
||||
streamWidth: Int,
|
||||
streamHeight: Int,
|
||||
streamRealFrameRate: Float,
|
||||
) {
|
||||
val result =
|
||||
RefreshRateService.findDisplayMode(
|
||||
displayModes = ALL_MODES,
|
||||
streamWidth = streamWidth,
|
||||
streamHeight = streamHeight,
|
||||
targetFrameRate = streamRealFrameRate,
|
||||
refreshRateSwitch = false,
|
||||
resolutionSwitch = true,
|
||||
)
|
||||
Assert.assertEquals(
|
||||
"streamWidth=$streamWidth, streamHeight=$streamHeight, streamRealFrameRate=$streamRealFrameRate",
|
||||
expected.modeId,
|
||||
result?.modeId,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test choose best resolution`() {
|
||||
test(HD720_30, 1280, 720, 30f)
|
||||
test(HD720_30, 1280, 548, 30f)
|
||||
test(HD720_30, 640, 480, 30f)
|
||||
test(HD720_30, 960, 720, 30f)
|
||||
test(HD720_24, 960, 720, 24f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test 60fps for 24 without resolution switch`() {
|
||||
val streamWidth = 1920
|
||||
val streamHeight = 1080
|
||||
val streamRealFrameRate = 24f
|
||||
|
||||
val displayModes =
|
||||
listOf(
|
||||
UHD_60,
|
||||
HD_60,
|
||||
HD_30,
|
||||
)
|
||||
|
||||
val result =
|
||||
RefreshRateService.findDisplayMode(
|
||||
displayModes = displayModes,
|
||||
streamWidth = streamWidth,
|
||||
streamHeight = streamHeight,
|
||||
targetFrameRate = streamRealFrameRate,
|
||||
refreshRateSwitch = true,
|
||||
resolutionSwitch = false,
|
||||
)
|
||||
Assert.assertEquals(UHD_60.modeId, result?.modeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test 60fps for 24 with resolution switch`() {
|
||||
val streamWidth = 1920
|
||||
val streamHeight = 1080
|
||||
val streamRealFrameRate = 24f
|
||||
|
||||
val displayModes =
|
||||
listOf(
|
||||
HD_60,
|
||||
HD_30,
|
||||
)
|
||||
|
||||
val result =
|
||||
RefreshRateService.findDisplayMode(
|
||||
displayModes = displayModes,
|
||||
streamWidth = streamWidth,
|
||||
streamHeight = streamHeight,
|
||||
targetFrameRate = streamRealFrameRate,
|
||||
refreshRateSwitch = true,
|
||||
resolutionSwitch = true,
|
||||
)
|
||||
Assert.assertEquals(HD_60.modeId, result?.modeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test prefer refresh rate over resolution`() {
|
||||
val streamWidth = 1280
|
||||
val streamHeight = 720
|
||||
val streamRealFrameRate = 24f
|
||||
|
||||
// 720@60 is an acceptable refresh rate, but want to prioritize exact refresh rate
|
||||
val displayModes =
|
||||
listOf(
|
||||
HD_24,
|
||||
HD720_60,
|
||||
)
|
||||
|
||||
val result =
|
||||
RefreshRateService.findDisplayMode(
|
||||
displayModes = displayModes,
|
||||
streamWidth = streamWidth,
|
||||
streamHeight = streamHeight,
|
||||
targetFrameRate = streamRealFrameRate,
|
||||
refreshRateSwitch = true,
|
||||
resolutionSwitch = true,
|
||||
)
|
||||
Assert.assertEquals(HD_24.modeId, result?.modeId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test prefer refresh rate over resolution2`() {
|
||||
val streamWidth = 1280
|
||||
val streamHeight = 720
|
||||
val streamRealFrameRate = 30f
|
||||
|
||||
// 720@60 is an acceptable refresh rate, but want to prioritize exact refresh rate
|
||||
val displayModes =
|
||||
listOf(
|
||||
HD_30,
|
||||
HD720_60,
|
||||
)
|
||||
|
||||
val result =
|
||||
RefreshRateService.findDisplayMode(
|
||||
displayModes = displayModes,
|
||||
streamWidth = streamWidth,
|
||||
streamHeight = streamHeight,
|
||||
targetFrameRate = streamRealFrameRate,
|
||||
refreshRateSwitch = true,
|
||||
resolutionSwitch = true,
|
||||
)
|
||||
Assert.assertEquals(HD_30.modeId, result?.modeId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,4 +62,32 @@ class TestSeerr {
|
|||
)
|
||||
Assert.assertEquals(expected, urls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateUrls5() {
|
||||
val urls =
|
||||
createUrls("10.0.0.2:443")
|
||||
.map { it.toString() }
|
||||
|
||||
val expected =
|
||||
listOf(
|
||||
"http://10.0.0.2:443/api/v1",
|
||||
"https://10.0.0.2/api/v1",
|
||||
)
|
||||
Assert.assertEquals(expected, urls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCreateUrls6() {
|
||||
val urls =
|
||||
createUrls("10.0.0.2:8080")
|
||||
.map { it.toString() }
|
||||
|
||||
val expected =
|
||||
listOf(
|
||||
"http://10.0.0.2:8080/api/v1",
|
||||
"https://10.0.0.2:8080/api/v1",
|
||||
)
|
||||
Assert.assertEquals(expected, urls)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue