Fix Home navigation ownership and merge watch lanes
This commit is contained in:
parent
c2f7ccc438
commit
12c201dc55
41 changed files with 671 additions and 307 deletions
|
|
@ -29,7 +29,7 @@ sealed interface HomeRowConfig {
|
|||
fun updateViewOptions(viewOptions: HomeRowViewOptions): HomeRowConfig
|
||||
|
||||
/**
|
||||
* Continue watching media that the user has started but not finished
|
||||
* Legacy serialized resume row, migrated to ContinueWatchingCombined when settings are read.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("ContinueWatching")
|
||||
|
|
@ -40,7 +40,7 @@ sealed interface HomeRowConfig {
|
|||
}
|
||||
|
||||
/**
|
||||
* Next up row for next episodes in a series the user has started
|
||||
* Legacy serialized next-up row, migrated to ContinueWatchingCombined when settings are read.
|
||||
*/
|
||||
@Serializable
|
||||
@SerialName("NextUp")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.github.damontecres.wholphin.data.model
|
||||
|
||||
/** Saved split rows become one watch row at their first position, retaining its presentation choices. */
|
||||
fun mergeHomeWatchRows(rows: List<HomeRowConfig>): List<HomeRowConfig> = buildList {
|
||||
var watchRowSeen = false
|
||||
for (row in rows) {
|
||||
when (row) {
|
||||
is HomeRowConfig.ContinueWatching, is HomeRowConfig.NextUp, is HomeRowConfig.ContinueWatchingCombined -> {
|
||||
if (!watchRowSeen) add(HomeRowConfig.ContinueWatchingCombined(row.viewOptions))
|
||||
watchRowSeen = true
|
||||
}
|
||||
else -> add(row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
|||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
|
||||
import com.github.damontecres.wholphin.ui.collectLatestIn
|
||||
import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
||||
import com.github.damontecres.wholphin.util.WholphinDispatchers
|
||||
import com.google.common.cache.CacheBuilder
|
||||
import dagger.hilt.android.qualifiers.ActivityContext
|
||||
|
|
@ -21,13 +20,15 @@ import org.jellyfin.sdk.api.client.ApiClient
|
|||
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.request.GetEpisodesRequest
|
||||
import timber.log.Timber
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Caches when media was lasted played. This is mostly used by the combined Continue Watching & Next Up home row.
|
||||
|
|
@ -46,50 +47,21 @@ class DatePlayedService
|
|||
.expireAfterWrite(2, TimeUnit.HOURS)
|
||||
.build<SeriesItemId, Deferred<LocalDateTime>>()
|
||||
|
||||
private fun getLastPlayed(
|
||||
seriesId: UUID,
|
||||
item: BaseItem,
|
||||
): Deferred<LocalDateTime> {
|
||||
val request =
|
||||
GetEpisodesRequest(
|
||||
seriesId = seriesId,
|
||||
adjacentTo = item.id,
|
||||
limit = 1,
|
||||
)
|
||||
return scope.async(WholphinDispatchers.IO) {
|
||||
val premiereDate = item.data.premiereDate
|
||||
private fun readLastPlayed(seriesId: UUID): Deferred<LocalDateTime> =
|
||||
scope.async(WholphinDispatchers.IO) {
|
||||
try {
|
||||
val result =
|
||||
GetEpisodesRequestHandler
|
||||
.execute(
|
||||
api,
|
||||
request,
|
||||
).content.items
|
||||
.firstOrNull()
|
||||
?.userData
|
||||
?.lastPlayedDate ?: LocalDateTime.MIN
|
||||
if (premiereDate != null && result.isBefore(premiereDate)) {
|
||||
premiereDate
|
||||
} else {
|
||||
result
|
||||
}
|
||||
api.itemsApi.getItems(parentId = seriesId, recursive = true,
|
||||
includeItemTypes = listOf(BaseItemKind.EPISODE), isPlayed = true, enableUserData = true,
|
||||
sortBy = listOf(ItemSortBy.DATE_PLAYED), sortOrder = listOf(SortOrder.DESCENDING), limit = 1)
|
||||
.content.items.firstOrNull()?.userData?.lastPlayedDate ?: LocalDateTime.MIN
|
||||
} catch (ex: InvalidStatusException) {
|
||||
Timber.w(
|
||||
"Error fetching series=%s, item=%s: %s",
|
||||
seriesId,
|
||||
item.id,
|
||||
ex.localizedMessage,
|
||||
)
|
||||
LocalDateTime.MIN.coerceAtLeast(premiereDate ?: LocalDateTime.MIN)
|
||||
Timber.w(ex, "Error fetching series=%s", seriesId)
|
||||
LocalDateTime.MIN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the logical timestamp when the item was last played.
|
||||
*
|
||||
* This is calculated using lastest of the actual last played timestamp of the item,
|
||||
* the previous episode's last played timestamp, and the episode's premiere date
|
||||
* Get the series' latest actual watch activity, never an upcoming episode's premiere date.
|
||||
*/
|
||||
suspend fun getLastPlayed(item: BaseItem): LocalDateTime? =
|
||||
withContext(WholphinDispatchers.IO) {
|
||||
|
|
@ -97,7 +69,7 @@ class DatePlayedService
|
|||
return@withContext if (seriesId != null) {
|
||||
datePlayedCache
|
||||
.get(SeriesItemId(seriesId, item.id)) {
|
||||
getLastPlayed(seriesId, item)
|
||||
readLastPlayed(seriesId)
|
||||
}.await()
|
||||
} else {
|
||||
null
|
||||
|
|
|
|||
|
|
@ -92,44 +92,8 @@ class HomeDataService
|
|||
usePaging: Boolean = false,
|
||||
): HomeRowLoadingState =
|
||||
when (row) {
|
||||
is HomeRowConfig.ContinueWatching -> {
|
||||
val resume =
|
||||
latestNextUpService.getResume(
|
||||
userDto.id,
|
||||
limit,
|
||||
true,
|
||||
row.viewOptions.useSeries,
|
||||
)
|
||||
|
||||
Success(
|
||||
title = ResStringProvider(R.string.continue_watching),
|
||||
items = resume,
|
||||
viewOptions = row.viewOptions,
|
||||
rowType = row,
|
||||
showViewMore = resume.size >= limit,
|
||||
)
|
||||
}
|
||||
|
||||
is HomeRowConfig.NextUp -> {
|
||||
val nextUp =
|
||||
latestNextUpService.getNextUp(
|
||||
userDto.id,
|
||||
limit,
|
||||
prefs.enableRewatchingNextUp,
|
||||
false,
|
||||
prefs.maxDaysNextUp,
|
||||
row.viewOptions.useSeries,
|
||||
)
|
||||
|
||||
Success(
|
||||
title = ResStringProvider(R.string.next_up),
|
||||
items = nextUp,
|
||||
viewOptions = row.viewOptions,
|
||||
rowType = row,
|
||||
showViewMore = nextUp.size >= limit,
|
||||
)
|
||||
}
|
||||
|
||||
is HomeRowConfig.ContinueWatching,
|
||||
is HomeRowConfig.NextUp,
|
||||
is HomeRowConfig.ContinueWatchingCombined -> {
|
||||
val resume =
|
||||
latestNextUpService.getResume(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.data.model.HomeRowConfig
|
|||
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.data.model.SUPPORTED_HOME_PAGE_SETTINGS_VERSION
|
||||
import com.github.damontecres.wholphin.data.model.mergeHomeWatchRows
|
||||
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
|
||||
import com.github.damontecres.wholphin.ui.AspectRatio
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
|
|
@ -159,7 +160,7 @@ class HomeSettingsService
|
|||
null
|
||||
}
|
||||
}.orEmpty()
|
||||
return HomePageSettings(rows, version)
|
||||
return HomePageSettings(mergeHomeWatchRows(rows), version)
|
||||
}
|
||||
|
||||
private suspend fun tryLoad(block: suspend () -> HomePageSettings?): HomePageSettings? =
|
||||
|
|
@ -191,7 +192,7 @@ class HomeSettingsService
|
|||
)
|
||||
return settings?.let {
|
||||
val resolvedRows =
|
||||
settings.rows.mapIndexed { index, config ->
|
||||
mergeHomeWatchRows(settings.rows).mapIndexed { index, config ->
|
||||
resolve(index, config)
|
||||
}
|
||||
HomePageResolvedSettings(userId, resolvedSource, resolvedRows)
|
||||
|
|
@ -261,7 +262,7 @@ class HomeSettingsService
|
|||
settings: HomePageSettings,
|
||||
) {
|
||||
val resolvedRows =
|
||||
settings.rows.mapIndexed { index, config ->
|
||||
mergeHomeWatchRows(settings.rows).mapIndexed { index, config ->
|
||||
resolve(index, config)
|
||||
}
|
||||
currentSettings.update { it.copy(rows = resolvedRows) }
|
||||
|
|
@ -313,7 +314,7 @@ class HomeSettingsService
|
|||
listOf(
|
||||
HomeRowConfigDisplay(
|
||||
id = includedIds.size + 1,
|
||||
title = ResStringProvider(R.string.combine_continue_next),
|
||||
title = ResStringProvider(R.string.continue_watching),
|
||||
config = HomeRowConfig.ContinueWatchingCombined(),
|
||||
),
|
||||
)
|
||||
|
|
@ -434,7 +435,8 @@ class HomeSettingsService
|
|||
null
|
||||
}
|
||||
}.flatten()
|
||||
HomePageResolvedSettings(userId, HomePageSettingsSource.WEB_CONFIG, rowConfigs)
|
||||
val merged = mergeHomeWatchRows(rowConfigs.map { it.config }).mapIndexed { index, config -> resolve(index, config) }
|
||||
HomePageResolvedSettings(userId, HomePageSettingsSource.WEB_CONFIG, merged)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
@ -457,19 +459,13 @@ class HomeSettingsService
|
|||
)
|
||||
}
|
||||
|
||||
is HomeRowConfig.ContinueWatching -> {
|
||||
HomeRowConfigDisplay(
|
||||
id,
|
||||
ResStringProvider(R.string.continue_watching),
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
is HomeRowConfig.ContinueWatching,
|
||||
is HomeRowConfig.NextUp,
|
||||
is HomeRowConfig.ContinueWatchingCombined -> {
|
||||
HomeRowConfigDisplay(
|
||||
id,
|
||||
ResStringProvider(R.string.combine_continue_next),
|
||||
config,
|
||||
ResStringProvider(R.string.continue_watching),
|
||||
HomeRowConfig.ContinueWatchingCombined(config.viewOptions),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -495,14 +491,6 @@ class HomeSettingsService
|
|||
HomeRowConfigDisplay(id, StringStringProvider(config.name), config)
|
||||
}
|
||||
|
||||
is HomeRowConfig.NextUp -> {
|
||||
HomeRowConfigDisplay(
|
||||
id,
|
||||
ResStringProvider(R.string.next_up),
|
||||
config,
|
||||
)
|
||||
}
|
||||
|
||||
is HomeRowConfig.RecentlyAdded -> {
|
||||
val title = getItemName(R.string.recently_added_in, config.parentId)
|
||||
HomeRowConfigDisplay(
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ class LatestNextUpService
|
|||
val timestamps = mutableMapOf<UUID, LocalDateTime?>()
|
||||
nextUp.map { it.id }.zip(nextUpLastPlayed).toMap(timestamps)
|
||||
resume.forEach { timestamps[it.id] = it.data.userData?.lastPlayedDate }
|
||||
val result = (resume + nextUp).sortedByDescending { timestamps[it.id] }
|
||||
val result = (resume + nextUp).distinctBy { it.data.seriesId ?: it.id }.sortedByDescending { timestamps[it.id] }
|
||||
val duration = (System.currentTimeMillis() - start).milliseconds
|
||||
Timber.v("buildCombined took %s", duration)
|
||||
return@withContext result
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
package com.github.damontecres.wholphin.ui.main.settings
|
||||
|
||||
sealed interface HomeRowConfigAction {
|
||||
data object Combine : HomeRowConfigAction
|
||||
|
||||
data object Split : HomeRowConfigAction
|
||||
}
|
||||
|
|
@ -60,7 +60,6 @@ fun HomeRowSettings(
|
|||
viewOptions: HomeRowViewOptions,
|
||||
onViewOptionsChange: (HomeRowViewOptions) -> Unit,
|
||||
onConfigChange: (HomeRowConfig) -> Unit,
|
||||
onConfigAction: (HomeRowConfigAction) -> Unit,
|
||||
onApplyApplyAll: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
defaultViewOptions: HomeRowViewOptions = HomeRowViewOptions(),
|
||||
|
|
@ -126,7 +125,6 @@ fun HomeRowSettings(
|
|||
addAdditionalConfig(
|
||||
config = config,
|
||||
onConfigChange = onConfigChange,
|
||||
onConfigAction = onConfigAction,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -135,29 +133,8 @@ fun HomeRowSettings(
|
|||
private fun LazyListScope.addAdditionalConfig(
|
||||
config: HomeRowConfig,
|
||||
onConfigChange: (HomeRowConfig) -> Unit,
|
||||
onConfigAction: (HomeRowConfigAction) -> Unit,
|
||||
) {
|
||||
when (config) {
|
||||
is HomeRowConfig.ContinueWatchingCombined -> {
|
||||
item {
|
||||
ClickPreference(
|
||||
title = stringResource(R.string.split_into_separate_rows),
|
||||
onClick = { onConfigAction.invoke(HomeRowConfigAction.Split) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is HomeRowConfig.ContinueWatching,
|
||||
is HomeRowConfig.NextUp,
|
||||
-> {
|
||||
item {
|
||||
ClickPreference(
|
||||
title = stringResource(R.string.combine_continue_next),
|
||||
onClick = { onConfigAction.invoke(HomeRowConfigAction.Combine) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is HomeRowConfig.ByParent -> {
|
||||
item {
|
||||
val sortAndDirection =
|
||||
|
|
|
|||
|
|
@ -35,8 +35,6 @@ fun HomeSettingsAddRow(
|
|||
) {
|
||||
itemsIndexed(
|
||||
listOf(
|
||||
MetaRowType.CONTINUE_WATCHING,
|
||||
MetaRowType.NEXT_UP,
|
||||
MetaRowType.COMBINED_CONTINUE_WATCHING,
|
||||
),
|
||||
) { index, type ->
|
||||
|
|
@ -94,9 +92,7 @@ fun HomeSettingsAddRow(
|
|||
enum class MetaRowType(
|
||||
@param:StringRes val stringId: Int,
|
||||
) {
|
||||
CONTINUE_WATCHING(R.string.continue_watching),
|
||||
NEXT_UP(R.string.next_up),
|
||||
COMBINED_CONTINUE_WATCHING(R.string.combine_continue_next),
|
||||
COMBINED_CONTINUE_WATCHING(R.string.continue_watching),
|
||||
FAVORITES(R.string.favorites),
|
||||
DISCOVER(R.string.discover),
|
||||
COLLECTION(R.string.collection),
|
||||
|
|
|
|||
|
|
@ -157,8 +157,6 @@ fun HomeSettingsPage(
|
|||
onClick = { backStack.add(ChooseRowType(it)) },
|
||||
onClickMeta = {
|
||||
when (it) {
|
||||
MetaRowType.CONTINUE_WATCHING,
|
||||
MetaRowType.NEXT_UP,
|
||||
MetaRowType.COMBINED_CONTINUE_WATCHING,
|
||||
-> {
|
||||
addRow { viewModel.addRow(it) }
|
||||
|
|
@ -246,18 +244,6 @@ fun HomeSettingsPage(
|
|||
onConfigChange = {
|
||||
viewModel.onConfigChange(row, it)
|
||||
},
|
||||
onConfigAction = {
|
||||
viewModel.onConfigAction(row, it)
|
||||
when (it) {
|
||||
HomeRowConfigAction.Combine,
|
||||
HomeRowConfigAction.Split,
|
||||
-> {
|
||||
backStack.removeAt(
|
||||
backStack.lastIndex,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.github.damontecres.wholphin.data.model.HomeRowConfig.TvChannels
|
|||
import com.github.damontecres.wholphin.data.model.HomeRowConfig.TvPrograms
|
||||
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
|
||||
import com.github.damontecres.wholphin.data.model.SUPPORTED_HOME_PAGE_SETTINGS_VERSION
|
||||
import com.github.damontecres.wholphin.data.model.mergeHomeWatchRows
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.HomeDataService
|
||||
|
|
@ -249,26 +250,10 @@ class HomeSettingsViewModel
|
|||
val id = idCounter++
|
||||
val newRow =
|
||||
when (type) {
|
||||
MetaRowType.CONTINUE_WATCHING -> {
|
||||
HomeRowConfigDisplay(
|
||||
id = id,
|
||||
title = ResStringProvider(R.string.continue_watching),
|
||||
config = ContinueWatching(),
|
||||
)
|
||||
}
|
||||
|
||||
MetaRowType.NEXT_UP -> {
|
||||
HomeRowConfigDisplay(
|
||||
id = id,
|
||||
title = ResStringProvider(R.string.next_up),
|
||||
config = NextUp(),
|
||||
)
|
||||
}
|
||||
|
||||
MetaRowType.COMBINED_CONTINUE_WATCHING -> {
|
||||
HomeRowConfigDisplay(
|
||||
id = id,
|
||||
title = ResStringProvider(R.string.combine_continue_next),
|
||||
title = ResStringProvider(R.string.continue_watching),
|
||||
config = ContinueWatchingCombined(),
|
||||
)
|
||||
}
|
||||
|
|
@ -813,7 +798,7 @@ class HomeSettingsViewModel
|
|||
if (result != null) {
|
||||
Timber.v("Got remote settings")
|
||||
val newRows =
|
||||
result.rows.mapIndexed { index, config ->
|
||||
mergeHomeWatchRows(result.rows).mapIndexed { index, config ->
|
||||
homeSettingsService.resolve(index, config)
|
||||
}
|
||||
idCounter = newRows.maxOfOrNull { it.id }?.plus(1) ?: 0
|
||||
|
|
@ -839,76 +824,6 @@ class HomeSettingsViewModel
|
|||
}
|
||||
}
|
||||
|
||||
fun onConfigAction(
|
||||
row: HomeRowConfigDisplay,
|
||||
action: HomeRowConfigAction,
|
||||
) {
|
||||
viewModelScope.launchDefault {
|
||||
when (action) {
|
||||
HomeRowConfigAction.Combine -> {
|
||||
val index =
|
||||
state.value.rows
|
||||
.indexOf(row)
|
||||
.coerceAtLeast(0)
|
||||
val rowsToRemove =
|
||||
state.value.rows
|
||||
.filter { it.config is ContinueWatching || it.config is NextUp }
|
||||
|
||||
updateState {
|
||||
it.copy(
|
||||
loading = LoadingState.Loading,
|
||||
rows =
|
||||
it.rows.toMutableList().apply {
|
||||
set(
|
||||
index,
|
||||
HomeRowConfigDisplay(
|
||||
id = it.rows[index].id,
|
||||
title = ResStringProvider(R.string.combine_continue_next),
|
||||
config = ContinueWatchingCombined(row.config.viewOptions),
|
||||
),
|
||||
)
|
||||
removeAll(rowsToRemove)
|
||||
},
|
||||
)
|
||||
}
|
||||
fetchRowData()
|
||||
}
|
||||
|
||||
HomeRowConfigAction.Split -> {
|
||||
val index =
|
||||
state.value.rows
|
||||
.indexOf(row)
|
||||
.coerceAtLeast(0)
|
||||
updateState {
|
||||
it.copy(
|
||||
loading = LoadingState.Loading,
|
||||
rows =
|
||||
it.rows.toMutableList().apply {
|
||||
set(
|
||||
index,
|
||||
HomeRowConfigDisplay(
|
||||
id = it.rows[index].id,
|
||||
title = ResStringProvider(R.string.continue_watching),
|
||||
config = ContinueWatching(row.config.viewOptions),
|
||||
),
|
||||
)
|
||||
add(
|
||||
index + 1,
|
||||
HomeRowConfigDisplay(
|
||||
id = idCounter++,
|
||||
title = ResStringProvider(R.string.next_up),
|
||||
config = NextUp(row.config.viewOptions),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
fetchRowData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onConfigChange(
|
||||
row: HomeRowConfigDisplay,
|
||||
config: HomeRowConfig,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue