mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Merge branch 'main' into develop/jellyseerr
This commit is contained in:
commit
99dc93d08e
22 changed files with 512 additions and 327 deletions
|
|
@ -59,6 +59,7 @@ android {
|
|||
debug {
|
||||
isMinifyEnabled = false
|
||||
isDebuggable = true
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
|
|
|
|||
4
app/src/debug/res/values/strings.xml
Normal file
4
app/src/debug/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Wholphin (Debug)</string>
|
||||
</resources>
|
||||
|
|
@ -32,7 +32,6 @@ import androidx.tv.material3.Surface
|
|||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.AppUpgradeHandler
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
|
|
@ -58,10 +57,7 @@ import com.github.damontecres.wholphin.ui.util.ProvideLocalClock
|
|||
import com.github.damontecres.wholphin.util.DebugLogTree
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
|
|
@ -110,7 +106,7 @@ class MainActivity : AppCompatActivity() {
|
|||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.i("MainActivity.onCreate")
|
||||
Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}")
|
||||
lifecycle.addObserver(playbackLifecycleObserver)
|
||||
if (savedInstanceState == null) {
|
||||
appUpgradeHandler.copySubfont(false)
|
||||
|
|
@ -217,12 +213,8 @@ class MainActivity : AppCompatActivity() {
|
|||
appPreferences,
|
||||
)
|
||||
val preferences =
|
||||
remember(appPreferences, current) {
|
||||
UserPreferences(
|
||||
appPreferences,
|
||||
current.userDto.configuration
|
||||
?: DefaultUserConfiguration,
|
||||
)
|
||||
remember(appPreferences) {
|
||||
UserPreferences(appPreferences)
|
||||
}
|
||||
ApplicationContent(
|
||||
user = current.user,
|
||||
|
|
@ -246,6 +238,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Timber.d("onResume")
|
||||
lifecycleScope.launchIO {
|
||||
appUpgradeHandler.run()
|
||||
}
|
||||
|
|
@ -253,7 +246,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
override fun onRestart() {
|
||||
super.onRestart()
|
||||
Timber.i("onRestart")
|
||||
Timber.d("onRestart")
|
||||
viewModel.appStart()
|
||||
// val signInAutomatically =
|
||||
// runBlocking { userPreferencesDataStore.data.firstOrNull()?.signInAutomatically } ?: true
|
||||
|
|
@ -264,6 +257,36 @@ class MainActivity : AppCompatActivity() {
|
|||
// serverRepository.closeSession()
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
Timber.d("onStop")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Timber.d("onPause")
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Timber.d("onStart")
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
Timber.d("onSaveInstanceState")
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
Timber.d("onRestoreInstanceState")
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Timber.d("onDestroy")
|
||||
}
|
||||
}
|
||||
|
||||
@HiltViewModel
|
||||
|
|
@ -277,40 +300,42 @@ class MainActivityViewModel
|
|||
private val backdropService: BackdropService,
|
||||
) : ViewModel() {
|
||||
fun appStart() {
|
||||
viewModelScope.launch {
|
||||
val prefs = preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
||||
if (prefs.signInAutomatically) {
|
||||
val current =
|
||||
withContext(Dispatchers.IO) {
|
||||
viewModelScope.launchIO {
|
||||
try {
|
||||
val prefs =
|
||||
preferences.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
||||
if (prefs.signInAutomatically) {
|
||||
val current =
|
||||
serverRepository.restoreSession(
|
||||
prefs.currentServerId?.toUUIDOrNull(),
|
||||
prefs.currentUserId?.toUUIDOrNull(),
|
||||
)
|
||||
}
|
||||
if (current != null) {
|
||||
// Restored
|
||||
navigationManager.navigateTo(SetupDestination.AppContent(current))
|
||||
} else {
|
||||
// Did not restore
|
||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
}
|
||||
} else {
|
||||
navigationManager.navigateTo(SetupDestination.Loading)
|
||||
backdropService.clearBackdrop()
|
||||
val currentServerId = prefs.currentServerId?.toUUIDOrNull()
|
||||
if (currentServerId != null) {
|
||||
val currentServer =
|
||||
withContext(Dispatchers.IO) {
|
||||
serverRepository.serverDao.getServer(currentServerId)?.server
|
||||
}
|
||||
if (currentServer != null) {
|
||||
navigationManager.navigateTo(SetupDestination.UserList(currentServer))
|
||||
if (current != null) {
|
||||
// Restored
|
||||
navigationManager.navigateTo(SetupDestination.AppContent(current))
|
||||
} else {
|
||||
// Did not restore
|
||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
}
|
||||
} else {
|
||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
navigationManager.navigateTo(SetupDestination.Loading)
|
||||
backdropService.clearBackdrop()
|
||||
val currentServerId = prefs.currentServerId?.toUUIDOrNull()
|
||||
if (currentServerId != null) {
|
||||
val currentServer =
|
||||
serverRepository.serverDao.getServer(currentServerId)?.server
|
||||
if (currentServer != null) {
|
||||
navigationManager.navigateTo(SetupDestination.UserList(currentServer))
|
||||
} else {
|
||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
}
|
||||
} else {
|
||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
}
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error during appStart")
|
||||
navigationManager.navigateTo(SetupDestination.ServerList)
|
||||
}
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
|
|
|
|||
|
|
@ -46,9 +46,11 @@ class ServerRepository
|
|||
private var _current = EqualityMutableLiveData<CurrentUser?>(null)
|
||||
val current: LiveData<CurrentUser?> = _current
|
||||
|
||||
private var _currentUserDto = EqualityMutableLiveData<UserDto?>(null)
|
||||
val currentUserDto: LiveData<UserDto?> = _currentUserDto
|
||||
|
||||
val currentServer: LiveData<JellyfinServer?> get() = _current.map { it?.server }
|
||||
val currentUser: LiveData<JellyfinUser?> get() = _current.map { it?.user }
|
||||
val currentUserDto: LiveData<UserDto?> get() = _current.map { it?.userDto }
|
||||
|
||||
/**
|
||||
* Adds a server to the app database and updated the [ApiClient] to the server's URL
|
||||
|
|
@ -101,7 +103,8 @@ class ServerRepository
|
|||
}.build()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_current.value = CurrentUser(updatedServer, updatedUser, userDto)
|
||||
_current.value = CurrentUser(updatedServer, updatedUser)
|
||||
_currentUserDto.value = userDto
|
||||
}
|
||||
sharedPreferences.edit(true) {
|
||||
putString(SERVER_URL_KEY, updatedServer.url)
|
||||
|
|
@ -128,11 +131,21 @@ class ServerRepository
|
|||
serverDao.getServer(serverId)
|
||||
}
|
||||
if (serverAndUsers != null) {
|
||||
val user = serverAndUsers.users.firstOrNull { it.id == userId }
|
||||
if (user != null) {
|
||||
// TODO pin-related
|
||||
val current = _current.value
|
||||
if (current != null && current.server.id == serverId && current.user.id == userId) {
|
||||
Timber.v("Restoring session for current user, so shortcut")
|
||||
apiClient.update(
|
||||
baseUrl = current.server.url,
|
||||
accessToken = current.user.accessToken,
|
||||
)
|
||||
return current
|
||||
} else {
|
||||
val user = serverAndUsers.users.firstOrNull { it.id == userId }
|
||||
if (user != null) {
|
||||
// TODO pin-related
|
||||
// if (user != null && !user.hasPin) {
|
||||
return changeUser(serverAndUsers.server, user)
|
||||
return changeUser(serverAndUsers.server, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
|
@ -271,5 +284,4 @@ class ServerRepository
|
|||
data class CurrentUser(
|
||||
val server: JellyfinServer,
|
||||
val user: JellyfinUser,
|
||||
val userDto: UserDto,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import org.jellyfin.sdk.model.api.UserConfiguration
|
|||
*/
|
||||
data class UserPreferences(
|
||||
val appPreferences: AppPreferences,
|
||||
val userConfig: UserConfiguration,
|
||||
)
|
||||
|
||||
val DefaultUserConfiguration =
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import org.jellyfin.sdk.model.api.MediaSourceInfo
|
|||
import org.jellyfin.sdk.model.api.MediaStream
|
||||
import org.jellyfin.sdk.model.api.MediaStreamType
|
||||
import org.jellyfin.sdk.model.api.SubtitlePlaybackMode
|
||||
import org.jellyfin.sdk.model.api.UserConfiguration
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
|
@ -26,6 +27,8 @@ class StreamChoiceService
|
|||
private val serverRepository: ServerRepository,
|
||||
private val playbackLanguageChoiceDao: PlaybackLanguageChoiceDao,
|
||||
) {
|
||||
private val userConfig: UserConfiguration? get() = serverRepository.currentUserDto.value?.configuration
|
||||
|
||||
suspend fun updateAudio(
|
||||
dto: BaseItemDto,
|
||||
audioLang: String,
|
||||
|
|
@ -117,7 +120,7 @@ class StreamChoiceService
|
|||
val seriesLang =
|
||||
playbackLanguageChoice?.audioLanguage?.takeIf { it.isNotNullOrBlank() }
|
||||
// If the user has chosen a different language for the series, prefer that
|
||||
val audioLanguage = seriesLang ?: prefs.userConfig.audioLanguagePreference
|
||||
val audioLanguage = seriesLang ?: userConfig?.audioLanguagePreference
|
||||
|
||||
if (audioLanguage.isNotNullOrBlank()) {
|
||||
val sorted =
|
||||
|
|
@ -174,7 +177,7 @@ class StreamChoiceService
|
|||
val seriesLang =
|
||||
playbackLanguageChoice?.subtitleLanguage?.takeIf { it.isNotNullOrBlank() }
|
||||
val subtitleLanguage =
|
||||
(seriesLang ?: prefs.userConfig.subtitleLanguagePreference)
|
||||
(seriesLang ?: userConfig?.subtitleLanguagePreference)
|
||||
?.takeIf { it.isNotNullOrBlank() }
|
||||
|
||||
val subtitleMode =
|
||||
|
|
@ -192,7 +195,7 @@ class StreamChoiceService
|
|||
|
||||
else -> {
|
||||
// Fallback to the user's preference
|
||||
prefs.userConfig.subtitleMode
|
||||
userConfig?.subtitleMode ?: SubtitlePlaybackMode.DEFAULT
|
||||
}
|
||||
}
|
||||
return when (subtitleMode) {
|
||||
|
|
@ -213,7 +216,7 @@ class StreamChoiceService
|
|||
}
|
||||
|
||||
SubtitlePlaybackMode.SMART -> {
|
||||
val audioLanguage = prefs.userConfig.audioLanguagePreference
|
||||
val audioLanguage = userConfig?.audioLanguagePreference
|
||||
val audioStreamLang = audioStream?.language
|
||||
if (audioLanguage.isNotNullOrBlank() && audioStreamLang.isNotNullOrBlank() && audioLanguage != audioStreamLang) {
|
||||
candidates.firstOrNull { it.language == subtitleLanguage }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ package com.github.damontecres.wholphin.services
|
|||
import androidx.datastore.core.DataStore
|
||||
import com.github.damontecres.wholphin.data.ServerRepository
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.DefaultUserConfiguration
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import javax.inject.Inject
|
||||
|
|
@ -21,7 +20,6 @@ class UserPreferencesService
|
|||
val appPrefs = preferencesDataStore.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
||||
UserPreferences(
|
||||
appPrefs,
|
||||
userConfig ?: DefaultUserConfiguration,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import androidx.media3.common.Player
|
||||
import coil3.request.ErrorResult
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
|
|
@ -167,6 +169,26 @@ fun OneTimeLaunchedEffect(runOnceBlock: suspend CoroutineScope.() -> Unit) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls [tryRequestFocus] on the provided [FocusRequester] when this composable launches or resumes
|
||||
*/
|
||||
@Composable
|
||||
fun RequestOrRestoreFocus(
|
||||
focusRequester: FocusRequester?,
|
||||
debugKey: String? = null,
|
||||
) {
|
||||
if (focusRequester != null) {
|
||||
LaunchedEffect(Unit) {
|
||||
debugKey?.let { Timber.v("RequestOrRestoreFocus: %s", it) }
|
||||
focusRequester.tryRequestFocus()
|
||||
}
|
||||
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
|
||||
debugKey?.let { Timber.v("RequestOrRestoreFocus onResume: %s", it) }
|
||||
focusRequester.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.enableMarquee(focused: Boolean) =
|
||||
if (focused) {
|
||||
basicMarquee(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.draw.alpha
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
|
@ -77,7 +78,7 @@ fun GenreCard(
|
|||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.alpha(.6f)
|
||||
.alpha(.75f)
|
||||
.aspectRatio(AspectRatios.WIDE)
|
||||
.fillMaxSize(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
|
|
@ -67,7 +68,7 @@ import com.github.damontecres.wholphin.services.BackdropService
|
|||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.cards.GridCard
|
||||
import com.github.damontecres.wholphin.ui.data.AddPlaylistViewModel
|
||||
|
|
@ -85,17 +86,18 @@ import com.github.damontecres.wholphin.ui.playback.scale
|
|||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.toServerString
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.GetPersonsHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
|
|
@ -114,13 +116,13 @@ import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
|||
import timber.log.Timber
|
||||
import java.util.TreeSet
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
|
||||
@HiltViewModel
|
||||
@HiltViewModel(assistedFactory = CollectionFolderViewModel.Factory::class)
|
||||
class CollectionFolderViewModel
|
||||
@Inject
|
||||
@AssistedInject
|
||||
constructor(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
api: ApiClient,
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val serverRepository: ServerRepository,
|
||||
|
|
@ -128,7 +130,25 @@ class CollectionFolderViewModel
|
|||
private val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val backdropService: BackdropService,
|
||||
val navigationManager: NavigationManager,
|
||||
@Assisted itemId: String,
|
||||
@Assisted initialSortAndDirection: SortAndDirection?,
|
||||
@Assisted("recursive") private val recursive: Boolean,
|
||||
@Assisted private val collectionFilter: CollectionFolderFilter,
|
||||
@Assisted("useSeriesForPrimary") private val useSeriesForPrimary: Boolean,
|
||||
@Assisted defaultViewOptions: ViewOptions,
|
||||
) : ItemViewModel(api) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
itemId: String,
|
||||
initialSortAndDirection: SortAndDirection?,
|
||||
@Assisted("recursive") recursive: Boolean,
|
||||
collectionFilter: CollectionFolderFilter,
|
||||
@Assisted("useSeriesForPrimary") useSeriesForPrimary: Boolean,
|
||||
defaultViewOptions: ViewOptions,
|
||||
): CollectionFolderViewModel
|
||||
}
|
||||
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val backgroundLoading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val pager = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
|
|
@ -136,26 +156,19 @@ class CollectionFolderViewModel
|
|||
val filter = MutableLiveData<GetItemsFilter>(GetItemsFilter())
|
||||
val viewOptions = MutableLiveData<ViewOptions>()
|
||||
|
||||
private var useSeriesForPrimary: Boolean = true
|
||||
private lateinit var collectionFilter: CollectionFolderFilter
|
||||
var position: Int
|
||||
get() = savedStateHandle.get<Int>("position") ?: 0
|
||||
set(value) {
|
||||
savedStateHandle["position"] = value
|
||||
}
|
||||
|
||||
fun init(
|
||||
itemId: String,
|
||||
initialSortAndDirection: SortAndDirection?,
|
||||
recursive: Boolean,
|
||||
collectionFilter: CollectionFolderFilter,
|
||||
useSeriesForPrimary: Boolean,
|
||||
defaultViewOptions: ViewOptions,
|
||||
): Job =
|
||||
init {
|
||||
viewModelScope.launch(
|
||||
LoadingExceptionHandler(
|
||||
loading,
|
||||
context.getString(R.string.error_loading_collection, itemId),
|
||||
) + Dispatchers.IO,
|
||||
) {
|
||||
this@CollectionFolderViewModel.collectionFilter = collectionFilter
|
||||
this@CollectionFolderViewModel.useSeriesForPrimary = useSeriesForPrimary
|
||||
this@CollectionFolderViewModel.itemId = itemId
|
||||
itemId.toUUIDOrNull()?.let {
|
||||
fetchItem(it)
|
||||
}
|
||||
|
|
@ -184,6 +197,7 @@ class CollectionFolderViewModel
|
|||
|
||||
loadResults(true, sortAndDirection, recursive, filterToUse, useSeriesForPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveLibraryDisplayInfo(
|
||||
newFilter: GetItemsFilter = this.filter.value!!,
|
||||
|
|
@ -537,25 +551,27 @@ fun CollectionFolderGrid(
|
|||
playEnabled: Boolean,
|
||||
defaultViewOptions: ViewOptions,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: CollectionFolderViewModel = hiltViewModel(key = itemId),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
initialSortAndDirection: SortAndDirection? = null,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
useSeriesForPrimary: Boolean = true,
|
||||
filterOptions: List<ItemFilterBy<*>> = DefaultFilterOptions,
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
viewModel: CollectionFolderViewModel =
|
||||
hiltViewModel<CollectionFolderViewModel, CollectionFolderViewModel.Factory>(
|
||||
key = itemId,
|
||||
) {
|
||||
it.create(
|
||||
itemId = itemId,
|
||||
initialSortAndDirection = initialSortAndDirection,
|
||||
recursive = recursive,
|
||||
collectionFilter = initialFilter,
|
||||
useSeriesForPrimary = useSeriesForPrimary,
|
||||
defaultViewOptions = defaultViewOptions,
|
||||
)
|
||||
},
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
OneTimeLaunchedEffect {
|
||||
viewModel.init(
|
||||
itemId,
|
||||
initialSortAndDirection,
|
||||
recursive,
|
||||
initialFilter,
|
||||
useSeriesForPrimary,
|
||||
defaultViewOptions,
|
||||
)
|
||||
}
|
||||
val sortAndDirection by viewModel.sortAndDirection.observeAsState(SortAndDirection.DEFAULT)
|
||||
val filter by viewModel.filter.observeAsState(initialFilter.filter)
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
|
|
@ -589,6 +605,7 @@ fun CollectionFolderGrid(
|
|||
Box(modifier = modifier) {
|
||||
CollectionFolderGridContent(
|
||||
preferences = preferences,
|
||||
initialPosition = viewModel.position,
|
||||
item = item,
|
||||
title = title,
|
||||
pager = pager,
|
||||
|
|
@ -611,7 +628,10 @@ fun CollectionFolderGrid(
|
|||
},
|
||||
showTitle = showTitle,
|
||||
sortOptions = sortOptions,
|
||||
positionCallback = positionCallback,
|
||||
positionCallback = { columns, position ->
|
||||
viewModel.position = position
|
||||
positionCallback?.invoke(columns, position)
|
||||
},
|
||||
letterPosition = { viewModel.positionOfLetter(it) ?: -1 },
|
||||
viewOptions = viewOptions,
|
||||
defaultViewOptions = defaultViewOptions,
|
||||
|
|
@ -728,6 +748,7 @@ fun CollectionFolderGridContent(
|
|||
onClickPlayAll: (shuffle: Boolean) -> Unit,
|
||||
onClickPlay: (Int, BaseItem) -> Unit,
|
||||
onChangeBackdrop: (BaseItem) -> Unit,
|
||||
initialPosition: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
|
|
@ -742,10 +763,10 @@ fun CollectionFolderGridContent(
|
|||
var viewOptions by remember { mutableStateOf(viewOptions) }
|
||||
|
||||
val gridFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) { gridFocusRequester.tryRequestFocus() }
|
||||
RequestOrRestoreFocus(gridFocusRequester)
|
||||
var backdropImageUrl by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
var position by rememberInt(0)
|
||||
var position by rememberInt(initialPosition)
|
||||
val focusedItem = pager.getOrNull(position)
|
||||
if (viewOptions.showDetails) {
|
||||
LaunchedEffect(focusedItem) {
|
||||
|
|
@ -861,7 +882,7 @@ fun CollectionFolderGridContent(
|
|||
showJumpButtons = false, // TODO add preference
|
||||
showLetterButtons = sortAndDirection.sort == ItemSortBy.SORT_NAME,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
initialPosition = 0,
|
||||
initialPosition = initialPosition,
|
||||
positionCallback = { columns, newPosition ->
|
||||
showHeader = newPosition < columns
|
||||
position = newPosition
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.times
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
|
@ -68,7 +72,10 @@ class GenreViewModel
|
|||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val genres = MutableLiveData<List<Genre>>(listOf())
|
||||
|
||||
fun init(itemId: UUID) {
|
||||
fun init(
|
||||
itemId: UUID,
|
||||
cardWidthPx: Int,
|
||||
) {
|
||||
loading.value = LoadingState.Loading
|
||||
this.itemId = itemId
|
||||
viewModelScope.launch(Dispatchers.IO + LoadingExceptionHandler(loading, "Failed to fetch genres")) {
|
||||
|
|
@ -133,7 +140,8 @@ class GenreViewModel
|
|||
item.type,
|
||||
null,
|
||||
false,
|
||||
ImageType.THUMB,
|
||||
ImageType.BACKDROP,
|
||||
fillWidth = cardWidthPx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -179,8 +187,23 @@ fun GenreCardGrid(
|
|||
modifier: Modifier = Modifier,
|
||||
viewModel: GenreViewModel = hiltViewModel(),
|
||||
) {
|
||||
val columns = 4
|
||||
val spacing = 16.dp
|
||||
val density = LocalDensity.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val cardWidthPx =
|
||||
remember {
|
||||
with(density) {
|
||||
// Grid has 16dp padding on either side & 16dp spacing between 4 cards
|
||||
// This isn't exact though because it doesn't account for nav drawer or letters, but it's close and the calculation is much faster
|
||||
// E.g. on 1080p, this results in 440px versus 395px actual, so only minimal scaling down is required
|
||||
(configuration.screenWidthDp.dp - (2 * 16.dp + 3 * spacing))
|
||||
.div(columns)
|
||||
.roundToPx()
|
||||
}
|
||||
}
|
||||
OneTimeLaunchedEffect {
|
||||
viewModel.init(itemId)
|
||||
viewModel.init(itemId, cardWidthPx)
|
||||
}
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
|
||||
val genres by viewModel.genres.observeAsState(listOf())
|
||||
|
|
@ -231,7 +254,8 @@ fun GenreCardGrid(
|
|||
initialPosition = 0,
|
||||
positionCallback = { columns, position ->
|
||||
},
|
||||
columns = 4,
|
||||
columns = columns,
|
||||
spacing = spacing,
|
||||
cardContent = { item: Genre?, onClick: () -> Unit, onLongClick: () -> Unit, mod: Modifier ->
|
||||
GenreCard(
|
||||
genre = item,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||
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.setValue
|
||||
|
|
@ -49,8 +50,8 @@ fun TabRow(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
LaunchedEffect(Unit) {
|
||||
state.animateScrollToItem(selectedTabIndex)
|
||||
LaunchedEffect(selectedTabIndex) {
|
||||
state.animateScrollToItem(selectedTabIndex, -(state.layoutInfo.viewportSize.width / 3.5).toInt())
|
||||
}
|
||||
val focusRequesters = remember(tabs) { List(tabs.size) { FocusRequester() } }
|
||||
var rowHasFocus by remember { mutableStateOf(false) }
|
||||
|
|
|
|||
|
|
@ -117,12 +117,16 @@ fun <T : CardGridItem> CardGrid(
|
|||
val startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0))
|
||||
|
||||
val fractionCacheWindow = LazyLayoutCacheWindow(aheadFraction = 1f, behindFraction = 0.5f)
|
||||
val gridState = rememberLazyGridState(cacheWindow = fractionCacheWindow)
|
||||
var focusedIndex by rememberSaveable { mutableIntStateOf(initialPosition) }
|
||||
val gridState =
|
||||
rememberLazyGridState(
|
||||
cacheWindow = fractionCacheWindow,
|
||||
initialFirstVisibleItemIndex = focusedIndex,
|
||||
)
|
||||
val scope = rememberCoroutineScope()
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
val zeroFocus = remember { FocusRequester() }
|
||||
var previouslyFocusedIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||
var focusedIndex by rememberSaveable { mutableIntStateOf(initialPosition) }
|
||||
|
||||
var alphabetFocus by remember { mutableStateOf(false) }
|
||||
val focusOn = { index: Int ->
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ fun DebugPage(
|
|||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = "User server settings: ${preferences.userConfig}",
|
||||
text = "User server settings: ${viewModel.serverRepository.currentUserDto.value?.configuration}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -31,6 +30,7 @@ import com.github.damontecres.wholphin.R
|
|||
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
|
|
@ -48,7 +48,6 @@ import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
|||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -291,9 +290,7 @@ fun EpisodeDetailsContent(
|
|||
val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
|
||||
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||
}
|
||||
RequestOrRestoreFocus(focusRequesters.getOrNull(position))
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
|||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -49,6 +48,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
|||
import com.github.damontecres.wholphin.services.TrailerService
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||
import com.github.damontecres.wholphin.ui.cards.ChapterRow
|
||||
import com.github.damontecres.wholphin.ui.cards.ExtrasRow
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
|
|
@ -73,7 +73,6 @@ import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
|||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForPerson
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -391,9 +390,9 @@ fun MovieDetailsContent(
|
|||
val resumePosition = dto.userData?.playbackPositionTicks?.ticks ?: Duration.ZERO
|
||||
|
||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||
}
|
||||
|
||||
RequestOrRestoreFocus(focusRequesters.getOrNull(position))
|
||||
|
||||
Box(modifier = modifier) {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import androidx.compose.foundation.relocation.bringIntoViewRequester
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -46,6 +45,7 @@ import com.github.damontecres.wholphin.data.model.Trailer
|
|||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.TrailerService
|
||||
import com.github.damontecres.wholphin.ui.Cards
|
||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||
import com.github.damontecres.wholphin.ui.cards.ExtrasRow
|
||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||
|
|
@ -74,7 +74,6 @@ import com.github.damontecres.wholphin.ui.detail.movie.TrailerRow
|
|||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -88,13 +87,15 @@ fun SeriesDetails(
|
|||
preferences: UserPreferences,
|
||||
destination: Destination.MediaItem,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SeriesViewModel = hiltViewModel(),
|
||||
viewModel: SeriesViewModel =
|
||||
hiltViewModel<SeriesViewModel, SeriesViewModel.Factory>(
|
||||
creationCallback = {
|
||||
it.create(destination.itemId, null, SeriesPageType.DETAILS)
|
||||
},
|
||||
),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init(preferences, destination.itemId, null, true)
|
||||
}
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
|
||||
val item by viewModel.item.observeAsState()
|
||||
|
|
@ -285,7 +286,7 @@ private const val SIMILAR_ROW = EXTRAS_ROW + 1
|
|||
fun SeriesDetailsContent(
|
||||
preferences: UserPreferences,
|
||||
series: BaseItem,
|
||||
seasons: List<BaseItem>,
|
||||
seasons: List<BaseItem?>,
|
||||
similar: List<BaseItem>,
|
||||
trailers: List<Trailer>,
|
||||
extras: List<ExtrasItem>,
|
||||
|
|
@ -311,9 +312,7 @@ fun SeriesDetailsContent(
|
|||
var position by rememberInt()
|
||||
val focusRequesters = remember { List(SIMILAR_ROW + 1) { FocusRequester() } }
|
||||
val playFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||
}
|
||||
RequestOrRestoreFocus(focusRequesters.getOrNull(position))
|
||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
|
||||
Box(
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ package com.github.damontecres.wholphin.ui.detail.series
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -22,7 +21,6 @@ import androidx.lifecycle.map
|
|||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
|
|
@ -36,13 +34,12 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
|||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItems
|
||||
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.seasonEpisode
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
|
|
@ -52,7 +49,6 @@ import org.jellyfin.sdk.model.extensions.ticks
|
|||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration
|
||||
|
||||
|
|
@ -80,10 +76,15 @@ data class SeriesOverviewPosition(
|
|||
fun SeriesOverview(
|
||||
preferences: UserPreferences,
|
||||
destination: Destination.SeriesOverview,
|
||||
initialSeasonEpisode: SeasonEpisodeIds?,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: SeriesViewModel = hiltViewModel(),
|
||||
viewModel: SeriesViewModel =
|
||||
hiltViewModel<SeriesViewModel, SeriesViewModel.Factory>(
|
||||
creationCallback = {
|
||||
it.create(destination.itemId, initialSeasonEpisode, SeriesPageType.OVERVIEW)
|
||||
},
|
||||
),
|
||||
playlistViewModel: AddPlaylistViewModel = hiltViewModel(),
|
||||
initialSeasonEpisode: SeasonEpisodeIds? = null,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val firstItemFocusRequester = remember { FocusRequester() }
|
||||
|
|
@ -91,18 +92,6 @@ fun SeriesOverview(
|
|||
val castCrewRowFocusRequester = remember { FocusRequester() }
|
||||
val guestStarRowFocusRequester = remember { FocusRequester() }
|
||||
|
||||
var initialLoadDone by rememberSaveable { mutableStateOf(false) }
|
||||
OneTimeLaunchedEffect {
|
||||
Timber.v("SeriesDetailParent: itemId=${destination.itemId}, initialSeasonEpisode=$initialSeasonEpisode")
|
||||
viewModel.init(
|
||||
preferences,
|
||||
destination.itemId,
|
||||
initialSeasonEpisode,
|
||||
false,
|
||||
)
|
||||
initialLoadDone = true
|
||||
}
|
||||
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Loading)
|
||||
|
||||
val series by viewModel.item.observeAsState(null)
|
||||
|
|
@ -111,27 +100,9 @@ fun SeriesOverview(
|
|||
val peopleInEpisode by viewModel.peopleInEpisode.map { it.people }.observeAsState(listOf())
|
||||
val episodeList = (episodes as? EpisodeList.Success)?.episodes
|
||||
|
||||
var position by rememberSaveable(
|
||||
destination,
|
||||
loading,
|
||||
stateSaver =
|
||||
Saver(
|
||||
save = { listOf(it.seasonTabIndex, it.episodeRowIndex) },
|
||||
restore = { SeriesOverviewPosition(it[0], it[1]) },
|
||||
),
|
||||
) {
|
||||
mutableStateOf(
|
||||
SeriesOverviewPosition(
|
||||
seasons.indexOfFirstOrNull {
|
||||
equalsNotNull(it.id, initialSeasonEpisode?.seasonId) ||
|
||||
equalsNotNull(it.indexNumber, initialSeasonEpisode?.seasonNumber)
|
||||
} ?: 0,
|
||||
(episodes as? EpisodeList.Success)?.initialIndex ?: 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (initialLoadDone) {
|
||||
LaunchedEffect(Unit) {
|
||||
val position by viewModel.position.collectAsState(SeriesOverviewPosition(0, 0))
|
||||
LaunchedEffect(Unit) {
|
||||
if (seasons.isNotEmpty()) {
|
||||
seasons.getOrNull(position.seasonTabIndex)?.let {
|
||||
viewModel.loadEpisodes(it.id)
|
||||
}
|
||||
|
|
@ -301,13 +272,20 @@ fun SeriesOverview(
|
|||
episodeRowFocusRequester = episodeRowFocusRequester,
|
||||
castCrewRowFocusRequester = castCrewRowFocusRequester,
|
||||
guestStarRowFocusRequester = guestStarRowFocusRequester,
|
||||
onFocus = {
|
||||
if (it.seasonTabIndex != position.seasonTabIndex) {
|
||||
seasons.getOrNull(it.seasonTabIndex)?.let { season ->
|
||||
onChangeSeason = { index ->
|
||||
if (index != position.seasonTabIndex) {
|
||||
seasons.getOrNull(index)?.let { season ->
|
||||
viewModel.loadEpisodes(season.id)
|
||||
viewModel.position.update {
|
||||
SeriesOverviewPosition(index, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
position = it
|
||||
},
|
||||
onFocusEpisode = { episodeIndex ->
|
||||
viewModel.position.update {
|
||||
it.copy(episodeRowIndex = episodeIndex)
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
rowFocused = EPISODE_ROW
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -50,7 +49,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
|||
import com.github.damontecres.wholphin.data.model.Person
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.RequestOrRestoreFocus
|
||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
|
|
@ -80,7 +79,8 @@ fun SeriesOverviewContent(
|
|||
episodeRowFocusRequester: FocusRequester,
|
||||
castCrewRowFocusRequester: FocusRequester,
|
||||
guestStarRowFocusRequester: FocusRequester,
|
||||
onFocus: (SeriesOverviewPosition) -> Unit,
|
||||
onChangeSeason: (Int) -> Unit,
|
||||
onFocusEpisode: (Int) -> Unit,
|
||||
onClick: (BaseItem) -> Unit,
|
||||
onLongClick: (BaseItem) -> Unit,
|
||||
playOnClick: (Duration) -> Unit,
|
||||
|
|
@ -141,11 +141,12 @@ fun SeriesOverviewContent(
|
|||
tabs =
|
||||
seasons.mapNotNull {
|
||||
it?.name
|
||||
?: (stringResource(R.string.tv_season) + " ${it?.data?.indexNumber}")
|
||||
?: it?.data?.indexNumber?.let { stringResource(R.string.tv_season) + " $it" }
|
||||
?: ""
|
||||
},
|
||||
onClick = {
|
||||
selectedTabIndex = it
|
||||
onFocus.invoke(SeriesOverviewPosition(it, 0))
|
||||
onChangeSeason.invoke(it)
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
|
|
@ -169,102 +170,93 @@ fun SeriesOverviewContent(
|
|||
modifier = Modifier.fillMaxWidth(.6f),
|
||||
)
|
||||
|
||||
key(position.seasonTabIndex) {
|
||||
when (val eps = episodes) {
|
||||
EpisodeList.Loading -> {
|
||||
LoadingPage()
|
||||
}
|
||||
// key(position.seasonTabIndex) {
|
||||
when (val eps = episodes) {
|
||||
EpisodeList.Loading -> {
|
||||
LoadingPage()
|
||||
}
|
||||
|
||||
is EpisodeList.Error -> {
|
||||
ErrorMessage(eps.message, eps.exception)
|
||||
}
|
||||
is EpisodeList.Error -> {
|
||||
ErrorMessage(eps.message, eps.exception)
|
||||
}
|
||||
|
||||
is EpisodeList.Success -> {
|
||||
val state = rememberLazyListState()
|
||||
OneTimeLaunchedEffect {
|
||||
if (state.firstVisibleItemIndex != position.episodeRowIndex) {
|
||||
state.scrollToItem(position.episodeRowIndex)
|
||||
is EpisodeList.Success -> {
|
||||
val state = rememberLazyListState(position.episodeRowIndex)
|
||||
RequestOrRestoreFocus(firstItemFocusRequester)
|
||||
|
||||
LazyRow(
|
||||
state = state,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRestorer(firstItemFocusRequester)
|
||||
.focusRequester(episodeRowFocusRequester)
|
||||
.onFocusChanged {
|
||||
cardRowHasFocus = it.hasFocus
|
||||
},
|
||||
) {
|
||||
itemsIndexed(eps.episodes) { episodeIndex, episode ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
if (interactionSource.collectIsFocusedAsState().value) {
|
||||
onFocusEpisode.invoke(episodeIndex)
|
||||
}
|
||||
firstItemFocusRequester.tryRequestFocus()
|
||||
}
|
||||
LazyRow(
|
||||
state = state,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRestorer(firstItemFocusRequester)
|
||||
.focusRequester(episodeRowFocusRequester)
|
||||
.onFocusChanged {
|
||||
cardRowHasFocus = it.hasFocus
|
||||
},
|
||||
) {
|
||||
itemsIndexed(eps.episodes) { episodeIndex, episode ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
if (interactionSource.collectIsFocusedAsState().value) {
|
||||
onFocus.invoke(
|
||||
SeriesOverviewPosition(
|
||||
selectedTabIndex,
|
||||
episodeIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
val cornerText =
|
||||
episode?.data?.indexNumber?.let { "E$it" }
|
||||
?: episode?.data?.premiereDate?.let(::formatDateTime)
|
||||
BannerCard(
|
||||
name = episode?.name,
|
||||
item = episode,
|
||||
aspectRatio =
|
||||
episode
|
||||
?.aspectRatio
|
||||
?.coerceAtLeast(AspectRatios.FOUR_THREE)
|
||||
?: (AspectRatios.WIDE),
|
||||
cornerText = cornerText,
|
||||
played = episode?.data?.userData?.played ?: false,
|
||||
playPercent =
|
||||
episode?.data?.userData?.playedPercentage
|
||||
?: 0.0,
|
||||
onClick = { if (episode != null) onClick.invoke(episode) },
|
||||
onLongClick = {
|
||||
if (episode != null) {
|
||||
onLongClick.invoke(
|
||||
episode,
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
episodeIndex == position.episodeRowIndex,
|
||||
Modifier.focusRequester(firstItemFocusRequester),
|
||||
).ifElse(
|
||||
episodeIndex != position.episodeRowIndex,
|
||||
Modifier
|
||||
.background(
|
||||
Color.Black,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
).alpha(dimming),
|
||||
).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
scope.launch {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
val cornerText =
|
||||
episode?.data?.indexNumber?.let { "E$it" }
|
||||
?: episode?.data?.premiereDate?.let(::formatDateTime)
|
||||
BannerCard(
|
||||
name = episode?.name,
|
||||
item = episode,
|
||||
aspectRatio =
|
||||
episode
|
||||
?.aspectRatio
|
||||
?.coerceAtLeast(AspectRatios.FOUR_THREE)
|
||||
?: (AspectRatios.WIDE),
|
||||
cornerText = cornerText,
|
||||
played = episode?.data?.userData?.played ?: false,
|
||||
playPercent =
|
||||
episode?.data?.userData?.playedPercentage
|
||||
?: 0.0,
|
||||
onClick = { if (episode != null) onClick.invoke(episode) },
|
||||
onLongClick = {
|
||||
if (episode != null) {
|
||||
onLongClick.invoke(
|
||||
episode,
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.ifElse(
|
||||
episodeIndex == position.episodeRowIndex,
|
||||
Modifier.focusRequester(firstItemFocusRequester),
|
||||
).ifElse(
|
||||
episodeIndex != position.episodeRowIndex,
|
||||
Modifier
|
||||
.background(
|
||||
Color.Black,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
).alpha(dimming),
|
||||
).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
scope.launch {
|
||||
bringIntoViewRequester.bringIntoView()
|
||||
}
|
||||
}.onKeyEvent {
|
||||
if (episode != null && isPlayKeyUp(it)) {
|
||||
onClick.invoke(episode)
|
||||
return@onKeyEvent true
|
||||
}
|
||||
return@onKeyEvent false
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
cardHeight = 120.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.onKeyEvent {
|
||||
if (episode != null && isPlayKeyUp(it)) {
|
||||
onClick.invoke(episode)
|
||||
return@onKeyEvent true
|
||||
}
|
||||
return@onKeyEvent false
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
cardHeight = 120.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
focusedEpisode?.let { ep ->
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import com.github.damontecres.wholphin.data.model.ItemPlayback
|
|||
import com.github.damontecres.wholphin.data.model.Person
|
||||
import com.github.damontecres.wholphin.data.model.Trailer
|
||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.ExtrasService
|
||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||
|
|
@ -26,8 +25,10 @@ import com.github.damontecres.wholphin.services.UserPreferencesService
|
|||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
||||
import com.github.damontecres.wholphin.ui.equalsNotNull
|
||||
import com.github.damontecres.wholphin.ui.gt
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||
import com.github.damontecres.wholphin.ui.lt
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
|
|
@ -37,11 +38,19 @@ import com.github.damontecres.wholphin.util.GetEpisodesRequestHandler
|
|||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
|
|
@ -57,11 +66,10 @@ import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
|||
import org.jellyfin.sdk.model.api.request.GetSimilarItemsRequest
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@HiltViewModel(assistedFactory = SeriesViewModel.Factory::class)
|
||||
class SeriesViewModel
|
||||
@Inject
|
||||
@AssistedInject
|
||||
constructor(
|
||||
api: ApiClient,
|
||||
@param:ApplicationContext val context: Context,
|
||||
|
|
@ -76,11 +84,21 @@ class SeriesViewModel
|
|||
val streamChoiceService: StreamChoiceService,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val backdropService: BackdropService,
|
||||
@Assisted val seriesId: UUID,
|
||||
@Assisted val seasonEpisodeIds: SeasonEpisodeIds?,
|
||||
@Assisted val seriesPageType: SeriesPageType,
|
||||
) : ItemViewModel(api) {
|
||||
private lateinit var seriesId: UUID
|
||||
private lateinit var prefs: UserPreferences
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(
|
||||
seriesId: UUID,
|
||||
seasonEpisodeIds: SeasonEpisodeIds?,
|
||||
seriesPageType: SeriesPageType,
|
||||
): SeriesViewModel
|
||||
}
|
||||
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val seasons = MutableLiveData<List<BaseItem>>(listOf())
|
||||
val seasons = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
|
||||
|
||||
val trailers = MutableLiveData<List<Trailer>>(listOf())
|
||||
|
|
@ -90,48 +108,76 @@ class SeriesViewModel
|
|||
|
||||
val peopleInEpisode = MutableLiveData<PeopleInItem>(PeopleInItem())
|
||||
|
||||
fun init(
|
||||
prefs: UserPreferences,
|
||||
itemId: UUID,
|
||||
seasonEpisodeIds: SeasonEpisodeIds?,
|
||||
loadAdditionalDetails: Boolean,
|
||||
) {
|
||||
this.seriesId = itemId
|
||||
this.prefs = prefs
|
||||
val position = MutableStateFlow(SeriesOverviewPosition(0, 0))
|
||||
|
||||
init {
|
||||
viewModelScope.launch(
|
||||
LoadingExceptionHandler(
|
||||
loading,
|
||||
"Error loading series $seriesId",
|
||||
) + Dispatchers.IO,
|
||||
) {
|
||||
Timber.v("Start")
|
||||
val item = fetchItem(seriesId)
|
||||
backdropService.submit(item)
|
||||
val seasons = getSeasons(item)
|
||||
|
||||
// If a particular season was requested, fetch those episodes, otherwise get the first season
|
||||
val initialSeason =
|
||||
if (seasonEpisodeIds != null) {
|
||||
seasons.firstOrNull {
|
||||
equalsNotNull(it.id, seasonEpisodeIds.seasonId) ||
|
||||
equalsNotNull(it.indexNumber, seasonEpisodeIds.seasonNumber)
|
||||
val seasonsDeferred = getSeasons(item, seasonEpisodeIds?.seasonNumber)
|
||||
|
||||
val episodeListDeferred =
|
||||
if (seriesPageType == SeriesPageType.OVERVIEW) {
|
||||
viewModelScope.async(Dispatchers.IO) {
|
||||
if (seasonEpisodeIds != null) {
|
||||
loadEpisodesInternal(
|
||||
seasonEpisodeIds.seasonId,
|
||||
seasonEpisodeIds.episodeId,
|
||||
seasonEpisodeIds.episodeNumber,
|
||||
)
|
||||
} else {
|
||||
seasonsDeferred.await().firstOrNull()?.let {
|
||||
loadEpisodesInternal(
|
||||
it.id,
|
||||
null,
|
||||
null,
|
||||
)
|
||||
} ?: EpisodeList.Error(message = "Could not determine season")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seasons.firstOrNull()
|
||||
CompletableDeferred(value = EpisodeList.Loading)
|
||||
}
|
||||
val episodeInfo =
|
||||
initialSeason?.let {
|
||||
loadEpisodesInternal(
|
||||
it.id,
|
||||
seasonEpisodeIds?.episodeId,
|
||||
seasonEpisodeIds?.episodeNumber,
|
||||
)
|
||||
} ?: EpisodeList.Error("Could not determine season for selected episode")
|
||||
val seasons = seasonsDeferred.await()
|
||||
val episodes = episodeListDeferred.await()
|
||||
Timber.v("Done")
|
||||
|
||||
if (seriesPageType == SeriesPageType.OVERVIEW && seasonEpisodeIds != null) {
|
||||
viewModelScope.launchIO {
|
||||
val index =
|
||||
(seasons as? ApiRequestPager<*>)?.let {
|
||||
findIndexOf(
|
||||
seasonEpisodeIds.seasonNumber,
|
||||
seasonEpisodeIds.seasonId,
|
||||
it,
|
||||
)
|
||||
} ?: 0
|
||||
Timber.v("Got initial season index: $index")
|
||||
position.update {
|
||||
it.copy(seasonTabIndex = index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
this@SeriesViewModel.position.update {
|
||||
it.copy(
|
||||
episodeRowIndex =
|
||||
(episodes as? EpisodeList.Success)?.initialEpisodeIndex ?: 0,
|
||||
)
|
||||
}
|
||||
this@SeriesViewModel.seasons.value = seasons
|
||||
episodes.value = episodeInfo
|
||||
this@SeriesViewModel.episodes.value = episodes
|
||||
loading.value = LoadingState.Success
|
||||
}
|
||||
if (loadAdditionalDetails) {
|
||||
if (seriesPageType == SeriesPageType.DETAILS) {
|
||||
viewModelScope.launchIO {
|
||||
val trailers = trailerService.getTrailers(item)
|
||||
withContext(Dispatchers.Main) {
|
||||
|
|
@ -153,7 +199,7 @@ class SeriesViewModel
|
|||
.getSimilarItems(
|
||||
GetSimilarItemsRequest(
|
||||
userId = serverRepository.currentUser.value?.id,
|
||||
itemId = itemId,
|
||||
itemId = seriesId,
|
||||
fields = SlimItemFields,
|
||||
limit = 25,
|
||||
),
|
||||
|
|
@ -185,31 +231,45 @@ class SeriesViewModel
|
|||
themeSongPlayer.stop()
|
||||
}
|
||||
|
||||
private suspend fun getSeasons(series: BaseItem): List<BaseItem> {
|
||||
val request =
|
||||
GetItemsRequest(
|
||||
parentId = series.id,
|
||||
recursive = false,
|
||||
includeItemTypes = listOf(BaseItemKind.SEASON),
|
||||
sortBy = listOf(ItemSortBy.INDEX_NUMBER),
|
||||
sortOrder = listOf(SortOrder.ASCENDING),
|
||||
fields =
|
||||
listOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
ItemFields.CHILD_COUNT,
|
||||
ItemFields.SEASON_USER_DATA,
|
||||
),
|
||||
)
|
||||
val seasons =
|
||||
GetItemsRequestHandler.execute(api, request).content.items.map {
|
||||
BaseItem.from(
|
||||
it,
|
||||
api,
|
||||
private fun getSeasons(
|
||||
series: BaseItem,
|
||||
seasonNum: Int?,
|
||||
): Deferred<List<BaseItem?>> =
|
||||
viewModelScope.async(Dispatchers.IO) {
|
||||
val request =
|
||||
GetItemsRequest(
|
||||
parentId = series.id,
|
||||
recursive = false,
|
||||
includeItemTypes = listOf(BaseItemKind.SEASON),
|
||||
sortBy = listOf(ItemSortBy.INDEX_NUMBER),
|
||||
sortOrder = listOf(SortOrder.ASCENDING),
|
||||
fields =
|
||||
if (seriesPageType == SeriesPageType.DETAILS) {
|
||||
listOf(
|
||||
ItemFields.PRIMARY_IMAGE_ASPECT_RATIO,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
Timber.v("Loaded ${seasons.size} seasons for series ${series.id}")
|
||||
return seasons
|
||||
}
|
||||
val pager =
|
||||
ApiRequestPager(
|
||||
api,
|
||||
request,
|
||||
GetItemsRequestHandler,
|
||||
viewModelScope,
|
||||
pageSize = 10,
|
||||
).init(seasonNum ?: 0)
|
||||
// val seasons =
|
||||
// GetItemsRequestHandler.execute(api, request).content.items.map {
|
||||
// BaseItem.from(
|
||||
// it,
|
||||
// api,
|
||||
// )
|
||||
// }
|
||||
// Timber.v("Loaded ${seasons.size} seasons for series ${series.id}")
|
||||
pager
|
||||
}
|
||||
|
||||
private suspend fun loadEpisodesInternal(
|
||||
seasonId: UUID,
|
||||
|
|
@ -233,14 +293,11 @@ class SeriesViewModel
|
|||
),
|
||||
)
|
||||
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
|
||||
pager.init()
|
||||
pager.init(episodeNumber ?: 0)
|
||||
val initialIndex =
|
||||
if (episodeId != null || episodeNumber != null) {
|
||||
pager
|
||||
.indexOfBlocking {
|
||||
equalsNotNull(it?.id, episodeId) ||
|
||||
equalsNotNull(it?.indexNumber, episodeNumber)
|
||||
}.coerceAtLeast(0)
|
||||
findIndexOf(episodeNumber, episodeId, pager)
|
||||
.coerceAtLeast(0)
|
||||
} else {
|
||||
// Force the first page to to be fetched
|
||||
if (pager.isNotEmpty()) {
|
||||
|
|
@ -272,7 +329,7 @@ class SeriesViewModel
|
|||
if (currentEpisodes == null || currentEpisodes.seasonId != seasonId) {
|
||||
(episodes as? EpisodeList.Success)
|
||||
?.let {
|
||||
it.episodes.getOrNull(it.initialIndex)
|
||||
it.episodes.getOrNull(it.initialEpisodeIndex)
|
||||
}?.let { lookupPeopleInEpisode(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -312,7 +369,7 @@ class SeriesViewModel
|
|||
) = viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||
setWatched(seasonId, played, null)
|
||||
val series = fetchItem(seriesId)
|
||||
val seasons = getSeasons(series)
|
||||
val seasons = getSeasons(series, null).await()
|
||||
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
||||
}
|
||||
|
||||
|
|
@ -320,7 +377,7 @@ class SeriesViewModel
|
|||
viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
|
||||
favoriteWatchManager.setWatched(seriesId, played)
|
||||
val series = fetchItem(seriesId)
|
||||
val seasons = getSeasons(series)
|
||||
val seasons = getSeasons(series, null).await()
|
||||
this@SeriesViewModel.seasons.setValueOnMain(seasons)
|
||||
}
|
||||
|
||||
|
|
@ -469,7 +526,7 @@ sealed interface EpisodeList {
|
|||
data class Success(
|
||||
val seasonId: UUID,
|
||||
val episodes: ApiRequestPager<GetEpisodesRequest>,
|
||||
val initialIndex: Int,
|
||||
val initialEpisodeIndex: Int,
|
||||
) : EpisodeList
|
||||
}
|
||||
|
||||
|
|
@ -477,3 +534,49 @@ data class PeopleInItem(
|
|||
val itemId: UUID? = null,
|
||||
val people: List<Person> = listOf(),
|
||||
)
|
||||
|
||||
enum class SeriesPageType {
|
||||
DETAILS,
|
||||
OVERVIEW,
|
||||
}
|
||||
|
||||
private suspend fun findIndexOf(
|
||||
targetNum: Int?,
|
||||
targetId: UUID?,
|
||||
pager: ApiRequestPager<*>,
|
||||
): Int {
|
||||
val index =
|
||||
if (targetId != null && (targetNum == null || targetNum !in pager.indices)) {
|
||||
// No hint info, so have to check everything
|
||||
pager.indexOfBlocking { equalsNotNull(it?.id, targetId) }
|
||||
} else if (targetNum != null && targetNum in pager.indices) {
|
||||
// Start searching from the season number and choose direction from there
|
||||
val num = pager.getBlocking(targetNum)?.indexNumber
|
||||
if (num.lt(targetNum)) {
|
||||
for (i in targetNum + 1 until pager.lastIndex) {
|
||||
val season = pager.getBlocking(i)
|
||||
if (equalsNotNull(season?.indexNumber, targetNum) ||
|
||||
equalsNotNull(season?.id, targetId)
|
||||
) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
} else if (num.gt(targetNum)) {
|
||||
for (i in targetNum - 1 downTo 0) {
|
||||
val season = pager.getBlocking(i)
|
||||
if (equalsNotNull(season?.indexNumber, targetNum) ||
|
||||
equalsNotNull(season?.id, targetId)
|
||||
) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
} else {
|
||||
targetNum
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,9 @@ class HomeViewModel
|
|||
),
|
||||
) {
|
||||
Timber.d("init HomeViewModel")
|
||||
backdropService.clearBackdrop()
|
||||
if (reload) {
|
||||
backdropService.clearBackdrop()
|
||||
}
|
||||
|
||||
serverRepository.currentUserDto.value?.let { userDto ->
|
||||
val includedIds =
|
||||
|
|
|
|||
|
|
@ -79,10 +79,10 @@ fun DestinationContent(
|
|||
|
||||
is Destination.SeriesOverview -> {
|
||||
SeriesOverview(
|
||||
preferences,
|
||||
destination,
|
||||
modifier,
|
||||
preferences = preferences,
|
||||
destination = destination,
|
||||
initialSeasonEpisode = destination.seasonEpisode,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue