mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +02:00
Merge branch 'main' into develop/customize-home
This commit is contained in:
commit
561b12fe87
7 changed files with 244 additions and 126 deletions
|
|
@ -37,9 +37,10 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.jellyfin.sdk.model.api.MediaType
|
import org.jellyfin.sdk.model.api.MediaType
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
|
@ -99,13 +100,13 @@ abstract class RecommendedViewModel(
|
||||||
abstract fun update(
|
abstract fun update(
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
row: HomeRowLoadingState,
|
row: HomeRowLoadingState,
|
||||||
)
|
): HomeRowLoadingState
|
||||||
|
|
||||||
fun update(
|
fun update(
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
block: suspend () -> List<BaseItem>,
|
block: suspend () -> List<BaseItem>,
|
||||||
) {
|
): Deferred<HomeRowLoadingState> =
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.async(Dispatchers.IO) {
|
||||||
val titleStr = context.getString(title)
|
val titleStr = context.getString(title)
|
||||||
val row =
|
val row =
|
||||||
try {
|
try {
|
||||||
|
|
@ -115,7 +116,6 @@ abstract class RecommendedViewModel(
|
||||||
}
|
}
|
||||||
update(title, row)
|
update(title, row)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
|
@ -123,6 +124,8 @@ class RecommendedMovieViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val jobs = mutableListOf<Deferred<HomeRowLoadingState>>()
|
||||||
|
|
||||||
update(R.string.recently_released) {
|
update(R.string.recently_released) {
|
||||||
val request =
|
val request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
|
|
@ -138,7 +141,7 @@ class RecommendedMovieViewModel
|
||||||
enableTotalRecordCount = false,
|
enableTotalRecordCount = false,
|
||||||
)
|
)
|
||||||
GetItemsRequestHandler.execute(api, request).toBaseItems(api, false)
|
GetItemsRequestHandler.execute(api, request).toBaseItems(api, false)
|
||||||
}
|
}.also(jobs::add)
|
||||||
|
|
||||||
update(R.string.recently_added) {
|
update(R.string.recently_added) {
|
||||||
val request =
|
val request =
|
||||||
|
|
@ -155,7 +158,7 @@ class RecommendedMovieViewModel
|
||||||
enableTotalRecordCount = false,
|
enableTotalRecordCount = false,
|
||||||
)
|
)
|
||||||
GetItemsRequestHandler.execute(api, request).toBaseItems(api, false)
|
GetItemsRequestHandler.execute(api, request).toBaseItems(api, false)
|
||||||
}
|
}.also(jobs::add)
|
||||||
|
|
||||||
update(R.string.top_unwatched) {
|
update(R.string.top_unwatched) {
|
||||||
val request =
|
val request =
|
||||||
|
|
@ -173,7 +176,7 @@ class RecommendedMovieViewModel
|
||||||
enableTotalRecordCount = false,
|
enableTotalRecordCount = false,
|
||||||
)
|
)
|
||||||
GetItemsRequestHandler.execute(api, request).toBaseItems(api, false)
|
GetItemsRequestHandler.execute(api, request).toBaseItems(api, false)
|
||||||
}
|
}.also(jobs::add)
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -216,8 +219,17 @@ class RecommendedMovieViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the continue watching row is empty, then wait until the first successful row
|
||||||
|
// is loaded before telling the UI that the page is loaded
|
||||||
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
|
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
|
||||||
loading.setValueOnMain(LoadingState.Success)
|
for (i in 0..<jobs.size) {
|
||||||
|
val result = jobs[i].await()
|
||||||
|
if (result.completed) {
|
||||||
|
Timber.v("First success")
|
||||||
|
loading.setValueOnMain(LoadingState.Success)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,10 +237,11 @@ class RecommendedMovieViewModel
|
||||||
override fun update(
|
override fun update(
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
row: HomeRowLoadingState,
|
row: HomeRowLoadingState,
|
||||||
) {
|
): HomeRowLoadingState {
|
||||||
rows.update { current ->
|
rows.update { current ->
|
||||||
current.toMutableList().apply { set(rowTitles[title]!!, row) }
|
current.toMutableList().apply { set(rowTitles[title]!!, row) }
|
||||||
}
|
}
|
||||||
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|
@ -170,6 +171,8 @@ class RecommendedTvShowViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val jobs = mutableListOf<Deferred<HomeRowLoadingState>>()
|
||||||
|
|
||||||
update(R.string.recently_released) {
|
update(R.string.recently_released) {
|
||||||
val request =
|
val request =
|
||||||
GetItemsRequest(
|
GetItemsRequest(
|
||||||
|
|
@ -185,7 +188,7 @@ class RecommendedTvShowViewModel
|
||||||
enableTotalRecordCount = false,
|
enableTotalRecordCount = false,
|
||||||
)
|
)
|
||||||
GetItemsRequestHandler.execute(api, request).toBaseItems(api, true)
|
GetItemsRequestHandler.execute(api, request).toBaseItems(api, true)
|
||||||
}
|
}.also(jobs::add)
|
||||||
|
|
||||||
update(R.string.recently_added) {
|
update(R.string.recently_added) {
|
||||||
val request =
|
val request =
|
||||||
|
|
@ -202,7 +205,7 @@ class RecommendedTvShowViewModel
|
||||||
enableTotalRecordCount = false,
|
enableTotalRecordCount = false,
|
||||||
)
|
)
|
||||||
GetItemsRequestHandler.execute(api, request).toBaseItems(api, true)
|
GetItemsRequestHandler.execute(api, request).toBaseItems(api, true)
|
||||||
}
|
}.also(jobs::add)
|
||||||
|
|
||||||
update(R.string.top_unwatched) {
|
update(R.string.top_unwatched) {
|
||||||
val request =
|
val request =
|
||||||
|
|
@ -220,7 +223,7 @@ class RecommendedTvShowViewModel
|
||||||
enableTotalRecordCount = false,
|
enableTotalRecordCount = false,
|
||||||
)
|
)
|
||||||
GetItemsRequestHandler.execute(api, request).toBaseItems(api, true)
|
GetItemsRequestHandler.execute(api, request).toBaseItems(api, true)
|
||||||
}
|
}.also(jobs::add)
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -264,7 +267,14 @@ class RecommendedTvShowViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
|
if (loading.value == LoadingState.Loading || loading.value == LoadingState.Pending) {
|
||||||
loading.setValueOnMain(LoadingState.Success)
|
for (i in 0..<jobs.size) {
|
||||||
|
val result = jobs[i].await()
|
||||||
|
if (result is HomeRowLoadingState.Success) {
|
||||||
|
Timber.v("First success")
|
||||||
|
loading.setValueOnMain(LoadingState.Success)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -272,10 +282,11 @@ class RecommendedTvShowViewModel
|
||||||
override fun update(
|
override fun update(
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
row: HomeRowLoadingState,
|
row: HomeRowLoadingState,
|
||||||
) {
|
): HomeRowLoadingState {
|
||||||
rows.update { current ->
|
rows.update { current ->
|
||||||
current.toMutableList().apply { set(rowTitles[title]!!, row) }
|
current.toMutableList().apply { set(rowTitles[title]!!, row) }
|
||||||
}
|
}
|
||||||
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.github.damontecres.wholphin.ui.discover
|
package com.github.damontecres.wholphin.ui.discover
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
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
|
||||||
|
|
@ -11,6 +13,7 @@ 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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -23,6 +26,7 @@ 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.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
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.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
@ -47,6 +51,7 @@ import com.github.damontecres.wholphin.ui.listToDotString
|
||||||
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
||||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
|
||||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||||
import com.google.common.cache.CacheBuilder
|
import com.google.common.cache.CacheBuilder
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -186,6 +191,7 @@ data class DiscoverState(
|
||||||
val upcomingTv: DiscoverRowData = DiscoverRowData.EMPTY,
|
val upcomingTv: DiscoverRowData = DiscoverRowData.EMPTY,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SeerrDiscoverPage(
|
fun SeerrDiscoverPage(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
|
|
@ -249,28 +255,41 @@ fun SeerrDiscoverPage(
|
||||||
.padding(top = 24.dp, bottom = 16.dp, start = 32.dp)
|
.padding(top = 24.dp, bottom = 16.dp, start = 32.dp)
|
||||||
.fillMaxHeight(.25f),
|
.fillMaxHeight(.25f),
|
||||||
)
|
)
|
||||||
LazyColumn(
|
val density = LocalDensity.current
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
val spaceAbovePx =
|
||||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 40.dp),
|
with(density) {
|
||||||
modifier =
|
// The size of the row titles & spacing
|
||||||
Modifier
|
50.dp.toPx()
|
||||||
.focusRestorer()
|
}
|
||||||
.fillMaxSize(),
|
val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalBringIntoViewSpec provides ScrollToTopBringIntoViewSpec(spaceAbovePx),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(rows) { rowIndex, row ->
|
LazyColumn(
|
||||||
DiscoverRow(
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
row = row,
|
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 40.dp),
|
||||||
onClickItem = { index, item ->
|
modifier =
|
||||||
position = RowColumn(rowIndex, index)
|
Modifier
|
||||||
viewModel.navigationManager.navigateTo(item.destination)
|
.focusRestorer()
|
||||||
},
|
.fillMaxSize(),
|
||||||
onLongClickItem = { index, item -> },
|
) {
|
||||||
onCardFocus = { index -> position = RowColumn(rowIndex, index) },
|
itemsIndexed(rows) { rowIndex, row ->
|
||||||
focusRequester = focusRequesters[rowIndex],
|
CompositionLocalProvider(LocalBringIntoViewSpec provides defaultBringIntoViewSpec) {
|
||||||
modifier =
|
DiscoverRow(
|
||||||
Modifier
|
row = row,
|
||||||
.fillMaxWidth(),
|
onClickItem = { index, item ->
|
||||||
)
|
position = RowColumn(rowIndex, index)
|
||||||
|
viewModel.navigationManager.navigateTo(item.destination)
|
||||||
|
},
|
||||||
|
onLongClickItem = { index, item -> },
|
||||||
|
onCardFocus = { index -> position = RowColumn(rowIndex, index) },
|
||||||
|
focusRequester = focusRequesters[rowIndex],
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.github.damontecres.wholphin.ui.main
|
package com.github.damontecres.wholphin.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
|
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -18,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -33,6 +36,7 @@ import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -72,6 +76,7 @@ import com.github.damontecres.wholphin.ui.playback.playable
|
||||||
import com.github.damontecres.wholphin.ui.playback.scale
|
import com.github.damontecres.wholphin.ui.playback.scale
|
||||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
@ -184,6 +189,7 @@ fun HomePage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePageContent(
|
fun HomePageContent(
|
||||||
homeRows: List<HomeRowLoadingState>,
|
homeRows: List<HomeRowLoadingState>,
|
||||||
|
|
@ -245,101 +251,123 @@ fun HomePageContent(
|
||||||
.padding(top = 48.dp, bottom = 32.dp, start = 8.dp)
|
.padding(top = 48.dp, bottom = 32.dp, start = 8.dp)
|
||||||
.fillMaxHeight(.33f),
|
.fillMaxHeight(.33f),
|
||||||
)
|
)
|
||||||
LazyColumn(
|
val density = LocalDensity.current
|
||||||
state = listState,
|
val spaceAbovePx =
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
with(density) {
|
||||||
contentPadding =
|
// The size of the row titles & spacing
|
||||||
PaddingValues(
|
50.dp.toPx()
|
||||||
bottom = Cards.height2x3,
|
}
|
||||||
),
|
val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
|
||||||
modifier =
|
CompositionLocalProvider(
|
||||||
Modifier
|
LocalBringIntoViewSpec provides ScrollToTopBringIntoViewSpec(spaceAbovePx),
|
||||||
.focusRestorer(),
|
|
||||||
) {
|
) {
|
||||||
itemsIndexed(homeRows) { rowIndex, row ->
|
LazyColumn(
|
||||||
when (val r = row) {
|
state = listState,
|
||||||
is HomeRowLoadingState.Loading,
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
is HomeRowLoadingState.Pending,
|
contentPadding =
|
||||||
-> {
|
PaddingValues(
|
||||||
FocusableItemRow(
|
bottom = Cards.height2x3,
|
||||||
title = r.title,
|
),
|
||||||
subtitle = stringResource(R.string.loading),
|
modifier =
|
||||||
modifier = Modifier.animateItem(),
|
Modifier
|
||||||
)
|
.focusRestorer(),
|
||||||
}
|
) {
|
||||||
|
itemsIndexed(homeRows) { rowIndex, row ->
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalBringIntoViewSpec provides defaultBringIntoViewSpec,
|
||||||
|
) {
|
||||||
|
when (val r = row) {
|
||||||
|
is HomeRowLoadingState.Loading,
|
||||||
|
is HomeRowLoadingState.Pending,
|
||||||
|
-> {
|
||||||
|
FocusableItemRow(
|
||||||
|
title = r.title,
|
||||||
|
subtitle = stringResource(R.string.loading),
|
||||||
|
modifier = Modifier.animateItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
is HomeRowLoadingState.Error -> {
|
is HomeRowLoadingState.Error -> {
|
||||||
FocusableItemRow(
|
FocusableItemRow(
|
||||||
title = r.title,
|
title = r.title,
|
||||||
subtitle = r.localizedMessage,
|
subtitle = r.localizedMessage,
|
||||||
isError = true,
|
isError = true,
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is HomeRowLoadingState.Success -> {
|
is HomeRowLoadingState.Success -> {
|
||||||
if (row.items.isNotEmpty()) {
|
if (row.items.isNotEmpty()) {
|
||||||
val viewOptions = row.viewOptions
|
val viewOptions = row.viewOptions
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = row.title,
|
title = row.title,
|
||||||
items = row.items,
|
items = row.items,
|
||||||
onClickItem = { index, item ->
|
onClickItem = { index, item ->
|
||||||
onClickItem.invoke(RowColumn(rowIndex, index), item)
|
onClickItem.invoke(RowColumn(rowIndex, index), item)
|
||||||
},
|
},
|
||||||
onLongClickItem = { index, item ->
|
onLongClickItem = { index, item ->
|
||||||
onLongClickItem.invoke(RowColumn(rowIndex, index), item)
|
onLongClickItem.invoke(
|
||||||
},
|
RowColumn(rowIndex, index),
|
||||||
modifier =
|
item,
|
||||||
Modifier
|
)
|
||||||
.fillMaxWidth()
|
},
|
||||||
.focusGroup()
|
|
||||||
.focusRequester(rowFocusRequesters[rowIndex])
|
|
||||||
.animateItem(),
|
|
||||||
horizontalPadding = viewOptions.spacing.dp,
|
|
||||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
|
||||||
HomePageCardContent(
|
|
||||||
index = index,
|
|
||||||
item = item,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
viewOptions = viewOptions,
|
|
||||||
modifier =
|
modifier =
|
||||||
cardModifier
|
Modifier
|
||||||
.onFocusChanged {
|
.fillMaxWidth()
|
||||||
if (it.isFocused) {
|
.focusGroup()
|
||||||
position = RowColumn(rowIndex, index)
|
.focusRequester(rowFocusRequesters[rowIndex])
|
||||||
}
|
.animateItem(),
|
||||||
if (it.isFocused && onFocusPosition != null) {
|
horizontalPadding = viewOptions.spacing.dp,
|
||||||
val nonEmptyRowBefore =
|
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||||
homeRows
|
HomePageCardContent(
|
||||||
.subList(0, rowIndex)
|
index = index,
|
||||||
.count {
|
item = item,
|
||||||
it is HomeRowLoadingState.Success && it.items.isEmpty()
|
onClick = onClick,
|
||||||
}
|
onLongClick = onLongClick,
|
||||||
onFocusPosition.invoke(
|
viewOptions = viewOptions,
|
||||||
RowColumn(
|
modifier =
|
||||||
rowIndex - nonEmptyRowBefore,
|
cardModifier
|
||||||
index,
|
.onFocusChanged {
|
||||||
),
|
if (it.isFocused) {
|
||||||
)
|
position =
|
||||||
}
|
RowColumn(rowIndex, index)
|
||||||
}.onKeyEvent {
|
}
|
||||||
if (isPlayKeyUp(it) && item?.type?.playable == true) {
|
if (it.isFocused && onFocusPosition != null) {
|
||||||
Timber.v("Clicked play on ${item.id}")
|
val nonEmptyRowBefore =
|
||||||
onClickPlay.invoke(position, item)
|
homeRows
|
||||||
return@onKeyEvent true
|
.subList(0, rowIndex)
|
||||||
}
|
.count {
|
||||||
return@onKeyEvent false
|
it is HomeRowLoadingState.Success && it.items.isEmpty()
|
||||||
},
|
}
|
||||||
|
onFocusPosition.invoke(
|
||||||
|
RowColumn(
|
||||||
|
rowIndex - nonEmptyRowBefore,
|
||||||
|
index,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.onKeyEvent {
|
||||||
|
if (isPlayKeyUp(it) && item?.type?.playable == true) {
|
||||||
|
Timber.v("Clicked play on ${item.id}")
|
||||||
|
onClickPlay.invoke(
|
||||||
|
position,
|
||||||
|
item,
|
||||||
|
)
|
||||||
|
return@onKeyEvent true
|
||||||
|
}
|
||||||
|
return@onKeyEvent false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
},
|
} else if (showEmptyRows) {
|
||||||
)
|
FocusableItemRow(
|
||||||
} else if (showEmptyRows) {
|
title = r.title,
|
||||||
FocusableItemRow(
|
subtitle = stringResource(R.string.no_results),
|
||||||
title = r.title,
|
modifier = Modifier.animateItem(),
|
||||||
subtitle = stringResource(R.string.no_results),
|
)
|
||||||
modifier = Modifier.animateItem(),
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.util
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.BringIntoViewSpec
|
||||||
|
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides scrolling so that the item being scrolled to is at the top of the view offset by the provided pixels
|
||||||
|
*
|
||||||
|
* Note: the offset is necessary for anything that is focuseable, but has content before (eg a title) that needs to be displayed too
|
||||||
|
*
|
||||||
|
* Note: this applies to ALL scrollable composables within its scope, so a [LazyColumn] of [androidx.compose.foundation.lazy.LazyRow]s likely needs nested [LocalBringIntoViewSpec] overrides
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```kotlin
|
||||||
|
* val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
|
||||||
|
* CompositionLocalProvider(LocalBringIntoViewSpec provides ScrollToTopBringIntoViewSpec(spaceAbovePx)) {
|
||||||
|
* LazyColumn{
|
||||||
|
* items(list){
|
||||||
|
* CompositionLocalProvider(LocalBringIntoViewSpec provides defaultBringIntoViewSpec) {
|
||||||
|
* // Content
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class ScrollToTopBringIntoViewSpec(
|
||||||
|
val spaceAbovePx: Float = 100f,
|
||||||
|
) : BringIntoViewSpec {
|
||||||
|
override fun calculateScrollDistance(
|
||||||
|
offset: Float,
|
||||||
|
size: Float,
|
||||||
|
containerSize: Float,
|
||||||
|
): Float {
|
||||||
|
// Timber.v(
|
||||||
|
// "calculateScrollDistance: offset=%s, size=%s, containerSize=%s",
|
||||||
|
// offset,
|
||||||
|
// size,
|
||||||
|
// containerSize,
|
||||||
|
// )
|
||||||
|
return offset - spaceAbovePx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,9 @@ sealed interface RowLoadingState {
|
||||||
sealed interface HomeRowLoadingState {
|
sealed interface HomeRowLoadingState {
|
||||||
val title: String
|
val title: String
|
||||||
|
|
||||||
|
val completed: Boolean
|
||||||
|
get() = this is Success || this is Error
|
||||||
|
|
||||||
data class Pending(
|
data class Pending(
|
||||||
override val title: String,
|
override val title: String,
|
||||||
) : HomeRowLoadingState
|
) : HomeRowLoadingState
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue