mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Simplify grid focus, handle items w/ missing images
This commit is contained in:
parent
2212e32c53
commit
b63e933386
13 changed files with 352 additions and 195 deletions
|
|
@ -103,6 +103,7 @@ android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro",
|
"proguard-rules.pro",
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,9 @@ data class BaseItem(
|
||||||
} else {
|
} else {
|
||||||
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY)
|
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY)
|
||||||
}
|
}
|
||||||
|
} else if (dto.imageTags == null || dto.imageTags!![ImageType.PRIMARY] == null) {
|
||||||
|
// TODO is this a bad assumption?
|
||||||
|
null
|
||||||
} else {
|
} else {
|
||||||
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY)
|
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ sealed class AppColors private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const val DEFAULT_PAGE_SIZE = 50
|
const val DEFAULT_PAGE_SIZE = 100
|
||||||
|
|
||||||
val DefaultItemFields =
|
val DefaultItemFields =
|
||||||
listOf(
|
listOf(
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,16 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.Card
|
import androidx.tv.material3.Card
|
||||||
|
|
@ -30,6 +35,7 @@ import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BannerCard(
|
fun BannerCard(
|
||||||
|
name: String?,
|
||||||
imageUrl: String?,
|
imageUrl: String?,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
|
|
@ -41,19 +47,39 @@ fun BannerCard(
|
||||||
aspectRatio: Float = 16f / 9,
|
aspectRatio: Float = 16f / 9,
|
||||||
interactionSource: MutableInteractionSource? = null,
|
interactionSource: MutableInteractionSource? = null,
|
||||||
) {
|
) {
|
||||||
|
var imageError by remember { mutableStateOf(false) }
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.size(cardHeight * aspectRatio, cardHeight),
|
modifier = modifier.size(cardHeight * aspectRatio, cardHeight),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(
|
||||||
AsyncImage(
|
modifier =
|
||||||
model = imageUrl,
|
Modifier
|
||||||
contentDescription = null,
|
.fillMaxSize()
|
||||||
contentScale = ContentScale.Fit,
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
modifier = Modifier.fillMaxSize(),
|
) {
|
||||||
)
|
if (!imageError && imageUrl.isNotNullOrBlank()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = imageUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
onError = { imageError = true },
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = name ?: "",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.Center),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (played || cornerText.isNotNullOrBlank()) {
|
if (played || cornerText.isNotNullOrBlank()) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
package com.github.damontecres.dolphin.ui.cards
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.Card
|
||||||
|
import androidx.tv.material3.CardDefaults
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.dolphin.ui.enableMarquee
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GridCard(
|
||||||
|
item: BaseItem?,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
val dto = item?.data
|
||||||
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
|
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
|
||||||
|
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
|
||||||
|
var focusedAfterDelay by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val hideOverlayDelay = 500L
|
||||||
|
if (focused) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(hideOverlayDelay)
|
||||||
|
if (focused) {
|
||||||
|
focusedAfterDelay = true
|
||||||
|
} else {
|
||||||
|
focusedAfterDelay = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
focusedAfterDelay = false
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(spaceBetween),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors =
|
||||||
|
CardDefaults.colors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
ItemCardImage(
|
||||||
|
imageUrl = item?.imageUrl,
|
||||||
|
name = item?.name,
|
||||||
|
// showOverlay = !focusedAfterDelay,
|
||||||
|
showOverlay = true,
|
||||||
|
favorite = dto?.userData?.isFavorite ?: false,
|
||||||
|
watched = dto?.userData?.played ?: false,
|
||||||
|
unwatchedCount = dto?.userData?.unplayedItemCount ?: -1,
|
||||||
|
watchedPercent = dto?.userData?.playedPercentage,
|
||||||
|
useFallbackText = false,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(2f / 3f) // TODO
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(0.dp),
|
||||||
|
modifier = Modifier.padding(bottom = spaceBelow).fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = item?.name ?: "",
|
||||||
|
maxLines = 1,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.enableMarquee(focusedAfterDelay),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = item?.data?.productionYear?.toString() ?: "",
|
||||||
|
maxLines = 1,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 4.dp)
|
||||||
|
.enableMarquee(focusedAfterDelay),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.dolphin.ui.cards
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
|
@ -27,10 +28,13 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
@ -47,6 +51,7 @@ import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.ui.FontAwesome
|
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||||
import com.github.damontecres.dolphin.ui.enableMarquee
|
import com.github.damontecres.dolphin.ui.enableMarquee
|
||||||
import com.github.damontecres.dolphin.ui.ifElse
|
import com.github.damontecres.dolphin.ui.ifElse
|
||||||
|
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.dolphin.util.seasonEpisode
|
import com.github.damontecres.dolphin.util.seasonEpisode
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
|
@ -161,24 +166,64 @@ fun ItemCardImage(
|
||||||
unwatchedCount: Int,
|
unwatchedCount: Int,
|
||||||
watchedPercent: Double?,
|
watchedPercent: Double?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
useFallbackText: Boolean = true,
|
||||||
) {
|
) {
|
||||||
|
var imageError by remember { mutableStateOf(false) }
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
AsyncImage(
|
if (!imageError && imageUrl.isNotNullOrBlank()) {
|
||||||
model = imageUrl,
|
AsyncImage(
|
||||||
contentDescription = name,
|
model = imageUrl,
|
||||||
contentScale = ContentScale.Fit,
|
contentDescription = name,
|
||||||
alignment = Alignment.Center,
|
contentScale = ContentScale.Fit,
|
||||||
// TODO error/fallback images
|
alignment = Alignment.Center,
|
||||||
error = null,
|
// TODO error/fallback images
|
||||||
fallback = null,
|
error = null,
|
||||||
onError = {
|
fallback = null,
|
||||||
Timber.e(it.result.throwable, "Error loading image: $imageUrl")
|
onError = {
|
||||||
},
|
Timber.e(it.result.throwable, "Error loading image: $imageUrl")
|
||||||
modifier =
|
imageError = true
|
||||||
Modifier
|
},
|
||||||
.fillMaxSize()
|
modifier =
|
||||||
.align(Alignment.TopCenter),
|
Modifier
|
||||||
)
|
.fillMaxSize()
|
||||||
|
.align(Alignment.TopCenter),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
.fillMaxSize()
|
||||||
|
.align(Alignment.TopCenter),
|
||||||
|
) {
|
||||||
|
if (useFallbackText && name.isNotNullOrBlank()) {
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.align(Alignment.Center),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.drawable.video_solid),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
colorFilter =
|
||||||
|
ColorFilter.tint(
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
BlendMode.SrcIn,
|
||||||
|
),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize(.4f)
|
||||||
|
.align(Alignment.Center),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = showOverlay,
|
visible = showOverlay,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,7 @@ fun BannerItemRow(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
cardContent = { index, item, modifier, onClick, onLongClick ->
|
cardContent = { index, item, modifier, onClick, onLongClick ->
|
||||||
BannerCard(
|
BannerCard(
|
||||||
|
name = title,
|
||||||
imageUrl = item?.imageUrl,
|
imageUrl = item?.imageUrl,
|
||||||
aspectRatio =
|
aspectRatio =
|
||||||
aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
|
aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat() ?: (16f / 9),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -44,7 +43,7 @@ import com.github.damontecres.dolphin.R
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.ui.AppColors
|
import com.github.damontecres.dolphin.ui.AppColors
|
||||||
import com.github.damontecres.dolphin.ui.FontAwesome
|
import com.github.damontecres.dolphin.ui.FontAwesome
|
||||||
import com.github.damontecres.dolphin.ui.cards.ItemCard
|
import com.github.damontecres.dolphin.ui.cards.GridCard
|
||||||
import com.github.damontecres.dolphin.ui.ifElse
|
import com.github.damontecres.dolphin.ui.ifElse
|
||||||
import com.github.damontecres.dolphin.ui.playback.isBackwardButton
|
import com.github.damontecres.dolphin.ui.playback.isBackwardButton
|
||||||
import com.github.damontecres.dolphin.ui.playback.isForwardButton
|
import com.github.damontecres.dolphin.ui.playback.isForwardButton
|
||||||
|
|
@ -53,7 +52,6 @@ import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||||
import com.github.damontecres.dolphin.util.ExceptionHandler
|
import com.github.damontecres.dolphin.util.ExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
private const val DEBUG = false
|
private const val DEBUG = false
|
||||||
|
|
||||||
|
|
@ -63,7 +61,6 @@ fun CardGrid(
|
||||||
itemOnClick: (BaseItem) -> Unit,
|
itemOnClick: (BaseItem) -> Unit,
|
||||||
longClicker: (BaseItem) -> Unit,
|
longClicker: (BaseItem) -> Unit,
|
||||||
letterPosition: suspend (Char) -> Int,
|
letterPosition: suspend (Char) -> Int,
|
||||||
requestFocus: Boolean,
|
|
||||||
gridFocusRequester: FocusRequester,
|
gridFocusRequester: FocusRequester,
|
||||||
showJumpButtons: Boolean,
|
showJumpButtons: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -79,45 +76,6 @@ fun CardGrid(
|
||||||
val zeroFocus = remember { FocusRequester() }
|
val zeroFocus = remember { FocusRequester() }
|
||||||
var previouslyFocusedIndex by rememberSaveable { mutableIntStateOf(0) }
|
var previouslyFocusedIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||||
var focusedIndex by rememberSaveable { mutableIntStateOf(initialPosition) }
|
var focusedIndex by rememberSaveable { mutableIntStateOf(initialPosition) }
|
||||||
var focusedIndexOnExit by rememberSaveable { mutableIntStateOf(-1) }
|
|
||||||
|
|
||||||
// Tracks whether the very first requestFocus has run, if the caller isn't requesting focus,
|
|
||||||
// then the first time will never run
|
|
||||||
var hasRequestFocusRun by rememberSaveable { mutableStateOf(!requestFocus) }
|
|
||||||
var savedFocusedIndex by rememberSaveable { mutableIntStateOf(-1) }
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
Timber.d(
|
|
||||||
"Grid: hasRun=$hasRequestFocusRun, requestFocus=$requestFocus, initialPosition=$initialPosition, focusedIndex=$focusedIndex",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
if (!hasRequestFocusRun) {
|
|
||||||
// On very first composition, if parent wants to focus on the grid, scroll to the item
|
|
||||||
if (requestFocus && initialPosition >= 0) {
|
|
||||||
if (DEBUG) {
|
|
||||||
Timber.d(
|
|
||||||
"focus on startPosition=$startPosition, from initialPosition=$initialPosition",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
focusedIndex = startPosition
|
|
||||||
gridState.scrollToItem(startPosition, 0)
|
|
||||||
firstFocus.tryRequestFocus()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val index = savedFocusedIndex
|
|
||||||
if (DEBUG) Timber.d("savedFocusedIndex=$index")
|
|
||||||
if (index in 0..<pager.size) {
|
|
||||||
// If this is a recomposition, but not the first
|
|
||||||
// focus on the restored index
|
|
||||||
// gridState.scrollToItem(index, -columns)
|
|
||||||
firstFocus.tryRequestFocus()
|
|
||||||
}
|
|
||||||
savedFocusedIndex = -1
|
|
||||||
}
|
|
||||||
// hasRun = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var alphabetFocus by remember { mutableStateOf(false) }
|
var alphabetFocus by remember { mutableStateOf(false) }
|
||||||
val focusOn = { index: Int ->
|
val focusOn = { index: Int ->
|
||||||
|
|
@ -129,12 +87,12 @@ fun CardGrid(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for a recomposition to focus
|
// Wait for a recomposition to focus
|
||||||
LaunchedEffect(alphabetFocus) {
|
// LaunchedEffect(alphabetFocus) {
|
||||||
if (alphabetFocus) {
|
// if (alphabetFocus) {
|
||||||
firstFocus.tryRequestFocus()
|
// firstFocus.tryRequestFocus()
|
||||||
}
|
// }
|
||||||
alphabetFocus = false
|
// alphabetFocus = false
|
||||||
}
|
// }
|
||||||
|
|
||||||
val useBackToJump = true // uiConfig.preferences.interfacePreferences.scrollTopOnBack
|
val useBackToJump = true // uiConfig.preferences.interfacePreferences.scrollTopOnBack
|
||||||
val showFooter = true // uiConfig.preferences.interfacePreferences.showPositionFooter
|
val showFooter = true // uiConfig.preferences.interfacePreferences.showPositionFooter
|
||||||
|
|
@ -169,7 +127,6 @@ fun CardGrid(
|
||||||
val newPosition =
|
val newPosition =
|
||||||
(gridState.firstVisibleItemIndex + jump).coerceIn(0..<pager.size)
|
(gridState.firstVisibleItemIndex + jump).coerceIn(0..<pager.size)
|
||||||
if (DEBUG) Timber.d("newPosition=$newPosition")
|
if (DEBUG) Timber.d("newPosition=$newPosition")
|
||||||
savedFocusedIndex = newPosition
|
|
||||||
focusOn(newPosition)
|
focusOn(newPosition)
|
||||||
gridState.scrollToItem(newPosition, 0)
|
gridState.scrollToItem(newPosition, 0)
|
||||||
}
|
}
|
||||||
|
|
@ -255,51 +212,38 @@ fun CardGrid(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusGroup()
|
.focusGroup()
|
||||||
.focusRestorer(firstFocus)
|
.focusRestorer(firstFocus)
|
||||||
.focusRequester(gridFocusRequester)
|
|
||||||
.focusProperties {
|
.focusProperties {
|
||||||
onExit = {
|
onExit = {
|
||||||
// Leaving the grid, so "forget" the position
|
// Leaving the grid, so "forget" the position
|
||||||
focusedIndexOnExit = focusedIndex
|
|
||||||
focusedIndex = -1
|
focusedIndex = -1
|
||||||
savedFocusedIndex = -1
|
|
||||||
}
|
}
|
||||||
onEnter = {
|
onEnter = {
|
||||||
focusedIndexOnExit = -1
|
|
||||||
if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) {
|
if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) {
|
||||||
Timber.d("onEnter: focusedIndex=$focusedIndex, savedFocusedIndex=$savedFocusedIndex")
|
|
||||||
focusedIndex = startPosition
|
focusedIndex = startPosition
|
||||||
firstFocus.tryRequestFocus()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
items(pager.size) { index ->
|
items(pager.size) { index ->
|
||||||
val mod =
|
val mod =
|
||||||
if (index == savedFocusedIndex) {
|
if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) {
|
||||||
if (DEBUG) Timber.d("Adding firstFocus to itemClickedIndex $index")
|
|
||||||
Modifier.focusRequester(firstFocus)
|
|
||||||
} else if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) {
|
|
||||||
if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index")
|
if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index")
|
||||||
Modifier.focusRequester(firstFocus)
|
Modifier
|
||||||
|
.focusRequester(firstFocus)
|
||||||
|
.focusRequester(gridFocusRequester)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
val item = pager[index]
|
val item = pager[index]
|
||||||
if (!hasRequestFocusRun && requestFocus && initialPosition >= 0) {
|
GridCard(
|
||||||
// On very first composition, if parent wants to focus on the grid, do so
|
item = item,
|
||||||
LaunchedEffect(Unit) {
|
onClick = {
|
||||||
if (DEBUG) {
|
if (item != null) {
|
||||||
Timber.d(
|
focusedIndex = index
|
||||||
"non-null focus on startPosition=$startPosition, from initialPosition=$initialPosition",
|
itemOnClick.invoke(item)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
// focus on startPosition
|
},
|
||||||
gridState.scrollToItem(startPosition, 0)
|
onLongClick = { if (item != null) longClicker.invoke(item) },
|
||||||
firstFocus.tryRequestFocus()
|
|
||||||
hasRequestFocusRun = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ItemCard(
|
|
||||||
modifier =
|
modifier =
|
||||||
mod
|
mod
|
||||||
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
|
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
|
||||||
|
|
@ -314,20 +258,11 @@ fun CardGrid(
|
||||||
focusOn(index)
|
focusOn(index)
|
||||||
positionCallback?.invoke(columns, index)
|
positionCallback?.invoke(columns, index)
|
||||||
} else if (focusedIndex == index) {
|
} else if (focusedIndex == index) {
|
||||||
savedFocusedIndex = index
|
// savedFocusedIndex = index
|
||||||
// Was focused on this, so mark unfocused
|
// // Was focused on this, so mark unfocused
|
||||||
focusedIndex = -1
|
// focusedIndex = -1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
item = item,
|
|
||||||
onClick = {
|
|
||||||
if (item != null) {
|
|
||||||
itemOnClick.invoke(item)
|
|
||||||
savedFocusedIndex = index
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = { if (item != null) longClicker.invoke(item) },
|
|
||||||
cardHeight = null,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -349,12 +284,12 @@ fun CardGrid(
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
.background(AppColors.TransparentBlack50),
|
.background(AppColors.TransparentBlack50),
|
||||||
) {
|
) {
|
||||||
val index =
|
val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?"
|
||||||
if (focusedIndex >= 0) {
|
// if (focusedIndex >= 0) {
|
||||||
focusedIndex + 1
|
// focusedIndex + 1
|
||||||
} else {
|
// } else {
|
||||||
max(savedFocusedIndex, focusedIndexOnExit) + 1
|
// max(savedFocusedIndex, focusedIndexOnExit) + 1
|
||||||
}
|
// }
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(4.dp),
|
modifier = Modifier.padding(4.dp),
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
|
|
||||||
|
|
@ -243,7 +243,6 @@ fun CollectionDetails(
|
||||||
},
|
},
|
||||||
longClicker = {},
|
longClicker = {},
|
||||||
letterPosition = { 0 },
|
letterPosition = { 0 },
|
||||||
requestFocus = true,
|
|
||||||
gridFocusRequester = gridFocusRequester,
|
gridFocusRequester = gridFocusRequester,
|
||||||
showJumpButtons = false, // TODO add preference
|
showJumpButtons = false, // TODO add preference
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,7 @@ fun SeriesOverviewContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BannerCard(
|
BannerCard(
|
||||||
|
name = episode?.name,
|
||||||
imageUrl = episode?.imageUrl,
|
imageUrl = episode?.imageUrl,
|
||||||
aspectRatio =
|
aspectRatio =
|
||||||
episode?.data?.primaryImageAspectRatio?.toFloat()
|
episode?.data?.primaryImageAspectRatio?.toFloat()
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
|
|
||||||
import com.github.damontecres.dolphin.ui.cards.BannerCard
|
import com.github.damontecres.dolphin.ui.cards.BannerCard
|
||||||
import com.github.damontecres.dolphin.ui.cards.ItemRow
|
import com.github.damontecres.dolphin.ui.cards.ItemRow
|
||||||
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
|
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
|
||||||
|
|
@ -71,7 +70,7 @@ fun HomePage(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: HomeViewModel = hiltViewModel(),
|
viewModel: HomeViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
OneTimeLaunchedEffect {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.init(preferences)
|
viewModel.init(preferences)
|
||||||
}
|
}
|
||||||
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
|
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
|
||||||
|
|
@ -96,14 +95,8 @@ fun HomePageContent(
|
||||||
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
||||||
mutableStateOf(RowColumn(0, 0))
|
mutableStateOf(RowColumn(0, 0))
|
||||||
}
|
}
|
||||||
|
var focusedItem = position.let { homeRows.getOrNull(it.row)?.items?.getOrNull(it.column) }
|
||||||
|
|
||||||
var focusedItem by remember {
|
|
||||||
mutableStateOf<BaseItem?>(
|
|
||||||
homeRows.getOrNull(0)?.items?.getOrNull(
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val positionFocusRequester = remember { FocusRequester() }
|
val positionFocusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
|
|
@ -181,6 +174,7 @@ fun HomePageContent(
|
||||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||||
// TODO better aspect ration handling?
|
// TODO better aspect ration handling?
|
||||||
BannerCard(
|
BannerCard(
|
||||||
|
name = item?.data?.seriesName ?: item?.name,
|
||||||
imageUrl = item?.imageUrl,
|
imageUrl = item?.imageUrl,
|
||||||
aspectRatio = (2f / 3f),
|
aspectRatio = (2f / 3f),
|
||||||
cornerText = item?.data?.indexNumber?.let { "E$it" },
|
cornerText = item?.data?.indexNumber?.let { "E$it" },
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,12 @@ import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||||
|
import org.jellyfin.sdk.model.api.UserDto
|
||||||
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
|
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
|
@ -44,6 +46,7 @@ class HomeViewModel
|
||||||
"Error loading home page",
|
"Error loading home page",
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
Timber.d("init HomeViewModel")
|
||||||
val user by api.userApi.getCurrentUser()
|
val user by api.userApi.getCurrentUser()
|
||||||
// val displayPrefs =
|
// val displayPrefs =
|
||||||
// api.displayPreferencesApi
|
// api.displayPreferencesApi
|
||||||
|
|
@ -72,68 +75,17 @@ class HomeViewModel
|
||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
|
||||||
val latestMediaIncludes =
|
|
||||||
user.configuration?.orderedViews.orEmpty().toMutableList().apply {
|
|
||||||
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
|
|
||||||
}
|
|
||||||
|
|
||||||
val views by api.userViewsApi.getUserViews()
|
|
||||||
|
|
||||||
val homeRows =
|
val homeRows =
|
||||||
homeSections
|
homeSections
|
||||||
.mapNotNull { section ->
|
.mapNotNull { section ->
|
||||||
Timber.Forest.v("Loading section: %s", section.name)
|
Timber.Forest.v("Loading section: %s", section.name)
|
||||||
when (section) {
|
when (section) {
|
||||||
HomeSection.LATEST_MEDIA -> {
|
HomeSection.LATEST_MEDIA -> {
|
||||||
latestMediaIncludes.mapNotNull { viewId ->
|
getLatest(user, limit)
|
||||||
val view =
|
|
||||||
views.items
|
|
||||||
.firstOrNull { it.id == viewId }
|
|
||||||
if (view?.collectionType in supportedCollectionTypes) {
|
|
||||||
val title =
|
|
||||||
view
|
|
||||||
?.name
|
|
||||||
?.let {
|
|
||||||
"Recently Added in $it"
|
|
||||||
}
|
|
||||||
val request =
|
|
||||||
GetLatestMediaRequest(
|
|
||||||
fields = DefaultItemFields,
|
|
||||||
imageTypeLimit = 1,
|
|
||||||
parentId = viewId,
|
|
||||||
groupItems = true,
|
|
||||||
limit = limit,
|
|
||||||
)
|
|
||||||
val latest =
|
|
||||||
api.userLibraryApi
|
|
||||||
.getLatestMedia(request)
|
|
||||||
.content
|
|
||||||
.map { BaseItem.from(it, api, true) }
|
|
||||||
HomeRow(
|
|
||||||
section = section,
|
|
||||||
items = latest,
|
|
||||||
title = title,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HomeSection.RESUME -> {
|
HomeSection.RESUME -> {
|
||||||
val request =
|
val items = getResume(user.id, limit)
|
||||||
GetResumeItemsRequest(
|
|
||||||
userId = user.id,
|
|
||||||
fields = DefaultItemFields,
|
|
||||||
limit = limit,
|
|
||||||
includeItemTypes = supportItemKinds,
|
|
||||||
)
|
|
||||||
val items =
|
|
||||||
api.itemsApi
|
|
||||||
.getResumeItems(request)
|
|
||||||
.content
|
|
||||||
.items
|
|
||||||
.map { BaseItem.Companion.from(it, api, true) }
|
|
||||||
listOf(
|
listOf(
|
||||||
HomeRow(
|
HomeRow(
|
||||||
section = section,
|
section = section,
|
||||||
|
|
@ -143,22 +95,12 @@ class HomeViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
HomeSection.NEXT_UP -> {
|
HomeSection.NEXT_UP -> {
|
||||||
val request =
|
|
||||||
GetNextUpRequest(
|
|
||||||
fields = DefaultItemFields,
|
|
||||||
imageTypeLimit = 1,
|
|
||||||
parentId = null,
|
|
||||||
limit = limit,
|
|
||||||
enableResumable = false,
|
|
||||||
enableUserData = true,
|
|
||||||
enableRewatching = preferences.appPreferences.homePagePreferences.enableRewatchingNextUp,
|
|
||||||
)
|
|
||||||
val nextUp =
|
val nextUp =
|
||||||
api.tvShowsApi
|
getNextUp(
|
||||||
.getNextUp(request)
|
user.id,
|
||||||
.content
|
limit,
|
||||||
.items
|
preferences.appPreferences.homePagePreferences.enableRewatchingNextUp,
|
||||||
.map { BaseItem.Companion.from(it, api, true) }
|
)
|
||||||
listOf(
|
listOf(
|
||||||
HomeRow(
|
HomeRow(
|
||||||
section = section,
|
section = section,
|
||||||
|
|
@ -186,4 +128,92 @@ class HomeViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun getResume(
|
||||||
|
userId: UUID,
|
||||||
|
limit: Int,
|
||||||
|
): List<BaseItem> {
|
||||||
|
val request =
|
||||||
|
GetResumeItemsRequest(
|
||||||
|
userId = userId,
|
||||||
|
fields = DefaultItemFields,
|
||||||
|
limit = limit,
|
||||||
|
includeItemTypes = supportItemKinds,
|
||||||
|
)
|
||||||
|
val items =
|
||||||
|
api.itemsApi
|
||||||
|
.getResumeItems(request)
|
||||||
|
.content
|
||||||
|
.items
|
||||||
|
.map { BaseItem.Companion.from(it, api, true) }
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getNextUp(
|
||||||
|
userId: UUID,
|
||||||
|
limit: Int,
|
||||||
|
enableRewatching: Boolean,
|
||||||
|
): List<BaseItem> {
|
||||||
|
val request =
|
||||||
|
GetNextUpRequest(
|
||||||
|
userId = userId,
|
||||||
|
fields = DefaultItemFields,
|
||||||
|
imageTypeLimit = 1,
|
||||||
|
parentId = null,
|
||||||
|
limit = limit,
|
||||||
|
enableResumable = false,
|
||||||
|
enableUserData = true,
|
||||||
|
enableRewatching = enableRewatching,
|
||||||
|
)
|
||||||
|
val nextUp =
|
||||||
|
api.tvShowsApi
|
||||||
|
.getNextUp(request)
|
||||||
|
.content
|
||||||
|
.items
|
||||||
|
.map { BaseItem.Companion.from(it, api, true) }
|
||||||
|
return nextUp
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getLatest(
|
||||||
|
user: UserDto,
|
||||||
|
limit: Int,
|
||||||
|
): List<HomeRow> {
|
||||||
|
val latestMediaIncludes =
|
||||||
|
user.configuration?.orderedViews.orEmpty().toMutableList().apply {
|
||||||
|
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
val views by api.userViewsApi.getUserViews()
|
||||||
|
val rows =
|
||||||
|
latestMediaIncludes
|
||||||
|
.mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } }
|
||||||
|
.filter { it.collectionType in supportedCollectionTypes }
|
||||||
|
.map { view ->
|
||||||
|
val title =
|
||||||
|
view
|
||||||
|
?.name
|
||||||
|
?.let {
|
||||||
|
"Recently Added in $it"
|
||||||
|
}
|
||||||
|
val request =
|
||||||
|
GetLatestMediaRequest(
|
||||||
|
fields = DefaultItemFields,
|
||||||
|
imageTypeLimit = 1,
|
||||||
|
parentId = view.id,
|
||||||
|
groupItems = true,
|
||||||
|
limit = limit,
|
||||||
|
)
|
||||||
|
val latest =
|
||||||
|
api.userLibraryApi
|
||||||
|
.getLatestMedia(request)
|
||||||
|
.content
|
||||||
|
.map { BaseItem.from(it, api, true) }
|
||||||
|
HomeRow(
|
||||||
|
section = HomeSection.LATEST_MEDIA,
|
||||||
|
items = latest,
|
||||||
|
title = title,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return rows
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
app/src/main/res/drawable/video_solid.xml
Normal file
5
app/src/main/res/drawable/video_solid.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="455dp" android:viewportHeight="512" android:viewportWidth="576" android:width="512dp">
|
||||||
|
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M0,128C0,92.7 28.7,64 64,64l256,0c35.3,0 64,28.7 64,64l0,256c0,35.3 -28.7,64 -64,64L64,448c-35.3,0 -64,-28.7 -64,-64L0,128zM559.1,99.8c10.4,5.6 16.9,16.4 16.9,28.2l0,256c0,11.8 -6.5,22.6 -16.9,28.2s-23,5 -32.9,-1.6l-96,-64L416,337.1l0,-17.1 0,-128 0,-17.1 14.2,-9.5 96,-64c9.8,-6.5 22.4,-7.2 32.9,-1.6z"/>
|
||||||
|
|
||||||
|
</vector>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue