mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +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.preference.ktx)
|
||||
implementation(libs.androidx.room.testing)
|
||||
implementation(libs.androidx.palette.ktx)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
|
|
|
|||
|
|
@ -659,6 +659,19 @@ sealed interface AppPreference<Pref, T> {
|
|||
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 =
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.one_click_pause,
|
||||
|
|
@ -909,6 +922,7 @@ val advancedPreferences =
|
|||
// Temporarily disabled, see https://github.com/damontecres/Wholphin/pull/127#issuecomment-3478058418
|
||||
// AppPreference.NavDrawerSwitchOnFocus,
|
||||
AppPreference.ControllerTimeout,
|
||||
AppPreference.BackdropStylePref,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ class AppPreferencesSerializer
|
|||
navDrawerSwitchOnFocus =
|
||||
AppPreference.NavDrawerSwitchOnFocus.defaultValue
|
||||
showClock = AppPreference.ShowClock.defaultValue
|
||||
backdropStyle = AppPreference.BackdropStylePref.defaultValue
|
||||
|
||||
subtitlesPreferences =
|
||||
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.LibraryDisplayInfo
|
||||
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.NavigationManager
|
||||
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.PlaylistLoadingState
|
||||
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.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.playback.scale
|
||||
|
|
@ -123,6 +125,7 @@ class CollectionFolderViewModel
|
|||
private val serverRepository: ServerRepository,
|
||||
private val libraryDisplayInfoDao: LibraryDisplayInfoDao,
|
||||
private val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val backdropService: BackdropService,
|
||||
val navigationManager: NavigationManager,
|
||||
) : ItemViewModel(api) {
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
|
|
@ -454,6 +457,12 @@ class CollectionFolderViewModel
|
|||
favoriteWatchManager.setFavorite(itemId, favorite)
|
||||
(pager.value as? ApiRequestPager<*>)?.refreshItem(position, itemId)
|
||||
}
|
||||
|
||||
fun updateBackdrop(item: BaseItem) {
|
||||
viewModelScope.launchIO {
|
||||
backdropService.submit(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -578,6 +587,7 @@ fun CollectionFolderGrid(
|
|||
viewOptions = viewOptions,
|
||||
defaultViewOptions = defaultViewOptions,
|
||||
onSaveViewOptions = { viewModel.saveViewOptions(it) },
|
||||
onChangeBackdrop = viewModel::updateBackdrop,
|
||||
playEnabled = playEnabled,
|
||||
onClickPlay = { _, item ->
|
||||
viewModel.navigationManager.navigateTo(Destination.Playback(item))
|
||||
|
|
@ -687,6 +697,7 @@ fun CollectionFolderGridContent(
|
|||
viewOptions: ViewOptions,
|
||||
onClickPlayAll: (shuffle: Boolean) -> Unit,
|
||||
onClickPlay: (Int, BaseItem) -> Unit,
|
||||
onChangeBackdrop: (BaseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
showTitle: Boolean = true,
|
||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||
|
|
@ -707,14 +718,13 @@ fun CollectionFolderGridContent(
|
|||
|
||||
var position by rememberInt(0)
|
||||
val focusedItem = pager.getOrNull(position)
|
||||
if (viewOptions.showDetails) {
|
||||
LaunchedEffect(focusedItem) {
|
||||
focusedItem?.let(onChangeBackdrop)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
if (viewOptions.showDetails) {
|
||||
DelayedDetailsBackdropImage(
|
||||
item = focusedItem,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||
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.data.model.BaseItem
|
||||
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.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
|
|
@ -45,6 +46,7 @@ abstract class RecommendedViewModel(
|
|||
val context: Context,
|
||||
val navigationManager: NavigationManager,
|
||||
val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val backdropService: BackdropService,
|
||||
) : ViewModel() {
|
||||
abstract fun init()
|
||||
|
||||
|
|
@ -86,6 +88,12 @@ abstract class RecommendedViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun updateBackdrop(item: BaseItem) {
|
||||
viewModelScope.launchIO {
|
||||
backdropService.submit(item)
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun update(
|
||||
@StringRes title: Int,
|
||||
row: HomeRowLoadingState,
|
||||
|
|
@ -152,6 +160,7 @@ fun RecommendedContent(
|
|||
},
|
||||
onFocusPosition = onFocusPosition,
|
||||
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
||||
onUpdateBackdrop = viewModel::updateBackdrop,
|
||||
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.AppPreferences
|
||||
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.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
|
|
@ -56,7 +57,8 @@ class RecommendedMovieViewModel
|
|||
@Assisted val parentId: UUID,
|
||||
navigationManager: NavigationManager,
|
||||
favoriteWatchManager: FavoriteWatchManager,
|
||||
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager) {
|
||||
backdropService: BackdropService,
|
||||
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager, backdropService) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
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.AppPreferences
|
||||
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.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
|
|
@ -59,7 +60,8 @@ class RecommendedTvShowViewModel
|
|||
@Assisted val parentId: UUID,
|
||||
navigationManager: NavigationManager,
|
||||
favoriteWatchManager: FavoriteWatchManager,
|
||||
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager) {
|
||||
backdropService: BackdropService,
|
||||
) : RecommendedViewModel(context, navigationManager, favoriteWatchManager, backdropService) {
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(parentId: UUID): RecommendedTvShowViewModel
|
||||
|
|
|
|||
|
|
@ -50,10 +50,10 @@ import androidx.tv.material3.Text
|
|||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import com.github.damontecres.wholphin.R
|
||||
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.ui.DefaultItemFields
|
||||
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.DialogParams
|
||||
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.enableMarquee
|
||||
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.roundMinutes
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
|
|
@ -88,6 +89,7 @@ class PlaylistViewModel
|
|||
constructor(
|
||||
api: ApiClient,
|
||||
val navigationManager: NavigationManager,
|
||||
private val backdropService: BackdropService,
|
||||
) : ItemViewModel(api) {
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val items = MutableLiveData<List<BaseItem?>>(listOf())
|
||||
|
|
@ -111,6 +113,12 @@ class PlaylistViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBackdrop(item: BaseItem) {
|
||||
viewModelScope.launchIO {
|
||||
backdropService.submit(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -146,6 +154,7 @@ fun PlaylistDetails(
|
|||
playlist = it,
|
||||
items = items,
|
||||
focusRequester = focusRequester,
|
||||
onChangeBackdrop = viewModel::updateBackdrop,
|
||||
onClickIndex = { index, _ ->
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.PlaybackList(
|
||||
|
|
@ -212,6 +221,7 @@ fun PlaylistDetailsContent(
|
|||
onClickIndex: (Int, BaseItem) -> Unit,
|
||||
onLongClickIndex: (Int, BaseItem) -> Unit,
|
||||
onClickPlay: (shuffle: Boolean) -> Unit,
|
||||
onChangeBackdrop: (BaseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
) {
|
||||
|
|
@ -219,13 +229,15 @@ fun PlaylistDetailsContent(
|
|||
var focusedIndex by remember { mutableIntStateOf(savedIndex) }
|
||||
val focus = remember { FocusRequester() }
|
||||
val focusedItem = items.getOrNull(focusedIndex)
|
||||
LaunchedEffect(focusedItem) {
|
||||
focusedItem?.let(onChangeBackdrop)
|
||||
}
|
||||
|
||||
val playButtonFocusRequester = remember { FocusRequester() }
|
||||
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
DelayedDetailsBackdropImage(focusedItem)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier =
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ 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.components.DetailsBackdropImage
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
|
|
@ -296,7 +295,6 @@ fun EpisodeDetailsContent(
|
|||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
DetailsBackdropImage(ep)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(16.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.ItemPlayback
|
||||
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.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||
|
|
@ -50,6 +51,7 @@ class EpisodeViewModel
|
|||
private val themeSongPlayer: ThemeSongPlayer,
|
||||
private val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val backdropService: BackdropService,
|
||||
@Assisted val itemId: UUID,
|
||||
) : ViewModel() {
|
||||
@AssistedFactory
|
||||
|
|
@ -96,6 +98,7 @@ class EpisodeViewModel
|
|||
this@EpisodeViewModel.item.value = item
|
||||
chosenStreams.value = result
|
||||
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.PersonRow
|
||||
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.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
|
|
@ -396,7 +395,6 @@ fun MovieDetailsContent(
|
|||
focusRequesters.getOrNull(position)?.tryRequestFocus()
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
DetailsBackdropImage(movie)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(16.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.Trailer
|
||||
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.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
|
|
@ -63,6 +64,7 @@ class MovieViewModel
|
|||
private val trailerService: TrailerService,
|
||||
private val extrasService: ExtrasService,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val backdropService: BackdropService,
|
||||
@Assisted val itemId: UUID,
|
||||
) : ViewModel() {
|
||||
@AssistedFactory
|
||||
|
|
@ -118,6 +120,7 @@ class MovieViewModel
|
|||
this@MovieViewModel.item.value = item
|
||||
chosenStreams.value = result
|
||||
loading.value = LoadingState.Success
|
||||
backdropService.submit(item)
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
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.SeasonCard
|
||||
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.DialogParams
|
||||
import com.github.damontecres.wholphin.ui.components.DialogPopup
|
||||
|
|
@ -320,8 +319,6 @@ fun SeriesDetailsContent(
|
|||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
DetailsBackdropImage(series)
|
||||
|
||||
Column(
|
||||
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.cards.BannerCard
|
||||
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.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.components.SeriesName
|
||||
|
|
@ -112,7 +111,6 @@ fun SeriesOverviewContent(
|
|||
modifier
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
DetailsBackdropImage(backdropImageUrl)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
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.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
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
|
|
@ -74,6 +75,7 @@ class SeriesViewModel
|
|||
private val extrasService: ExtrasService,
|
||||
val streamChoiceService: StreamChoiceService,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val backdropService: BackdropService,
|
||||
) : ItemViewModel(api) {
|
||||
private lateinit var seriesId: UUID
|
||||
private lateinit var prefs: UserPreferences
|
||||
|
|
@ -103,6 +105,7 @@ class SeriesViewModel
|
|||
) + Dispatchers.IO,
|
||||
) {
|
||||
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
|
||||
|
|
@ -332,6 +335,8 @@ class SeriesViewModel
|
|||
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.ItemRow
|
||||
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.DialogPopup
|
||||
import com.github.damontecres.wholphin.ui.components.EpisodeQuickDetails
|
||||
|
|
@ -167,6 +166,7 @@ fun HomePage(
|
|||
},
|
||||
loadingState = refreshing,
|
||||
showClock = preferences.appPreferences.interfacePreferences.showClock,
|
||||
onUpdateBackdrop = viewModel::updateBackdrop,
|
||||
modifier = modifier,
|
||||
)
|
||||
dialog?.let { params ->
|
||||
|
|
@ -203,6 +203,7 @@ fun HomePageContent(
|
|||
onLongClickItem: (RowColumn, BaseItem) -> Unit,
|
||||
onClickPlay: (RowColumn, BaseItem) -> Unit,
|
||||
showClock: Boolean,
|
||||
onUpdateBackdrop: (BaseItem) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onFocusPosition: ((RowColumn) -> Unit)? = null,
|
||||
loadingState: LoadingState? = null,
|
||||
|
|
@ -249,12 +250,10 @@ fun HomePageContent(
|
|||
LaunchedEffect(position) {
|
||||
listState.animateScrollToItem(position.row)
|
||||
}
|
||||
LaunchedEffect(focusedItem) {
|
||||
focusedItem?.let(onUpdateBackdrop)
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
DelayedDetailsBackdropImage(
|
||||
item = focusedItem,
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
HomePageHeader(
|
||||
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.model.BaseItem
|
||||
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.FavoriteWatchManager
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
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.setValueOnMain
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
|
|
@ -58,6 +60,7 @@ class HomeViewModel
|
|||
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
private val favoriteWatchManager: FavoriteWatchManager,
|
||||
private val datePlayedService: DatePlayedService,
|
||||
private val backdropService: BackdropService,
|
||||
) : ViewModel() {
|
||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val refreshState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
|
|
@ -316,6 +319,12 @@ class HomeViewModel
|
|||
init(preferences)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateBackdrop(item: BaseItem) {
|
||||
viewModelScope.launchIO {
|
||||
backdropService.submit(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val supportedLatestCollectionTypes =
|
||||
|
|
|
|||
|
|
@ -1,17 +1,59 @@
|
|||
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.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.navigation3.runtime.NavEntry
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
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.JellyfinUser
|
||||
import com.github.damontecres.wholphin.preferences.BackdropStyle
|
||||
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.ui.CrossFadeFactory
|
||||
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
|
||||
|
|
@ -25,7 +67,125 @@ fun ApplicationContent(
|
|||
navigationManager: NavigationManager,
|
||||
preferences: UserPreferences,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ApplicationContentViewModel = hiltViewModel(),
|
||||
) {
|
||||
val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle()
|
||||
val backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
val baseBackgroundColor = MaterialTheme.colorScheme.background
|
||||
if (backdrop.hasColors &&
|
||||
(backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED)
|
||||
) {
|
||||
val animPrimary by animateColorAsState(
|
||||
backdrop.primaryColor,
|
||||
animationSpec = tween(1250),
|
||||
label = "dynamic_backdrop_primary",
|
||||
)
|
||||
val animSecondary by animateColorAsState(
|
||||
backdrop.secondaryColor,
|
||||
animationSpec = tween(1250),
|
||||
label = "dynamic_backdrop_secondary",
|
||||
)
|
||||
val animTertiary by animateColorAsState(
|
||||
backdrop.tertiaryColor,
|
||||
animationSpec = tween(1250),
|
||||
label = "dynamic_backdrop_tertiary",
|
||||
)
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.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() },
|
||||
|
|
@ -42,7 +202,8 @@ fun ApplicationContent(
|
|||
DestinationContent(
|
||||
destination = key,
|
||||
preferences = preferences,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
onClearBackdrop = viewModel::clearBackdrop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
} else if (user != null && server != null) {
|
||||
NavDrawer(
|
||||
|
|
@ -50,7 +211,8 @@ fun ApplicationContent(
|
|||
preferences = preferences,
|
||||
user = user,
|
||||
server = server,
|
||||
modifier = modifier,
|
||||
onClearBackdrop = viewModel::clearBackdrop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
} else {
|
||||
ErrorMessage("Trying to go to $key without a user logged in", null)
|
||||
|
|
@ -58,4 +220,5 @@ fun ApplicationContent(
|
|||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.damontecres.wholphin.ui.nav
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.data.filter.DefaultForGenresFilterOptions
|
||||
|
|
@ -41,8 +42,12 @@ import timber.log.Timber
|
|||
fun DestinationContent(
|
||||
destination: Destination,
|
||||
preferences: UserPreferences,
|
||||
onClearBackdrop: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (destination.fullScreen) {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
}
|
||||
when (destination) {
|
||||
is Destination.Home -> {
|
||||
HomePage(
|
||||
|
|
@ -122,6 +127,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
BaseItemKind.BOX_SET -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
CollectionFolderBoxSet(
|
||||
preferences = preferences,
|
||||
itemId = destination.itemId,
|
||||
|
|
@ -133,6 +139,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
BaseItemKind.PLAYLIST -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
PlaylistDetails(
|
||||
destination = destination,
|
||||
modifier = modifier,
|
||||
|
|
@ -140,6 +147,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
BaseItemKind.COLLECTION_FOLDER -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
CollectionFolder(
|
||||
preferences = preferences,
|
||||
destination = destination,
|
||||
|
|
@ -151,6 +159,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
BaseItemKind.FOLDER -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
CollectionFolder(
|
||||
preferences = preferences,
|
||||
destination = destination,
|
||||
|
|
@ -162,6 +171,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
BaseItemKind.USER_VIEW -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
CollectionFolder(
|
||||
preferences = preferences,
|
||||
destination = destination,
|
||||
|
|
@ -173,6 +183,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
BaseItemKind.PERSON -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
PersonPage(
|
||||
preferences,
|
||||
destination,
|
||||
|
|
@ -188,6 +199,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
is Destination.FilteredCollection -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
CollectionFolderGeneric(
|
||||
preferences = preferences,
|
||||
itemId = destination.itemId,
|
||||
|
|
@ -201,6 +213,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
is Destination.Recordings -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
CollectionFolderRecordings(
|
||||
preferences,
|
||||
destination.itemId,
|
||||
|
|
@ -210,6 +223,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
is Destination.ItemGrid -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
ItemGrid(
|
||||
destination,
|
||||
modifier,
|
||||
|
|
@ -217,6 +231,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
Destination.Favorites -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
FavoritesPage(
|
||||
preferences = preferences,
|
||||
modifier = modifier,
|
||||
|
|
@ -232,6 +247,7 @@ fun DestinationContent(
|
|||
}
|
||||
|
||||
Destination.Search -> {
|
||||
LaunchedEffect(Unit) { onClearBackdrop.invoke() }
|
||||
SearchPage(
|
||||
userPreferences = preferences,
|
||||
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.preferences.AppThemeColors
|
||||
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.ui.FontAwesome
|
||||
import com.github.damontecres.wholphin.ui.components.TimeDisplay
|
||||
|
|
@ -105,6 +106,7 @@ class NavDrawerViewModel
|
|||
constructor(
|
||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
val navigationManager: NavigationManager,
|
||||
val backdropService: BackdropService,
|
||||
) : ViewModel() {
|
||||
private var all: List<NavDrawerItem>? = null
|
||||
val moreLibraries = MutableLiveData<List<NavDrawerItem>>(null)
|
||||
|
|
@ -210,6 +212,7 @@ fun NavDrawer(
|
|||
preferences: UserPreferences,
|
||||
user: JellyfinUser,
|
||||
server: JellyfinServer,
|
||||
onClearBackdrop: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: NavDrawerViewModel =
|
||||
hiltViewModel(
|
||||
|
|
@ -300,11 +303,9 @@ fun NavDrawer(
|
|||
val drawerPadding by animateDpAsState(if (drawerState.isOpen) 0.dp else 8.dp)
|
||||
val drawerBackground by animateColorAsState(
|
||||
if (drawerState.isOpen) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
1.dp,
|
||||
)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
)
|
||||
val spacedBy = 4.dp
|
||||
|
|
@ -528,6 +529,7 @@ fun NavDrawer(
|
|||
DestinationContent(
|
||||
destination = destination,
|
||||
preferences = preferences,
|
||||
onClearBackdrop = onClearBackdrop,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
|
|
|
|||
|
|
@ -15,13 +15,7 @@ import com.github.damontecres.wholphin.ui.theme.colors.PurpleThemeColors
|
|||
val LocalTheme =
|
||||
compositionLocalOf<AppThemeColors> { AppThemeColors.PURPLE }
|
||||
|
||||
@Composable
|
||||
fun WholphinTheme(
|
||||
darkTheme: Boolean = true,
|
||||
appThemeColors: AppThemeColors = AppThemeColors.PURPLE,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val themeColors =
|
||||
fun getThemeColors(appThemeColors: AppThemeColors): ThemeColors =
|
||||
when (appThemeColors) {
|
||||
AppThemeColors.PURPLE -> PurpleThemeColors
|
||||
AppThemeColors.BLUE -> BlueThemeColors
|
||||
|
|
@ -32,6 +26,14 @@ fun WholphinTheme(
|
|||
AppThemeColors.UNRECOGNIZED -> PurpleThemeColors
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WholphinTheme(
|
||||
darkTheme: Boolean = true,
|
||||
appThemeColors: AppThemeColors = AppThemeColors.PURPLE,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val themeColors = getThemeColors(appThemeColors)
|
||||
|
||||
val colorScheme =
|
||||
when {
|
||||
darkTheme -> themeColors.darkScheme
|
||||
|
|
|
|||
|
|
@ -132,6 +132,12 @@ message LiveTvPreferences {
|
|||
bool color_code_programs = 4;
|
||||
}
|
||||
|
||||
enum BackdropStyle{
|
||||
BACKDROP_DYNAMIC_COLOR = 0;
|
||||
BACKDROP_IMAGE_ONLY = 1;
|
||||
BACKDROP_NONE = 2;
|
||||
}
|
||||
|
||||
message InterfacePreferences {
|
||||
ThemeSongVolume play_theme_songs = 1;
|
||||
bool remember_selected_tab = 2;
|
||||
|
|
@ -141,6 +147,7 @@ message InterfacePreferences {
|
|||
bool show_clock = 6;
|
||||
SubtitlePreferences subtitles_preferences = 7;
|
||||
LiveTvPreferences live_tv_preferences = 8;
|
||||
BackdropStyle backdrop_style = 9;
|
||||
}
|
||||
|
||||
message AdvancedPreferences {
|
||||
|
|
|
|||
|
|
@ -394,6 +394,7 @@
|
|||
<string name="sort_channels_recently_watched">Sort channels by recently watched</string>
|
||||
<string name="color_code_programs">Color-code programs</string>
|
||||
<string name="subtitle_delay">Subtitle delay</string>
|
||||
<string name="backdrop_display">Backdrop style</string>
|
||||
|
||||
<string-array name="theme_song_volume">
|
||||
<item>Disabled</item>
|
||||
|
|
@ -482,4 +483,10 @@
|
|||
<!-- <item>Banner</item>-->
|
||||
</string-array>
|
||||
|
||||
<string-array name="backdrop_style_options">
|
||||
<item>Image with dynamic color</item>
|
||||
<item>Image only</item>
|
||||
<item>None</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ protobuf-javalite = "4.33.1"
|
|||
hilt = "2.57.2"
|
||||
room = "2.8.4"
|
||||
preferenceKtx = "1.2.1"
|
||||
paletteKtx = "1.0.0"
|
||||
|
||||
[libraries]
|
||||
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" }
|
||||
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-palette-ktx = { group = "androidx.palette", name = "palette-ktx", version.ref = "paletteKtx" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue