mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Merge branch 'main' into fea/live-tv
This commit is contained in:
commit
c8f34a8634
17 changed files with 437 additions and 80 deletions
|
|
@ -4,6 +4,7 @@ package com.github.damontecres.wholphin.data.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.UseSerializers
|
import kotlinx.serialization.UseSerializers
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -17,9 +18,11 @@ data class GetItemsFilter(
|
||||||
val played: Boolean? = null,
|
val played: Boolean? = null,
|
||||||
val studios: List<UUID>? = null,
|
val studios: List<UUID>? = null,
|
||||||
val tags: List<String>? = null,
|
val tags: List<String>? = null,
|
||||||
|
val includeItemTypes: List<BaseItemKind>? = null,
|
||||||
) {
|
) {
|
||||||
fun applyTo(req: GetItemsRequest) =
|
fun applyTo(req: GetItemsRequest) =
|
||||||
req.copy(
|
req.copy(
|
||||||
|
includeItemTypes = includeItemTypes,
|
||||||
isFavorite = favorite,
|
isFavorite = favorite,
|
||||||
genreIds = genres,
|
genreIds = genres,
|
||||||
minCriticRating = minCriticRating,
|
minCriticRating = minCriticRating,
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class CollectionFolderViewModel
|
||||||
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
itemId: UUID,
|
itemId: UUID?,
|
||||||
potential: BaseItem?,
|
potential: BaseItem?,
|
||||||
sortAndDirection: SortAndDirection,
|
sortAndDirection: SortAndDirection,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
|
|
@ -81,7 +81,9 @@ class CollectionFolderViewModel
|
||||||
"Error loading collection $itemId",
|
"Error loading collection $itemId",
|
||||||
) + Dispatchers.IO,
|
) + Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
fetchItem(itemId, potential)
|
if (itemId != null) {
|
||||||
|
fetchItem(itemId, potential)
|
||||||
|
}
|
||||||
loadResults(sortAndDirection, recursive, filter)
|
loadResults(sortAndDirection, recursive, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,68 +92,67 @@ class CollectionFolderViewModel
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
filter: GetItemsFilter,
|
filter: GetItemsFilter,
|
||||||
) {
|
) {
|
||||||
item.value?.let { item ->
|
val item = item.value
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
pager.value = listOf()
|
pager.value = listOf()
|
||||||
loading.value = LoadingState.Loading
|
loading.value = LoadingState.Loading
|
||||||
this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection
|
this@CollectionFolderViewModel.sortAndDirection.value = sortAndDirection
|
||||||
this@CollectionFolderViewModel.filter.value = filter
|
this@CollectionFolderViewModel.filter.value = filter
|
||||||
|
}
|
||||||
|
val includeItemTypes =
|
||||||
|
when (item?.data?.collectionType) {
|
||||||
|
CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE)
|
||||||
|
CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES)
|
||||||
|
CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO)
|
||||||
|
CollectionType.MUSIC ->
|
||||||
|
listOf(
|
||||||
|
BaseItemKind.AUDIO,
|
||||||
|
BaseItemKind.MUSIC_ARTIST,
|
||||||
|
BaseItemKind.MUSIC_ALBUM,
|
||||||
|
)
|
||||||
|
|
||||||
|
CollectionType.BOXSETS -> listOf(BaseItemKind.BOX_SET)
|
||||||
|
CollectionType.PLAYLISTS -> listOf(BaseItemKind.PLAYLIST)
|
||||||
|
|
||||||
|
else -> listOf()
|
||||||
}
|
}
|
||||||
val includeItemTypes =
|
val request =
|
||||||
when (item.data.collectionType) {
|
filter.applyTo(
|
||||||
CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE)
|
GetItemsRequest(
|
||||||
CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES)
|
parentId = item?.id,
|
||||||
CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO)
|
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
|
||||||
CollectionType.MUSIC ->
|
includeItemTypes = includeItemTypes,
|
||||||
|
recursive = recursive,
|
||||||
|
excludeItemIds = item?.let { listOf(item.id) },
|
||||||
|
sortBy =
|
||||||
listOf(
|
listOf(
|
||||||
BaseItemKind.AUDIO,
|
sortAndDirection.sort,
|
||||||
BaseItemKind.MUSIC_ARTIST,
|
ItemSortBy.SORT_NAME,
|
||||||
BaseItemKind.MUSIC_ALBUM,
|
ItemSortBy.PRODUCTION_YEAR,
|
||||||
)
|
),
|
||||||
|
sortOrder =
|
||||||
CollectionType.BOXSETS -> listOf(BaseItemKind.BOX_SET)
|
listOf(
|
||||||
CollectionType.PLAYLISTS -> listOf(BaseItemKind.PLAYLIST)
|
sortAndDirection.direction,
|
||||||
|
SortOrder.ASCENDING,
|
||||||
else -> listOf()
|
SortOrder.ASCENDING,
|
||||||
}
|
),
|
||||||
val request =
|
fields = SlimItemFields,
|
||||||
filter.applyTo(
|
),
|
||||||
GetItemsRequest(
|
)
|
||||||
parentId = item.id,
|
val newPager =
|
||||||
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
|
ApiRequestPager(
|
||||||
includeItemTypes = includeItemTypes,
|
api,
|
||||||
recursive = recursive,
|
request,
|
||||||
excludeItemIds = listOf(item.id),
|
GetItemsRequestHandler,
|
||||||
sortBy =
|
viewModelScope,
|
||||||
listOf(
|
useSeriesForPrimary = true,
|
||||||
sortAndDirection.sort,
|
)
|
||||||
ItemSortBy.SORT_NAME,
|
newPager.init()
|
||||||
ItemSortBy.PRODUCTION_YEAR,
|
if (newPager.isNotEmpty()) newPager.getBlocking(0)
|
||||||
),
|
withContext(Dispatchers.Main) {
|
||||||
sortOrder =
|
pager.value = newPager
|
||||||
listOf(
|
loading.value = LoadingState.Success
|
||||||
sortAndDirection.direction,
|
|
||||||
SortOrder.ASCENDING,
|
|
||||||
SortOrder.ASCENDING,
|
|
||||||
),
|
|
||||||
fields = SlimItemFields,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
val newPager =
|
|
||||||
ApiRequestPager(
|
|
||||||
api,
|
|
||||||
request,
|
|
||||||
GetItemsRequestHandler,
|
|
||||||
viewModelScope,
|
|
||||||
useSeriesForPrimary = true,
|
|
||||||
)
|
|
||||||
newPager.init()
|
|
||||||
if (newPager.isNotEmpty()) newPager.getBlocking(0)
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
pager.value = newPager
|
|
||||||
loading.value = LoadingState.Success
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -188,7 +189,7 @@ class CollectionFolderViewModel
|
||||||
@Composable
|
@Composable
|
||||||
fun CollectionFolderGrid(
|
fun CollectionFolderGrid(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
itemId: UUID,
|
itemId: UUID?,
|
||||||
item: BaseItem?,
|
item: BaseItem?,
|
||||||
initialFilter: GetItemsFilter,
|
initialFilter: GetItemsFilter,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
|
|
@ -221,7 +222,7 @@ fun CollectionFolderGrid(
|
||||||
pager?.let { pager ->
|
pager?.let { pager ->
|
||||||
CollectionFolderGridContent(
|
CollectionFolderGridContent(
|
||||||
preferences,
|
preferences,
|
||||||
item!!,
|
item,
|
||||||
pager,
|
pager,
|
||||||
sortAndDirection = sortAndDirection,
|
sortAndDirection = sortAndDirection,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -241,7 +242,7 @@ fun CollectionFolderGrid(
|
||||||
@Composable
|
@Composable
|
||||||
fun CollectionFolderGridContent(
|
fun CollectionFolderGridContent(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
item: BaseItem,
|
item: BaseItem?,
|
||||||
pager: List<BaseItem?>,
|
pager: List<BaseItem?>,
|
||||||
sortAndDirection: SortAndDirection,
|
sortAndDirection: SortAndDirection,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
|
|
@ -251,9 +252,9 @@ fun CollectionFolderGridContent(
|
||||||
showTitle: Boolean = true,
|
showTitle: Boolean = true,
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val title = item.name ?: item.data.collectionType?.name ?: "Collection"
|
val title = item?.name ?: item?.data?.collectionType?.name ?: "Collection"
|
||||||
val sortOptions =
|
val sortOptions =
|
||||||
when (item.data.collectionType) {
|
when (item?.data?.collectionType) {
|
||||||
CollectionType.MOVIES -> MovieSortOptions
|
CollectionType.MOVIES -> MovieSortOptions
|
||||||
CollectionType.TVSHOWS -> SeriesSortOptions
|
CollectionType.TVSHOWS -> SeriesSortOptions
|
||||||
CollectionType.HOMEVIDEOS -> VideoSortOptions
|
CollectionType.HOMEVIDEOS -> VideoSortOptions
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -103,6 +105,7 @@ fun ExpandableFaButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
iconColor: Color = Color.Unspecified,
|
||||||
) {
|
) {
|
||||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||||
Button(
|
Button(
|
||||||
|
|
@ -114,16 +117,17 @@ fun ExpandableFaButton(
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(iconStringRes),
|
text = stringResource(iconStringRes),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = iconColor,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
fontFamily = FontAwesome,
|
fontFamily = FontAwesome,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(isFocused) {
|
AnimatedVisibility(isFocused) {
|
||||||
Spacer(Modifier.size(8.dp))
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(title),
|
text = stringResource(title),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import androidx.compose.ui.focus.FocusState
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
|
|
@ -124,8 +125,10 @@ fun PlayButtons(
|
||||||
fun ExpandablePlayButtons(
|
fun ExpandablePlayButtons(
|
||||||
resumePosition: Duration,
|
resumePosition: Duration,
|
||||||
watched: Boolean,
|
watched: Boolean,
|
||||||
|
favorite: Boolean,
|
||||||
playOnClick: (position: Duration) -> Unit,
|
playOnClick: (position: Duration) -> Unit,
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
|
favoriteOnClick: () -> Unit,
|
||||||
moreOnClick: () -> Unit,
|
moreOnClick: () -> Unit,
|
||||||
buttonOnFocusChanged: (FocusState) -> Unit,
|
buttonOnFocusChanged: (FocusState) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -185,6 +188,17 @@ fun ExpandablePlayButtons(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Favorite button
|
||||||
|
item("favorite") {
|
||||||
|
ExpandableFaButton(
|
||||||
|
title = if (favorite) R.string.remove_favorite else R.string.add_favorite,
|
||||||
|
iconStringRes = R.string.fa_heart,
|
||||||
|
onClick = favoriteOnClick,
|
||||||
|
iconColor = if (favorite) Color.Red else Color.Unspecified,
|
||||||
|
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// More button
|
// More button
|
||||||
item("more") {
|
item("more") {
|
||||||
ExpandablePlayButton(
|
ExpandablePlayButton(
|
||||||
|
|
@ -205,8 +219,10 @@ private fun ExpandablePlayButtonsPreview() {
|
||||||
ExpandablePlayButtons(
|
ExpandablePlayButtons(
|
||||||
resumePosition = 10.seconds,
|
resumePosition = 10.seconds,
|
||||||
watched = false,
|
watched = false,
|
||||||
|
favorite = false,
|
||||||
playOnClick = {},
|
playOnClick = {},
|
||||||
watchOnClick = {},
|
watchOnClick = {},
|
||||||
|
favoriteOnClick = {},
|
||||||
moreOnClick = {},
|
moreOnClick = {},
|
||||||
buttonOnFocusChanged = {},
|
buttonOnFocusChanged = {},
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CollectionFolderMovie(
|
fun CollectionFolderMovie(
|
||||||
|
|
@ -161,7 +162,10 @@ fun CollectionFolderMovie(
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
item = destination.item,
|
item = destination.item,
|
||||||
initialFilter = GetItemsFilter(),
|
initialFilter =
|
||||||
|
GetItemsFilter(
|
||||||
|
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||||
|
),
|
||||||
showTitle = false,
|
showTitle = false,
|
||||||
recursive = true,
|
recursive = true,
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CollectionFolderTv(
|
fun CollectionFolderTv(
|
||||||
|
|
@ -160,7 +161,10 @@ fun CollectionFolderTv(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
item = destination.item,
|
item = destination.item,
|
||||||
initialFilter = GetItemsFilter(),
|
initialFilter =
|
||||||
|
GetItemsFilter(
|
||||||
|
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||||
|
),
|
||||||
showTitle = false,
|
showTitle = false,
|
||||||
recursive = true,
|
recursive = true,
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Tab
|
||||||
|
import androidx.tv.material3.TabDefaults
|
||||||
|
import androidx.tv.material3.TabRow
|
||||||
|
import androidx.tv.material3.TabRowDefaults
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FavoritesPage(
|
||||||
|
preferences: UserPreferences,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
preferencesViewModel: PreferencesViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val uiPrefs = preferences.appPreferences.interfacePreferences
|
||||||
|
val rememberedTabIndex = 0
|
||||||
|
// TODO remember tab
|
||||||
|
// if (uiPrefs.rememberSelectedTab) {
|
||||||
|
// uiPrefs.getRememberedTabsOrDefault(destination.itemId.toString(), 0)
|
||||||
|
// } else {
|
||||||
|
// 0
|
||||||
|
// }
|
||||||
|
|
||||||
|
val tabs = listOf("Movies", "TV Shows", "Episodes")
|
||||||
|
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
|
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
val firstTabFocusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() }
|
||||||
|
|
||||||
|
// if (uiPrefs.rememberSelectedTab) {
|
||||||
|
// LaunchedEffect(selectedTabIndex) {
|
||||||
|
// preferencesViewModel.preferenceDataStore.updateData {
|
||||||
|
// preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
var showHeader by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
val onClickItem = { item: BaseItem ->
|
||||||
|
preferencesViewModel.navigationManager.navigateTo(item.destination())
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
showHeader,
|
||||||
|
enter = slideInVertically() + fadeIn(),
|
||||||
|
exit = slideOutVertically() + fadeOut(),
|
||||||
|
) {
|
||||||
|
TabRow(
|
||||||
|
selectedTabIndex = focusTabIndex,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
||||||
|
.focusRestorer(firstTabFocusRequester)
|
||||||
|
.onFocusChanged {
|
||||||
|
if (!it.isFocused) {
|
||||||
|
focusTabIndex = selectedTabIndex
|
||||||
|
}
|
||||||
|
},
|
||||||
|
indicator =
|
||||||
|
@Composable { tabPositions, doesTabRowHaveFocus ->
|
||||||
|
tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition ->
|
||||||
|
// TabRowDefaults.PillIndicator(
|
||||||
|
// currentTabPosition = currentTabPosition,
|
||||||
|
// doesTabRowHaveFocus = doesTabRowHaveFocus,
|
||||||
|
// )
|
||||||
|
TabRowDefaults.UnderlinedIndicator(
|
||||||
|
currentTabPosition = currentTabPosition,
|
||||||
|
doesTabRowHaveFocus = doesTabRowHaveFocus,
|
||||||
|
activeColor = MaterialTheme.colorScheme.border,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tabs = {
|
||||||
|
tabs.forEachIndexed { index, title ->
|
||||||
|
Tab(
|
||||||
|
selected = focusTabIndex == index,
|
||||||
|
onClick = { selectedTabIndex = index },
|
||||||
|
onFocus = { focusTabIndex = index },
|
||||||
|
colors =
|
||||||
|
TabDefaults.pillIndicatorTabColors(
|
||||||
|
focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.ifElse(
|
||||||
|
index == selectedTabIndex,
|
||||||
|
Modifier.focusRequester(firstTabFocusRequester),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
when (selectedTabIndex) {
|
||||||
|
0 -> {
|
||||||
|
CollectionFolderGrid(
|
||||||
|
preferences = preferences,
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
itemId = null,
|
||||||
|
item = null,
|
||||||
|
initialFilter =
|
||||||
|
GetItemsFilter(
|
||||||
|
favorite = true,
|
||||||
|
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||||
|
),
|
||||||
|
showTitle = false,
|
||||||
|
recursive = true,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
positionCallback = { columns, position ->
|
||||||
|
showHeader = position < columns
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
CollectionFolderGrid(
|
||||||
|
preferences = preferences,
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
itemId = null,
|
||||||
|
item = null,
|
||||||
|
initialFilter =
|
||||||
|
GetItemsFilter(
|
||||||
|
favorite = true,
|
||||||
|
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||||
|
),
|
||||||
|
showTitle = false,
|
||||||
|
recursive = true,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
positionCallback = { columns, position ->
|
||||||
|
showHeader = position < columns
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
CollectionFolderGrid(
|
||||||
|
preferences = preferences,
|
||||||
|
onClickItem = onClickItem,
|
||||||
|
itemId = null,
|
||||||
|
item = null,
|
||||||
|
initialFilter =
|
||||||
|
GetItemsFilter(
|
||||||
|
favorite = true,
|
||||||
|
includeItemTypes = listOf(BaseItemKind.EPISODE),
|
||||||
|
),
|
||||||
|
showTitle = false,
|
||||||
|
recursive = true,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.fillMaxSize()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
positionCallback = { columns, position ->
|
||||||
|
showHeader = position < columns
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> ErrorMessage("Invalid tab index $selectedTabIndex", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -122,6 +122,7 @@ fun SeriesDetails(
|
||||||
people = people,
|
people = people,
|
||||||
similar = similar,
|
similar = similar,
|
||||||
played = played,
|
played = played,
|
||||||
|
favorite = item.data.userData?.isFavorite ?: false,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClickItem = { viewModel.navigateTo(it.destination()) },
|
onClickItem = { viewModel.navigateTo(it.destination()) },
|
||||||
onLongClickItem = { season ->
|
onLongClickItem = { season ->
|
||||||
|
|
@ -144,6 +145,10 @@ fun SeriesDetails(
|
||||||
},
|
},
|
||||||
playOnClick = { viewModel.playNextUp() },
|
playOnClick = { viewModel.playNextUp() },
|
||||||
watchOnClick = { showWatchConfirmation = true },
|
watchOnClick = { showWatchConfirmation = true },
|
||||||
|
favoriteOnClick = {
|
||||||
|
val favorite = item.data.userData?.isFavorite ?: false
|
||||||
|
viewModel.setFavorite(item.id, !favorite, null)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if (showWatchConfirmation) {
|
if (showWatchConfirmation) {
|
||||||
ConfirmDialog(
|
ConfirmDialog(
|
||||||
|
|
@ -186,11 +191,13 @@ fun SeriesDetailsContent(
|
||||||
similar: List<BaseItem>,
|
similar: List<BaseItem>,
|
||||||
people: List<Person>,
|
people: List<Person>,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
|
favorite: Boolean,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
onLongClickItem: (BaseItem) -> Unit,
|
onLongClickItem: (BaseItem) -> Unit,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
playOnClick: () -> Unit,
|
playOnClick: () -> Unit,
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
|
favoriteOnClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
|
|
@ -247,9 +254,11 @@ fun SeriesDetailsContent(
|
||||||
SeriesDetailsHeader(
|
SeriesDetailsHeader(
|
||||||
series = series,
|
series = series,
|
||||||
played = played,
|
played = played,
|
||||||
|
favorite = favorite,
|
||||||
overviewOnClick = overviewOnClick,
|
overviewOnClick = overviewOnClick,
|
||||||
playOnClick = playOnClick,
|
playOnClick = playOnClick,
|
||||||
watchOnClick = watchOnClick,
|
watchOnClick = watchOnClick,
|
||||||
|
favoriteOnClick = favoriteOnClick,
|
||||||
bringIntoViewRequester = bringIntoViewRequester,
|
bringIntoViewRequester = bringIntoViewRequester,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -337,10 +346,12 @@ fun SeriesDetailsContent(
|
||||||
fun SeriesDetailsHeader(
|
fun SeriesDetailsHeader(
|
||||||
series: BaseItem,
|
series: BaseItem,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
|
favorite: Boolean,
|
||||||
bringIntoViewRequester: BringIntoViewRequester,
|
bringIntoViewRequester: BringIntoViewRequester,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
playOnClick: () -> Unit,
|
playOnClick: () -> Unit,
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
|
favoriteOnClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
@ -429,6 +440,20 @@ fun SeriesDetailsHeader(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
ExpandableFaButton(
|
||||||
|
title = if (favorite) R.string.remove_favorite else R.string.add_favorite,
|
||||||
|
iconStringRes = R.string.fa_heart,
|
||||||
|
onClick = favoriteOnClick,
|
||||||
|
iconColor = if (favorite) Color.Red else Color.Unspecified,
|
||||||
|
modifier =
|
||||||
|
Modifier.onFocusChanged {
|
||||||
|
if (it.isFocused) {
|
||||||
|
scope.launch(ExceptionHandler()) {
|
||||||
|
bringIntoViewRequester.bringIntoView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,23 @@ class SeriesViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setFavorite(
|
||||||
|
itemId: UUID,
|
||||||
|
favorite: Boolean,
|
||||||
|
listIndex: Int?,
|
||||||
|
) = viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
if (favorite) {
|
||||||
|
api.userLibraryApi.markFavoriteItem(itemId)
|
||||||
|
} else {
|
||||||
|
api.userLibraryApi.unmarkFavoriteItem(itemId)
|
||||||
|
}
|
||||||
|
if (listIndex != null) {
|
||||||
|
refreshEpisode(itemId, listIndex)
|
||||||
|
} else {
|
||||||
|
fetchItem(seriesId, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setSeasonWatched(
|
fun setSeasonWatched(
|
||||||
seasonId: UUID,
|
seasonId: UUID,
|
||||||
played: Boolean,
|
played: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,16 @@ class MovieViewModel
|
||||||
fetchAndSetItem(itemId)
|
fetchAndSetItem(itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setFavorite(favorite: Boolean) =
|
||||||
|
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||||
|
if (favorite) {
|
||||||
|
api.userLibraryApi.markFavoriteItem(itemId)
|
||||||
|
} else {
|
||||||
|
api.userLibraryApi.unmarkFavoriteItem(itemId)
|
||||||
|
}
|
||||||
|
fetchAndSetItem(itemId)
|
||||||
|
}
|
||||||
|
|
||||||
fun savePlayVersion(
|
fun savePlayVersion(
|
||||||
item: BaseItem,
|
item: BaseItem,
|
||||||
sourceId: UUID,
|
sourceId: UUID,
|
||||||
|
|
@ -344,6 +354,9 @@ fun MovieDetails(
|
||||||
watchOnClick = {
|
watchOnClick = {
|
||||||
viewModel.setWatched((movie.data.userData?.played ?: false).not())
|
viewModel.setWatched((movie.data.userData?.played ?: false).not())
|
||||||
},
|
},
|
||||||
|
favoriteOnClick = {
|
||||||
|
viewModel.setFavorite((movie.data.userData?.isFavorite ?: false).not())
|
||||||
|
},
|
||||||
trailerOnClick = { trailer ->
|
trailerOnClick = { trailer ->
|
||||||
when (trailer) {
|
when (trailer) {
|
||||||
is LocalTrailer ->
|
is LocalTrailer ->
|
||||||
|
|
@ -407,6 +420,7 @@ fun MovieDetailsContent(
|
||||||
trailerOnClick: (Trailer) -> Unit,
|
trailerOnClick: (Trailer) -> Unit,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
|
favoriteOnClick: () -> Unit,
|
||||||
moreOnClick: () -> Unit,
|
moreOnClick: () -> Unit,
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -478,9 +492,11 @@ fun MovieDetailsContent(
|
||||||
ExpandablePlayButtons(
|
ExpandablePlayButtons(
|
||||||
resumePosition = resumePosition,
|
resumePosition = resumePosition,
|
||||||
watched = dto.userData?.played ?: false,
|
watched = dto.userData?.played ?: false,
|
||||||
|
favorite = dto.userData?.isFavorite ?: false,
|
||||||
playOnClick = playOnClick,
|
playOnClick = playOnClick,
|
||||||
moreOnClick = moreOnClick,
|
moreOnClick = moreOnClick,
|
||||||
watchOnClick = watchOnClick,
|
watchOnClick = watchOnClick,
|
||||||
|
favoriteOnClick = favoriteOnClick,
|
||||||
buttonOnFocusChanged = {
|
buttonOnFocusChanged = {
|
||||||
if (it.isFocused) {
|
if (it.isFocused) {
|
||||||
scope.launch(ExceptionHandler()) {
|
scope.launch(ExceptionHandler()) {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ fun FocusedEpisodeFooter(
|
||||||
playOnClick: (Duration) -> Unit,
|
playOnClick: (Duration) -> Unit,
|
||||||
moreOnClick: () -> Unit,
|
moreOnClick: () -> Unit,
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
|
favoriteOnClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val dto = ep.data
|
val dto = ep.data
|
||||||
|
|
@ -45,9 +46,11 @@ fun FocusedEpisodeFooter(
|
||||||
ExpandablePlayButtons(
|
ExpandablePlayButtons(
|
||||||
resumePosition = resumePosition,
|
resumePosition = resumePosition,
|
||||||
watched = dto.userData?.played ?: false,
|
watched = dto.userData?.played ?: false,
|
||||||
|
favorite = dto.userData?.isFavorite ?: false,
|
||||||
playOnClick = playOnClick,
|
playOnClick = playOnClick,
|
||||||
moreOnClick = moreOnClick,
|
moreOnClick = moreOnClick,
|
||||||
watchOnClick = watchOnClick,
|
watchOnClick = watchOnClick,
|
||||||
|
favoriteOnClick = favoriteOnClick,
|
||||||
buttonOnFocusChanged = {},
|
buttonOnFocusChanged = {},
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,12 @@ fun SeriesOverview(
|
||||||
viewModel.setWatched(it.id, !played, position.episodeRowIndex)
|
viewModel.setWatched(it.id, !played, position.episodeRowIndex)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
favoriteOnClick = {
|
||||||
|
episodeList?.getOrNull(position.episodeRowIndex)?.let {
|
||||||
|
val favorite = it.data.userData?.isFavorite ?: false
|
||||||
|
viewModel.setFavorite(it.id, !favorite, position.episodeRowIndex)
|
||||||
|
}
|
||||||
|
},
|
||||||
moreOnClick = {
|
moreOnClick = {
|
||||||
episodeList?.getOrNull(position.episodeRowIndex)?.let { ep ->
|
episodeList?.getOrNull(position.episodeRowIndex)?.let { ep ->
|
||||||
moreDialog = buildMoreForEpisode(ep, false)
|
moreDialog = buildMoreForEpisode(ep, false)
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ fun SeriesOverviewContent(
|
||||||
onLongClick: (BaseItem) -> Unit,
|
onLongClick: (BaseItem) -> Unit,
|
||||||
playOnClick: (Duration) -> Unit,
|
playOnClick: (Duration) -> Unit,
|
||||||
watchOnClick: () -> Unit,
|
watchOnClick: () -> Unit,
|
||||||
|
favoriteOnClick: () -> Unit,
|
||||||
moreOnClick: () -> Unit,
|
moreOnClick: () -> Unit,
|
||||||
overviewOnClick: () -> Unit,
|
overviewOnClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -293,6 +294,7 @@ fun SeriesOverviewContent(
|
||||||
watchOnClick.invoke()
|
watchOnClick.invoke()
|
||||||
episodeRowFocusRequester.tryRequestFocus()
|
episodeRowFocusRequester.tryRequestFocus()
|
||||||
},
|
},
|
||||||
|
favoriteOnClick = favoriteOnClick,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,9 @@ sealed class Destination(
|
||||||
val recursive: Boolean,
|
val recursive: Boolean,
|
||||||
) : Destination(false)
|
) : Destination(false)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object Favorites : Destination(false)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object UpdateApp : Destination(true)
|
data object UpdateApp : Destination(true)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.ui.detail.CollectionFolderLiveTv
|
||||||
import com.github.damontecres.wholphin.ui.detail.CollectionFolderMovie
|
import com.github.damontecres.wholphin.ui.detail.CollectionFolderMovie
|
||||||
import com.github.damontecres.wholphin.ui.detail.CollectionFolderTv
|
import com.github.damontecres.wholphin.ui.detail.CollectionFolderTv
|
||||||
import com.github.damontecres.wholphin.ui.detail.DebugPage
|
import com.github.damontecres.wholphin.ui.detail.DebugPage
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.FavoritesPage
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDetails
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDetails
|
||||||
import com.github.damontecres.wholphin.ui.detail.SeriesDetails
|
import com.github.damontecres.wholphin.ui.detail.SeriesDetails
|
||||||
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
|
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
|
||||||
|
|
@ -188,6 +189,12 @@ fun DestinationContent(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Destination.Favorites ->
|
||||||
|
FavoritesPage(
|
||||||
|
preferences = preferences,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
|
||||||
Destination.UpdateApp -> InstallUpdatePage(preferences, modifier)
|
Destination.UpdateApp -> InstallUpdatePage(preferences, modifier)
|
||||||
|
|
||||||
Destination.License -> LicenseInfo(modifier)
|
Destination.License -> LicenseInfo(modifier)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.github.damontecres.wholphin.ui.nav
|
package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
|
@ -66,6 +68,7 @@ import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.supportedCollectionTypes
|
import com.github.damontecres.wholphin.util.supportedCollectionTypes
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
@ -74,17 +77,19 @@ import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||||
import org.jellyfin.sdk.model.api.CollectionType
|
import org.jellyfin.sdk.model.api.CollectionType
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class NavDrawerViewModel
|
class NavDrawerViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
@param:ApplicationContext val context: Context,
|
||||||
val serverRepository: ServerRepository,
|
val serverRepository: ServerRepository,
|
||||||
val api: ApiClient,
|
val api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val libraries = MutableLiveData<List<BaseItem>>(listOf())
|
val libraries = MutableLiveData<List<NavDrawerItem>>(listOf())
|
||||||
val selectedIndex = MutableLiveData<Int>(-1)
|
val selectedIndex = MutableLiveData<Int>(-1)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
@ -96,10 +101,28 @@ class NavDrawerViewModel
|
||||||
val libraries =
|
val libraries =
|
||||||
userViews
|
userViews
|
||||||
.filter { it.collectionType in supportedCollectionTypes }
|
.filter { it.collectionType in supportedCollectionTypes }
|
||||||
.map { BaseItem.from(it, api) }
|
.map {
|
||||||
|
NavDrawerItem(
|
||||||
|
id = it.id,
|
||||||
|
name = it.name ?: it.id.toString(),
|
||||||
|
destination = BaseItem.from(it, api).destination(),
|
||||||
|
type = it.collectionType ?: CollectionType.UNKNOWN,
|
||||||
|
iconStringRes = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val extra =
|
||||||
|
listOf(
|
||||||
|
NavDrawerItem(
|
||||||
|
id = UUID.randomUUID(),
|
||||||
|
name = context.getString(R.string.favorites),
|
||||||
|
destination = Destination.Favorites,
|
||||||
|
type = CollectionType.UNKNOWN,
|
||||||
|
iconStringRes = R.string.fa_heart,
|
||||||
|
),
|
||||||
|
)
|
||||||
Timber.d("Got ${userViews.size} user views filtered to ${libraries.size}")
|
Timber.d("Got ${userViews.size} user views filtered to ${libraries.size}")
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@NavDrawerViewModel.libraries.value = libraries
|
this@NavDrawerViewModel.libraries.value = extra + libraries
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +132,14 @@ class NavDrawerViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class NavDrawerItem(
|
||||||
|
val id: UUID,
|
||||||
|
val name: String,
|
||||||
|
val destination: Destination,
|
||||||
|
val type: CollectionType,
|
||||||
|
@param:StringRes val iconStringRes: Int?,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the left side navigation drawer with [DestinationContent] on the right
|
* Display the left side navigation drawer with [DestinationContent] on the right
|
||||||
*/
|
*/
|
||||||
|
|
@ -213,12 +244,12 @@ fun NavDrawer(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
itemsIndexed(libraries) { index, it ->
|
itemsIndexed(libraries) { index, it ->
|
||||||
LibraryNavItem(
|
NavItem(
|
||||||
library = it,
|
library = it,
|
||||||
selected = selectedIndex == index,
|
selected = selectedIndex == index,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setIndex(index)
|
viewModel.setIndex(index)
|
||||||
viewModel.navigationManager.navigateToFromDrawer(it.destination())
|
viewModel.navigationManager.navigateToFromDrawer(it.destination)
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.ifElse(
|
Modifier.ifElse(
|
||||||
|
|
@ -304,15 +335,15 @@ fun NavigationDrawerScope.IconNavItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavigationDrawerScope.LibraryNavItem(
|
fun NavigationDrawerScope.NavItem(
|
||||||
library: BaseItem,
|
library: NavDrawerItem,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
) {
|
) {
|
||||||
val icon =
|
val icon =
|
||||||
when (library.data.collectionType) {
|
when (library.type) {
|
||||||
CollectionType.MOVIES -> R.string.fa_film
|
CollectionType.MOVIES -> R.string.fa_film
|
||||||
CollectionType.TVSHOWS -> R.string.fa_tv
|
CollectionType.TVSHOWS -> R.string.fa_tv
|
||||||
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
||||||
|
|
@ -320,8 +351,8 @@ fun NavigationDrawerScope.LibraryNavItem(
|
||||||
CollectionType.MUSIC -> R.string.fa_music
|
CollectionType.MUSIC -> R.string.fa_music
|
||||||
CollectionType.BOXSETS -> R.string.fa_open_folder
|
CollectionType.BOXSETS -> R.string.fa_open_folder
|
||||||
CollectionType.PLAYLISTS -> R.string.fa_list_ul
|
CollectionType.PLAYLISTS -> R.string.fa_list_ul
|
||||||
else -> R.string.fa_film
|
else -> library.iconStringRes
|
||||||
}
|
} ?: R.string.fa_film
|
||||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||||
val color =
|
val color =
|
||||||
when {
|
when {
|
||||||
|
|
|
||||||
|
|
@ -85,5 +85,8 @@
|
||||||
<string name="trailers">Trailers</string>
|
<string name="trailers">Trailers</string>
|
||||||
<string name="pass_out_protection">Passout Protection</string>
|
<string name="pass_out_protection">Passout Protection</string>
|
||||||
<string name="more_like_this">More like this</string>
|
<string name="more_like_this">More like this</string>
|
||||||
|
<string name="remove_favorite">Unfavorite</string>
|
||||||
|
<string name="add_favorite">Favorite</string>
|
||||||
|
<string name="favorites">Favorites</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue