mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Backdrop improvements (#476)
## Description The backdrop image shown on most pages is now configurable in advanced settings: * Image w/ dynamic color - Shows the backdrop with dynamic color filling the entire space (new default) * Image only - Shows only the backdrop image (current Wholphin behavior) * None - Don't show any backdrops Additionally, the backdrop is retained between page loads for the same items. ## Acknowledgements Big thanks to @YogiBear12 from which the idea and implementation for the dynamic color was adapted from! Code adapted from https://github.com/YogiBear12/Halfin/blob/main/COLOR_EXTRACTION_AND_BACKDROP.md#backdrop-fading-and-transitions ## Issues Closes #221 Closes #461 Closes #442 https://github.com/user-attachments/assets/1acbc487-c697-44d7-89ed-e7e4c9169360
This commit is contained in:
parent
8224f83678
commit
bbfbc13532
26 changed files with 569 additions and 224 deletions
|
|
@ -223,6 +223,7 @@ dependencies {
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
implementation(libs.androidx.preference.ktx)
|
implementation(libs.androidx.preference.ktx)
|
||||||
implementation(libs.androidx.room.testing)
|
implementation(libs.androidx.room.testing)
|
||||||
|
implementation(libs.androidx.palette.ktx)
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -659,6 +659,19 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summaryOff = R.string.disabled,
|
summaryOff = R.string.disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val BackdropStylePref =
|
||||||
|
AppChoicePreference<AppPreferences, BackdropStyle>(
|
||||||
|
title = R.string.backdrop_display,
|
||||||
|
defaultValue = BackdropStyle.BACKDROP_DYNAMIC_COLOR,
|
||||||
|
getter = { it.interfacePreferences.backdropStyle },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateInterfacePreferences { backdropStyle = value }
|
||||||
|
},
|
||||||
|
displayValues = R.array.backdrop_style_options,
|
||||||
|
indexToValue = { BackdropStyle.forNumber(it) },
|
||||||
|
valueToIndex = { it.number },
|
||||||
|
)
|
||||||
|
|
||||||
val OneClickPause =
|
val OneClickPause =
|
||||||
AppSwitchPreference<AppPreferences>(
|
AppSwitchPreference<AppPreferences>(
|
||||||
title = R.string.one_click_pause,
|
title = R.string.one_click_pause,
|
||||||
|
|
@ -909,6 +922,7 @@ val advancedPreferences =
|
||||||
// Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418
|
// Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418
|
||||||
// AppPreference.NavDrawerSwitchOnFocus,
|
// AppPreference.NavDrawerSwitchOnFocus,
|
||||||
AppPreference.ControllerTimeout,
|
AppPreference.ControllerTimeout,
|
||||||
|
AppPreference.BackdropStylePref,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ class AppPreferencesSerializer
|
||||||
navDrawerSwitchOnFocus =
|
navDrawerSwitchOnFocus =
|
||||||
AppPreference.NavDrawerSwitchOnFocus.defaultValue
|
AppPreference.NavDrawerSwitchOnFocus.defaultValue
|
||||||
showClock = AppPreference.ShowClock.defaultValue
|
showClock = AppPreference.ShowClock.defaultValue
|
||||||
|
backdropStyle = AppPreference.BackdropStylePref.defaultValue
|
||||||
|
|
||||||
subtitlesPreferences =
|
subtitlesPreferences =
|
||||||
SubtitlePreferences
|
SubtitlePreferences
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,239 @@
|
||||||
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.util.LruCache
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.isSpecified
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.palette.graphics.Palette
|
||||||
|
import coil3.asDrawable
|
||||||
|
import coil3.imageLoader
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.SuccessResult
|
||||||
|
import coil3.request.allowHardware
|
||||||
|
import coil3.request.bitmapConfig
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.BackdropStyle
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
class BackdropService
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
private val imageUrlService: ImageUrlService,
|
||||||
|
private val preferences: DataStore<AppPreferences>,
|
||||||
|
) {
|
||||||
|
private val extractedColorCache = LruCache<String, ExtractedColors>(50)
|
||||||
|
|
||||||
|
private val _backdropFlow = MutableStateFlow<BackdropResult>(BackdropResult.NONE)
|
||||||
|
val backdropFlow = _backdropFlow
|
||||||
|
|
||||||
|
suspend fun submit(item: BaseItem) =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val imageUrl = imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
|
||||||
|
if (backdropFlow.firstOrNull()?.imageUrl != imageUrl) {
|
||||||
|
_backdropFlow.update {
|
||||||
|
it.copy(
|
||||||
|
itemId = item.id,
|
||||||
|
imageUrl = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
extractColors(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearBackdrop() {
|
||||||
|
_backdropFlow.update {
|
||||||
|
BackdropResult.NONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun extractColors(item: BaseItem) {
|
||||||
|
delay(500)
|
||||||
|
val imageUrl = imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
|
||||||
|
val backdropStyle =
|
||||||
|
preferences.data
|
||||||
|
.firstOrNull()
|
||||||
|
?.interfacePreferences
|
||||||
|
?.backdropStyle
|
||||||
|
val dynamicEnabled =
|
||||||
|
backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR ||
|
||||||
|
backdropStyle == BackdropStyle.UNRECOGNIZED
|
||||||
|
val (primaryColor, secondaryColor, tertiaryColor) =
|
||||||
|
if (dynamicEnabled) {
|
||||||
|
extractColorsFromBackdrop(imageUrl)
|
||||||
|
} else {
|
||||||
|
ExtractedColors.DEFAULT
|
||||||
|
}
|
||||||
|
_backdropFlow.update {
|
||||||
|
if (it.itemId == item.id) {
|
||||||
|
BackdropResult(
|
||||||
|
itemId = item.id,
|
||||||
|
imageUrl = imageUrl,
|
||||||
|
primaryColor = primaryColor,
|
||||||
|
secondaryColor = secondaryColor,
|
||||||
|
tertiaryColor = tertiaryColor,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun extractColorsFromBackdrop(imageUrl: String?): ExtractedColors =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (imageUrl.isNullOrBlank()) {
|
||||||
|
return@withContext ExtractedColors.DEFAULT
|
||||||
|
}
|
||||||
|
extractedColorCache.get(imageUrl)?.let {
|
||||||
|
return@withContext it
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val loader = context.imageLoader
|
||||||
|
val request =
|
||||||
|
ImageRequest
|
||||||
|
.Builder(context)
|
||||||
|
.data(imageUrl)
|
||||||
|
.allowHardware(false)
|
||||||
|
.bitmapConfig(Bitmap.Config.ARGB_8888)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val result = loader.execute(request)
|
||||||
|
if (result is SuccessResult) {
|
||||||
|
val drawable = result.image.asDrawable(context.resources)
|
||||||
|
val bitmap = drawable.toBitmap(config = Bitmap.Config.ARGB_8888)
|
||||||
|
extractColorsFromBitmap(bitmap).also {
|
||||||
|
extractedColorCache.put(imageUrl, it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ExtractedColors.DEFAULT
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Error extracting colors from URL: $imageUrl")
|
||||||
|
ExtractedColors.DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to determine if a color is "cool" (blue/purple/green) vs "warm" (red/orange/yellow)
|
||||||
|
*
|
||||||
|
* Cool colors have more blue/green than red
|
||||||
|
*/
|
||||||
|
private val Palette.Swatch.coolColor: Boolean
|
||||||
|
get() {
|
||||||
|
val r = (rgb shr 16) and 0xFF
|
||||||
|
val g = (rgb shr 8) and 0xFF
|
||||||
|
val b = rgb and 0xFF
|
||||||
|
return b > r && (b + g) > (r * 1.5f)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toColor(
|
||||||
|
swatch: Palette.Swatch?,
|
||||||
|
alpha: Float,
|
||||||
|
): Color = swatch?.rgb?.let(::Color)?.copy(alpha = alpha) ?: Color.Transparent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts colors from a bitmap using Android's Palette API.
|
||||||
|
*
|
||||||
|
* - Primary (Bottom-Right): darkVibrant -> darkMuted -> default
|
||||||
|
* - Secondary (Top-Left): Smart selection based on color temperature (prefers cool colors)
|
||||||
|
* - Tertiary (Top-Right): vibrant -> lightVibrant -> default
|
||||||
|
*
|
||||||
|
* @param bitmap The bitmap to extract colors from
|
||||||
|
* @return ExtractedColors containing primary, secondary, and tertiary colors
|
||||||
|
*/
|
||||||
|
private suspend fun extractColorsFromBitmap(bitmap: Bitmap): ExtractedColors =
|
||||||
|
try {
|
||||||
|
val palette = Palette.from(bitmap).generate()
|
||||||
|
|
||||||
|
val vibrant = palette.vibrantSwatch
|
||||||
|
val darkVibrant = palette.darkVibrantSwatch
|
||||||
|
val lightVibrant = palette.lightVibrantSwatch
|
||||||
|
val muted = palette.mutedSwatch
|
||||||
|
val darkMuted = palette.darkMutedSwatch
|
||||||
|
val lightMuted = palette.lightMutedSwatch
|
||||||
|
val dominant = palette.dominantSwatch
|
||||||
|
|
||||||
|
// Primary (Bottom-Right)
|
||||||
|
val primaryColor = toColor(darkVibrant ?: darkMuted, .4f)
|
||||||
|
|
||||||
|
// Secondary (Top-Left): Smart selection based on color properties
|
||||||
|
// If Vibrant is cool (blue/purple), use it. If Vibrant is warm (yellow/orange) and Muted is cool, use Muted.
|
||||||
|
// This ensures we get cool tones (blue/purple) for top-left when available
|
||||||
|
val secondaryColor =
|
||||||
|
when {
|
||||||
|
vibrant != null && vibrant.coolColor -> vibrant
|
||||||
|
muted != null && muted.coolColor -> muted
|
||||||
|
vibrant != null -> vibrant
|
||||||
|
muted != null -> muted
|
||||||
|
else -> null
|
||||||
|
}.let { toColor(it, .4f) }
|
||||||
|
|
||||||
|
// Tertiary (Top-Right under image)
|
||||||
|
val tertiaryColor = toColor(vibrant ?: lightVibrant, .35f)
|
||||||
|
|
||||||
|
Timber.v(
|
||||||
|
"Colors extracted: primary=%s, secondary=%s, tertiary=%s",
|
||||||
|
primaryColor,
|
||||||
|
secondaryColor,
|
||||||
|
tertiaryColor,
|
||||||
|
)
|
||||||
|
ExtractedColors(primaryColor, secondaryColor, tertiaryColor)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e, "Error extracting palette colors")
|
||||||
|
ExtractedColors.DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearColorCache() {
|
||||||
|
extractedColorCache.evictAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BackdropResult(
|
||||||
|
val itemId: UUID?,
|
||||||
|
val imageUrl: String?,
|
||||||
|
val primaryColor: Color = Color.Unspecified,
|
||||||
|
val secondaryColor: Color = Color.Unspecified,
|
||||||
|
val tertiaryColor: Color = Color.Unspecified,
|
||||||
|
) {
|
||||||
|
val hasColors: Boolean =
|
||||||
|
primaryColor.isSpecified ||
|
||||||
|
secondaryColor.isSpecified ||
|
||||||
|
tertiaryColor.isSpecified
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val NONE =
|
||||||
|
BackdropResult(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ExtractedColors(
|
||||||
|
val primary: Color,
|
||||||
|
val secondary: Color,
|
||||||
|
val tertiary: Color,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val DEFAULT = ExtractedColors(Color.Unspecified, Color.Unspecified, Color.Unspecified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,7 @@ import com.github.damontecres.wholphin.data.model.GetItemsFilter
|
||||||
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
import com.github.damontecres.wholphin.data.model.GetItemsFilterOverride
|
||||||
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
import com.github.damontecres.wholphin.data.model.LibraryDisplayInfo
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.AspectRatios
|
import com.github.damontecres.wholphin.ui.AspectRatios
|
||||||
|
|
@ -76,6 +77,7 @@ import com.github.damontecres.wholphin.ui.detail.MoreDialogActions
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
import com.github.damontecres.wholphin.ui.detail.buildMoreDialogItemsForHome
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
import com.github.damontecres.wholphin.ui.main.HomePageHeader
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.playback.scale
|
import com.github.damontecres.wholphin.ui.playback.scale
|
||||||
|
|
@ -123,6 +125,7 @@ class CollectionFolderViewModel
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
|
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
|
||||||
private val favoriteWatchManager: FavoriteWatchManager,
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
private val backdropService: BackdropService,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
) : ItemViewModel(api) {
|
) : ItemViewModel(api) {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||||
|
|
@ -454,6 +457,12 @@ class CollectionFolderViewModel
|
||||||
favoriteWatchManager.setFavorite(itemId, favorite)
|
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||||
(pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
(pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateBackdrop(item: BaseItem) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
backdropService.submit(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -578,6 +587,7 @@ fun CollectionFolderGrid(
|
||||||
viewOptions = viewOptions,
|
viewOptions = viewOptions,
|
||||||
defaultViewOptions = defaultViewOptions,
|
defaultViewOptions = defaultViewOptions,
|
||||||
onSaveViewOptions = { viewModel.saveViewOptions(it) },
|
onSaveViewOptions = { viewModel.saveViewOptions(it) },
|
||||||
|
onChangeBackdrop = viewModel::updateBackdrop,
|
||||||
playEnabled = playEnabled,
|
playEnabled = playEnabled,
|
||||||
onClickPlay = { _, item ->
|
onClickPlay = { _, item ->
|
||||||
viewModel.navigationManager.navigateTo(Destination.Playback(item))
|
viewModel.navigationManager.navigateTo(Destination.Playback(item))
|
||||||
|
|
@ -687,6 +697,7 @@ fun CollectionFolderGridContent(
|
||||||
viewOptions: ViewOptions,
|
viewOptions: ViewOptions,
|
||||||
onClickPlayAll: (shuffle: Boolean) -> Unit,
|
onClickPlayAll: (shuffle: Boolean) -> Unit,
|
||||||
onClickPlay: (Int, BaseItem) -> Unit,
|
onClickPlay: (Int, BaseItem) -> Unit,
|
||||||
|
onChangeBackdrop: (BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showTitle: Boolean = true,
|
showTitle: Boolean = true,
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
|
|
@ -707,14 +718,13 @@ fun CollectionFolderGridContent(
|
||||||
|
|
||||||
var position by rememberInt(0)
|
var position by rememberInt(0)
|
||||||
val focusedItem = pager.getOrNull(position)
|
val focusedItem = pager.getOrNull(position)
|
||||||
|
if (viewOptions.showDetails) {
|
||||||
|
LaunchedEffect(focusedItem) {
|
||||||
|
focusedItem?.let(onChangeBackdrop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
if (viewOptions.showDetails) {
|
|
||||||
DelayedDetailsBackdropImage(
|
|
||||||
item = focusedItem,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
|
||||||
import coil3.compose.AsyncImage
|
|
||||||
import coil3.request.ImageRequest
|
|
||||||
import coil3.request.transitionFactory
|
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
|
||||||
import com.github.damontecres.wholphin.ui.CrossFadeFactory
|
|
||||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BoxScope.DetailsBackdropImage(
|
|
||||||
item: BaseItem?,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val imageUrlService = LocalImageUrlService.current
|
|
||||||
val backdropImageUrl =
|
|
||||||
remember(item) {
|
|
||||||
if (item != null) {
|
|
||||||
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DetailsBackdropImage(backdropImageUrl, modifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BoxScope.DetailsBackdropImage(
|
|
||||||
backdropImageUrl: String?,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
if (backdropImageUrl.isNotNullOrBlank()) {
|
|
||||||
val gradientColor = MaterialTheme.colorScheme.background
|
|
||||||
AsyncImage(
|
|
||||||
model = backdropImageUrl,
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
alignment = Alignment.TopEnd,
|
|
||||||
modifier =
|
|
||||||
modifier
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.fillMaxHeight(.85f)
|
|
||||||
.alpha(.75f)
|
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawRect(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(Color.Transparent, gradientColor),
|
|
||||||
startY = size.height * .5f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
drawRect(
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
colors = listOf(Color.Transparent, gradientColor),
|
|
||||||
endX = 0f,
|
|
||||||
startX = size.width * .75f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun BoxScope.DelayedDetailsBackdropImage(
|
|
||||||
item: BaseItem?,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val imageUrlService = LocalImageUrlService.current
|
|
||||||
val backdropImageUrl =
|
|
||||||
remember(item) {
|
|
||||||
if (item != null) {
|
|
||||||
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DelayedDetailsBackdropImage(backdropImageUrl, modifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a backdrop image, but with a crossfade & delay
|
|
||||||
*
|
|
||||||
* Used for change backdrops when change items frequently
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun BoxScope.DelayedDetailsBackdropImage(
|
|
||||||
focusedBackdropImageUrl: String?,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
var backdropImageUrl by remember { mutableStateOf<String?>(null) }
|
|
||||||
LaunchedEffect(focusedBackdropImageUrl) {
|
|
||||||
backdropImageUrl = null
|
|
||||||
delay(150)
|
|
||||||
backdropImageUrl = focusedBackdropImageUrl
|
|
||||||
}
|
|
||||||
val gradientColor = MaterialTheme.colorScheme.background
|
|
||||||
AsyncImage(
|
|
||||||
model =
|
|
||||||
ImageRequest
|
|
||||||
.Builder(context)
|
|
||||||
.data(backdropImageUrl)
|
|
||||||
.transitionFactory(CrossFadeFactory(250.milliseconds))
|
|
||||||
.build(),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
alignment = Alignment.TopEnd,
|
|
||||||
modifier =
|
|
||||||
modifier
|
|
||||||
.fillMaxHeight(.7f)
|
|
||||||
.fillMaxWidth(.7f)
|
|
||||||
.alpha(.75f)
|
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
drawRect(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(Color.Transparent, gradientColor),
|
|
||||||
startY = size.height * .33f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
drawRect(
|
|
||||||
Brush.horizontalGradient(
|
|
||||||
colors = listOf(gradientColor, Color.Transparent),
|
|
||||||
startX = 0f,
|
|
||||||
endX = size.width * .5f,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -20,6 +20,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||||
|
|
@ -45,6 +46,7 @@ abstract class RecommendedViewModel(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
val favoriteWatchManager: FavoriteWatchManager,
|
val favoriteWatchManager: FavoriteWatchManager,
|
||||||
|
private val backdropService: BackdropService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
abstract fun init()
|
abstract fun init()
|
||||||
|
|
||||||
|
|
@ -86,6 +88,12 @@ abstract class RecommendedViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateBackdrop(item: BaseItem) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
backdropService.submit(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract fun update(
|
abstract fun update(
|
||||||
@StringRes title: Int,
|
@StringRes title: Int,
|
||||||
row: HomeRowLoadingState,
|
row: HomeRowLoadingState,
|
||||||
|
|
@ -152,6 +160,7 @@ fun RecommendedContent(
|
||||||
},
|
},
|
||||||
onFocusPosition = onFocusPosition,
|
onFocusPosition = onFocusPosition,
|
||||||
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
||||||
|
onUpdateBackdrop = viewModel::updateBackdrop,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
|
|
@ -56,7 +57,8 @@ class RecommendedMovieViewModel
|
||||||
@Assisted val parentId: UUID,
|
@Assisted val parentId: UUID,
|
||||||
navigationManager: NavigationManager,
|
navigationManager: NavigationManager,
|
||||||
favoriteWatchManager: FavoriteWatchManager,
|
favoriteWatchManager: FavoriteWatchManager,
|
||||||
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager) {
|
backdropService: BackdropService,
|
||||||
|
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager, backdropService) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(parentId: UUID): RecommendedMovieViewModel
|
fun create(parentId: UUID): RecommendedMovieViewModel
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
|
|
@ -59,7 +60,8 @@ class RecommendedTvShowViewModel
|
||||||
@Assisted val parentId: UUID,
|
@Assisted val parentId: UUID,
|
||||||
navigationManager: NavigationManager,
|
navigationManager: NavigationManager,
|
||||||
favoriteWatchManager: FavoriteWatchManager,
|
favoriteWatchManager: FavoriteWatchManager,
|
||||||
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager) {
|
backdropService: BackdropService,
|
||||||
|
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager, backdropService) {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(parentId: UUID): RecommendedTvShowViewModel
|
fun create(parentId: UUID): RecommendedTvShowViewModel
|
||||||
|
|
|
||||||
|
|
@ -50,10 +50,10 @@ import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemCardImage
|
import com.github.damontecres.wholphin.ui.cards.ItemCardImage
|
||||||
import com.github.damontecres.wholphin.ui.components.DelayedDetailsBackdropImage
|
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
|
|
@ -64,6 +64,7 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
|
@ -88,6 +89,7 @@ class PlaylistViewModel
|
||||||
constructor(
|
constructor(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
|
private val backdropService: BackdropService,
|
||||||
) : ItemViewModel(api) {
|
) : ItemViewModel(api) {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
val items = MutableLiveData<List<BaseItem?>>(listOf())
|
val items = MutableLiveData<List<BaseItem?>>(listOf())
|
||||||
|
|
@ -111,6 +113,12 @@ class PlaylistViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateBackdrop(item: BaseItem) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
backdropService.submit(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -146,6 +154,7 @@ fun PlaylistDetails(
|
||||||
playlist = it,
|
playlist = it,
|
||||||
items = items,
|
items = items,
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
|
onChangeBackdrop = viewModel::updateBackdrop,
|
||||||
onClickIndex = { index, _ ->
|
onClickIndex = { index, _ ->
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigationManager.navigateTo(
|
||||||
Destination.PlaybackList(
|
Destination.PlaybackList(
|
||||||
|
|
@ -212,6 +221,7 @@ fun PlaylistDetailsContent(
|
||||||
onClickIndex: (Int, BaseItem) -> Unit,
|
onClickIndex: (Int, BaseItem) -> Unit,
|
||||||
onLongClickIndex: (Int, BaseItem) -> Unit,
|
onLongClickIndex: (Int, BaseItem) -> Unit,
|
||||||
onClickPlay: (shuffle: Boolean) -> Unit,
|
onClickPlay: (shuffle: Boolean) -> Unit,
|
||||||
|
onChangeBackdrop: (BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||||
) {
|
) {
|
||||||
|
|
@ -219,13 +229,15 @@ fun PlaylistDetailsContent(
|
||||||
var focusedIndex by remember { mutableIntStateOf(savedIndex) }
|
var focusedIndex by remember { mutableIntStateOf(savedIndex) }
|
||||||
val focus = remember { FocusRequester() }
|
val focus = remember { FocusRequester() }
|
||||||
val focusedItem = items.getOrNull(focusedIndex)
|
val focusedItem = items.getOrNull(focusedIndex)
|
||||||
|
LaunchedEffect(focusedItem) {
|
||||||
|
focusedItem?.let(onChangeBackdrop)
|
||||||
|
}
|
||||||
|
|
||||||
val playButtonFocusRequester = remember { FocusRequester() }
|
val playButtonFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
DelayedDetailsBackdropImage(focusedItem)
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.ChosenStreams
|
import com.github.damontecres.wholphin.data.ChosenStreams
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
|
@ -296,7 +295,6 @@ fun EpisodeDetailsContent(
|
||||||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||||
}
|
}
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
DetailsBackdropImage(ep)
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||||
|
|
@ -50,6 +51,7 @@ class EpisodeViewModel
|
||||||
private val themeSongPlayer: ThemeSongPlayer,
|
private val themeSongPlayer: ThemeSongPlayer,
|
||||||
private val favoriteWatchManager: FavoriteWatchManager,
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
private val userPreferencesService: UserPreferencesService,
|
private val userPreferencesService: UserPreferencesService,
|
||||||
|
private val backdropService: BackdropService,
|
||||||
@Assisted val itemId: UUID,
|
@Assisted val itemId: UUID,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -96,6 +98,7 @@ class EpisodeViewModel
|
||||||
this@EpisodeViewModel.item.value = item
|
this@EpisodeViewModel.item.value = item
|
||||||
chosenStreams.value = result
|
chosenStreams.value = result
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
|
backdropService.submit(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,6 @@ import com.github.damontecres.wholphin.ui.cards.ExtrasRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||||
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
|
@ -396,7 +395,6 @@ fun MovieDetailsContent(
|
||||||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||||
}
|
}
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
DetailsBackdropImage(movie)
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.data.model.ItemPlayback
|
||||||
import com.github.damontecres.wholphin.data.model.Person
|
import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.ExtrasService
|
import com.github.damontecres.wholphin.services.ExtrasService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
|
@ -63,6 +64,7 @@ class MovieViewModel
|
||||||
private val trailerService: TrailerService,
|
private val trailerService: TrailerService,
|
||||||
private val extrasService: ExtrasService,
|
private val extrasService: ExtrasService,
|
||||||
private val userPreferencesService: UserPreferencesService,
|
private val userPreferencesService: UserPreferencesService,
|
||||||
|
private val backdropService: BackdropService,
|
||||||
@Assisted val itemId: UUID,
|
@Assisted val itemId: UUID,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
|
|
@ -118,6 +120,7 @@ class MovieViewModel
|
||||||
this@MovieViewModel.item.value = item
|
this@MovieViewModel.item.value = item
|
||||||
chosenStreams.value = result
|
chosenStreams.value = result
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
|
backdropService.submit(item)
|
||||||
}
|
}
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
val trailers = trailerService.getTrailers(item)
|
val trailers = trailerService.getTrailers(item)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||||
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
import com.github.damontecres.wholphin.ui.cards.SeasonCard
|
||||||
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
|
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
|
|
@ -320,8 +319,6 @@ fun SeriesDetailsContent(
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
DetailsBackdropImage(series)
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,6 @@ import com.github.damontecres.wholphin.ui.AspectRatios
|
||||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||||
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
import com.github.damontecres.wholphin.ui.cards.PersonRow
|
||||||
import com.github.damontecres.wholphin.ui.components.DetailsBackdropImage
|
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.SeriesName
|
import com.github.damontecres.wholphin.ui.components.SeriesName
|
||||||
|
|
@ -112,7 +111,6 @@ fun SeriesOverviewContent(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
DetailsBackdropImage(backdropImageUrl)
|
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.data.model.Person
|
||||||
import com.github.damontecres.wholphin.data.model.Trailer
|
import com.github.damontecres.wholphin.data.model.Trailer
|
||||||
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
import com.github.damontecres.wholphin.preferences.ThemeSongVolume
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
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.ExtrasService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
|
@ -74,6 +75,7 @@ class SeriesViewModel
|
||||||
private val extrasService: ExtrasService,
|
private val extrasService: ExtrasService,
|
||||||
val streamChoiceService: StreamChoiceService,
|
val streamChoiceService: StreamChoiceService,
|
||||||
private val userPreferencesService: UserPreferencesService,
|
private val userPreferencesService: UserPreferencesService,
|
||||||
|
private val backdropService: BackdropService,
|
||||||
) : ItemViewModel(api) {
|
) : ItemViewModel(api) {
|
||||||
private lateinit var seriesId: UUID
|
private lateinit var seriesId: UUID
|
||||||
private lateinit var prefs: UserPreferences
|
private lateinit var prefs: UserPreferences
|
||||||
|
|
@ -103,6 +105,7 @@ class SeriesViewModel
|
||||||
) + Dispatchers.IO,
|
) + Dispatchers.IO,
|
||||||
) {
|
) {
|
||||||
val item = fetchItem(seriesId)
|
val item = fetchItem(seriesId)
|
||||||
|
backdropService.submit(item)
|
||||||
val seasons = getSeasons(item)
|
val seasons = getSeasons(item)
|
||||||
|
|
||||||
// If a particular season was requested, fetch those episodes, otherwise get the first season
|
// If a particular season was requested, fetch those episodes, otherwise get the first season
|
||||||
|
|
@ -332,6 +335,8 @@ class SeriesViewModel
|
||||||
episodes.value = eps
|
episodes.value = eps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Kind of hack to ensure the backdrop is reloaded if needed
|
||||||
|
item.value?.let { backdropService.submit(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ import com.github.damontecres.wholphin.ui.Cards
|
||||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
||||||
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
import com.github.damontecres.wholphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.DelayedDetailsBackdropImage
|
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||||
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
||||||
|
|
@ -167,6 +166,7 @@ fun HomePage(
|
||||||
},
|
},
|
||||||
loadingState = refreshing,
|
loadingState = refreshing,
|
||||||
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
||||||
|
onUpdateBackdrop = viewModel::updateBackdrop,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
dialog?.let { params ->
|
dialog?.let { params ->
|
||||||
|
|
@ -203,6 +203,7 @@ fun HomePageContent(
|
||||||
onLongClickItem: (RowColumn, BaseItem) -> Unit,
|
onLongClickItem: (RowColumn, BaseItem) -> Unit,
|
||||||
onClickPlay: (RowColumn, BaseItem) -> Unit,
|
onClickPlay: (RowColumn, BaseItem) -> Unit,
|
||||||
showClock: Boolean,
|
showClock: Boolean,
|
||||||
|
onUpdateBackdrop: (BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onFocusPosition: ((RowColumn) -> Unit)? = null,
|
onFocusPosition: ((RowColumn) -> Unit)? = null,
|
||||||
loadingState: LoadingState? = null,
|
loadingState: LoadingState? = null,
|
||||||
|
|
@ -249,12 +250,10 @@ fun HomePageContent(
|
||||||
LaunchedEffect(position) {
|
LaunchedEffect(position) {
|
||||||
listState.animateScrollToItem(position.row)
|
listState.animateScrollToItem(position.row)
|
||||||
}
|
}
|
||||||
|
LaunchedEffect(focusedItem) {
|
||||||
|
focusedItem?.let(onUpdateBackdrop)
|
||||||
|
}
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
DelayedDetailsBackdropImage(
|
|
||||||
item = focusedItem,
|
|
||||||
modifier = Modifier,
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
HomePageHeader(
|
HomePageHeader(
|
||||||
item = focusedItem,
|
item = focusedItem,
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.DatePlayedService
|
import com.github.damontecres.wholphin.services.DatePlayedService
|
||||||
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
import com.github.damontecres.wholphin.services.FavoriteWatchManager
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
||||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -58,6 +60,7 @@ class HomeViewModel
|
||||||
val navDrawerItemRepository: NavDrawerItemRepository,
|
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
private val favoriteWatchManager: FavoriteWatchManager,
|
private val favoriteWatchManager: FavoriteWatchManager,
|
||||||
private val datePlayedService: DatePlayedService,
|
private val datePlayedService: DatePlayedService,
|
||||||
|
private val backdropService: BackdropService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
|
@ -316,6 +319,12 @@ class HomeViewModel
|
||||||
init(preferences)
|
init(preferences)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateBackdrop(item: BaseItem) {
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
backdropService.submit(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val supportedLatestCollectionTypes =
|
val supportedLatestCollectionTypes =
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,59 @@
|
||||||
package com.github.damontecres.wholphin.ui.nav
|
package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||||
import androidx.navigation3.runtime.NavEntry
|
import androidx.navigation3.runtime.NavEntry
|
||||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import coil3.compose.AsyncImage
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import coil3.request.transitionFactory
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
|
import com.github.damontecres.wholphin.preferences.BackdropStyle
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
|
import com.github.damontecres.wholphin.ui.CrossFadeFactory
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class ApplicationContentViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
val backdropService: BackdropService,
|
||||||
|
) : ViewModel() {
|
||||||
|
fun clearBackdrop() {
|
||||||
|
viewModelScope.launchIO { backdropService.clearBackdrop() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is generally the root composable of the of the app
|
* This is generally the root composable of the of the app
|
||||||
|
|
@ -25,37 +67,158 @@ fun ApplicationContent(
|
||||||
navigationManager: NavigationManager,
|
navigationManager: NavigationManager,
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: ApplicationContentViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
NavDisplay(
|
val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle()
|
||||||
backStack = navigationManager.backStack,
|
val backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle
|
||||||
onBack = { navigationManager.goBack() },
|
Box(
|
||||||
entryDecorators =
|
modifier = modifier,
|
||||||
listOf(
|
) {
|
||||||
rememberSaveableStateHolderNavEntryDecorator(),
|
val baseBackgroundColor = MaterialTheme.colorScheme.background
|
||||||
rememberViewModelStoreNavEntryDecorator(),
|
if (backdrop.hasColors &&
|
||||||
),
|
(backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED)
|
||||||
entryProvider = { key ->
|
) {
|
||||||
key as Destination
|
val animPrimary by animateColorAsState(
|
||||||
val contentKey = "${key}_${server?.id}_${user?.id}"
|
backdrop.primaryColor,
|
||||||
NavEntry(key, contentKey = contentKey) {
|
animationSpec = tween(1250),
|
||||||
if (key.fullScreen) {
|
label = "dynamic_backdrop_primary",
|
||||||
DestinationContent(
|
)
|
||||||
destination = key,
|
val animSecondary by animateColorAsState(
|
||||||
preferences = preferences,
|
backdrop.secondaryColor,
|
||||||
modifier = modifier.fillMaxSize(),
|
animationSpec = tween(1250),
|
||||||
)
|
label = "dynamic_backdrop_secondary",
|
||||||
} else if (user != null && server != null) {
|
)
|
||||||
NavDrawer(
|
val animTertiary by animateColorAsState(
|
||||||
destination = key,
|
backdrop.tertiaryColor,
|
||||||
preferences = preferences,
|
animationSpec = tween(1250),
|
||||||
user = user,
|
label = "dynamic_backdrop_tertiary",
|
||||||
server = server,
|
)
|
||||||
modifier = modifier,
|
Box(
|
||||||
)
|
modifier =
|
||||||
} else {
|
Modifier
|
||||||
ErrorMessage("Trying to go to $key without a user logged in", null)
|
.fillMaxSize()
|
||||||
}
|
.drawBehind {
|
||||||
|
drawRect(color = baseBackgroundColor)
|
||||||
|
// Top Left (Vibrant/Muted)
|
||||||
|
drawRect(
|
||||||
|
brush =
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors = listOf(animSecondary, Color.Transparent),
|
||||||
|
center = Offset(0f, 0f),
|
||||||
|
radius = size.width * 0.8f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Bottom Right (DarkVibrant/DarkMuted)
|
||||||
|
drawRect(
|
||||||
|
brush =
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors = listOf(animPrimary, Color.Transparent),
|
||||||
|
center = Offset(size.width, size.height),
|
||||||
|
radius = size.width * 0.8f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Bottom Left (Dark / Bridge)
|
||||||
|
drawRect(
|
||||||
|
brush =
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors =
|
||||||
|
listOf(
|
||||||
|
baseBackgroundColor,
|
||||||
|
Color.Transparent,
|
||||||
|
),
|
||||||
|
center = Offset(0f, size.height),
|
||||||
|
radius = size.width * 0.8f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Top Right (Under Image - Vibrant/Bright)
|
||||||
|
drawRect(
|
||||||
|
brush =
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors = listOf(animTertiary, Color.Transparent),
|
||||||
|
center = Offset(size.width, 0f),
|
||||||
|
radius = size.width * 0.8f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (backdropStyle != BackdropStyle.BACKDROP_NONE) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model =
|
||||||
|
ImageRequest
|
||||||
|
.Builder(LocalContext.current)
|
||||||
|
.data(backdrop.imageUrl)
|
||||||
|
.transitionFactory(CrossFadeFactory(800.milliseconds))
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
alignment = Alignment.TopEnd,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.fillMaxHeight(.7f)
|
||||||
|
.fillMaxWidth(.7f)
|
||||||
|
.alpha(.95f)
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
brush =
|
||||||
|
Brush.horizontalGradient(
|
||||||
|
colors = listOf(Color.Transparent, Color.Black),
|
||||||
|
startX = 0f,
|
||||||
|
endX = size.width * 0.6f,
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn,
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
brush =
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(Color.Black, Color.Transparent),
|
||||||
|
startY = 0f,
|
||||||
|
endY = size.height,
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
NavDisplay(
|
||||||
|
backStack = navigationManager.backStack,
|
||||||
|
onBack = { navigationManager.goBack() },
|
||||||
|
entryDecorators =
|
||||||
|
listOf(
|
||||||
|
rememberSaveableStateHolderNavEntryDecorator(),
|
||||||
|
rememberViewModelStoreNavEntryDecorator(),
|
||||||
|
),
|
||||||
|
entryProvider = { key ->
|
||||||
|
key as Destination
|
||||||
|
val contentKey = "${key}_${server?.id}_${user?.id}"
|
||||||
|
NavEntry(key, contentKey = contentKey) {
|
||||||
|
if (key.fullScreen) {
|
||||||
|
DestinationContent(
|
||||||
|
destination = key,
|
||||||
|
preferences = preferences,
|
||||||
|
onClearBackdrop = viewModel::clearBackdrop,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else if (user != null && server != null) {
|
||||||
|
NavDrawer(
|
||||||
|
destination = key,
|
||||||
|
preferences = preferences,
|
||||||
|
user = user,
|
||||||
|
server = server,
|
||||||
|
onClearBackdrop = viewModel::clearBackdrop,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ErrorMessage("Trying to go to $key without a user logged in", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.github.damontecres.wholphin.ui.nav
|
package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.data.filter.DefaultForGenresFilterOptions
|
import com.github.damontecres.wholphin.data.filter.DefaultForGenresFilterOptions
|
||||||
|
|
@ -41,8 +42,12 @@ import timber.log.Timber
|
||||||
fun DestinationContent(
|
fun DestinationContent(
|
||||||
destination: Destination,
|
destination: Destination,
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
|
onClearBackdrop: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
if (destination.fullScreen) {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
|
}
|
||||||
when (destination) {
|
when (destination) {
|
||||||
is Destination.Home -> {
|
is Destination.Home -> {
|
||||||
HomePage(
|
HomePage(
|
||||||
|
|
@ -122,6 +127,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItemKind.BOX_SET -> {
|
BaseItemKind.BOX_SET -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
CollectionFolderBoxSet(
|
CollectionFolderBoxSet(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
|
|
@ -133,6 +139,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItemKind.PLAYLIST -> {
|
BaseItemKind.PLAYLIST -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
PlaylistDetails(
|
PlaylistDetails(
|
||||||
destination = destination,
|
destination = destination,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -140,6 +147,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItemKind.COLLECTION_FOLDER -> {
|
BaseItemKind.COLLECTION_FOLDER -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
CollectionFolder(
|
CollectionFolder(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
|
|
@ -151,6 +159,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItemKind.FOLDER -> {
|
BaseItemKind.FOLDER -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
CollectionFolder(
|
CollectionFolder(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
|
|
@ -162,6 +171,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItemKind.USER_VIEW -> {
|
BaseItemKind.USER_VIEW -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
CollectionFolder(
|
CollectionFolder(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
|
|
@ -173,6 +183,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
BaseItemKind.PERSON -> {
|
BaseItemKind.PERSON -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
PersonPage(
|
PersonPage(
|
||||||
preferences,
|
preferences,
|
||||||
destination,
|
destination,
|
||||||
|
|
@ -188,6 +199,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
is Destination.FilteredCollection -> {
|
is Destination.FilteredCollection -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
CollectionFolderGeneric(
|
CollectionFolderGeneric(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
itemId = destination.itemId,
|
itemId = destination.itemId,
|
||||||
|
|
@ -201,6 +213,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
is Destination.Recordings -> {
|
is Destination.Recordings -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
CollectionFolderRecordings(
|
CollectionFolderRecordings(
|
||||||
preferences,
|
preferences,
|
||||||
destination.itemId,
|
destination.itemId,
|
||||||
|
|
@ -210,6 +223,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
is Destination.ItemGrid -> {
|
is Destination.ItemGrid -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
ItemGrid(
|
ItemGrid(
|
||||||
destination,
|
destination,
|
||||||
modifier,
|
modifier,
|
||||||
|
|
@ -217,6 +231,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
Destination.Favorites -> {
|
Destination.Favorites -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
FavoritesPage(
|
FavoritesPage(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -232,6 +247,7 @@ fun DestinationContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
Destination.Search -> {
|
Destination.Search -> {
|
||||||
|
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||||
SearchPage(
|
SearchPage(
|
||||||
userPreferences = preferences,
|
userPreferences = preferences,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import com.github.damontecres.wholphin.preferences.AppThemeColors
|
import com.github.damontecres.wholphin.preferences.AppThemeColors
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||||
import com.github.damontecres.wholphin.ui.components.TimeDisplay
|
import com.github.damontecres.wholphin.ui.components.TimeDisplay
|
||||||
|
|
@ -105,6 +106,7 @@ class NavDrawerViewModel
|
||||||
constructor(
|
constructor(
|
||||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
|
val backdropService: BackdropService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private var all: List<NavDrawerItem>? = null
|
private var all: List<NavDrawerItem>? = null
|
||||||
val moreLibraries = MutableLiveData<List<NavDrawerItem>>(null)
|
val moreLibraries = MutableLiveData<List<NavDrawerItem>>(null)
|
||||||
|
|
@ -210,6 +212,7 @@ fun NavDrawer(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
user: JellyfinUser,
|
user: JellyfinUser,
|
||||||
server: JellyfinServer,
|
server: JellyfinServer,
|
||||||
|
onClearBackdrop: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: NavDrawerViewModel =
|
viewModel: NavDrawerViewModel =
|
||||||
hiltViewModel(
|
hiltViewModel(
|
||||||
|
|
@ -300,11 +303,9 @@ fun NavDrawer(
|
||||||
val drawerPadding by animateDpAsState(if (drawerState.isOpen) 0.dp else 8.dp)
|
val drawerPadding by animateDpAsState(if (drawerState.isOpen) 0.dp else 8.dp)
|
||||||
val drawerBackground by animateColorAsState(
|
val drawerBackground by animateColorAsState(
|
||||||
if (drawerState.isOpen) {
|
if (drawerState.isOpen) {
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
|
||||||
1.dp,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.surface
|
MaterialTheme.colorScheme.surface
|
||||||
|
} else {
|
||||||
|
Color.Transparent
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
val spacedBy = 4.dp
|
val spacedBy = 4.dp
|
||||||
|
|
@ -528,6 +529,7 @@ fun NavDrawer(
|
||||||
DestinationContent(
|
DestinationContent(
|
||||||
destination = destination,
|
destination = destination,
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
|
onClearBackdrop = onClearBackdrop,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
|
|
|
||||||
|
|
@ -15,22 +15,24 @@ import com.github.damontecres.wholphin.ui.theme.colors.PurpleThemeColors
|
||||||
val LocalTheme =
|
val LocalTheme =
|
||||||
compositionLocalOf<AppThemeColors> { AppThemeColors.PURPLE }
|
compositionLocalOf<AppThemeColors> { AppThemeColors.PURPLE }
|
||||||
|
|
||||||
|
fun getThemeColors(appThemeColors: AppThemeColors): ThemeColors =
|
||||||
|
when (appThemeColors) {
|
||||||
|
AppThemeColors.PURPLE -> PurpleThemeColors
|
||||||
|
AppThemeColors.BLUE -> BlueThemeColors
|
||||||
|
AppThemeColors.GREEN -> GreenThemeColors
|
||||||
|
AppThemeColors.ORANGE -> OrangeThemeColors
|
||||||
|
AppThemeColors.OLED_BLACK -> OledThemeColors
|
||||||
|
AppThemeColors.BOLD_BLUE -> BoldBlueThemeColors
|
||||||
|
AppThemeColors.UNRECOGNIZED -> PurpleThemeColors
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WholphinTheme(
|
fun WholphinTheme(
|
||||||
darkTheme: Boolean = true,
|
darkTheme: Boolean = true,
|
||||||
appThemeColors: AppThemeColors = AppThemeColors.PURPLE,
|
appThemeColors: AppThemeColors = AppThemeColors.PURPLE,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val themeColors =
|
val themeColors = getThemeColors(appThemeColors)
|
||||||
when (appThemeColors) {
|
|
||||||
AppThemeColors.PURPLE -> PurpleThemeColors
|
|
||||||
AppThemeColors.BLUE -> BlueThemeColors
|
|
||||||
AppThemeColors.GREEN -> GreenThemeColors
|
|
||||||
AppThemeColors.ORANGE -> OrangeThemeColors
|
|
||||||
AppThemeColors.OLED_BLACK -> OledThemeColors
|
|
||||||
AppThemeColors.BOLD_BLUE -> BoldBlueThemeColors
|
|
||||||
AppThemeColors.UNRECOGNIZED -> PurpleThemeColors
|
|
||||||
}
|
|
||||||
|
|
||||||
val colorScheme =
|
val colorScheme =
|
||||||
when {
|
when {
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,12 @@ message LiveTvPreferences {
|
||||||
bool color_code_programs = 4;
|
bool color_code_programs = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum BackdropStyle{
|
||||||
|
BACKDROP_DYNAMIC_COLOR = 0;
|
||||||
|
BACKDROP_IMAGE_ONLY = 1;
|
||||||
|
BACKDROP_NONE = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message InterfacePreferences {
|
message InterfacePreferences {
|
||||||
ThemeSongVolume play_theme_songs = 1;
|
ThemeSongVolume play_theme_songs = 1;
|
||||||
bool remember_selected_tab = 2;
|
bool remember_selected_tab = 2;
|
||||||
|
|
@ -141,6 +147,7 @@ message InterfacePreferences {
|
||||||
bool show_clock = 6;
|
bool show_clock = 6;
|
||||||
SubtitlePreferences subtitles_preferences = 7;
|
SubtitlePreferences subtitles_preferences = 7;
|
||||||
LiveTvPreferences live_tv_preferences = 8;
|
LiveTvPreferences live_tv_preferences = 8;
|
||||||
|
BackdropStyle backdrop_style = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdvancedPreferences {
|
message AdvancedPreferences {
|
||||||
|
|
|
||||||
|
|
@ -394,6 +394,7 @@
|
||||||
<string name="sort_channels_recently_watched">Sort channels by recently watched</string>
|
<string name="sort_channels_recently_watched">Sort channels by recently watched</string>
|
||||||
<string name="color_code_programs">Color-code programs</string>
|
<string name="color_code_programs">Color-code programs</string>
|
||||||
<string name="subtitle_delay">Subtitle delay</string>
|
<string name="subtitle_delay">Subtitle delay</string>
|
||||||
|
<string name="backdrop_display">Backdrop style</string>
|
||||||
|
|
||||||
<string-array name="theme_song_volume">
|
<string-array name="theme_song_volume">
|
||||||
<item>Disabled</item>
|
<item>Disabled</item>
|
||||||
|
|
@ -482,4 +483,10 @@
|
||||||
<!-- <item>Banner</item>-->
|
<!-- <item>Banner</item>-->
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="backdrop_style_options">
|
||||||
|
<item>Image with dynamic color</item>
|
||||||
|
<item>Image only</item>
|
||||||
|
<item>None</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ protobuf-javalite = "4.33.1"
|
||||||
hilt = "2.57.2"
|
hilt = "2.57.2"
|
||||||
room = "2.8.4"
|
room = "2.8.4"
|
||||||
preferenceKtx = "1.2.1"
|
preferenceKtx = "1.2.1"
|
||||||
|
paletteKtx = "1.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" }
|
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" }
|
||||||
|
|
@ -100,6 +101,7 @@ slf4j2-timber = { module = "uk.kulikov:slf4j2-timber", version.ref = "slf4j2Timb
|
||||||
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||||
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
|
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
|
||||||
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
|
androidx-room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
|
||||||
|
androidx-palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "paletteKtx" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue