mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +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.UseSerializers
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import java.util.UUID
|
||||
|
|
@ -17,9 +18,11 @@ data class GetItemsFilter(
|
|||
val played: Boolean? = null,
|
||||
val studios: List<UUID>? = null,
|
||||
val tags: List<String>? = null,
|
||||
val includeItemTypes: List<BaseItemKind>? = null,
|
||||
) {
|
||||
fun applyTo(req: GetItemsRequest) =
|
||||
req.copy(
|
||||
includeItemTypes = includeItemTypes,
|
||||
isFavorite = favorite,
|
||||
genreIds = genres,
|
||||
minCriticRating = minCriticRating,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class CollectionFolderViewModel
|
|||
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||
|
||||
fun init(
|
||||
itemId: UUID,
|
||||
itemId: UUID?,
|
||||
potential: BaseItem?,
|
||||
sortAndDirection: SortAndDirection,
|
||||
recursive: Boolean,
|
||||
|
|
@ -81,7 +81,9 @@ class CollectionFolderViewModel
|
|||
"Error loading collection $itemId",
|
||||
) + Dispatchers.IO,
|
||||
) {
|
||||
if (itemId != null) {
|
||||
fetchItem(itemId, potential)
|
||||
}
|
||||
loadResults(sortAndDirection, recursive, filter)
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +92,7 @@ class CollectionFolderViewModel
|
|||
recursive: Boolean,
|
||||
filter: GetItemsFilter,
|
||||
) {
|
||||
item.value?.let { item ->
|
||||
val item = item.value
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
pager.value = listOf()
|
||||
|
|
@ -99,7 +101,7 @@ class CollectionFolderViewModel
|
|||
this@CollectionFolderViewModel.filter.value = filter
|
||||
}
|
||||
val includeItemTypes =
|
||||
when (item.data.collectionType) {
|
||||
when (item?.data?.collectionType) {
|
||||
CollectionType.MOVIES -> listOf(BaseItemKind.MOVIE)
|
||||
CollectionType.TVSHOWS -> listOf(BaseItemKind.SERIES)
|
||||
CollectionType.HOMEVIDEOS -> listOf(BaseItemKind.VIDEO)
|
||||
|
|
@ -118,11 +120,11 @@ class CollectionFolderViewModel
|
|||
val request =
|
||||
filter.applyTo(
|
||||
GetItemsRequest(
|
||||
parentId = item.id,
|
||||
parentId = item?.id,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.THUMB),
|
||||
includeItemTypes = includeItemTypes,
|
||||
recursive = recursive,
|
||||
excludeItemIds = listOf(item.id),
|
||||
excludeItemIds = item?.let { listOf(item.id) },
|
||||
sortBy =
|
||||
listOf(
|
||||
sortAndDirection.sort,
|
||||
|
|
@ -154,7 +156,6 @@ class CollectionFolderViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun positionOfLetter(letter: Char): Int? =
|
||||
item.value?.let { item ->
|
||||
|
|
@ -188,7 +189,7 @@ class CollectionFolderViewModel
|
|||
@Composable
|
||||
fun CollectionFolderGrid(
|
||||
preferences: UserPreferences,
|
||||
itemId: UUID,
|
||||
itemId: UUID?,
|
||||
item: BaseItem?,
|
||||
initialFilter: GetItemsFilter,
|
||||
recursive: Boolean,
|
||||
|
|
@ -221,7 +222,7 @@ fun CollectionFolderGrid(
|
|||
pager?.let { pager ->
|
||||
CollectionFolderGridContent(
|
||||
preferences,
|
||||
item!!,
|
||||
item,
|
||||
pager,
|
||||
sortAndDirection = sortAndDirection,
|
||||
modifier = modifier,
|
||||
|
|
@ -241,7 +242,7 @@ fun CollectionFolderGrid(
|
|||
@Composable
|
||||
fun CollectionFolderGridContent(
|
||||
preferences: UserPreferences,
|
||||
item: BaseItem,
|
||||
item: BaseItem?,
|
||||
pager: List<BaseItem?>,
|
||||
sortAndDirection: SortAndDirection,
|
||||
onClickItem: (BaseItem) -> Unit,
|
||||
|
|
@ -251,9 +252,9 @@ fun CollectionFolderGridContent(
|
|||
showTitle: Boolean = true,
|
||||
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 =
|
||||
when (item.data.collectionType) {
|
||||
when (item?.data?.collectionType) {
|
||||
CollectionType.MOVIES -> MovieSortOptions
|
||||
CollectionType.TVSHOWS -> SeriesSortOptions
|
||||
CollectionType.HOMEVIDEOS -> VideoSortOptions
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
|||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -103,6 +105,7 @@ fun ExpandableFaButton(
|
|||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
iconColor: Color = Color.Unspecified,
|
||||
) {
|
||||
val isFocused = interactionSource.collectIsFocusedAsState().value
|
||||
Button(
|
||||
|
|
@ -114,16 +117,17 @@ fun ExpandableFaButton(
|
|||
Text(
|
||||
text = stringResource(iconStringRes),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = iconColor,
|
||||
fontSize = 16.sp,
|
||||
fontFamily = FontAwesome,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier,
|
||||
)
|
||||
AnimatedVisibility(isFocused) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(
|
||||
text = stringResource(title),
|
||||
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.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.tv.material3.Button
|
||||
|
|
@ -124,8 +125,10 @@ fun PlayButtons(
|
|||
fun ExpandablePlayButtons(
|
||||
resumePosition: Duration,
|
||||
watched: Boolean,
|
||||
favorite: Boolean,
|
||||
playOnClick: (position: Duration) -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
buttonOnFocusChanged: (FocusState) -> Unit,
|
||||
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
|
||||
item("more") {
|
||||
ExpandablePlayButton(
|
||||
|
|
@ -205,8 +219,10 @@ private fun ExpandablePlayButtonsPreview() {
|
|||
ExpandablePlayButtons(
|
||||
resumePosition = 10.seconds,
|
||||
watched = false,
|
||||
favorite = false,
|
||||
playOnClick = {},
|
||||
watchOnClick = {},
|
||||
favoriteOnClick = {},
|
||||
moreOnClick = {},
|
||||
buttonOnFocusChanged = {},
|
||||
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.preferences.PreferencesViewModel
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
|
||||
@Composable
|
||||
fun CollectionFolderMovie(
|
||||
|
|
@ -161,7 +162,10 @@ fun CollectionFolderMovie(
|
|||
onClickItem = onClickItem,
|
||||
itemId = destination.itemId,
|
||||
item = destination.item,
|
||||
initialFilter = GetItemsFilter(),
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
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.preferences.PreferencesViewModel
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
|
||||
@Composable
|
||||
fun CollectionFolderTv(
|
||||
|
|
@ -160,7 +161,10 @@ fun CollectionFolderTv(
|
|||
preferences = preferences,
|
||||
itemId = destination.itemId,
|
||||
item = destination.item,
|
||||
initialFilter = GetItemsFilter(),
|
||||
initialFilter =
|
||||
GetItemsFilter(
|
||||
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||
),
|
||||
showTitle = false,
|
||||
recursive = true,
|
||||
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,
|
||||
similar = similar,
|
||||
played = played,
|
||||
favorite = item.data.userData?.isFavorite ?: false,
|
||||
modifier = modifier,
|
||||
onClickItem = { viewModel.navigateTo(it.destination()) },
|
||||
onLongClickItem = { season ->
|
||||
|
|
@ -144,6 +145,10 @@ fun SeriesDetails(
|
|||
},
|
||||
playOnClick = { viewModel.playNextUp() },
|
||||
watchOnClick = { showWatchConfirmation = true },
|
||||
favoriteOnClick = {
|
||||
val favorite = item.data.userData?.isFavorite ?: false
|
||||
viewModel.setFavorite(item.id, !favorite, null)
|
||||
},
|
||||
)
|
||||
if (showWatchConfirmation) {
|
||||
ConfirmDialog(
|
||||
|
|
@ -186,11 +191,13 @@ fun SeriesDetailsContent(
|
|||
similar: List<BaseItem>,
|
||||
people: List<Person>,
|
||||
played: Boolean,
|
||||
favorite: Boolean,
|
||||
onClickItem: (BaseItem) -> Unit,
|
||||
onLongClickItem: (BaseItem) -> Unit,
|
||||
overviewOnClick: () -> Unit,
|
||||
playOnClick: () -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
|
|
@ -247,9 +254,11 @@ fun SeriesDetailsContent(
|
|||
SeriesDetailsHeader(
|
||||
series = series,
|
||||
played = played,
|
||||
favorite = favorite,
|
||||
overviewOnClick = overviewOnClick,
|
||||
playOnClick = playOnClick,
|
||||
watchOnClick = watchOnClick,
|
||||
favoriteOnClick = favoriteOnClick,
|
||||
bringIntoViewRequester = bringIntoViewRequester,
|
||||
modifier =
|
||||
Modifier
|
||||
|
|
@ -337,10 +346,12 @@ fun SeriesDetailsContent(
|
|||
fun SeriesDetailsHeader(
|
||||
series: BaseItem,
|
||||
played: Boolean,
|
||||
favorite: Boolean,
|
||||
bringIntoViewRequester: BringIntoViewRequester,
|
||||
overviewOnClick: () -> Unit,
|
||||
playOnClick: () -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
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(
|
||||
seasonId: UUID,
|
||||
played: Boolean,
|
||||
|
|
|
|||
|
|
@ -192,6 +192,16 @@ class MovieViewModel
|
|||
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(
|
||||
item: BaseItem,
|
||||
sourceId: UUID,
|
||||
|
|
@ -344,6 +354,9 @@ fun MovieDetails(
|
|||
watchOnClick = {
|
||||
viewModel.setWatched((movie.data.userData?.played ?: false).not())
|
||||
},
|
||||
favoriteOnClick = {
|
||||
viewModel.setFavorite((movie.data.userData?.isFavorite ?: false).not())
|
||||
},
|
||||
trailerOnClick = { trailer ->
|
||||
when (trailer) {
|
||||
is LocalTrailer ->
|
||||
|
|
@ -407,6 +420,7 @@ fun MovieDetailsContent(
|
|||
trailerOnClick: (Trailer) -> Unit,
|
||||
overviewOnClick: () -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
onClickItem: (BaseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -478,9 +492,11 @@ fun MovieDetailsContent(
|
|||
ExpandablePlayButtons(
|
||||
resumePosition = resumePosition,
|
||||
watched = dto.userData?.played ?: false,
|
||||
favorite = dto.userData?.isFavorite ?: false,
|
||||
playOnClick = playOnClick,
|
||||
moreOnClick = moreOnClick,
|
||||
watchOnClick = watchOnClick,
|
||||
favoriteOnClick = favoriteOnClick,
|
||||
buttonOnFocusChanged = {
|
||||
if (it.isFocused) {
|
||||
scope.launch(ExceptionHandler()) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ fun FocusedEpisodeFooter(
|
|||
playOnClick: (Duration) -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val dto = ep.data
|
||||
|
|
@ -45,9 +46,11 @@ fun FocusedEpisodeFooter(
|
|||
ExpandablePlayButtons(
|
||||
resumePosition = resumePosition,
|
||||
watched = dto.userData?.played ?: false,
|
||||
favorite = dto.userData?.isFavorite ?: false,
|
||||
playOnClick = playOnClick,
|
||||
moreOnClick = moreOnClick,
|
||||
watchOnClick = watchOnClick,
|
||||
favoriteOnClick = favoriteOnClick,
|
||||
buttonOnFocusChanged = {},
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -266,6 +266,12 @@ fun SeriesOverview(
|
|||
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 = {
|
||||
episodeList?.getOrNull(position.episodeRowIndex)?.let { ep ->
|
||||
moreDialog = buildMoreForEpisode(ep, false)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ fun SeriesOverviewContent(
|
|||
onLongClick: (BaseItem) -> Unit,
|
||||
playOnClick: (Duration) -> Unit,
|
||||
watchOnClick: () -> Unit,
|
||||
favoriteOnClick: () -> Unit,
|
||||
moreOnClick: () -> Unit,
|
||||
overviewOnClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -293,6 +294,7 @@ fun SeriesOverviewContent(
|
|||
watchOnClick.invoke()
|
||||
episodeRowFocusRequester.tryRequestFocus()
|
||||
},
|
||||
favoriteOnClick = favoriteOnClick,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ sealed class Destination(
|
|||
val recursive: Boolean,
|
||||
) : Destination(false)
|
||||
|
||||
@Serializable
|
||||
data object Favorites : Destination(false)
|
||||
|
||||
@Serializable
|
||||
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.CollectionFolderTv
|
||||
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.SeriesDetails
|
||||
import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails
|
||||
|
|
@ -188,6 +189,12 @@ fun DestinationContent(
|
|||
modifier = modifier,
|
||||
)
|
||||
|
||||
Destination.Favorites ->
|
||||
FavoritesPage(
|
||||
preferences = preferences,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
Destination.UpdateApp -> InstallUpdatePage(preferences, modifier)
|
||||
|
||||
Destination.License -> LicenseInfo(modifier)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.github.damontecres.wholphin.ui.nav
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusGroup
|
||||
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.supportedCollectionTypes
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
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.DeviceProfile
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NavDrawerViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
@param:ApplicationContext val context: Context,
|
||||
val serverRepository: ServerRepository,
|
||||
val api: ApiClient,
|
||||
val navigationManager: NavigationManager,
|
||||
) : ViewModel() {
|
||||
val libraries = MutableLiveData<List<BaseItem>>(listOf())
|
||||
val libraries = MutableLiveData<List<NavDrawerItem>>(listOf())
|
||||
val selectedIndex = MutableLiveData<Int>(-1)
|
||||
|
||||
init {
|
||||
|
|
@ -96,10 +101,28 @@ class NavDrawerViewModel
|
|||
val libraries =
|
||||
userViews
|
||||
.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}")
|
||||
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
|
||||
*/
|
||||
|
|
@ -213,12 +244,12 @@ fun NavDrawer(
|
|||
)
|
||||
}
|
||||
itemsIndexed(libraries) { index, it ->
|
||||
LibraryNavItem(
|
||||
NavItem(
|
||||
library = it,
|
||||
selected = selectedIndex == index,
|
||||
onClick = {
|
||||
viewModel.setIndex(index)
|
||||
viewModel.navigationManager.navigateToFromDrawer(it.destination())
|
||||
viewModel.navigationManager.navigateToFromDrawer(it.destination)
|
||||
},
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
|
|
@ -304,15 +335,15 @@ fun NavigationDrawerScope.IconNavItem(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationDrawerScope.LibraryNavItem(
|
||||
library: BaseItem,
|
||||
fun NavigationDrawerScope.NavItem(
|
||||
library: NavDrawerItem,
|
||||
onClick: () -> Unit,
|
||||
selected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val icon =
|
||||
when (library.data.collectionType) {
|
||||
when (library.type) {
|
||||
CollectionType.MOVIES -> R.string.fa_film
|
||||
CollectionType.TVSHOWS -> R.string.fa_tv
|
||||
CollectionType.HOMEVIDEOS -> R.string.fa_video
|
||||
|
|
@ -320,8 +351,8 @@ fun NavigationDrawerScope.LibraryNavItem(
|
|||
CollectionType.MUSIC -> R.string.fa_music
|
||||
CollectionType.BOXSETS -> R.string.fa_open_folder
|
||||
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 color =
|
||||
when {
|
||||
|
|
|
|||
|
|
@ -85,5 +85,8 @@
|
|||
<string name="trailers">Trailers</string>
|
||||
<string name="pass_out_protection">Passout Protection</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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue