mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Basic home page & images
This commit is contained in:
parent
76d78246db
commit
51ad92f539
16 changed files with 421 additions and 42 deletions
|
|
@ -76,6 +76,8 @@ dependencies {
|
||||||
implementation(libs.androidx.compose.ui)
|
implementation(libs.androidx.compose.ui)
|
||||||
implementation(libs.androidx.compose.ui.graphics)
|
implementation(libs.androidx.compose.ui.graphics)
|
||||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||||
|
implementation(libs.androidx.compose.runtime)
|
||||||
|
implementation(libs.androidx.compose.runtime.livedata)
|
||||||
implementation(libs.androidx.tv.foundation)
|
implementation(libs.androidx.tv.foundation)
|
||||||
implementation(libs.androidx.tv.material)
|
implementation(libs.androidx.tv.material)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
|
|
@ -113,6 +115,8 @@ dependencies {
|
||||||
ksp(libs.androidx.room.compiler)
|
ksp(libs.androidx.room.compiler)
|
||||||
ksp(libs.hilt.android.compiler)
|
ksp(libs.hilt.android.compiler)
|
||||||
|
|
||||||
|
implementation(libs.timber)
|
||||||
|
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.github.damontecres.dolphin
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.annotation.ExperimentalCoilApi
|
||||||
|
import coil3.compose.setSingletonImageLoaderFactory
|
||||||
|
import coil3.disk.DiskCache
|
||||||
|
import coil3.disk.directory
|
||||||
|
import coil3.network.cachecontrol.CacheControlCacheStrategy
|
||||||
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import coil3.util.DebugLogger
|
||||||
|
import com.github.damontecres.dolphin.data.ServerRepository
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import kotlin.time.ExperimentalTime
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTime::class, ExperimentalCoilApi::class)
|
||||||
|
@Composable
|
||||||
|
fun CoilConfig(
|
||||||
|
serverRepository: ServerRepository,
|
||||||
|
okHttpClient: OkHttpClient,
|
||||||
|
debugLogging: Boolean,
|
||||||
|
) {
|
||||||
|
setSingletonImageLoaderFactory { ctx ->
|
||||||
|
ImageLoader
|
||||||
|
.Builder(ctx)
|
||||||
|
.diskCache(
|
||||||
|
DiskCache
|
||||||
|
.Builder()
|
||||||
|
.directory(ctx.cacheDir.resolve("coil3_image_cache"))
|
||||||
|
.maxSizeBytes(100L * 1024 * 1024)
|
||||||
|
.build(),
|
||||||
|
).crossfade(true)
|
||||||
|
.logger(if (debugLogging) DebugLogger() else null)
|
||||||
|
.components {
|
||||||
|
add(
|
||||||
|
OkHttpNetworkFetcherFactory(
|
||||||
|
cacheStrategy = { CacheControlCacheStrategy() },
|
||||||
|
callFactory = {
|
||||||
|
Call.Factory { request ->
|
||||||
|
// Ref: https://gist.github.com/nielsvanvelzen/ea047d9028f676185832e51ffaf12a6f
|
||||||
|
val token = serverRepository.currentUser?.accessToken
|
||||||
|
okHttpClient.newCall(
|
||||||
|
request
|
||||||
|
.newBuilder()
|
||||||
|
.addHeader("Authorization", "MediaBrowser Token=\"$token\"")
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,15 @@ package com.github.damontecres.dolphin
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class DolphinApplication : Application() {
|
class DolphinApplication : Application() {
|
||||||
init {
|
init {
|
||||||
instance = this
|
instance = this
|
||||||
|
|
||||||
|
// TODO only plant in debug builds
|
||||||
|
Timber.plant(Timber.DebugTree())
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.github.damontecres.dolphin
|
||||||
|
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
fun CharSequence?.isNotNullOrBlank(): Boolean {
|
||||||
|
contract {
|
||||||
|
returns(true) implies (this@isNotNullOrBlank != null)
|
||||||
|
}
|
||||||
|
return !this.isNullOrBlank()
|
||||||
|
}
|
||||||
|
|
@ -12,13 +12,14 @@ import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
import androidx.tv.material3.Surface
|
import androidx.tv.material3.Surface
|
||||||
import androidx.tv.material3.Text
|
|
||||||
import com.github.damontecres.dolphin.data.ServerRepository
|
import com.github.damontecres.dolphin.data.ServerRepository
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
import com.github.damontecres.dolphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.dolphin.ui.ServerLoginPage
|
import com.github.damontecres.dolphin.ui.ServerLoginPage
|
||||||
|
import com.github.damontecres.dolphin.ui.main.MainPage
|
||||||
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
|
import com.github.damontecres.dolphin.ui.theme.DolphinTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
|
@ -29,6 +30,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var userPreferencesDataStore: DataStore<UserPreferences>
|
lateinit var userPreferencesDataStore: DataStore<UserPreferences>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var okHttpClient: OkHttpClient
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -39,6 +43,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
shape = RectangleShape,
|
shape = RectangleShape,
|
||||||
) {
|
) {
|
||||||
|
CoilConfig(serverRepository, okHttpClient, false)
|
||||||
|
|
||||||
val preferences by userPreferencesDataStore.data.collectAsState(null)
|
val preferences by userPreferencesDataStore.data.collectAsState(null)
|
||||||
preferences?.let { preferences ->
|
preferences?.let { preferences ->
|
||||||
if (preferences.currentServerId.isNotBlank() && preferences.currentUserId.isNotBlank()) {
|
if (preferences.currentServerId.isNotBlank() && preferences.currentUserId.isNotBlank()) {
|
||||||
|
|
@ -54,7 +60,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
val server = serverRepository.currentServer
|
val server = serverRepository.currentServer
|
||||||
val user = serverRepository.currentUser
|
val user = serverRepository.currentUser
|
||||||
if (server != null && user != null) {
|
if (server != null && user != null) {
|
||||||
Text("Logged in as ${user.name} on ${server.url}")
|
// TODO navigation
|
||||||
|
MainPage(
|
||||||
|
preferences = preferences,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.api.client.ApiClient
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
import org.jellyfin.sdk.api.client.exception.InvalidStatusException
|
||||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ServerRepository
|
class ServerRepository
|
||||||
|
|
@ -51,6 +52,7 @@ class ServerRepository
|
||||||
server: JellyfinServer,
|
server: JellyfinServer,
|
||||||
user: JellyfinUser,
|
user: JellyfinUser,
|
||||||
) {
|
) {
|
||||||
|
Timber.v("Changing user to ${user.name} on ${server.url}")
|
||||||
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
apiClient.update(baseUrl = server.url, accessToken = user.accessToken)
|
||||||
try {
|
try {
|
||||||
apiClient.userApi
|
apiClient.userApi
|
||||||
|
|
@ -60,6 +62,7 @@ class ServerRepository
|
||||||
_currentUser = user
|
_currentUser = user
|
||||||
} catch (e: InvalidStatusException) {
|
} catch (e: InvalidStatusException) {
|
||||||
// TODO
|
// TODO
|
||||||
|
Timber.e(e)
|
||||||
if (e.status == 401) {
|
if (e.status == 401) {
|
||||||
// Unauthorized
|
// Unauthorized
|
||||||
_currentServer = null
|
_currentServer = null
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.github.damontecres.dolphin.data.model
|
||||||
|
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
sealed interface DolphinModel {
|
||||||
|
val id: UUID
|
||||||
|
val name: String?
|
||||||
|
val imageUrl: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertModel(
|
||||||
|
dto: BaseItemDto,
|
||||||
|
api: ApiClient,
|
||||||
|
): DolphinModel =
|
||||||
|
when (dto.type) {
|
||||||
|
BaseItemKind.VIDEO -> Video.fromDto(dto, api)
|
||||||
|
else -> throw IllegalArgumentException("Unsupported item type: ${dto.type}")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package com.github.damontecres.dolphin.data.model
|
||||||
|
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class Video(
|
||||||
|
override val id: UUID,
|
||||||
|
override val name: String?,
|
||||||
|
override val imageUrl: String?,
|
||||||
|
) : DolphinModel {
|
||||||
|
companion object {
|
||||||
|
fun fromDto(
|
||||||
|
dto: BaseItemDto,
|
||||||
|
api: ApiClient,
|
||||||
|
): Video =
|
||||||
|
Video(
|
||||||
|
id = dto.id,
|
||||||
|
name = dto.name,
|
||||||
|
imageUrl = api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,11 +15,13 @@ import org.jellyfin.sdk.api.okhttp.OkHttpFactory
|
||||||
import org.jellyfin.sdk.createJellyfin
|
import org.jellyfin.sdk.createJellyfin
|
||||||
import org.jellyfin.sdk.model.ClientInfo
|
import org.jellyfin.sdk.model.ClientInfo
|
||||||
import org.jellyfin.sdk.model.DeviceInfo
|
import org.jellyfin.sdk.model.DeviceInfo
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object DolphinModule {
|
object DolphinModule {
|
||||||
@Provides
|
@Provides
|
||||||
|
@Singleton
|
||||||
fun okHttpClient() =
|
fun okHttpClient() =
|
||||||
OkHttpClient
|
OkHttpClient
|
||||||
.Builder()
|
.Builder()
|
||||||
|
|
@ -28,9 +30,11 @@ object DolphinModule {
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
@Singleton
|
||||||
fun okHttpFactory(okHttpClient: OkHttpClient) = OkHttpFactory(okHttpClient)
|
fun okHttpFactory(okHttpClient: OkHttpClient) = OkHttpFactory(okHttpClient)
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
@Singleton
|
||||||
fun jellyfin(
|
fun jellyfin(
|
||||||
okHttpFactory: OkHttpFactory,
|
okHttpFactory: OkHttpFactory,
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
|
|
@ -53,5 +57,6 @@ object DolphinModule {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
@Singleton
|
||||||
fun apiClient(jellyfin: Jellyfin) = jellyfin.createApi()
|
fun apiClient(jellyfin: Jellyfin) = jellyfin.createApi()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,14 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Dolphin</string>
|
<string name="app_name">Dolphin</string>
|
||||||
|
|
||||||
|
<string name="home_section_latest_media">Recently added media</string>
|
||||||
|
<string name="home_section_library">My media</string>
|
||||||
|
<string name="home_section_library_small">My media (small)</string>
|
||||||
|
<string name="home_section_resume">Continue watching</string>
|
||||||
|
<string name="home_section_resume_audio">Continue listening</string>
|
||||||
|
<string name="home_section_resume_book">Continue reading</string>
|
||||||
|
<string name="home_section_active_recordings">Active recordings</string>
|
||||||
|
<string name="home_section_next_up">Next up</string>
|
||||||
|
<string name="home_section_livetv">Live TV</string>
|
||||||
|
<string name="home_section_none">None</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -5,6 +5,8 @@ ksp = "2.2.20-2.0.2" # https://github.com/google/ksp/issues/2596 2.0.3 is broken
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
appcompat = "1.7.1"
|
appcompat = "1.7.1"
|
||||||
composeBom = "2025.09.00"
|
composeBom = "2025.09.00"
|
||||||
|
compose-runtime = "1.9.1"
|
||||||
|
timber = "5.0.1"
|
||||||
tvFoundation = "1.0.0-alpha12"
|
tvFoundation = "1.0.0-alpha12"
|
||||||
tvMaterial = "1.0.1"
|
tvMaterial = "1.0.1"
|
||||||
lifecycleRuntimeKtx = "2.9.4"
|
lifecycleRuntimeKtx = "2.9.4"
|
||||||
|
|
@ -34,6 +36,10 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin
|
||||||
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
|
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
||||||
|
androidx-compose-runtime = { module = "androidx.compose.runtime:runtime-android", version.ref = "compose-runtime" }
|
||||||
|
androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "compose-runtime" }
|
||||||
|
|
||||||
androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" }
|
androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" }
|
||||||
androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" }
|
androidx-tv-material = { group = "androidx.tv", name = "tv-material", version.ref = "tvMaterial" }
|
||||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||||
|
|
@ -71,7 +77,8 @@ androidx-material3-adaptive-navigation3 = { group = "androidx.compose.material3.
|
||||||
androidx-room-common-jvm = { group = "androidx.room", name = "room-common-jvm", version.ref = "room" }
|
androidx-room-common-jvm = { group = "androidx.room", name = "room-common-jvm", version.ref = "room" }
|
||||||
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||||
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
|
||||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
|
||||||
|
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue