mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Show loading/error for large seasons
This commit is contained in:
parent
ba45c3f509
commit
d4b3c554f8
8 changed files with 173 additions and 112 deletions
|
|
@ -72,6 +72,7 @@ android {
|
|||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
|
|
@ -227,4 +228,5 @@ dependencies {
|
|||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@ data class BaseItem(
|
|||
val subtitle =
|
||||
if (type == BaseItemKind.EPISODE) data.seasonEpisode + " - " + name else data.productionYear?.toString()
|
||||
|
||||
@Transient
|
||||
val indexNumber = data.indexNumber
|
||||
|
||||
@Transient
|
||||
val resumeMs =
|
||||
data.userData
|
||||
|
|
@ -41,12 +38,23 @@ data class BaseItem(
|
|||
?.ticks
|
||||
?.inWholeMilliseconds
|
||||
|
||||
@Transient
|
||||
val indexNumber = data.indexNumber ?: dateAsIndex()
|
||||
|
||||
private fun dateAsIndex(): Int? =
|
||||
data.premiereDate
|
||||
?.let {
|
||||
it.year.toString() +
|
||||
it.monthValue.toString().padStart(2, '0') +
|
||||
it.dayOfMonth.toString().padStart(2, '0')
|
||||
}?.toIntOrNull()
|
||||
|
||||
fun destination(): Destination {
|
||||
val result =
|
||||
// Redirect episodes & seasons to their series if possible
|
||||
when (type) {
|
||||
BaseItemKind.EPISODE -> {
|
||||
data.indexNumber?.let { episode ->
|
||||
indexNumber?.let { episode ->
|
||||
data.parentIndexNumber?.let { season ->
|
||||
Destination.SeriesOverview(
|
||||
data.seriesId!!,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class SeriesViewModel
|
|||
private lateinit var prefs: UserPreferences
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Loading)
|
||||
val seasons = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
|
||||
val episodes = MutableLiveData<ItemListAndMapping>(ItemListAndMapping.empty())
|
||||
val episodes = MutableLiveData<EpisodeList>(EpisodeList.Loading)
|
||||
val people = MutableLiveData<List<Person>>(listOf())
|
||||
|
||||
fun init(
|
||||
|
|
@ -82,33 +82,25 @@ class SeriesViewModel
|
|||
) + Dispatchers.IO,
|
||||
) {
|
||||
val item = fetchItem(seriesId, potential)
|
||||
if (item != null) {
|
||||
val seasonsInfo = getSeasons(item)
|
||||
val seasonsInfo = getSeasons(item)
|
||||
|
||||
// If a particular season was requested, fetch those episodes, otherwise get the first season
|
||||
val episodeInfo =
|
||||
(season ?: seasonsInfo.items.firstOrNull()?.indexNumber)
|
||||
?.let { seasonNum ->
|
||||
loadEpisodesInternal(seasonNum)
|
||||
} ?: ItemListAndMapping.empty()
|
||||
withContext(Dispatchers.Main) {
|
||||
seasons.value = seasonsInfo
|
||||
episodes.value = episodeInfo
|
||||
loading.value = LoadingState.Success
|
||||
people.value =
|
||||
item.data.people
|
||||
?.letNotEmpty { people ->
|
||||
people.map { Person.fromDto(it, api) }
|
||||
}.orEmpty()
|
||||
}
|
||||
maybePlayThemeSong(prefs.appPreferences.interfacePreferences.playThemeSongs)
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
seasons.value = ItemListAndMapping.empty()
|
||||
episodes.value = ItemListAndMapping.empty()
|
||||
loading.value = LoadingState.Error("Series $seriesId not found")
|
||||
}
|
||||
// If a particular season was requested, fetch those episodes, otherwise get the first season
|
||||
val episodeInfo =
|
||||
(season ?: seasonsInfo.items.firstOrNull()?.indexNumber)
|
||||
?.let { seasonNum ->
|
||||
loadEpisodesInternal(seasonNum)
|
||||
} ?: EpisodeList.Error("Could not determine season")
|
||||
withContext(Dispatchers.Main) {
|
||||
seasons.value = seasonsInfo
|
||||
episodes.value = episodeInfo
|
||||
loading.value = LoadingState.Success
|
||||
people.value =
|
||||
item.data.people
|
||||
?.letNotEmpty { people ->
|
||||
people.map { Person.fromDto(it, api) }
|
||||
}.orEmpty()
|
||||
}
|
||||
maybePlayThemeSong(prefs.appPreferences.interfacePreferences.playThemeSongs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +195,7 @@ class SeriesViewModel
|
|||
return ItemListAndMapping(pager, seasonNumToIndex, indexToSeasonNum)
|
||||
}
|
||||
|
||||
private suspend fun loadEpisodesInternal(season: Int): ItemListAndMapping {
|
||||
private suspend fun loadEpisodesInternal(season: Int): EpisodeList {
|
||||
val request =
|
||||
GetEpisodesRequest(
|
||||
seriesId = item.value!!.id,
|
||||
|
|
@ -222,23 +214,24 @@ class SeriesViewModel
|
|||
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
|
||||
pager.init()
|
||||
Timber.Forest.v("Loaded ${pager.size} episodes for season $season")
|
||||
return convertPager(pager)
|
||||
return EpisodeList.Success(convertPager(pager))
|
||||
}
|
||||
|
||||
fun loadEpisodes(season: Int) =
|
||||
fun loadEpisodes(season: Int) {
|
||||
this@SeriesViewModel.episodes.value = EpisodeList.Loading
|
||||
viewModelScope.async(ExceptionHandler(true)) {
|
||||
val episodes =
|
||||
try {
|
||||
loadEpisodesInternal(season)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error loading episodes for $seriesId for season $season")
|
||||
// TODO show error in UI?
|
||||
ItemListAndMapping.empty()
|
||||
EpisodeList.Error(e)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
this@SeriesViewModel.episodes.value = episodes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setWatched(
|
||||
itemId: UUID,
|
||||
|
|
@ -272,14 +265,15 @@ class SeriesViewModel
|
|||
val base = api.userLibraryApi.getItem(itemId).content
|
||||
val item = BaseItem.Companion.from(base, api)
|
||||
val eps = episodes.value!!
|
||||
withContext(Dispatchers.Main) {
|
||||
episodes.value =
|
||||
eps.copy(
|
||||
items =
|
||||
eps.items.toMutableList().apply {
|
||||
this[listIndex] = item
|
||||
},
|
||||
)
|
||||
if (eps is EpisodeList.Success) {
|
||||
val newItems =
|
||||
eps.episodes.items.toMutableList().apply {
|
||||
this[listIndex] = item
|
||||
}
|
||||
val newValue = EpisodeList.Success(eps.episodes.copy(items = newItems))
|
||||
withContext(Dispatchers.Main) {
|
||||
episodes.value = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -325,17 +319,32 @@ data class ItemListAndMapping(
|
|||
}
|
||||
|
||||
/**
|
||||
* Calculate the index<->season number pairings
|
||||
* Calculate the index<->season/ep number pairings
|
||||
*
|
||||
* This allows for handling of missing seasons
|
||||
*/
|
||||
private suspend fun convertPager(pager: ApiRequestPager<*>): ItemListAndMapping {
|
||||
val pairs =
|
||||
pager.mapIndexed { index, _ ->
|
||||
val season = pager.getBlocking(index)
|
||||
Pair(season?.indexNumber!!, index)
|
||||
val item = pager.getBlocking(index)
|
||||
Pair(item?.indexNumber ?: index, index)
|
||||
}
|
||||
val seasonNumToIndex = mapOf(*pairs.toTypedArray())
|
||||
val indexToSeasonNum = mapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray())
|
||||
return ItemListAndMapping(pager, seasonNumToIndex, indexToSeasonNum)
|
||||
}
|
||||
|
||||
sealed interface EpisodeList {
|
||||
data object Loading : EpisodeList
|
||||
|
||||
data class Error(
|
||||
val message: String? = null,
|
||||
val exception: Throwable? = null,
|
||||
) : EpisodeList {
|
||||
constructor(exception: Throwable) : this(null, exception)
|
||||
}
|
||||
|
||||
data class Success(
|
||||
val episodes: ItemListAndMapping,
|
||||
) : EpisodeList
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
|||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.dolphin.ui.data.ItemDetailsDialog
|
||||
import com.github.damontecres.dolphin.ui.data.ItemDetailsDialogInfo
|
||||
import com.github.damontecres.dolphin.ui.detail.EpisodeList
|
||||
import com.github.damontecres.dolphin.ui.detail.ItemListAndMapping
|
||||
import com.github.damontecres.dolphin.ui.detail.SeriesViewModel
|
||||
import com.github.damontecres.dolphin.ui.nav.Destination
|
||||
|
|
@ -75,7 +76,8 @@ fun SeriesOverview(
|
|||
|
||||
val series by viewModel.item.observeAsState(null)
|
||||
val seasons by viewModel.seasons.observeAsState(ItemListAndMapping.empty())
|
||||
val episodes by viewModel.episodes.observeAsState(ItemListAndMapping.empty())
|
||||
val episodes by viewModel.episodes.observeAsState(EpisodeList.Loading)
|
||||
val episodeList = (episodes as? EpisodeList.Success)?.episodes?.items
|
||||
|
||||
var position by rememberSaveable(
|
||||
destination,
|
||||
|
|
@ -89,7 +91,10 @@ fun SeriesOverview(
|
|||
mutableStateOf(
|
||||
SeriesOverviewPosition(
|
||||
seasons.numberToIndex[initialSeasonEpisode?.season ?: 0] ?: 0,
|
||||
episodes.numberToIndex[initialSeasonEpisode?.episode ?: 0] ?: 0,
|
||||
(episodes as? EpisodeList.Success)?.episodes?.numberToIndex[
|
||||
initialSeasonEpisode?.episode
|
||||
?: 0,
|
||||
] ?: 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -97,12 +102,16 @@ fun SeriesOverview(
|
|||
var overviewDialog by remember { mutableStateOf<ItemDetailsDialogInfo?>(null) }
|
||||
var moreDialog by remember { mutableStateOf<DialogParams?>(null) }
|
||||
|
||||
LaunchedEffect(episodes.items) {
|
||||
if (episodes.items.isNotEmpty()) {
|
||||
// TODO focus on first episode when changing seasons?
|
||||
LaunchedEffect(episodes) {
|
||||
episodes?.let { episodes ->
|
||||
if (episodes is EpisodeList.Success) {
|
||||
if (episodes.episodes.items.isNotEmpty()) {
|
||||
// TODO focus on first episode when changing seasons?
|
||||
// firstItemFocusRequester.requestFocus()
|
||||
episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
||||
viewModel.refreshEpisode(it.id, position.episodeRowIndex)
|
||||
episodes.episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
||||
viewModel.refreshEpisode(it.id, position.episodeRowIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -120,7 +129,7 @@ fun SeriesOverview(
|
|||
SeriesOverviewContent(
|
||||
series = series,
|
||||
seasons = seasons.items,
|
||||
episodes = episodes.items,
|
||||
episodes = episodes,
|
||||
position = position,
|
||||
backdropImageUrl =
|
||||
remember {
|
||||
|
|
@ -140,7 +149,6 @@ fun SeriesOverview(
|
|||
position = it
|
||||
},
|
||||
onClick = {
|
||||
viewModel.release()
|
||||
val resumePosition =
|
||||
it.data.userData
|
||||
?.playbackPositionTicks
|
||||
|
|
@ -157,7 +165,7 @@ fun SeriesOverview(
|
|||
// TODO
|
||||
},
|
||||
playOnClick = { resume ->
|
||||
episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
||||
episodeList?.getOrNull(position.episodeRowIndex)?.let {
|
||||
viewModel.release()
|
||||
viewModel.navigateTo(
|
||||
Destination.Playback(
|
||||
|
|
@ -169,13 +177,13 @@ fun SeriesOverview(
|
|||
}
|
||||
},
|
||||
watchOnClick = {
|
||||
episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
||||
episodeList?.getOrNull(position.episodeRowIndex)?.let {
|
||||
val played = it.data.userData?.played ?: false
|
||||
viewModel.setWatched(it.id, !played, position.episodeRowIndex)
|
||||
}
|
||||
},
|
||||
moreOnClick = {
|
||||
episodes.items.getOrNull(position.episodeRowIndex)?.let { ep ->
|
||||
episodeList?.getOrNull(position.episodeRowIndex)?.let { ep ->
|
||||
moreDialog =
|
||||
DialogParams(
|
||||
fromLongClick = false,
|
||||
|
|
@ -227,7 +235,7 @@ fun SeriesOverview(
|
|||
}
|
||||
},
|
||||
overviewOnClick = {
|
||||
episodes.items.getOrNull(position.episodeRowIndex)?.let {
|
||||
episodeList?.getOrNull(position.episodeRowIndex)?.let {
|
||||
overviewDialog =
|
||||
ItemDetailsDialogInfo(
|
||||
title = it.name ?: "Unknown",
|
||||
|
|
|
|||
|
|
@ -46,16 +46,20 @@ import com.github.damontecres.dolphin.data.model.BaseItem
|
|||
import com.github.damontecres.dolphin.data.model.aspectRatioFloat
|
||||
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.dolphin.ui.cards.BannerCard
|
||||
import com.github.damontecres.dolphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.dolphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.dolphin.ui.detail.EpisodeList
|
||||
import com.github.damontecres.dolphin.ui.ifElse
|
||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.dolphin.util.formatDateTime
|
||||
import kotlin.time.Duration
|
||||
|
||||
@Composable
|
||||
fun SeriesOverviewContent(
|
||||
series: BaseItem,
|
||||
seasons: List<BaseItem?>,
|
||||
episodes: List<BaseItem?>,
|
||||
episodes: EpisodeList,
|
||||
position: SeriesOverviewPosition,
|
||||
backdropImageUrl: String?,
|
||||
firstItemFocusRequester: FocusRequester,
|
||||
|
|
@ -76,7 +80,8 @@ fun SeriesOverviewContent(
|
|||
var resolvedTabIndex by remember { mutableIntStateOf(selectedTabIndex) }
|
||||
val tabRowFocusRequester = remember { FocusRequester() }
|
||||
|
||||
val focusedEpisode = episodes.getOrNull(position.episodeRowIndex)
|
||||
val focusedEpisode =
|
||||
(episodes as? EpisodeList.Success)?.episodes?.items?.getOrNull(position.episodeRowIndex)
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
|
|
@ -200,56 +205,74 @@ fun SeriesOverviewContent(
|
|||
}
|
||||
item {
|
||||
key(position.seasonTabIndex) {
|
||||
val state = rememberLazyListState()
|
||||
OneTimeLaunchedEffect {
|
||||
if (state.firstVisibleItemIndex != position.episodeRowIndex) {
|
||||
state.scrollToItem(position.episodeRowIndex)
|
||||
firstItemFocusRequester.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
LazyRow(
|
||||
state = state,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.focusRestorer(firstItemFocusRequester)
|
||||
.focusRequester(episodeRowFocusRequester),
|
||||
) {
|
||||
itemsIndexed(episodes) { episodeIndex, episode ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
if (interactionSource.collectIsFocusedAsState().value) {
|
||||
onFocus.invoke(
|
||||
SeriesOverviewPosition(
|
||||
selectedTabIndex,
|
||||
episodeIndex,
|
||||
),
|
||||
)
|
||||
when (val eps = episodes) {
|
||||
EpisodeList.Loading -> LoadingPage()
|
||||
is EpisodeList.Error -> ErrorMessage(eps.message, eps.exception)
|
||||
is EpisodeList.Success -> {
|
||||
val state = rememberLazyListState()
|
||||
OneTimeLaunchedEffect {
|
||||
if (state.firstVisibleItemIndex != position.episodeRowIndex) {
|
||||
state.scrollToItem(position.episodeRowIndex)
|
||||
}
|
||||
firstItemFocusRequester.tryRequestFocus()
|
||||
}
|
||||
|
||||
BannerCard(
|
||||
name = episode?.name,
|
||||
imageUrl = episode?.imageUrl,
|
||||
aspectRatio =
|
||||
episode
|
||||
?.data
|
||||
?.primaryImageAspectRatio
|
||||
?.toFloat()
|
||||
?.coerceAtLeast(episode.data.aspectRatioFloat ?: (4f / 3f))
|
||||
?: (16f / 9), // TODO some episode images don't match the file's aspect ratio
|
||||
cornerText = "E${episode?.data?.indexNumber}",
|
||||
played = episode?.data?.userData?.played ?: false,
|
||||
playPercent = episode?.data?.userData?.playedPercentage ?: 0.0,
|
||||
onClick = { if (episode != null) onClick.invoke(episode) },
|
||||
onLongClick = { if (episode != null) onLongClick.invoke(episode) },
|
||||
LazyRow(
|
||||
state = state,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
episodeIndex == position.episodeRowIndex,
|
||||
Modifier.focusRequester(firstItemFocusRequester),
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
cardHeight = 120.dp,
|
||||
)
|
||||
Modifier
|
||||
.focusRestorer(firstItemFocusRequester)
|
||||
.focusRequester(episodeRowFocusRequester),
|
||||
) {
|
||||
itemsIndexed(eps.episodes.items) { episodeIndex, episode ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
if (interactionSource.collectIsFocusedAsState().value) {
|
||||
onFocus.invoke(
|
||||
SeriesOverviewPosition(
|
||||
selectedTabIndex,
|
||||
episodeIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
val cornerText =
|
||||
episode?.data?.indexNumber?.let { "E$it" }
|
||||
?: episode?.data?.premiereDate?.let(::formatDateTime)
|
||||
BannerCard(
|
||||
name = episode?.name,
|
||||
imageUrl = episode?.imageUrl,
|
||||
aspectRatio =
|
||||
episode
|
||||
?.data
|
||||
?.primaryImageAspectRatio
|
||||
?.toFloat()
|
||||
?.coerceAtLeast(
|
||||
episode.data.aspectRatioFloat ?: (4f / 3f),
|
||||
)
|
||||
?: (16f / 9), // TODO some episode images don't match the file's aspect ratio
|
||||
cornerText = cornerText,
|
||||
played = episode?.data?.userData?.played ?: false,
|
||||
playPercent =
|
||||
episode?.data?.userData?.playedPercentage
|
||||
?: 0.0,
|
||||
onClick = { if (episode != null) onClick.invoke(episode) },
|
||||
onLongClick = {
|
||||
if (episode != null) {
|
||||
onLongClick.invoke(
|
||||
episode,
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
episodeIndex == position.episodeRowIndex,
|
||||
Modifier.focusRequester(firstItemFocusRequester),
|
||||
),
|
||||
interactionSource = interactionSource,
|
||||
cardHeight = 120.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import com.github.damontecres.dolphin.ui.timeRemaining
|
|||
import com.github.damontecres.dolphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.dolphin.util.LoadingState
|
||||
import com.github.damontecres.dolphin.util.formatDateTime
|
||||
import com.github.damontecres.dolphin.util.seasonEpisode
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
|
||||
|
|
@ -231,7 +232,13 @@ fun MainPageHeader(
|
|||
val details =
|
||||
buildList {
|
||||
if (isEpisode) {
|
||||
add("S${dto.parentIndexNumber} E${dto.indexNumber}")
|
||||
val se = dto.seasonEpisode
|
||||
if (se != null) {
|
||||
add(se)
|
||||
} else if (dto.parentIndexNumber != null) {
|
||||
// Maybe a daily episode, so just show season, the date is added below
|
||||
add("S${dto.parentIndexNumber}")
|
||||
}
|
||||
}
|
||||
if (isEpisode) {
|
||||
dto.premiereDate?.let { add(formatDateTime(it)) }
|
||||
|
|
@ -271,13 +278,13 @@ fun MainPageHeader(
|
|||
val overviewModifier =
|
||||
Modifier
|
||||
.padding(0.dp)
|
||||
.height(48.dp)
|
||||
.height(48.dp + if (!isEpisode) 12.dp else 0.dp)
|
||||
if (overview.isNotNullOrBlank()) {
|
||||
Text(
|
||||
text = overview,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 2,
|
||||
maxLines = if (isEpisode) 2 else 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = overviewModifier,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import java.time.format.DateTimeFormatter
|
|||
*/
|
||||
fun formatDateTime(dateTime: LocalDateTime): String =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// TODO server returns in UTC, but sdk converts to local time
|
||||
// eg 2020-02-14T00:00:00.0000000Z => 2020-02-13T17:00:00 PT => Feb 13, 2020
|
||||
val formatter = DateTimeFormatter.ofPattern("MMM d, yyyy")
|
||||
formatter.format(dateTime)
|
||||
} else if (dateTime.toString().length >= 10) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
[versions]
|
||||
aboutLibraries = "12.2.4"
|
||||
agp = "8.13.0"
|
||||
desugar_jdk_libs = "2.1.5"
|
||||
hiltNavigationCompose = "1.3.0"
|
||||
kotlin = "2.2.20"
|
||||
ksp = "2.2.20-2.0.2" # https://github.com/google/ksp/issues/2596 2.0.3 is broken
|
||||
|
|
@ -51,6 +52,7 @@ androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.re
|
|||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" }
|
||||
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
|
||||
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf-javalite" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue