Add row for favorite items

This commit is contained in:
Damontecres 2026-01-27 13:40:44 -05:00
parent d99c947068
commit ccb79257fd
No known key found for this signature in database
9 changed files with 334 additions and 24 deletions

View file

@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.ui.data.SortAndDirection
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
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
@ -86,13 +87,33 @@ sealed interface HomeRowConfig {
val parentId: UUID, val parentId: UUID,
override val viewOptions: HomeRowViewOptions = override val viewOptions: HomeRowViewOptions =
HomeRowViewOptions( HomeRowViewOptions(
heightDp = (Cards.HEIGHT_2X3_DP * .75f).toInt().let { it - it % 4 }, heightDp = Cards.HEIGHT_EPISODE,
aspectRatio = AspectRatio.WIDE, aspectRatio = AspectRatio.WIDE,
), ),
) : HomeRowConfig { ) : HomeRowConfig {
override fun updateViewOptions(viewOptions: HomeRowViewOptions): Genres = this.copy(viewOptions = viewOptions) override fun updateViewOptions(viewOptions: HomeRowViewOptions): Genres = this.copy(viewOptions = viewOptions)
} }
/**
* Favorites for a specific type
*/
@Serializable
@SerialName("Favorite")
data class Favorite(
val kind: BaseItemKind,
override val viewOptions: HomeRowViewOptions =
if (kind == BaseItemKind.EPISODE) {
HomeRowViewOptions(
heightDp = Cards.HEIGHT_EPISODE,
aspectRatio = AspectRatio.WIDE,
)
} else {
HomeRowViewOptions()
},
) : HomeRowConfig {
override fun updateViewOptions(viewOptions: HomeRowViewOptions): Favorite = this.copy(viewOptions = viewOptions)
}
/** /**
* Fetch by parent ID such as a library, collection, or playlist with optional simple sorting * Fetch by parent ID such as a library, collection, or playlist with optional simple sorting
*/ */

View file

@ -15,7 +15,9 @@ import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.util.GetGenresRequestHandler import com.github.damontecres.wholphin.util.GetGenresRequestHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.GetPersonsHandler
import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.HomeRowLoadingState.Success
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -31,12 +33,15 @@ import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
import org.jellyfin.sdk.model.api.UserDto import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.api.request.GetGenresRequest import org.jellyfin.sdk.model.api.request.GetGenresRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ -183,7 +188,19 @@ class HomeSettingsService
suspend fun loadCurrentSettings(userId: UUID) { suspend fun loadCurrentSettings(userId: UUID) {
Timber.v("Getting setting for %s", userId) Timber.v("Getting setting for %s", userId)
// User local then server/remote otherwise create a default // User local then server/remote otherwise create a default
val settings = loadFromLocal(userId) ?: loadFromServer(userId) val settings =
try {
loadFromLocal(userId)
} catch (ex: Exception) {
Timber.w(ex, "Error loading local settings")
// TODO show toast?
null
} ?: try {
loadFromServer(userId)
} catch (ex: Exception) {
Timber.w(ex, "Error loading remote settings")
null
}
val resolvedSettings = val resolvedSettings =
if (settings != null) { if (settings != null) {
Timber.v("Found settings") Timber.v("Found settings")
@ -341,6 +358,11 @@ class HomeSettingsService
config, config,
) )
} }
is HomeRowConfig.Favorite -> {
val name = context.getString(R.string.favorites) // TODO "Favorite <type>"
HomeRowConfigDisplay(id, name, config)
}
} }
/** /**
@ -358,7 +380,7 @@ class HomeSettingsService
is HomeRowConfig.ContinueWatching -> { is HomeRowConfig.ContinueWatching -> {
val resume = latestNextUpService.getResume(userDto.id, limit, true) val resume = latestNextUpService.getResume(userDto.id, limit, true)
HomeRowLoadingState.Success( Success(
title = context.getString(R.string.continue_watching), title = context.getString(R.string.continue_watching),
items = resume, items = resume,
viewOptions = row.viewOptions, viewOptions = row.viewOptions,
@ -374,7 +396,7 @@ class HomeSettingsService
false, false,
) )
HomeRowLoadingState.Success( Success(
title = context.getString(R.string.next_up), title = context.getString(R.string.next_up),
items = nextUp, items = nextUp,
viewOptions = row.viewOptions, viewOptions = row.viewOptions,
@ -392,7 +414,7 @@ class HomeSettingsService
false, false,
) )
HomeRowLoadingState.Success( Success(
title = context.getString(R.string.continue_watching), title = context.getString(R.string.continue_watching),
items = items =
latestNextUpService.buildCombined( latestNextUpService.buildCombined(
@ -438,7 +460,7 @@ class HomeSettingsService
name?.let { context.getString(R.string.genres_in, it) } name?.let { context.getString(R.string.genres_in, it) }
?: context.getString(R.string.genres) ?: context.getString(R.string.genres)
HomeRowLoadingState.Success( Success(
title, title,
genres, genres,
viewOptions = row.viewOptions, viewOptions = row.viewOptions,
@ -468,7 +490,7 @@ class HomeSettingsService
.content .content
.map { BaseItem.Companion.from(it, api, true) } .map { BaseItem.Companion.from(it, api, true) }
.let { .let {
HomeRowLoadingState.Success( Success(
title, title,
it, it,
row.viewOptions, row.viewOptions,
@ -500,7 +522,7 @@ class HomeSettingsService
.content.items .content.items
.map { BaseItem.Companion.from(it, api, true) } .map { BaseItem.Companion.from(it, api, true) }
.let { .let {
HomeRowLoadingState.Success( Success(
title, title,
it, it,
row.viewOptions, row.viewOptions,
@ -528,7 +550,7 @@ class HomeSettingsService
.content.items .content.items
.map { BaseItem(it, true) } .map { BaseItem(it, true) }
.let { .let {
HomeRowLoadingState.Success( Success(
name ?: context.getString(R.string.collection), name ?: context.getString(R.string.collection),
it, it,
row.viewOptions, row.viewOptions,
@ -555,13 +577,59 @@ class HomeSettingsService
.content.items .content.items
.map { BaseItem(it, true) } .map { BaseItem(it, true) }
.let { .let {
HomeRowLoadingState.Success( Success(
row.name, row.name,
it, it,
row.viewOptions, row.viewOptions,
) )
} }
} }
is HomeRowConfig.Favorite -> {
if (row.kind == BaseItemKind.PERSON) {
val request =
GetPersonsRequest(
userId = userDto.id,
limit = limit,
fields = DefaultItemFields,
isFavorite = true,
enableImages = true,
enableImageTypes = listOf(ImageType.PRIMARY),
)
GetPersonsHandler
.execute(api, request)
.content.items
.map { BaseItem(it, true) }
.let {
Success(
context.getString(R.string.favorites), // TODO
it,
row.viewOptions,
)
}
} else {
val request =
GetItemsRequest(
userId = userDto.id,
recursive = true,
limit = limit,
fields = DefaultItemFields,
includeItemTypes = listOf(row.kind),
isFavorite = true,
)
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem(it, false) }
.let {
Success(
context.getString(R.string.favorites), // TODO
it,
row.viewOptions,
)
}
}
}
} }
companion object { companion object {

View file

@ -75,7 +75,8 @@ val SlimItemFields =
object Cards { object Cards {
const val HEIGHT_2X3_DP = 172 const val HEIGHT_2X3_DP = 172
val height2x3 = HEIGHT_2X3_DP.dp val height2x3 = HEIGHT_2X3_DP.dp
val heightEpisode = height2x3 * .75f val HEIGHT_EPISODE = (HEIGHT_2X3_DP * .75f).toInt().let { it - it % 4 }
val heightEpisode = HEIGHT_EPISODE.dp
val playedPercentHeight = 6.dp val playedPercentHeight = 6.dp
val serverUserCircle = height2x3 * .75f val serverUserCircle = height2x3 * .75f
} }

View file

@ -17,4 +17,8 @@ sealed interface HomeSettingsDestination : NavKey {
data class RowSettings( data class RowSettings(
val rowId: Int, val rowId: Int,
) : HomeSettingsDestination ) : HomeSettingsDestination
data object ChooseFavorite : HomeSettingsDestination
data object ChooseDiscover : HomeSettingsDestination
} }

View file

@ -0,0 +1,69 @@
package com.github.damontecres.wholphin.ui.main.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
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.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ListItem
import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.tryRequestFocus
import org.jellyfin.sdk.model.api.BaseItemKind
@Composable
fun HomeSettingsFavoriteList(
onClick: (BaseItemKind) -> Unit,
modifier: Modifier = Modifier,
firstFocus: FocusRequester = remember { FocusRequester() },
) {
LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
Column(modifier = modifier) {
Text(
text = "Add favorites row...",
)
LazyColumn(
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
modifier
.fillMaxHeight()
.focusRestorer(firstFocus),
) {
itemsIndexed(favoriteOptionsList) { index, type ->
ListItem(
selected = false,
headlineContent = {
Text(
text = stringResource(favoriteOptions[type]!!),
)
},
onClick = { onClick.invoke(type) },
modifier = Modifier.ifElse(index == 0, Modifier.focusRequester(firstFocus)),
)
}
}
}
}
val favoriteOptions =
mapOf(
BaseItemKind.MOVIE to R.string.movies,
BaseItemKind.SERIES to R.string.tv_shows,
BaseItemKind.EPISODE to R.string.episodes,
BaseItemKind.VIDEO to R.string.videos,
BaseItemKind.PLAYLIST to R.string.playlists,
BaseItemKind.PERSON to R.string.people,
)
val favoriteOptionsList = favoriteOptions.keys.toList()

View file

@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
@ -16,8 +18,10 @@ import androidx.compose.ui.focus.FocusRequester
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.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.tv.material3.ListItem import androidx.tv.material3.ListItem
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
@ -26,6 +30,7 @@ import com.github.damontecres.wholphin.ui.tryRequestFocus
@Composable @Composable
fun HomeSettingsLibraryList( fun HomeSettingsLibraryList(
libraries: List<Library>, libraries: List<Library>,
showDiscover: Boolean,
onClick: (Library) -> Unit, onClick: (Library) -> Unit,
onClickMeta: (MetaRowType) -> Unit, onClickMeta: (MetaRowType) -> Unit,
modifier: Modifier, modifier: Modifier,
@ -44,7 +49,25 @@ fun HomeSettingsLibraryList(
.fillMaxHeight() .fillMaxHeight()
.focusRestorer(firstFocus), .focusRestorer(firstFocus),
) { ) {
itemsIndexed(MetaRowType.entries) { index, type -> item {
Text(
text = stringResource(R.string.continue_watching),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Start,
modifier =
Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 4.dp),
)
}
itemsIndexed(
listOf(
MetaRowType.CONTINUE_WATCHING,
MetaRowType.NEXT_UP,
MetaRowType.COMBINED_CONTINUE_WATCHING,
),
) { index, type ->
ListItem( ListItem(
selected = false, selected = false,
headlineContent = { headlineContent = {
@ -56,6 +79,16 @@ fun HomeSettingsLibraryList(
} }
item { item {
HorizontalDivider() HorizontalDivider()
Text(
text = stringResource(R.string.library),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Start,
modifier =
Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 4.dp),
)
} }
itemsIndexed(libraries) { index, library -> itemsIndexed(libraries) { index, library ->
ListItem( ListItem(
@ -67,6 +100,41 @@ fun HomeSettingsLibraryList(
modifier = Modifier, // .ifElse(index == 0, Modifier.focusRequester(firstFocus)), modifier = Modifier, // .ifElse(index == 0, Modifier.focusRequester(firstFocus)),
) )
} }
item {
HorizontalDivider()
Text(
text = stringResource(R.string.more),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Start,
modifier =
Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 4.dp),
)
}
item {
ListItem(
selected = false,
headlineContent = {
Text(stringResource(MetaRowType.FAVORITES.stringId))
},
onClick = { onClickMeta.invoke(MetaRowType.FAVORITES) },
modifier = Modifier,
)
}
if (showDiscover) {
item {
ListItem(
selected = false,
headlineContent = {
Text(stringResource(MetaRowType.DISCOVER.stringId))
},
onClick = { onClickMeta.invoke(MetaRowType.DISCOVER) },
modifier = Modifier,
)
}
}
} }
} }
} }
@ -77,4 +145,6 @@ enum class MetaRowType(
CONTINUE_WATCHING(R.string.continue_watching), CONTINUE_WATCHING(R.string.continue_watching),
NEXT_UP(R.string.next_up), NEXT_UP(R.string.next_up),
COMBINED_CONTINUE_WATCHING(R.string.combine_continue_next), COMBINED_CONTINUE_WATCHING(R.string.combine_continue_next),
FAVORITES(R.string.favorites),
DISCOVER(R.string.discover),
} }

View file

@ -22,6 +22,8 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import com.github.damontecres.wholphin.ui.main.HomePageContent import com.github.damontecres.wholphin.ui.main.HomePageContent
import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsDestination.ChooseRowType
import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsDestination.RowSettings
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@ -38,14 +40,15 @@ fun HomeSettingsPage(
var destination by remember { mutableStateOf<HomeSettingsDestination>(HomeSettingsDestination.RowList) } var destination by remember { mutableStateOf<HomeSettingsDestination>(HomeSettingsDestination.RowList) }
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val discoverEnabled by viewModel.discoverEnabled.collectAsState(false)
BackHandler(destination is HomeSettingsDestination.ChooseRowType) { BackHandler(destination is ChooseRowType) {
destination = HomeSettingsDestination.ChooseLibrary destination = HomeSettingsDestination.ChooseLibrary
} }
BackHandler(destination is HomeSettingsDestination.ChooseLibrary) { BackHandler(destination is HomeSettingsDestination.ChooseLibrary) {
destination = HomeSettingsDestination.RowList destination = HomeSettingsDestination.RowList
} }
BackHandler(destination is HomeSettingsDestination.RowSettings) { BackHandler(destination is RowSettings) {
destination = HomeSettingsDestination.RowList destination = HomeSettingsDestination.RowList
} }
Row( Row(
@ -72,7 +75,7 @@ fun HomeSettingsPage(
onClickMove = viewModel::moveRow, onClickMove = viewModel::moveRow,
onClickDelete = viewModel::deleteRow, onClickDelete = viewModel::deleteRow,
onClick = { index, row -> onClick = { index, row ->
destination = HomeSettingsDestination.RowSettings(row.id) destination = RowSettings(row.id)
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
Timber.v("Scroll to $index") Timber.v("Scroll to $index")
listState.scrollToItem(index) listState.scrollToItem(index)
@ -88,16 +91,32 @@ fun HomeSettingsPage(
is HomeSettingsDestination.ChooseLibrary -> { is HomeSettingsDestination.ChooseLibrary -> {
HomeSettingsLibraryList( HomeSettingsLibraryList(
libraries = state.libraries, libraries = state.libraries,
onClick = { destination = HomeSettingsDestination.ChooseRowType(it) }, showDiscover = discoverEnabled,
onClick = { destination = ChooseRowType(it) },
onClickMeta = { onClickMeta = {
when (it) {
MetaRowType.CONTINUE_WATCHING,
MetaRowType.NEXT_UP,
MetaRowType.COMBINED_CONTINUE_WATCHING,
-> {
viewModel.addRow(it) viewModel.addRow(it)
destination = HomeSettingsDestination.RowList destination = HomeSettingsDestination.RowList
}
MetaRowType.FAVORITES -> {
destination = HomeSettingsDestination.ChooseFavorite
}
MetaRowType.DISCOVER -> {
destination = HomeSettingsDestination.ChooseDiscover
}
}
}, },
modifier = destModifier, modifier = destModifier,
) )
} }
is HomeSettingsDestination.ChooseRowType -> { is ChooseRowType -> {
HomeLibraryRowTypeList( HomeLibraryRowTypeList(
library = dest.library, library = dest.library,
onClick = { onClick = {
@ -108,7 +127,7 @@ fun HomeSettingsPage(
) )
} }
is HomeSettingsDestination.RowSettings -> { is RowSettings -> {
val row = val row =
state.rows state.rows
.first { it.id == dest.rowId } .first { it.id == dest.rowId }
@ -124,6 +143,18 @@ fun HomeSettingsPage(
modifier = destModifier, modifier = destModifier,
) )
} }
HomeSettingsDestination.ChooseDiscover -> {
TODO()
}
HomeSettingsDestination.ChooseFavorite -> {
HomeSettingsFavoriteList(
onClick = { type ->
viewModel.addFavoriteRow(type)
},
)
}
} }
} }
HomePageContent( HomePageContent(

View file

@ -23,9 +23,11 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -57,6 +59,7 @@ fun HomeSettingsRowList(
modifier: Modifier, modifier: Modifier,
firstFocus: FocusRequester = remember { FocusRequester() }, firstFocus: FocusRequester = remember { FocusRequester() },
) { ) {
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) { firstFocus.tryRequestFocus() } LaunchedEffect(Unit) { firstFocus.tryRequestFocus() }
Column(modifier = modifier) { Column(modifier = modifier) {
LazyColumn( LazyColumn(
@ -148,7 +151,14 @@ fun HomeSettingsRowList(
moveDownAllowed = index != state.rows.lastIndex, moveDownAllowed = index != state.rows.lastIndex,
deleteAllowed = state.rows.size > 1, deleteAllowed = state.rows.size > 1,
onClickMove = { onClickMove.invoke(it, index) }, onClickMove = { onClickMove.invoke(it, index) },
onClickDelete = { onClickDelete.invoke(index) }, onClickDelete = {
if (index != state.rows.lastIndex) {
focusManager.moveFocus(FocusDirection.Down)
} else {
focusManager.moveFocus(FocusDirection.Up)
}
onClickDelete.invoke(index)
},
onClick = { onClick.invoke(index, row) }, onClick = { onClick.invoke(index, row) },
modifier = modifier =
Modifier Modifier

View file

@ -11,11 +11,15 @@ import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.HomePageSettings import com.github.damontecres.wholphin.data.model.HomePageSettings
import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowConfig
import com.github.damontecres.wholphin.data.model.HomeRowConfig.ContinueWatching
import com.github.damontecres.wholphin.data.model.HomeRowConfig.ContinueWatchingCombined
import com.github.damontecres.wholphin.data.model.HomeRowConfig.NextUp
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.HomePageResolvedSettings import com.github.damontecres.wholphin.services.HomePageResolvedSettings
import com.github.damontecres.wholphin.services.HomeRowConfigDisplay import com.github.damontecres.wholphin.services.HomeRowConfigDisplay
import com.github.damontecres.wholphin.services.HomeSettingsService import com.github.damontecres.wholphin.services.HomeSettingsService
import com.github.damontecres.wholphin.services.SeerrServerRepository
import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
@ -30,6 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.api.CollectionType
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
@ -45,11 +50,14 @@ class HomeSettingsViewModel
private val userPreferencesService: UserPreferencesService, private val userPreferencesService: UserPreferencesService,
private val navDrawerItemRepository: NavDrawerItemRepository, private val navDrawerItemRepository: NavDrawerItemRepository,
private val backdropService: BackdropService, private val backdropService: BackdropService,
private val seerrServerRepository: SeerrServerRepository,
@param:IoCoroutineScope private val ioScope: CoroutineScope, @param:IoCoroutineScope private val ioScope: CoroutineScope,
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(HomePageSettingsState.EMPTY) private val _state = MutableStateFlow(HomePageSettingsState.EMPTY)
val state: StateFlow<HomePageSettingsState> = _state val state: StateFlow<HomePageSettingsState> = _state
val discoverEnabled = seerrServerRepository.active
init { init {
addCloseable { saveToLocal() } addCloseable { saveToLocal() }
viewModelScope.launchIO { viewModelScope.launchIO {
@ -164,7 +172,7 @@ class HomeSettingsViewModel
HomeRowConfigDisplay( HomeRowConfigDisplay(
id = id, id = id,
title = context.getString(R.string.continue_watching), title = context.getString(R.string.continue_watching),
config = HomeRowConfig.ContinueWatching(), config = ContinueWatching(),
) )
} }
@ -172,7 +180,7 @@ class HomeSettingsViewModel
HomeRowConfigDisplay( HomeRowConfigDisplay(
id = id, id = id,
title = context.getString(R.string.continue_watching), title = context.getString(R.string.continue_watching),
config = HomeRowConfig.NextUp(), config = NextUp(),
) )
} }
@ -180,9 +188,17 @@ class HomeSettingsViewModel
HomeRowConfigDisplay( HomeRowConfigDisplay(
id = id, id = id,
title = context.getString(R.string.combine_continue_next), title = context.getString(R.string.combine_continue_next),
config = HomeRowConfig.ContinueWatchingCombined(), config = ContinueWatchingCombined(),
) )
} }
MetaRowType.FAVORITES -> {
throw IllegalArgumentException("Should use addRow(BaseItemKind) instead")
}
MetaRowType.DISCOVER -> {
TODO()
}
} }
updateState { updateState {
it.copy( it.copy(
@ -246,6 +262,26 @@ class HomeSettingsViewModel
} }
} }
fun addFavoriteRow(type: BaseItemKind) {
viewModelScope.launchIO {
Timber.v("Adding favorite row for $type")
val id = state.value.rows.size
val newRow =
HomeRowConfigDisplay(
id = id,
title = context.getString(favoriteOptions[type]!!),
config = HomeRowConfig.Favorite(type),
)
updateState {
it.copy(
loading = LoadingState.Loading,
rows = it.rows.toMutableList().apply { add(newRow) },
)
}
fetchRowData()
}
}
fun updateViewOptions( fun updateViewOptions(
rowId: Int, rowId: Int,
viewOptions: HomeRowViewOptions, viewOptions: HomeRowViewOptions,