Basic home page & images

This commit is contained in:
Damontecres 2025-09-21 22:16:42 -04:00
parent 76d78246db
commit 51ad92f539
No known key found for this signature in database
16 changed files with 421 additions and 42 deletions

View file

@ -1,39 +0,0 @@
package com.github.damontecres.dolphin.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.github.damontecres.dolphin.preferences.UserPreferences
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.userApi
import javax.inject.Inject
@HiltViewModel
class MainViewModel
@Inject
constructor(
val api: ApiClient,
) : ViewModel() {
init {
viewModelScope.launch {
api.userApi.getCurrentUser().content.configuration?.let {
it.orderedViews
}
}
}
fun todo() {
}
}
@Composable
fun MainPage(
preferences: UserPreferences,
modifier: Modifier,
viewModel: MainViewModel = viewModel(),
) {
}

View file

@ -0,0 +1,23 @@
package com.github.damontecres.dolphin.ui.cards
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.github.damontecres.dolphin.data.model.DolphinModel
import com.github.damontecres.dolphin.data.model.Video
@Composable
fun DolphinCard(
item: DolphinModel?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
when (item) {
is Video ->
VideoCard(
item = item,
onClick = onClick,
modifier = modifier,
)
null -> TODO()
}
}

View file

@ -0,0 +1,38 @@
package com.github.damontecres.dolphin.ui.cards
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card
import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.data.model.Video
@Composable
fun VideoCard(
item: Video,
onClick: () -> Unit,
modifier: Modifier = Modifier,
cardWidth: Dp = 150.dp,
cardHeight: Dp = 200.dp,
) {
Card(
modifier = modifier,
onClick = onClick,
) {
Column(
modifier = Modifier.size(cardWidth, cardHeight),
) {
Text(
text = item.name ?: "",
)
AsyncImage(
model = item.imageUrl,
contentDescription = item.name,
)
}
}
}

View file

@ -0,0 +1,30 @@
package com.github.damontecres.dolphin.ui.main
import androidx.annotation.StringRes
import com.github.damontecres.dolphin.R
/**
* All possible homesections, "synced" with jellyfin-web.
*
* https://github.com/jellyfin/jellyfin-web/blob/master/src/components/homesections/homesections.js
*/
enum class HomeSection(
val key: String,
@param:StringRes val nameRes: Int,
) {
LATEST_MEDIA("latestmedia", R.string.home_section_latest_media),
LIBRARY_TILES_SMALL("smalllibrarytiles", R.string.home_section_library),
LIBRARY_BUTTONS("librarybuttons", R.string.home_section_library_small),
RESUME("resume", R.string.home_section_resume),
RESUME_AUDIO("resumeaudio", R.string.home_section_resume_audio),
RESUME_BOOK("resumebook", R.string.home_section_resume_book),
ACTIVE_RECORDINGS("activerecordings", R.string.home_section_active_recordings),
NEXT_UP("nextup", R.string.home_section_next_up),
LIVE_TV("livetv", R.string.home_section_livetv),
NONE("none", R.string.home_section_none),
;
companion object {
fun fromKey(key: String): HomeSection = entries.firstOrNull { it.key == key } ?: NONE
}
}

View file

@ -0,0 +1,169 @@
package com.github.damontecres.dolphin.ui.main
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.data.model.DolphinModel
import com.github.damontecres.dolphin.data.model.convertModel
import com.github.damontecres.dolphin.isNotNullOrBlank
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.cards.DolphinCard
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi
import org.jellyfin.sdk.api.client.extensions.userApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
import timber.log.Timber
import javax.inject.Inject
data class HomeRow(
val section: HomeSection,
val items: List<DolphinModel>,
)
@HiltViewModel
class MainViewModel
@Inject
constructor(
val api: ApiClient,
) : ViewModel() {
val homeRows = MutableLiveData<List<HomeRow>>()
init {
viewModelScope.launch(Dispatchers.IO) {
val user = api.userApi.getCurrentUser().content
val displayPrefs =
api.displayPreferencesApi
.getDisplayPreferences(
displayPreferencesId = "usersettings",
client = "emby",
).content
val homeSections =
displayPrefs.customPrefs.entries
.filter { it.key.startsWith("homesection") && it.value.isNotNullOrBlank() }
.sortedBy { it.key }
.map { HomeSection.fromKey(it.value ?: "") }
.filterNot { it == HomeSection.NONE }
val homeRows =
homeSections
.mapNotNull { section ->
Timber.v("Loading section: %s", section.name)
when (section) {
HomeSection.LATEST_MEDIA -> {
user.configuration?.orderedViews?.firstOrNull()?.let { viewId ->
val request =
GetLatestMediaRequest(
fields = listOf(),
imageTypeLimit = 1,
parentId = viewId,
groupItems = true,
limit = 25,
)
val latest =
api.userLibraryApi
.getLatestMedia(request)
.content
.map { convertModel(it, api) }
Timber.v("latest: %s", latest)
// api.itemsApi.getItems(request)
HomeRow(
section = section,
items = latest,
)
}
}
// TODO
HomeSection.LIBRARY_TILES_SMALL -> null
HomeSection.RESUME -> null
HomeSection.ACTIVE_RECORDINGS -> null
HomeSection.NEXT_UP -> null
HomeSection.LIVE_TV -> null
// TODO Not supported?
HomeSection.LIBRARY_BUTTONS -> null
HomeSection.RESUME_AUDIO -> null
HomeSection.RESUME_BOOK -> null
HomeSection.NONE -> null
}
}.filter { it.items.isNotEmpty() }
withContext(Dispatchers.Main) {
this@MainViewModel.homeRows.value = homeRows
}
}
}
}
@Composable
fun MainPage(
preferences: UserPreferences,
modifier: Modifier,
viewModel: MainViewModel = viewModel(),
) {
val homeRows by viewModel.homeRows.observeAsState(listOf())
Column(modifier = modifier) {
// TODO header?
LazyColumn {
homeRows.forEach { row ->
item {
HomePageRow(
row = row,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
}
@Composable
fun HomePageRow(
row: HomeRow,
modifier: Modifier = Modifier,
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier,
) {
Text(
text = stringResource(row.section.nameRes),
)
LazyRow(
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(8.dp),
modifier =
Modifier
.padding(start = 16.dp)
.fillMaxWidth(),
) {
items(row.items) { item ->
DolphinCard(
item = item,
onClick = {},
modifier = Modifier,
)
}
}
}
}