mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 16:11:20 +02:00
Merge branch 'main' into develop/customize-home
This commit is contained in:
commit
d5e3baf040
25 changed files with 528 additions and 148 deletions
|
|
@ -7,7 +7,6 @@ import androidx.work.BackoffPolicy
|
||||||
import androidx.work.Constraints
|
import androidx.work.Constraints
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
import androidx.work.NetworkType
|
import androidx.work.NetworkType
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
|
|
@ -23,6 +22,7 @@ import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.toJavaDuration
|
import kotlin.time.toJavaDuration
|
||||||
|
|
||||||
@ActivityScoped
|
@ActivityScoped
|
||||||
|
|
@ -72,21 +72,7 @@ class SuggestionsSchedulerService
|
||||||
SuggestionsWorker.PARAM_SERVER_ID to serverId.toString(),
|
SuggestionsWorker.PARAM_SERVER_ID to serverId.toString(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (cache.isEmpty()) {
|
val periodicWorkRequestBuilder =
|
||||||
Timber.i("Suggestions cache empty, scheduling immediate fetch")
|
|
||||||
workManager.enqueue(
|
|
||||||
OneTimeWorkRequestBuilder<SuggestionsWorker>()
|
|
||||||
.setConstraints(constraints)
|
|
||||||
.setInputData(inputData)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.i("Scheduling periodic SuggestionsWorker")
|
|
||||||
workManager.enqueueUniquePeriodicWork(
|
|
||||||
uniqueWorkName = SuggestionsWorker.WORK_NAME,
|
|
||||||
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.UPDATE,
|
|
||||||
request =
|
|
||||||
PeriodicWorkRequestBuilder<SuggestionsWorker>(
|
PeriodicWorkRequestBuilder<SuggestionsWorker>(
|
||||||
repeatInterval = 12.hours.toJavaDuration(),
|
repeatInterval = 12.hours.toJavaDuration(),
|
||||||
).setConstraints(constraints)
|
).setConstraints(constraints)
|
||||||
|
|
@ -94,7 +80,18 @@ class SuggestionsSchedulerService
|
||||||
BackoffPolicy.EXPONENTIAL,
|
BackoffPolicy.EXPONENTIAL,
|
||||||
15.minutes.toJavaDuration(),
|
15.minutes.toJavaDuration(),
|
||||||
).setInputData(inputData)
|
).setInputData(inputData)
|
||||||
.build(),
|
|
||||||
|
if (cache.isEmpty()) {
|
||||||
|
Timber.i("Suggestions cache empty, scheduling periodic fetch with 30s delay")
|
||||||
|
periodicWorkRequestBuilder.setInitialDelay(30.seconds.toJavaDuration())
|
||||||
|
} else {
|
||||||
|
Timber.i("Scheduling periodic SuggestionsWorker")
|
||||||
|
}
|
||||||
|
|
||||||
|
workManager.enqueueUniquePeriodicWork(
|
||||||
|
uniqueWorkName = SuggestionsWorker.WORK_NAME,
|
||||||
|
existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.UPDATE,
|
||||||
|
request = periodicWorkRequestBuilder.build(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,10 @@ class SuggestionsWorker
|
||||||
val isSeries = itemKind == BaseItemKind.SERIES
|
val isSeries = itemKind == BaseItemKind.SERIES
|
||||||
val historyItemType = if (isSeries) BaseItemKind.EPISODE else itemKind
|
val historyItemType = if (isSeries) BaseItemKind.EPISODE else itemKind
|
||||||
|
|
||||||
|
val contextualLimit = (itemsPerRow * 0.4).toInt().coerceAtLeast(1)
|
||||||
|
val randomLimit = (itemsPerRow * 0.3).toInt().coerceAtLeast(1)
|
||||||
|
val freshLimit = (itemsPerRow * 0.3).toInt().coerceAtLeast(1)
|
||||||
|
|
||||||
val historyDeferred =
|
val historyDeferred =
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
fetchItems(
|
fetchItems(
|
||||||
|
|
@ -141,11 +145,38 @@ class SuggestionsWorker
|
||||||
itemKind = historyItemType,
|
itemKind = historyItemType,
|
||||||
sortBy = ItemSortBy.DATE_PLAYED,
|
sortBy = ItemSortBy.DATE_PLAYED,
|
||||||
isPlayed = true,
|
isPlayed = true,
|
||||||
limit = 10,
|
limit = 20,
|
||||||
extraFields = listOf(ItemFields.GENRES),
|
extraFields = listOf(ItemFields.GENRES),
|
||||||
).distinctBy { it.relevantId }.take(3)
|
).distinctBy { it.relevantId }.take(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val seedItems = historyDeferred.await()
|
||||||
|
|
||||||
|
val allGenreIds =
|
||||||
|
seedItems
|
||||||
|
.flatMap { it.genreItems?.mapNotNull { g -> g.id } ?: emptyList() }
|
||||||
|
.distinct()
|
||||||
|
|
||||||
|
val excludeIds = seedItems.mapTo(HashSet()) { it.relevantId }
|
||||||
|
|
||||||
|
val contextualDeferred =
|
||||||
|
async(Dispatchers.IO) {
|
||||||
|
if (allGenreIds.isEmpty()) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
fetchItems(
|
||||||
|
parentId = parentId,
|
||||||
|
userId = userId,
|
||||||
|
itemKind = itemKind,
|
||||||
|
sortBy = ItemSortBy.RANDOM,
|
||||||
|
isPlayed = false,
|
||||||
|
limit = contextualLimit,
|
||||||
|
genreIds = allGenreIds,
|
||||||
|
excludeItemIds = excludeIds.toList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val randomDeferred =
|
val randomDeferred =
|
||||||
async(Dispatchers.IO) {
|
async(Dispatchers.IO) {
|
||||||
fetchItems(
|
fetchItems(
|
||||||
|
|
@ -154,7 +185,7 @@ class SuggestionsWorker
|
||||||
itemKind = itemKind,
|
itemKind = itemKind,
|
||||||
sortBy = ItemSortBy.RANDOM,
|
sortBy = ItemSortBy.RANDOM,
|
||||||
isPlayed = false,
|
isPlayed = false,
|
||||||
limit = itemsPerRow,
|
limit = randomLimit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,17 +198,15 @@ class SuggestionsWorker
|
||||||
sortBy = ItemSortBy.DATE_CREATED,
|
sortBy = ItemSortBy.DATE_CREATED,
|
||||||
sortOrder = SortOrder.DESCENDING,
|
sortOrder = SortOrder.DESCENDING,
|
||||||
isPlayed = false,
|
isPlayed = false,
|
||||||
limit = (itemsPerRow * FRESH_CONTENT_RATIO).toInt().coerceAtLeast(1),
|
limit = freshLimit,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val seedItems = historyDeferred.await()
|
val contextual = contextualDeferred.await()
|
||||||
val random = randomDeferred.await()
|
val random = randomDeferred.await()
|
||||||
val fresh = freshDeferred.await()
|
val fresh = freshDeferred.await()
|
||||||
|
|
||||||
val excludeIds = seedItems.mapTo(HashSet()) { it.relevantId }
|
(contextual + fresh + random)
|
||||||
|
|
||||||
(fresh + random)
|
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.distinctBy { it.id }
|
.distinctBy { it.id }
|
||||||
.filterNot { excludeIds.contains(it.relevantId) }
|
.filterNot { excludeIds.contains(it.relevantId) }
|
||||||
|
|
@ -195,6 +224,7 @@ class SuggestionsWorker
|
||||||
limit: Int,
|
limit: Int,
|
||||||
sortOrder: SortOrder? = null,
|
sortOrder: SortOrder? = null,
|
||||||
genreIds: List<UUID>? = null,
|
genreIds: List<UUID>? = null,
|
||||||
|
excludeItemIds: List<UUID>? = null,
|
||||||
extraFields: List<ItemFields> = emptyList(),
|
extraFields: List<ItemFields> = emptyList(),
|
||||||
): List<BaseItemDto> {
|
): List<BaseItemDto> {
|
||||||
val request =
|
val request =
|
||||||
|
|
@ -206,6 +236,7 @@ class SuggestionsWorker
|
||||||
genreIds = genreIds,
|
genreIds = genreIds,
|
||||||
recursive = true,
|
recursive = true,
|
||||||
isPlayed = isPlayed,
|
isPlayed = isPlayed,
|
||||||
|
excludeItemIds = excludeItemIds,
|
||||||
sortBy = listOf(sortBy),
|
sortBy = listOf(sortBy),
|
||||||
sortOrder = sortOrder?.let { listOf(it) },
|
sortOrder = sortOrder?.let { listOf(it) },
|
||||||
limit = limit,
|
limit = limit,
|
||||||
|
|
@ -222,7 +253,6 @@ class SuggestionsWorker
|
||||||
const val WORK_NAME = "com.github.damontecres.wholphin.services.SuggestionsWorker"
|
const val WORK_NAME = "com.github.damontecres.wholphin.services.SuggestionsWorker"
|
||||||
const val PARAM_USER_ID = "userId"
|
const val PARAM_USER_ID = "userId"
|
||||||
const val PARAM_SERVER_ID = "serverId"
|
const val PARAM_SERVER_ID = "serverId"
|
||||||
private const val FRESH_CONTENT_RATIO = 0.4
|
|
||||||
|
|
||||||
fun getTypeForCollection(collectionType: CollectionType?): BaseItemKind? =
|
fun getTypeForCollection(collectionType: CollectionType?): BaseItemKind? =
|
||||||
when (collectionType) {
|
when (collectionType) {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
|
@ -58,7 +57,7 @@ fun <T> ItemRow(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
modifier = Modifier.padding(start = 8.dp),
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
LazyRow(
|
LazyRow(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ fun <T : CardGridItem> CardGrid(
|
||||||
horizontalArrangement = Arrangement.spacedBy(spacing),
|
horizontalArrangement = Arrangement.spacedBy(spacing),
|
||||||
verticalArrangement = Arrangement.spacedBy(spacing),
|
verticalArrangement = Arrangement.spacedBy(spacing),
|
||||||
state = gridState,
|
state = gridState,
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(vertical = 16.dp),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -394,7 +394,7 @@ fun <T : CardGridItem> CardGrid(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.CenterVertically)
|
.align(Alignment.CenterVertically)
|
||||||
.padding(end = 16.dp),
|
.padding(start = 16.dp),
|
||||||
// Add end padding to push away from edge
|
// Add end padding to push away from edge
|
||||||
letterClicked = { letter ->
|
letterClicked = { letter ->
|
||||||
scope.launch(ExceptionHandler()) {
|
scope.launch(ExceptionHandler()) {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail
|
package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -40,9 +38,7 @@ fun CollectionFolderBoxSet(
|
||||||
recursive = recursive,
|
recursive = recursive,
|
||||||
sortOptions = BoxSetSortOptions,
|
sortOptions = BoxSetSortOptions,
|
||||||
initialSortAndDirection = SortAndDirection(ItemSortBy.DEFAULT, SortOrder.ASCENDING),
|
initialSortAndDirection = SortAndDirection(ItemSortBy.DEFAULT, SortOrder.ASCENDING),
|
||||||
modifier =
|
modifier = modifier,
|
||||||
modifier
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
showHeader = position < columns
|
showHeader = position < columns
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail
|
package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions
|
import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions
|
||||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||||
|
|
@ -53,9 +51,7 @@ fun CollectionFolderGeneric(
|
||||||
showTitle = showHeader,
|
showTitle = showHeader,
|
||||||
recursive = recursive,
|
recursive = recursive,
|
||||||
sortOptions = sortOptions,
|
sortOptions = sortOptions,
|
||||||
modifier =
|
modifier = modifier,
|
||||||
modifier
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
showHeader = position < columns
|
showHeader = position < columns
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ fun CollectionFolderLiveTv(
|
||||||
selectedTabIndex = selectedTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.focusRequester(firstTabFocusRequester),
|
.focusRequester(firstTabFocusRequester),
|
||||||
tabs = tabs.map { it.title },
|
tabs = tabs.map { it.title },
|
||||||
onClick = { selectedTabIndex = it },
|
onClick = { selectedTabIndex = it },
|
||||||
|
|
@ -176,9 +176,7 @@ fun CollectionFolderLiveTv(
|
||||||
showTitle = false,
|
showTitle = false,
|
||||||
recursive = false,
|
recursive = false,
|
||||||
sortOptions = VideoSortOptions,
|
sortOptions = VideoSortOptions,
|
||||||
modifier =
|
modifier = Modifier,
|
||||||
Modifier
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
showHeader = position < columns
|
showHeader = position < columns
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ fun CollectionFolderMovie(
|
||||||
selectedTabIndex = selectedTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.focusRequester(firstTabFocusRequester),
|
.focusRequester(firstTabFocusRequester),
|
||||||
tabs = tabs,
|
tabs = tabs,
|
||||||
onClick = { selectedTabIndex = it },
|
onClick = { selectedTabIndex = it },
|
||||||
|
|
@ -101,7 +101,6 @@ fun CollectionFolderMovie(
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
)
|
)
|
||||||
|
|
@ -129,7 +128,6 @@ fun CollectionFolderMovie(
|
||||||
defaultViewOptions = ViewOptionsPoster,
|
defaultViewOptions = ViewOptionsPoster,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
|
|
@ -162,7 +160,6 @@ fun CollectionFolderMovie(
|
||||||
defaultViewOptions = ViewOptionsPoster,
|
defaultViewOptions = ViewOptionsPoster,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
|
|
@ -180,7 +177,6 @@ fun CollectionFolderMovie(
|
||||||
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
includeItemTypes = listOf(BaseItemKind.MOVIE),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail
|
package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions
|
import com.github.damontecres.wholphin.data.filter.DefaultFilterOptions
|
||||||
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
import com.github.damontecres.wholphin.data.filter.ItemFilterBy
|
||||||
|
|
@ -74,9 +72,7 @@ fun CollectionFolderPhotoAlbum(
|
||||||
showTitle = showHeader,
|
showTitle = showHeader,
|
||||||
recursive = recursive,
|
recursive = recursive,
|
||||||
sortOptions = sortOptions,
|
sortOptions = sortOptions,
|
||||||
modifier =
|
modifier = modifier,
|
||||||
modifier
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
showHeader = position < columns
|
showHeader = position < columns
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail
|
package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -35,9 +33,7 @@ fun CollectionFolderPlaylist(
|
||||||
showTitle = showHeader,
|
showTitle = showHeader,
|
||||||
recursive = recursive,
|
recursive = recursive,
|
||||||
sortOptions = PlaylistSortOptions,
|
sortOptions = PlaylistSortOptions,
|
||||||
modifier =
|
modifier = modifier,
|
||||||
modifier
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
showHeader = position < columns
|
showHeader = position < columns
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
package com.github.damontecres.wholphin.ui.detail
|
package com.github.damontecres.wholphin.ui.detail
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
import com.github.damontecres.wholphin.data.model.CollectionFolderFilter
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
|
@ -35,9 +33,7 @@ fun CollectionFolderRecordings(
|
||||||
showTitle = showHeader,
|
showTitle = showHeader,
|
||||||
recursive = recursive,
|
recursive = recursive,
|
||||||
sortOptions = MovieSortOptions,
|
sortOptions = MovieSortOptions,
|
||||||
modifier =
|
modifier = modifier,
|
||||||
modifier
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
showHeader = position < columns
|
showHeader = position < columns
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ fun CollectionFolderTv(
|
||||||
selectedTabIndex = selectedTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 32.dp, top = 16.dp, bottom = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.focusRequester(firstTabFocusRequester),
|
.focusRequester(firstTabFocusRequester),
|
||||||
tabs = tabs,
|
tabs = tabs,
|
||||||
onClick = { selectedTabIndex = it },
|
onClick = { selectedTabIndex = it },
|
||||||
|
|
@ -105,7 +105,6 @@ fun CollectionFolderTv(
|
||||||
},
|
},
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
)
|
)
|
||||||
|
|
@ -129,7 +128,6 @@ fun CollectionFolderTv(
|
||||||
defaultViewOptions = ViewOptionsPoster,
|
defaultViewOptions = ViewOptionsPoster,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
positionCallback = { columns, position ->
|
positionCallback = { columns, position ->
|
||||||
|
|
@ -150,7 +148,6 @@ fun CollectionFolderTv(
|
||||||
includeItemTypes = listOf(BaseItemKind.SERIES),
|
includeItemTypes = listOf(BaseItemKind.SERIES),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp)
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.focusRequester(focusRequester),
|
.focusRequester(focusRequester),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -298,9 +298,7 @@ fun PersonPageContent(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
userScrollEnabled = !focusedOnHeader,
|
userScrollEnabled = !focusedOnHeader,
|
||||||
modifier =
|
modifier = modifier,
|
||||||
modifier
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
PersonHeader(
|
PersonHeader(
|
||||||
|
|
|
||||||
|
|
@ -373,7 +373,6 @@ fun PlaylistDetailsContent(
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
PlaylistDetailsHeader(
|
PlaylistDetailsHeader(
|
||||||
|
|
@ -386,7 +385,7 @@ fun PlaylistDetailsContent(
|
||||||
getPossibleFilterValues = getPossibleFilterValues,
|
getPossibleFilterValues = getPossibleFilterValues,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(start = 16.dp, top = 80.dp)
|
.padding(top = 80.dp)
|
||||||
.fillMaxWidth(.25f),
|
.fillMaxWidth(.25f),
|
||||||
)
|
)
|
||||||
when (loadingState) {
|
when (loadingState) {
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,7 @@ fun EpisodeDetailsContent(
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
|
|
|
||||||
|
|
@ -410,7 +410,7 @@ fun MovieDetailsContent(
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
|
|
|
||||||
|
|
@ -333,7 +333,7 @@ fun SeriesDetailsContent(
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
|
||||||
|
|
@ -127,7 +127,7 @@ fun SeriesOverviewContent(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(16.dp)
|
.padding(vertical = 16.dp)
|
||||||
.focusGroup()
|
.focusGroup()
|
||||||
.nestedScroll(scrollConnection)
|
.nestedScroll(scrollConnection)
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
|
|
@ -142,9 +142,9 @@ fun SeriesOverviewContent(
|
||||||
) {
|
) {
|
||||||
val paddingValues =
|
val paddingValues =
|
||||||
if (preferences.appPreferences.interfacePreferences.showClock) {
|
if (preferences.appPreferences.interfacePreferences.showClock) {
|
||||||
PaddingValues(start = 16.dp, end = 100.dp)
|
PaddingValues(start = 0.dp, end = 184.dp)
|
||||||
} else {
|
} else {
|
||||||
PaddingValues(start = 16.dp, end = 16.dp)
|
PaddingValues(start = 0.dp, end = 16.dp)
|
||||||
}
|
}
|
||||||
TabRow(
|
TabRow(
|
||||||
selectedTabIndex = selectedTabIndex,
|
selectedTabIndex = selectedTabIndex,
|
||||||
|
|
|
||||||
|
|
@ -241,7 +241,7 @@ fun HomePageContent(
|
||||||
item = focusedItem,
|
item = focusedItem,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.padding(top = 48.dp, bottom = 32.dp, start = 32.dp)
|
.padding(top = 48.dp, bottom = 32.dp, start = 8.dp)
|
||||||
.fillMaxHeight(.33f),
|
.fillMaxHeight(.33f),
|
||||||
)
|
)
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
@ -249,9 +249,6 @@ fun HomePageContent(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
contentPadding =
|
contentPadding =
|
||||||
PaddingValues(
|
PaddingValues(
|
||||||
start = 24.dp,
|
|
||||||
end = 16.dp,
|
|
||||||
top = 0.dp,
|
|
||||||
bottom = Cards.height2x3,
|
bottom = Cards.height2x3,
|
||||||
),
|
),
|
||||||
modifier =
|
modifier =
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,15 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.saveable.rememberSerializable
|
import androidx.compose.runtime.saveable.rememberSerializable
|
||||||
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.alpha
|
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
@ -32,7 +34,9 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||||
import androidx.navigation3.runtime.serialization.NavBackStackSerializer
|
import androidx.navigation3.runtime.serialization.NavBackStackSerializer
|
||||||
import androidx.navigation3.runtime.serialization.NavKeySerializer
|
import androidx.navigation3.runtime.serialization.NavKeySerializer
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
|
import androidx.tv.material3.DrawerValue
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.rememberDrawerState
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.transitionFactory
|
import coil3.request.transitionFactory
|
||||||
|
|
@ -91,6 +95,7 @@ fun ApplicationContent(
|
||||||
navigationManager.backStack = backStack
|
navigationManager.backStack = backStack
|
||||||
val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle()
|
val backdrop by viewModel.backdropService.backdropFlow.collectAsStateWithLifecycle()
|
||||||
val backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle
|
val backdropStyle = preferences.appPreferences.interfacePreferences.backdropStyle
|
||||||
|
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -181,9 +186,15 @@ fun ApplicationContent(
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.fillMaxHeight(.7f)
|
.fillMaxHeight(.7f)
|
||||||
.fillMaxWidth(.7f)
|
.fillMaxWidth(.7f)
|
||||||
.alpha(.95f)
|
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||||
.drawWithContent {
|
.drawWithContent {
|
||||||
drawContent()
|
drawContent()
|
||||||
|
if (drawerState.isOpen) {
|
||||||
|
drawRect(
|
||||||
|
brush = SolidColor(Color.Black),
|
||||||
|
alpha = .75f,
|
||||||
|
)
|
||||||
|
}
|
||||||
// Subtle top scrim for system UI readability (clock, tabs)
|
// Subtle top scrim for system UI readability (clock, tabs)
|
||||||
if (enableTopScrim) {
|
if (enableTopScrim) {
|
||||||
drawRect(
|
drawRect(
|
||||||
|
|
@ -245,6 +256,7 @@ fun ApplicationContent(
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
user = user,
|
user = user,
|
||||||
server = server,
|
server = server,
|
||||||
|
drawerState = drawerState,
|
||||||
onClearBackdrop = viewModel::clearBackdrop,
|
onClearBackdrop = viewModel::clearBackdrop,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.core.VisibilityThreshold
|
||||||
|
import androidx.compose.animation.core.animateIntOffsetAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.focusGroup
|
import androidx.compose.foundation.focusGroup
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
|
@ -12,7 +14,9 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
|
@ -24,13 +28,13 @@ import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
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.drawBehind
|
|
||||||
import androidx.compose.ui.focus.FocusDirection
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
|
|
@ -44,6 +48,7 @@ import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.painterResource
|
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.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
|
@ -56,13 +61,10 @@ import androidx.tv.material3.DrawerValue
|
||||||
import androidx.tv.material3.Icon
|
import androidx.tv.material3.Icon
|
||||||
import androidx.tv.material3.LocalContentColor
|
import androidx.tv.material3.LocalContentColor
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.ModalNavigationDrawer
|
|
||||||
import androidx.tv.material3.NavigationDrawerItem
|
|
||||||
import androidx.tv.material3.NavigationDrawerItemDefaults
|
import androidx.tv.material3.NavigationDrawerItemDefaults
|
||||||
import androidx.tv.material3.NavigationDrawerScope
|
import androidx.tv.material3.NavigationDrawerScope
|
||||||
import androidx.tv.material3.ProvideTextStyle
|
import androidx.tv.material3.ProvideTextStyle
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.rememberDrawerState
|
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
import com.github.damontecres.wholphin.data.NavDrawerItemRepository
|
||||||
|
|
@ -269,6 +271,7 @@ fun NavDrawer(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
user: JellyfinUser,
|
user: JellyfinUser,
|
||||||
server: JellyfinServer,
|
server: JellyfinServer,
|
||||||
|
drawerState: DrawerState,
|
||||||
onClearBackdrop: () -> Unit,
|
onClearBackdrop: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: NavDrawerViewModel =
|
viewModel: NavDrawerViewModel =
|
||||||
|
|
@ -277,13 +280,11 @@ fun NavDrawer(
|
||||||
key = "${server.id}_${user.id}", // Keyed to the server & user to ensure its reset when switching either
|
key = "${server.id}_${user.id}", // Keyed to the server & user to ensure its reset when switching either
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
val drawerState = rememberDrawerState(DrawerValue.Closed)
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val searchFocusRequester = remember { FocusRequester() }
|
|
||||||
|
|
||||||
// If the user presses back while on the home page, open the nav drawer, another back press will quit the app
|
// If the user presses back while on the home page, open the nav drawer, another back press will quit the app
|
||||||
BackHandler(enabled = (drawerState.currentValue == DrawerValue.Closed && destination is Destination.Home)) {
|
BackHandler(enabled = (drawerState.currentValue == DrawerValue.Closed && destination is Destination.Home)) {
|
||||||
|
|
@ -302,26 +303,43 @@ fun NavDrawer(
|
||||||
viewModel.setShowMore(false)
|
viewModel.setShowMore(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val closedDrawerWidth = NavigationDrawerItemDefaults.CollapsedDrawerItemWidth
|
val closedDrawerWidth = CollapsedDrawerItemWidth
|
||||||
val drawerBackground by animateColorAsState(
|
val openDrawerWidth = ExpandedDrawerItemWidth
|
||||||
if (drawerState.isOpen) {
|
val offset by animateIntOffsetAsState(
|
||||||
MaterialTheme.colorScheme.surface
|
targetValue =
|
||||||
} else {
|
IntOffset(
|
||||||
Color.Transparent
|
x =
|
||||||
|
with(density) {
|
||||||
|
if (drawerState.isOpen) (openDrawerWidth - closedDrawerWidth).roundToPx() else 0
|
||||||
},
|
},
|
||||||
|
y = 0,
|
||||||
|
),
|
||||||
|
animationSpec =
|
||||||
|
spring(
|
||||||
|
stiffness = DrawerAnimationStiffness,
|
||||||
|
visibilityThreshold = IntOffset.VisibilityThreshold,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
val spacedBy = 4.dp
|
|
||||||
val config = LocalConfiguration.current
|
val config = LocalConfiguration.current
|
||||||
val density = LocalDensity.current
|
|
||||||
val heightInPx = remember { with(density) { config.screenHeightDp.dp.roundToPx() } }
|
val heightInPx = remember { with(density) { config.screenHeightDp.dp.roundToPx() } }
|
||||||
|
|
||||||
|
ModalNavigationDrawer(
|
||||||
|
modifier = modifier,
|
||||||
|
drawerState = drawerState,
|
||||||
|
drawerContent = { drawerValue ->
|
||||||
|
val isOpen = drawerValue.isOpen
|
||||||
|
val spacedBy = 4.dp
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val searchFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
suspend fun scrollToSelected() {
|
suspend fun scrollToSelected() {
|
||||||
val target = selectedIndex + 2
|
val target = selectedIndex + 2
|
||||||
try {
|
try {
|
||||||
if (target !in
|
if (target !in
|
||||||
listState.firstVisibleItemIndex..<listState.layoutInfo.visibleItemsInfo.lastIndex
|
listState.firstVisibleItemIndex..<listState.layoutInfo.visibleItemsInfo.lastIndex
|
||||||
) {
|
) {
|
||||||
val mult = if ((target - 2) < listState.layoutInfo.totalItemsCount / 2) -1 else 1
|
val mult =
|
||||||
|
if ((target - 2) < listState.layoutInfo.totalItemsCount / 2) -1 else 1
|
||||||
listState.animateScrollToItem(selectedIndex + 2, mult * (heightInPx / 2))
|
listState.animateScrollToItem(selectedIndex + 2, mult * (heightInPx / 2))
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
|
|
@ -333,20 +351,11 @@ fun NavDrawer(
|
||||||
scrollToSelected()
|
scrollToSelected()
|
||||||
}
|
}
|
||||||
|
|
||||||
ModalNavigationDrawer(
|
|
||||||
modifier = modifier,
|
|
||||||
drawerState = drawerState,
|
|
||||||
drawerContent = {
|
|
||||||
ProvideTextStyle(MaterialTheme.typography.labelMedium) {
|
ProvideTextStyle(MaterialTheme.typography.labelMedium) {
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(spacedBy),
|
verticalArrangement = Arrangement.spacedBy(spacedBy),
|
||||||
modifier =
|
modifier = Modifier.fillMaxHeight(),
|
||||||
Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.drawBehind {
|
|
||||||
drawRect(drawerBackground)
|
|
||||||
},
|
|
||||||
) {
|
) {
|
||||||
// Even though some must be clicked, focusing on it should clear other focused items
|
// Even though some must be clicked, focusing on it should clear other focused items
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
|
@ -355,7 +364,7 @@ fun NavDrawer(
|
||||||
user = user,
|
user = user,
|
||||||
imageUrl = userImageUrl,
|
imageUrl = userImageUrl,
|
||||||
serverName = server.name ?: server.url,
|
serverName = server.name ?: server.url,
|
||||||
drawerOpen = drawerState.isOpen,
|
drawerOpen = isOpen,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setupNavigationManager.navigateTo(
|
viewModel.setupNavigationManager.navigateTo(
|
||||||
|
|
@ -393,7 +402,7 @@ fun NavDrawer(
|
||||||
text = stringResource(R.string.search),
|
text = stringResource(R.string.search),
|
||||||
icon = Icons.Default.Search,
|
icon = Icons.Default.Search,
|
||||||
selected = selectedIndex == -2,
|
selected = selectedIndex == -2,
|
||||||
drawerOpen = drawerState.isOpen,
|
drawerOpen = isOpen,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setIndex(-2)
|
viewModel.setIndex(-2)
|
||||||
|
|
@ -414,7 +423,7 @@ fun NavDrawer(
|
||||||
text = stringResource(R.string.home),
|
text = stringResource(R.string.home),
|
||||||
icon = Icons.Default.Home,
|
icon = Icons.Default.Home,
|
||||||
selected = selectedIndex == -1,
|
selected = selectedIndex == -1,
|
||||||
drawerOpen = drawerState.isOpen,
|
drawerOpen = isOpen,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.setIndex(-1)
|
viewModel.setIndex(-1)
|
||||||
|
|
@ -438,7 +447,7 @@ fun NavDrawer(
|
||||||
library = it,
|
library = it,
|
||||||
selected = selectedIndex == index,
|
selected = selectedIndex == index,
|
||||||
moreExpanded = showMore,
|
moreExpanded = showMore,
|
||||||
drawerOpen = drawerState.isOpen,
|
drawerOpen = isOpen,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onClickDrawerItem(index, it)
|
viewModel.onClickDrawerItem(index, it)
|
||||||
|
|
@ -459,10 +468,10 @@ fun NavDrawer(
|
||||||
library = it,
|
library = it,
|
||||||
selected = selectedIndex == adjustedIndex,
|
selected = selectedIndex == adjustedIndex,
|
||||||
moreExpanded = showMore,
|
moreExpanded = showMore,
|
||||||
drawerOpen = drawerState.isOpen,
|
drawerOpen = isOpen,
|
||||||
onClick = { viewModel.onClickDrawerItem(adjustedIndex, it) },
|
onClick = { viewModel.onClickDrawerItem(adjustedIndex, it) },
|
||||||
containerColor =
|
containerColor =
|
||||||
if (drawerState.isOpen) {
|
if (isOpen) {
|
||||||
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||||
} else {
|
} else {
|
||||||
Color.Unspecified
|
Color.Unspecified
|
||||||
|
|
@ -483,7 +492,7 @@ fun NavDrawer(
|
||||||
text = stringResource(R.string.settings),
|
text = stringResource(R.string.settings),
|
||||||
icon = Icons.Default.Settings,
|
icon = Icons.Default.Settings,
|
||||||
selected = false,
|
selected = false,
|
||||||
drawerOpen = drawerState.isOpen,
|
drawerOpen = isOpen,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigationManager.navigateTo(
|
||||||
|
|
@ -501,10 +510,7 @@ fun NavDrawer(
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier = Modifier.fillMaxSize(),
|
||||||
Modifier
|
|
||||||
.padding(start = closedDrawerWidth)
|
|
||||||
.fillMaxSize(),
|
|
||||||
) {
|
) {
|
||||||
// Drawer content
|
// Drawer content
|
||||||
DestinationContent(
|
DestinationContent(
|
||||||
|
|
@ -513,7 +519,10 @@ fun NavDrawer(
|
||||||
onClearBackdrop = onClearBackdrop,
|
onClearBackdrop = onClearBackdrop,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize()
|
||||||
|
.offset {
|
||||||
|
offset
|
||||||
|
}.padding(start = closedDrawerWidth + 8.dp, end = 16.dp),
|
||||||
)
|
)
|
||||||
if (preferences.appPreferences.interfacePreferences.showClock) {
|
if (preferences.appPreferences.interfacePreferences.showClock) {
|
||||||
TimeDisplay()
|
TimeDisplay()
|
||||||
|
|
@ -542,7 +551,7 @@ fun NavigationDrawerScope.ProfileIcon(
|
||||||
name = user.name,
|
name = user.name,
|
||||||
imageUrl = imageUrl,
|
imageUrl = imageUrl,
|
||||||
alpha = if (drawerOpen) 1f else .5f,
|
alpha = if (drawerOpen) 1f else .5f,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.size(DrawerIconSize),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
|
|
@ -583,7 +592,7 @@ fun NavigationDrawerScope.IconNavItem(
|
||||||
icon,
|
icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = color,
|
tint = color,
|
||||||
modifier = Modifier,
|
modifier = Modifier.size(DrawerIconSize),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent =
|
supportingContent =
|
||||||
|
|
@ -673,7 +682,7 @@ fun NavigationDrawerScope.NavItem(
|
||||||
painter = painterResource(icon),
|
painter = painterResource(icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = color,
|
tint = color,
|
||||||
modifier = Modifier,
|
modifier = Modifier.size(DrawerIconSize),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -700,6 +709,7 @@ fun NavigationDrawerScope.NavItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
fun navItemColor(
|
fun navItemColor(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
focused: Boolean,
|
focused: Boolean,
|
||||||
|
|
@ -753,4 +763,6 @@ fun navItemColor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val DrawerState.isOpen: Boolean get() = this.currentValue == DrawerValue.Open
|
val DrawerState.isOpen: Boolean get() = this.currentValue.isOpen
|
||||||
|
|
||||||
|
val DrawerValue.isOpen: Boolean get() = this == DrawerValue.Open
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
/*
|
||||||
|
* This file contains code copied and modified from:
|
||||||
|
* https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-androidx-tv-material-release/tv/tv-material/src/main/java/androidx/tv/material3/
|
||||||
|
*
|
||||||
|
* Their license & required attribution is below.
|
||||||
|
*
|
||||||
|
* Copyright 2023 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.github.damontecres.wholphin.ui.nav
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.VisibilityThreshold
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.focusGroup
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
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.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.FocusState
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.layout.layout
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.DrawerState
|
||||||
|
import androidx.tv.material3.DrawerValue
|
||||||
|
import androidx.tv.material3.ListItem
|
||||||
|
import androidx.tv.material3.ListItemDefaults
|
||||||
|
import androidx.tv.material3.NavigationDrawerItemColors
|
||||||
|
import androidx.tv.material3.NavigationDrawerItemDefaults
|
||||||
|
import androidx.tv.material3.NavigationDrawerScope
|
||||||
|
import androidx.tv.material3.rememberDrawerState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ModalNavigationDrawer(
|
||||||
|
drawerContent: @Composable NavigationDrawerScope.(DrawerValue) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
DrawerSheet(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.CenterStart),
|
||||||
|
drawerState = drawerState,
|
||||||
|
content = drawerContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DrawerSheet(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
drawerState: DrawerState = remember { DrawerState() },
|
||||||
|
content: @Composable NavigationDrawerScope.(DrawerValue) -> Unit,
|
||||||
|
) {
|
||||||
|
// indicates that the drawer has been set to its initial state and has grabbed focus if
|
||||||
|
// necessary. Controls whether focus is used to decide the state of the drawer going forward.
|
||||||
|
var initializationComplete: Boolean by remember { mutableStateOf(false) }
|
||||||
|
var focusState by remember { mutableStateOf<FocusState?>(null) }
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
LaunchedEffect(key1 = drawerState.currentValue) {
|
||||||
|
if (drawerState.currentValue == DrawerValue.Open && focusState?.hasFocus == false) {
|
||||||
|
// used to grab focus if the drawer state is set to Open on start.
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
initializationComplete = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val internalModifier =
|
||||||
|
Modifier
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.animateContentSize(
|
||||||
|
animationSpec =
|
||||||
|
spring(
|
||||||
|
stiffness = DrawerAnimationStiffness,
|
||||||
|
visibilityThreshold = IntSize.VisibilityThreshold,
|
||||||
|
),
|
||||||
|
).fillMaxHeight()
|
||||||
|
// adding passed-in modifier here to ensure animateContentSize is called before other
|
||||||
|
// size based modifiers.
|
||||||
|
.then(modifier)
|
||||||
|
.onFocusChanged {
|
||||||
|
focusState = it
|
||||||
|
|
||||||
|
if (initializationComplete) {
|
||||||
|
drawerState.setValue(if (it.hasFocus) DrawerValue.Open else DrawerValue.Closed)
|
||||||
|
}
|
||||||
|
}.focusGroup()
|
||||||
|
|
||||||
|
Box(modifier = internalModifier) {
|
||||||
|
NavigationDrawerScopeImpl(drawerState.currentValue == DrawerValue.Open).apply {
|
||||||
|
content(drawerState.currentValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class NavigationDrawerScopeImpl(
|
||||||
|
override val hasFocus: Boolean,
|
||||||
|
) : NavigationDrawerScope
|
||||||
|
|
||||||
|
internal val CollapsedDrawerItemWidth = 64.dp
|
||||||
|
internal val ExpandedDrawerItemWidth = 224.dp
|
||||||
|
internal val DrawerIconSize = 24.dp
|
||||||
|
internal val DrawerIconPadding = (ListItemDefaults.IconSize - DrawerIconSize) / 2
|
||||||
|
internal val DrawerAnimationStiffness = Spring.StiffnessMedium
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun NavigationDrawerScope.NavigationDrawerItem(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
leadingContent: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
supportingContent: (@Composable () -> Unit)? = null,
|
||||||
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
|
tonalElevation: Dp = NavigationDrawerItemDefaults.NavigationDrawerItemElevation,
|
||||||
|
colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(),
|
||||||
|
interactionSource: MutableInteractionSource? = null,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val animatedWidth by
|
||||||
|
animateDpAsState(
|
||||||
|
targetValue =
|
||||||
|
if (hasFocus) {
|
||||||
|
ExpandedDrawerItemWidth
|
||||||
|
} else {
|
||||||
|
CollapsedDrawerItemWidth
|
||||||
|
},
|
||||||
|
label = "NavigationDrawerItem width open/closed state of the drawer item",
|
||||||
|
)
|
||||||
|
val navDrawerItemHeight =
|
||||||
|
if (supportingContent == null) {
|
||||||
|
NavigationDrawerItemDefaults.ContainerHeightOneLine
|
||||||
|
} else {
|
||||||
|
NavigationDrawerItemDefaults.ContainerHeightTwoLine
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
headlineContent = content,
|
||||||
|
leadingContent = {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = DrawerIconPadding)
|
||||||
|
.size(DrawerIconSize),
|
||||||
|
) { leadingContent() }
|
||||||
|
},
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
supportingContent = supportingContent,
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.layout { measurable, constraints ->
|
||||||
|
val width = animatedWidth.roundToPx()
|
||||||
|
val height = navDrawerItemHeight.roundToPx()
|
||||||
|
val placeable =
|
||||||
|
measurable.measure(
|
||||||
|
constraints.copy(
|
||||||
|
minWidth = width,
|
||||||
|
maxWidth = width,
|
||||||
|
minHeight = height,
|
||||||
|
maxHeight = height,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
layout(placeable.width, placeable.height) { placeable.place(0, 0) }
|
||||||
|
},
|
||||||
|
enabled = enabled,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
colors = colors.toToggleableListItemColors(hasFocus),
|
||||||
|
scale = ListItemDefaults.scale(1f, 1f),
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavigationDrawerItemColors.toToggleableListItemColors(doesNavigationDrawerHaveFocus: Boolean) =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor = containerColor,
|
||||||
|
contentColor = if (doesNavigationDrawerHaveFocus) contentColor else inactiveContentColor,
|
||||||
|
focusedContainerColor = focusedContainerColor,
|
||||||
|
focusedContentColor = focusedContentColor,
|
||||||
|
pressedContainerColor = pressedContainerColor,
|
||||||
|
pressedContentColor = pressedContentColor,
|
||||||
|
selectedContainerColor = selectedContainerColor,
|
||||||
|
selectedContentColor = selectedContentColor,
|
||||||
|
disabledContainerColor = disabledContainerColor,
|
||||||
|
disabledContentColor =
|
||||||
|
if (doesNavigationDrawerHaveFocus) {
|
||||||
|
disabledContentColor
|
||||||
|
} else {
|
||||||
|
disabledInactiveContentColor
|
||||||
|
},
|
||||||
|
focusedSelectedContainerColor = focusedSelectedContainerColor,
|
||||||
|
focusedSelectedContentColor = focusedSelectedContentColor,
|
||||||
|
pressedSelectedContainerColor = pressedSelectedContainerColor,
|
||||||
|
pressedSelectedContentColor = pressedSelectedContentColor,
|
||||||
|
)
|
||||||
|
|
@ -555,16 +555,19 @@ fun Controller(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (showClock) {
|
if (showClock) {
|
||||||
var remaining by remember { mutableStateOf((playerControls.duration - playerControls.currentPosition).milliseconds) }
|
var endTimeStr by remember { mutableStateOf("...") }
|
||||||
LaunchedEffect(playerControls) {
|
LaunchedEffect(playerControls) {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
remaining =
|
val remaining =
|
||||||
(playerControls.duration - playerControls.currentPosition).milliseconds
|
(playerControls.duration - playerControls.currentPosition)
|
||||||
|
.div(playerControls.playbackParameters.speed)
|
||||||
|
.toLong()
|
||||||
|
.milliseconds
|
||||||
|
val endTime = LocalTime.now().plusSeconds(remaining.inWholeSeconds)
|
||||||
|
endTimeStr = TimeFormatter.format(endTime)
|
||||||
delay(1.seconds)
|
delay(1.seconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val endTime = LocalTime.now().plusSeconds(remaining.inWholeSeconds)
|
|
||||||
val endTimeStr = TimeFormatter.format(endTime)
|
|
||||||
Text(
|
Text(
|
||||||
text = "Ends $endTimeStr",
|
text = "Ends $endTimeStr",
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LifecycleRegistry
|
import androidx.lifecycle.LifecycleRegistry
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.work.PeriodicWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.github.damontecres.wholphin.data.CurrentUser
|
import com.github.damontecres.wholphin.data.CurrentUser
|
||||||
import com.github.damontecres.wholphin.data.ServerRepository
|
import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
|
|
@ -14,6 +15,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import io.mockk.slot
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -23,10 +25,12 @@ import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class SuggestionsSchedulerServiceTest {
|
class SuggestionsSchedulerServiceTest {
|
||||||
|
|
@ -87,4 +91,54 @@ class SuggestionsSchedulerServiceTest {
|
||||||
advanceUntilIdle()
|
advanceUntilIdle()
|
||||||
verify { mockWorkManager.cancelUniqueWork(SuggestionsWorker.WORK_NAME) }
|
verify { mockWorkManager.cancelUniqueWork(SuggestionsWorker.WORK_NAME) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun schedules_periodic_work_with_delay_when_cache_empty() =
|
||||||
|
runTest {
|
||||||
|
coEvery { mockCache.isEmpty() } returns true
|
||||||
|
val workRequestSlot = slot<PeriodicWorkRequest>()
|
||||||
|
every {
|
||||||
|
mockWorkManager.enqueueUniquePeriodicWork(
|
||||||
|
SuggestionsWorker.WORK_NAME,
|
||||||
|
any(),
|
||||||
|
capture(workRequestSlot),
|
||||||
|
)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
createService()
|
||||||
|
currentLiveData.value =
|
||||||
|
CurrentUser(
|
||||||
|
user = JellyfinUser(id = UUID.randomUUID(), name = "User", serverId = UUID.randomUUID(), accessToken = "token"),
|
||||||
|
server = JellyfinServer(id = UUID.randomUUID(), name = "Server", url = "http://localhost", version = null),
|
||||||
|
)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
verify { mockWorkManager.enqueueUniquePeriodicWork(SuggestionsWorker.WORK_NAME, any(), any()) }
|
||||||
|
assertEquals(30000L, workRequestSlot.captured.workSpec.initialDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun schedules_periodic_work_without_delay_when_cache_not_empty() =
|
||||||
|
runTest {
|
||||||
|
coEvery { mockCache.isEmpty() } returns false
|
||||||
|
val workRequestSlot = slot<PeriodicWorkRequest>()
|
||||||
|
every {
|
||||||
|
mockWorkManager.enqueueUniquePeriodicWork(
|
||||||
|
SuggestionsWorker.WORK_NAME,
|
||||||
|
any(),
|
||||||
|
capture(workRequestSlot),
|
||||||
|
)
|
||||||
|
} returns mockk()
|
||||||
|
|
||||||
|
createService()
|
||||||
|
currentLiveData.value =
|
||||||
|
CurrentUser(
|
||||||
|
user = JellyfinUser(id = UUID.randomUUID(), name = "User", serverId = UUID.randomUUID(), accessToken = "token"),
|
||||||
|
server = JellyfinServer(id = UUID.randomUUID(), name = "Server", url = "http://localhost", version = null),
|
||||||
|
)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
verify { mockWorkManager.enqueueUniquePeriodicWork(SuggestionsWorker.WORK_NAME, any(), any()) }
|
||||||
|
assertEquals(0L, workRequestSlot.captured.workSpec.initialDelay)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,85 @@ class SuggestionsWorkerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fetches_contextual_suggestions_when_genres_available() =
|
||||||
|
runTest {
|
||||||
|
val viewId = UUID.randomUUID()
|
||||||
|
val view =
|
||||||
|
mockk<BaseItemDto>(relaxed = true) {
|
||||||
|
every { id } returns viewId
|
||||||
|
every { this@mockk.collectionType } returns CollectionType.MOVIES
|
||||||
|
}
|
||||||
|
every { mockPreferences.data } returns flowOf(mockPrefs())
|
||||||
|
coEvery { mockUserViewsApi.getUserViews(userId = testUserId) } returns mockQueryResult(listOf(view))
|
||||||
|
mockkObject(GetItemsRequestHandler)
|
||||||
|
|
||||||
|
val genreId = UUID.randomUUID()
|
||||||
|
val historyItem =
|
||||||
|
mockk<BaseItemDto>(relaxed = true) {
|
||||||
|
every { id } returns UUID.randomUUID()
|
||||||
|
every { genreItems } returns listOf(mockk { every { id } returns genreId })
|
||||||
|
}
|
||||||
|
val contextualItem = mockk<BaseItemDto>(relaxed = true) { every { id } returns UUID.randomUUID() }
|
||||||
|
val randomItem = mockk<BaseItemDto>(relaxed = true) { every { id } returns UUID.randomUUID() }
|
||||||
|
val freshItem = mockk<BaseItemDto>(relaxed = true) { every { id } returns UUID.randomUUID() }
|
||||||
|
|
||||||
|
var callCount = 0
|
||||||
|
coEvery { GetItemsRequestHandler.execute(mockApi, any()) } answers {
|
||||||
|
callCount++
|
||||||
|
when (callCount) {
|
||||||
|
1 -> mockQueryResult(listOf(historyItem))
|
||||||
|
2 -> mockQueryResult(listOf(contextualItem))
|
||||||
|
3 -> mockQueryResult(listOf(randomItem))
|
||||||
|
4 -> mockQueryResult(listOf(freshItem))
|
||||||
|
else -> mockQueryResult(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = createWorker().doWork()
|
||||||
|
|
||||||
|
assertEquals(ListenableWorker.Result.success(), result)
|
||||||
|
coVerify { mockCache.put(testUserId, viewId, BaseItemKind.MOVIE, any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun skips_contextual_suggestions_when_no_genres_available() =
|
||||||
|
runTest {
|
||||||
|
val viewId = UUID.randomUUID()
|
||||||
|
val view =
|
||||||
|
mockk<BaseItemDto>(relaxed = true) {
|
||||||
|
every { id } returns viewId
|
||||||
|
every { this@mockk.collectionType } returns CollectionType.MOVIES
|
||||||
|
}
|
||||||
|
every { mockPreferences.data } returns flowOf(mockPrefs())
|
||||||
|
coEvery { mockUserViewsApi.getUserViews(userId = testUserId) } returns mockQueryResult(listOf(view))
|
||||||
|
mockkObject(GetItemsRequestHandler)
|
||||||
|
|
||||||
|
val historyItem =
|
||||||
|
mockk<BaseItemDto>(relaxed = true) {
|
||||||
|
every { id } returns UUID.randomUUID()
|
||||||
|
every { genreItems } returns emptyList()
|
||||||
|
}
|
||||||
|
val randomItem = mockk<BaseItemDto>(relaxed = true) { every { id } returns UUID.randomUUID() }
|
||||||
|
val freshItem = mockk<BaseItemDto>(relaxed = true) { every { id } returns UUID.randomUUID() }
|
||||||
|
|
||||||
|
var callCount = 0
|
||||||
|
coEvery { GetItemsRequestHandler.execute(mockApi, any()) } answers {
|
||||||
|
callCount++
|
||||||
|
when (callCount) {
|
||||||
|
1 -> mockQueryResult(listOf(historyItem))
|
||||||
|
2 -> mockQueryResult(listOf(randomItem))
|
||||||
|
3 -> mockQueryResult(listOf(freshItem))
|
||||||
|
else -> mockQueryResult(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = createWorker().doWork()
|
||||||
|
|
||||||
|
assertEquals(ListenableWorker.Result.success(), result)
|
||||||
|
coVerify { mockCache.put(testUserId, viewId, BaseItemKind.MOVIE, any()) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun returns_retry_on_network_error() =
|
fun returns_retry_on_network_error() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue